Răsfoiți Sursa

Merge pull request #2518 from eliasto/feat/add-ovhcloud-ai-endpoints-provider

feat: Add OVHCloud AI Endpoints provider
Christiaan Arnoldus 4 luni în urmă
părinte
comite
634d9b80e5
57 a modificat fișierele cu 766 adăugiri și 22 ștergeri
  1. 5 0
      .changeset/gentle-yaks-pay.md
  2. 50 0
      apps/kilocode-docs/docs/providers/ovhcloud.md
  3. 3 3
      apps/kilocode-docs/docs/providers/synthetic.md
  4. 50 0
      apps/kilocode-docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/providers/ovhcloud.md
  5. 1 0
      apps/kilocode-docs/sidebars.ts
  6. 1 0
      packages/types/src/global-settings.ts
  7. 14 1
      packages/types/src/provider-settings.ts
  8. 2 1
      packages/types/src/providers/index.ts
  9. 17 0
      packages/types/src/providers/ovhcloud.ts
  10. 5 0
      src/api/index.ts
  11. 223 0
      src/api/providers/__tests__/ovhcloud.spec.ts
  12. 22 0
      src/api/providers/fetchers/__tests__/modelCache.spec.ts
  13. 9 1
      src/api/providers/fetchers/modelCache.ts
  14. 31 0
      src/api/providers/fetchers/ovhcloud.ts
  15. 1 0
      src/api/providers/index.ts
  16. 92 0
      src/api/providers/ovhcloud.ts
  17. 15 2
      src/core/webview/__tests__/ClineProvider.spec.ts
  18. 22 3
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  19. 7 0
      src/core/webview/webviewMessageHandler.ts
  20. 4 0
      src/shared/ProfileValidator.ts
  21. 17 0
      src/shared/__tests__/ProfileValidator.spec.ts
  22. 1 0
      src/shared/api.ts
  23. 4 1
      webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts
  24. 9 0
      webview-ui/src/components/kilocode/hooks/useProviderModels.ts
  25. 6 0
      webview-ui/src/components/kilocode/hooks/useSelectedModel.ts
  26. 2 7
      webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx
  27. 0 2
      webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx
  28. 15 0
      webview-ui/src/components/settings/ApiOptions.tsx
  29. 1 0
      webview-ui/src/components/settings/ModelPicker.tsx
  30. 1 0
      webview-ui/src/components/settings/constants.ts
  31. 72 0
      webview-ui/src/components/settings/providers/OvhCloud.tsx
  32. 1 0
      webview-ui/src/components/settings/providers/index.ts
  33. 8 0
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  34. 2 0
      webview-ui/src/i18n/locales/ar/settings.json
  35. 2 0
      webview-ui/src/i18n/locales/ca/settings.json
  36. 2 0
      webview-ui/src/i18n/locales/cs/settings.json
  37. 2 0
      webview-ui/src/i18n/locales/de/settings.json
  38. 2 0
      webview-ui/src/i18n/locales/en/settings.json
  39. 2 0
      webview-ui/src/i18n/locales/es/settings.json
  40. 2 0
      webview-ui/src/i18n/locales/fr/settings.json
  41. 2 0
      webview-ui/src/i18n/locales/hi/settings.json
  42. 2 0
      webview-ui/src/i18n/locales/id/settings.json
  43. 2 0
      webview-ui/src/i18n/locales/it/settings.json
  44. 2 0
      webview-ui/src/i18n/locales/ja/settings.json
  45. 2 0
      webview-ui/src/i18n/locales/ko/settings.json
  46. 2 0
      webview-ui/src/i18n/locales/nl/settings.json
  47. 2 0
      webview-ui/src/i18n/locales/pl/settings.json
  48. 2 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  49. 2 0
      webview-ui/src/i18n/locales/ru/settings.json
  50. 2 0
      webview-ui/src/i18n/locales/th/settings.json
  51. 2 0
      webview-ui/src/i18n/locales/tr/settings.json
  52. 2 0
      webview-ui/src/i18n/locales/uk/settings.json
  53. 2 0
      webview-ui/src/i18n/locales/vi/settings.json
  54. 2 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  55. 2 0
      webview-ui/src/i18n/locales/zh-TW/settings.json
  56. 4 1
      webview-ui/src/utils/__tests__/validate.test.ts
  57. 7 0
      webview-ui/src/utils/validate.ts

+ 5 - 0
.changeset/gentle-yaks-pay.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": minor
+---
+
+OVHcloud AI Endpoints provider added

+ 50 - 0
apps/kilocode-docs/docs/providers/ovhcloud.md

@@ -0,0 +1,50 @@
+---
+sidebar_label: OVHCloud AI Endpoints
+---
+
+# Using OVHCloud AI Endpoints with Kilo Code
+
+OVHCloud is a French leading Cloud provider in Europe with data sovereignty and privacy.
+
+Access world-renowned pre-trained AI models with ease. Innovate using straightforward, secure APIs on OVHcloud's robust and privacy-first infrastructure. Enhance your applications with scalable AI capabilities, eliminating the need for extensive expertise. Achieve more with powerful AI endpoints designed for simplicity et reliability.
+
+**Website:** [https://endpoints.ai.cloud.ovh.net](https://endpoints.ai.cloud.ovh.net)
+
+:::info
+
+You can report any bugs or feedbacks by chatting with us in our [Discord server](https://discord.gg/ovhcloud), in the AI Endpoints channel.
+
+:::
+
+## Getting an API Key
+
+1.  **Sign Up/Sign In:** Go to the [OVHCloud manager](https://www.ovh.com/manager). Create an account or sign in.
+2.  **Navigate to Public Cloud:** Go to the Public Cloud section, and create a new project. Navigate to AI Endpoints in the _AI & Machine Learning_ section.
+3.  **Create a Key:** Click to _API keys_ and create a new key.
+
+## Supported Models
+
+Kilo Code supports the following OVHCloud AI Endpoints models:
+
+- gpt-oss-120b
+- gpt-oss-20b
+- Qwen2.5-VL-72B-Instruct
+- llava-next-mistral-7b
+- Meta-Llama-3_3-70B-Instruct
+- Qwen2.5-Coder-32B-Instruct
+- Mixtral-8x7B-Instruct-v0.1
+- Meta-Llama-3_1-70B-Instruct
+- Mistral-Small-3.2-24B-Instruct-2506
+- DeepSeek-R1-Distill-Llama-70B
+- Llama-3.1-8B-Instruct
+- Mistral-7B-Instruct-v0.3
+- Mistral-Nemo-Instruct-2407
+- Qwen3-32B
+- mamba-codestral-7B-v0.1
+
+## Configuration in Kilo Code
+
+1.  **Open Kilo Code Settings:** Click the gear icon (<Codicon name="gear" />) in the Kilo Code panel.
+2.  **Select Provider:** Choose "OVHCloud AI Endpoints" from the "API Provider" dropdown.
+3.  **Enter API Key:** Paste your AI Endpoints API key into the "OVHCloud AI Endpoints API Key" field.
+4.  **Select Model:** Choose your desired model from the "Model" dropdown.

+ 3 - 3
apps/kilocode-docs/docs/providers/synthetic.md

@@ -29,6 +29,6 @@ Kilo Code supports all "always on" Synthetic AI models. The available models inc
 
 ## Tips and Notes
 
-* **Pricing Options:** Synthetic offers both subscriptions and pay-as-you-go usage-based [pricing](https://synthetic.new/pricing).
-* **Privacy:** Strong privacy policy with no training on user data and automatic deletion of API data within 14 days.
-* **OpenAI Compatibility:** Synthetic models work with any OpenAI-compatible tools and applications.
+- **Pricing Options:** Synthetic offers both subscriptions and pay-as-you-go usage-based [pricing](https://synthetic.new/pricing).
+- **Privacy:** Strong privacy policy with no training on user data and automatic deletion of API data within 14 days.
+- **OpenAI Compatibility:** Synthetic models work with any OpenAI-compatible tools and applications.

+ 50 - 0
apps/kilocode-docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/providers/ovhcloud.md

@@ -0,0 +1,50 @@
+---
+sidebar_label: OVHCloud AI Endpoints
+---
+
+# 在 Kilo Code 中使用 OVHCloud AI Endpoints
+
+OVHCloud 是欧洲领先的法国云服务提供商,保障数据主权与隐私。
+
+轻松访问世界知名的预训练 AI 模型。在 OVHcloud 稳健且以隐私为先的基础设施上,通过简单、安全的 API 创新。为您的应用增强可扩展的 AI 能力,无需深厚的专业知识。借助为简洁与可靠性而设计的强大 AI 接口,助您实现更多可能。
+
+**网站:** [https://endpoints.ai.cloud.ovh.net](https://endpoints.ai.cloud.ovh.net)
+
+:::info
+
+您可以在我们的 [Discord 服务器](https://discord.gg/ovhcloud) 上,通过 **AI Endpoints** 频道向我们反馈问题或建议。
+
+:::
+
+## 获取 API 密钥
+
+1.  **注册/登录:** 前往 [OVHCloud 管理器](https://www.ovh.com/manager)。创建账户或登录。
+2.  **进入公有云:** 进入“Public Cloud”部分并创建新项目。在 _AI & Machine Learning_ 部分进入 **AI Endpoints**。
+3.  **创建密钥:** 点击 _API keys_ 并创建新的密钥。
+
+## 支持的模型
+
+Kilo Code 支持以下 OVHCloud AI Endpoints 模型:
+
+- gpt-oss-120b
+- gpt-oss-20b
+- Qwen2.5-VL-72B-Instruct
+- llava-next-mistral-7b
+- Meta-Llama-3_3-70B-Instruct
+- Qwen2.5-Coder-32B-Instruct
+- Mixtral-8x7B-Instruct-v0.1
+- Meta-Llama-3_1-70B-Instruct
+- Mistral-Small-3.2-24B-Instruct-2506
+- DeepSeek-R1-Distill-Llama-70B
+- Llama-3.1-8B-Instruct
+- Mistral-7B-Instruct-v0.3
+- Mistral-Nemo-Instruct-2407
+- Qwen3-32B
+- mamba-codestral-7B-v0.1
+
+## 在 Kilo Code 中的配置
+
+1.  **打开 Kilo Code 设置:** 点击 Kilo Code 面板中的齿轮图标 (<Codicon name="gear" />)。
+2.  **选择提供商:** 在 “API Provider” 下拉菜单中选择 “OVHCloud AI Endpoints”。
+3.  **输入 API 密钥:** 将您的 AI Endpoints API 密钥粘贴到 “OVHCloud AI Endpoints API Key” 字段中。
+4.  **选择模型:** 在 “Model” 下拉菜单中选择所需的模型。

+ 1 - 0
apps/kilocode-docs/sidebars.ts

@@ -69,6 +69,7 @@ const sidebars: SidebarsConfig = {
 								"providers/openai",
 								"providers/openai-compatible",
 								"providers/openrouter",
+								"providers/ovhcloud", // kilocode_change
 								"providers/requesty",
 								"providers/unbound",
 								"providers/v0",

+ 1 - 0
packages/types/src/global-settings.ts

@@ -230,6 +230,7 @@ export const SECRET_STATE_KEYS = [
 	"featherlessApiKey",
 	"ioIntelligenceApiKey",
 	"vercelAiGatewayApiKey",
+	"ovhCloudAiEndpointsApiKey", // kilocode_change
 ] as const
 
 // Global secrets that are part of GlobalSettings (not ProviderSettings)

+ 14 - 1
packages/types/src/provider-settings.ts

@@ -45,13 +45,16 @@ export const dynamicProviders = [
 	"vercel-ai-gateway",
 	"huggingface",
 	"litellm",
+	// kilocode_change start
 	"kilocode-openrouter",
+	"ovhcloud",
+	"chutes",
+	// kilocode_change end
 	"deepinfra",
 	"io-intelligence",
 	"requesty",
 	"unbound",
 	"glama",
-	"chutes", // kilocode_change
 ] as const
 
 export type DynamicProvider = (typeof dynamicProviders)[number]
@@ -399,6 +402,11 @@ const sambaNovaSchema = apiModelIdProviderModelSchema.extend({
 })
 
 // kilocode_change start
+const ovhcloudSchema = baseProviderSettingsSchema.extend({
+	ovhCloudAiEndpointsApiKey: z.string().optional(),
+	ovhCloudAiEndpointsModelId: z.string().optional(),
+})
+
 const kilocodeSchema = baseProviderSettingsSchema.extend({
 	kilocodeToken: z.string().optional(),
 	kilocodeOrganizationId: z.string().optional(),
@@ -488,6 +496,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })),
 	geminiSchema.merge(z.object({ apiProvider: z.literal("gemini") })),
 	openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })),
+	ovhcloudSchema.merge(z.object({ apiProvider: z.literal("ovhcloud") })), // kilocode_change
 	mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })),
 	deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })),
 	deepInfraSchema.merge(z.object({ apiProvider: z.literal("deepinfra") })),
@@ -564,6 +573,7 @@ export const providerSettingsSchema = z.object({
 	...rooSchema.shape,
 	...vercelAiGatewaySchema.shape,
 	...codebaseIndexProviderSchema.shape,
+	...ovhcloudSchema.shape, // kilocode_change
 })
 
 export type ProviderSettings = z.infer<typeof providerSettingsSchema>
@@ -598,6 +608,7 @@ export const modelIdKeys = [
 	"vercelAiGatewayModelId",
 	"deepInfraModelId",
 	"kilocodeModel",
+	"ovhCloudAiEndpointsModelId", // kilocode_change
 ] as const satisfies readonly (keyof ProviderSettings)[]
 
 export type ModelIdKey = (typeof modelIdKeys)[number]
@@ -653,6 +664,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
 	"vercel-ai-gateway": "vercelAiGatewayModelId",
 	kilocode: "kilocodeModel",
 	"virtual-quota-fallback": "apiModelId",
+	ovhcloud: "ovhCloudAiEndpointsModelId", // kilocode_change
 }
 
 /**
@@ -784,6 +796,7 @@ export const MODELS_BY_PROVIDER: Record<
 	openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
 	requesty: { id: "requesty", label: "Requesty", models: [] },
 	unbound: { id: "unbound", label: "Unbound", models: [] },
+	ovhcloud: { id: "ovhcloud", label: "OVHcloud AI Endpoints", models: [] }, // kilocode_change
 
 	// kilocode_change start
 	kilocode: { id: "kilocode", label: "Kilocode", models: [] },

+ 2 - 1
packages/types/src/providers/index.ts

@@ -7,10 +7,11 @@ export * from "./deepseek.js"
 export * from "./doubao.js"
 export * from "./featherless.js"
 export * from "./fireworks.js"
-export * from "./synthetic.js" // kilocode_change
 export * from "./gemini.js"
 // kilocode_change start
 export * from "./gemini-cli.js"
+export * from "./ovhcloud.js"
+export * from "./synthetic.js"
 // kilocode_change end
 export * from "./glama.js"
 export * from "./groq.js"

+ 17 - 0
packages/types/src/providers/ovhcloud.ts

@@ -0,0 +1,17 @@
+// kilocode_change - file added
+import type { ModelInfo } from "../model.js"
+
+// https://endpoints.ai.cloud.ovh.net/docs
+export const ovhCloudAiEndpointsDefaultModelId = "gpt-oss-120b"
+
+export const ovhCloudAiEndpointsDefaultModelInfo: ModelInfo = {
+	maxTokens: 131000,
+	contextWindow: 131000,
+	supportsImages: false,
+	supportsPromptCache: false,
+	inputPrice: 0.08,
+	outputPrice: 0.4,
+	description:
+		"gpt-oss-120b is a cutting-edge model designed for high-level reasoning, instruction-following, and advanced agent capabilities.",
+	supportsReasoningEffort: true,
+}

+ 5 - 0
src/api/index.ts

@@ -45,6 +45,7 @@ import {
 	FeatherlessHandler,
 	VercelAiGatewayHandler,
 	DeepInfraHandler,
+	OVHCloudAIEndpointsHandler, // kilocode_change
 } from "./providers"
 // kilocode_change start
 import { KilocodeOpenrouterHandler } from "./providers/kilocode-openrouter"
@@ -187,6 +188,10 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new FeatherlessHandler(options)
 		case "vercel-ai-gateway":
 			return new VercelAiGatewayHandler(options)
+		// kilocode_change start
+		case "ovhcloud":
+			return new OVHCloudAIEndpointsHandler(options)
+		// kilocode_change end
 		default:
 			apiProvider satisfies "gemini-cli" | undefined
 			return new AnthropicHandler(options)

+ 223 - 0
src/api/providers/__tests__/ovhcloud.spec.ts

@@ -0,0 +1,223 @@
+// kilocode_change - file added
+// npx vitest run src/api/providers/__tests__/ovhcloud.spec.ts
+
+// Mock vscode first to avoid import errors
+vitest.mock("vscode", () => ({}))
+
+import OpenAI from "openai"
+import { Anthropic } from "@anthropic-ai/sdk"
+
+import { OVHCloudAIEndpointsHandler } from "../ovhcloud"
+import { ovhCloudAiEndpointsDefaultModelId } from "@roo-code/types"
+import { calculateApiCostOpenAI } from "../../../shared/cost"
+
+vitest.mock("openai", () => {
+	const createMock = vitest.fn()
+	return {
+		default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })),
+	}
+})
+
+const ovhCloudAiEndpointsApiKey = "test-ovhcloud-ai-endpoints-api-key"
+
+describe("OVHCloudAIEndpointsHandler", () => {
+	let handler: OVHCloudAIEndpointsHandler
+	let mockCreate: any
+
+	beforeEach(() => {
+		vitest.clearAllMocks()
+		mockCreate = (OpenAI as unknown as any)().chat.completions.create
+		handler = new OVHCloudAIEndpointsHandler({ ovhCloudAiEndpointsApiKey })
+	})
+
+	describe("Initialization", () => {
+		it("should configure OpenAI client with correct OVHCloud AI Endpoints base URL", () => {
+			new OVHCloudAIEndpointsHandler({ ovhCloudAiEndpointsApiKey })
+			expect(OpenAI).toHaveBeenCalledWith(
+				expect.objectContaining({
+					baseURL: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1",
+				}),
+			)
+		})
+
+		it("should initialize with the provided API key", () => {
+			new OVHCloudAIEndpointsHandler({ ovhCloudAiEndpointsApiKey })
+			expect(OpenAI).toHaveBeenCalledWith(
+				expect.objectContaining({
+					apiKey: ovhCloudAiEndpointsApiKey,
+				}),
+			)
+		})
+
+		it("should accept custom model configuration during initialization", () => {
+			const customModelId = "gpt-oss-120b"
+			const customHandler = new OVHCloudAIEndpointsHandler({
+				apiModelId: customModelId,
+				ovhCloudAiEndpointsApiKey,
+			})
+			expect(customHandler).toBeDefined()
+		})
+	})
+
+	describe("Model Management", () => {
+		it("should retrieve default model configuration when none specified", () => {
+			const model = handler.getModel()
+			expect(model.id).toBe(ovhCloudAiEndpointsDefaultModelId)
+		})
+
+		it("should retrieve specific model configuration when provided", () => {
+			const targetModelId = "gpt-oss-120b"
+			const handlerWithModel = new OVHCloudAIEndpointsHandler({
+				apiModelId: targetModelId,
+				ovhCloudAiEndpointsApiKey,
+			})
+			const model = handlerWithModel.getModel()
+			expect(model.id).toBe(targetModelId)
+		})
+
+		it("should provide access to model metadata", () => {
+			const model = handler.getModel()
+			expect(model.info).toBeDefined()
+			expect(model.info.maxTokens).toBeDefined()
+		})
+	})
+
+	describe("Prompt Completion", () => {
+		it("should successfully complete a prompt via OVHCloud API", async () => {
+			const mockResponse = "Generated response from OVHCloud AI Endpoints"
+			mockCreate.mockResolvedValueOnce({
+				choices: [{ message: { content: mockResponse } }],
+			})
+
+			const result = await handler.completePrompt("test prompt")
+			expect(result).toBe(mockResponse)
+		})
+
+		it("should handle API errors gracefully during completion", async () => {
+			const errorMessage = "OVHCloud AI Endpoints API error"
+			mockCreate.mockRejectedValueOnce(new Error(errorMessage))
+
+			await expect(handler.completePrompt("test prompt")).rejects.toThrow(
+				`OVHCloud AI Endpoints completion error: ${errorMessage}`,
+			)
+		})
+
+		it("should pass correct parameters for prompt completion", async () => {
+			mockCreate.mockResolvedValueOnce({
+				choices: [{ message: { content: "response" } }],
+			})
+
+			await handler.completePrompt("test prompt")
+
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					model: ovhCloudAiEndpointsDefaultModelId,
+					messages: expect.arrayContaining([{ role: "user", content: "test prompt" }]),
+				}),
+			)
+		})
+	})
+
+	describe("Streaming Message Creation", () => {
+		it("should stream text content from OVHCloud AI response", async () => {
+			const streamContent = "Streaming response from OVHCloud AI Endpoints"
+
+			mockCreate.mockImplementationOnce(() => {
+				return {
+					[Symbol.asyncIterator]: () => ({
+						next: vitest
+							.fn()
+							.mockResolvedValueOnce({
+								done: false,
+								value: { choices: [{ delta: { content: streamContent } }] },
+							})
+							.mockResolvedValueOnce({ done: true }),
+					}),
+				}
+			})
+
+			const messageStream = handler.createMessage("system prompt", [])
+			const chunk = await messageStream.next()
+
+			expect(chunk.done).toBe(false)
+			expect(chunk.value).toEqual({ type: "text", text: streamContent })
+		})
+
+		it("should provide token usage information from stream", async () => {
+			mockCreate.mockImplementationOnce(() => {
+				return {
+					[Symbol.asyncIterator]: () => ({
+						next: vitest
+							.fn()
+							.mockResolvedValueOnce({
+								done: false,
+								value: {
+									choices: [{ delta: {} }],
+									usage: { prompt_tokens: 15, completion_tokens: 25 },
+								},
+							})
+							.mockResolvedValueOnce({ done: true }),
+					}),
+				}
+			})
+
+			const messageStream = handler.createMessage("system prompt", [])
+			const chunk = await messageStream.next()
+			const info = handler.getModel().info
+
+			expect(chunk.done).toBe(false)
+			expect(chunk.value).toEqual({
+				type: "usage",
+				inputTokens: 15,
+				outputTokens: 25,
+				totalCost: calculateApiCostOpenAI(info, 15, 25),
+			})
+		})
+
+		it("should configure streaming with appropriate OVHCloud parameters", async () => {
+			const modelId = "gpt-oss-120b"
+			const modelInfo = handler.getModel().info
+			const customHandler = new OVHCloudAIEndpointsHandler({
+				apiModelId: modelId,
+				ovhCloudAiEndpointsApiKey,
+			})
+
+			mockCreate.mockImplementationOnce(() => {
+				return {
+					[Symbol.asyncIterator]: () => ({
+						async next() {
+							return { done: true }
+						},
+					}),
+				}
+			})
+
+			const systemPrompt = "System configuration for OVHCloud AI Endpoints"
+			const userMessages: Anthropic.Messages.MessageParam[] = [
+				{ role: "user", content: "User query for OVHCloud AI Endpoints processing" },
+			]
+
+			const streamGenerator = customHandler.createMessage(systemPrompt, userMessages)
+			await streamGenerator.next()
+
+			expect(mockCreate).toHaveBeenCalledWith({
+				model: modelId,
+				max_tokens: modelInfo.maxTokens,
+				messages: [{ role: "system", content: systemPrompt }, userMessages[0]],
+				stream: true,
+				stream_options: { include_usage: true },
+			})
+		})
+	})
+
+	describe("Error Handling", () => {
+		it("should handle network errors during streaming", async () => {
+			const networkError = new Error("Network connection failed")
+			mockCreate.mockRejectedValueOnce(networkError)
+
+			const messageStream = handler.createMessage("system prompt", [])
+
+			await expect(messageStream.next()).rejects.toThrow("Network connection failed")
+		})
+	})
+})

+ 22 - 0
src/api/providers/fetchers/__tests__/modelCache.spec.ts

@@ -25,6 +25,7 @@ vi.mock("../requesty")
 vi.mock("../glama")
 vi.mock("../unbound")
 vi.mock("../io-intelligence")
+vi.mock("../ovhcloud") // kilocode_change
 
 // Then imports
 import type { Mock } from "vitest"
@@ -35,6 +36,7 @@ import { getRequestyModels } from "../requesty"
 import { getGlamaModels } from "../glama"
 import { getUnboundModels } from "../unbound"
 import { getIOIntelligenceModels } from "../io-intelligence"
+import { getOvhCloudAiEndpointsModels } from "../ovhcloud" // kilocode_change
 
 const mockGetLiteLLMModels = getLiteLLMModels as Mock<typeof getLiteLLMModels>
 const mockGetOpenRouterModels = getOpenRouterModels as Mock<typeof getOpenRouterModels>
@@ -42,6 +44,7 @@ const mockGetRequestyModels = getRequestyModels as Mock<typeof getRequestyModels
 const mockGetGlamaModels = getGlamaModels as Mock<typeof getGlamaModels>
 const mockGetUnboundModels = getUnboundModels as Mock<typeof getUnboundModels>
 const mockGetIOIntelligenceModels = getIOIntelligenceModels as Mock<typeof getIOIntelligenceModels>
+const mockGetOvhCloudAiEndpointsModels = getOvhCloudAiEndpointsModels as Mock<typeof getOvhCloudAiEndpointsModels> // kilocode_change
 
 const DUMMY_REQUESTY_KEY = "requesty-key-for-testing"
 const DUMMY_UNBOUND_KEY = "unbound-key-for-testing"
@@ -158,6 +161,25 @@ describe("getModels with new GetModelsOptions", () => {
 		expect(result).toEqual(mockModels)
 	})
 
+	// kilocode_change start
+	it("calls OvhCloudAiEndpointsModels for ovhcloud provider", async () => {
+		const mockModels = {
+			"ovhcloud/model": {
+				maxTokens: 4096,
+				contextWindow: 8192,
+				supportsPromptCache: false,
+				description: "OVHCloud AI Endpoints Model",
+			},
+		}
+		mockGetOvhCloudAiEndpointsModels.mockResolvedValue(mockModels)
+
+		const result = await getModels({ provider: "ovhcloud" })
+
+		expect(mockGetOvhCloudAiEndpointsModels).toHaveBeenCalled()
+		expect(result).toEqual(mockModels)
+	})
+	// kilocode_change end
+
 	it("handles errors and re-throws them", async () => {
 		const expectedError = new Error("LiteLLM connection failed")
 		mockGetLiteLLMModels.mockRejectedValue(expectedError)

+ 9 - 1
src/api/providers/fetchers/modelCache.ts

@@ -23,7 +23,10 @@ import { getKiloBaseUriFromToken } from "../../../shared/kilocode/token"
 import { getOllamaModels } from "./ollama"
 import { getLMStudioModels } from "./lmstudio"
 import { getIOIntelligenceModels } from "./io-intelligence"
-import { getChutesModels } from "./chutes" // kilocode_change
+// kilocode_change start
+import { getOvhCloudAiEndpointsModels } from "./ovhcloud"
+import { getChutesModels } from "./chutes"
+// kilocode_change end
 
 import { getDeepInfraModels } from "./deepinfra"
 import { getHuggingFaceModels } from "./huggingface"
@@ -122,6 +125,11 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 			case "huggingface":
 				models = await getHuggingFaceModels()
 				break
+			// kilocode_change start
+			case "ovhcloud":
+				models = await getOvhCloudAiEndpointsModels()
+				break
+			// kilocode_change end
 			default: {
 				// Ensures router is exhaustively checked if RouterName is a strict union.
 				const exhaustiveCheck: never = provider

+ 31 - 0
src/api/providers/fetchers/ovhcloud.ts

@@ -0,0 +1,31 @@
+// kilocode_change - file added
+import axios from "axios"
+
+import type { ModelInfo } from "@roo-code/types"
+
+import { parseApiPrice } from "../../../shared/cost"
+
+export async function getOvhCloudAiEndpointsModels(): Promise<Record<string, ModelInfo>> {
+	const models: Record<string, ModelInfo> = {}
+
+	try {
+		const response = await axios.get("https://catalog.endpoints.ai.ovh.net/rest/v2/openrouter")
+		for (const model of response.data.data) {
+			models[model.name] = {
+				maxTokens: model.max_output_length,
+				contextWindow: model.context_length,
+				supportsImages: model.input_modalities.includes("image"),
+				supportsPromptCache: model.input_modalities.includes("cache"),
+				inputPrice: parseApiPrice(model.pricing.prompt),
+				outputPrice: parseApiPrice(model.pricing.completion),
+				description: model.description,
+			} satisfies ModelInfo
+		}
+	} catch (error) {
+		console.error(
+			`Error fetching OVHCloud AI Endpoints models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+		)
+	}
+
+	return models
+}

+ 1 - 0
src/api/providers/index.ts

@@ -21,6 +21,7 @@ export { OllamaHandler } from "./ollama"
 export { OpenAiNativeHandler } from "./openai-native"
 export { OpenAiHandler } from "./openai"
 export { OpenRouterHandler } from "./openrouter"
+export { OVHCloudAIEndpointsHandler } from "./ovhcloud" // kilocode_change
 export { QwenCodeHandler } from "./qwen-code"
 export { RequestyHandler } from "./requesty"
 export { SambaNovaHandler } from "./sambanova"

+ 92 - 0
src/api/providers/ovhcloud.ts

@@ -0,0 +1,92 @@
+// kilocode_change - file added
+import { ovhCloudAiEndpointsDefaultModelId, ovhCloudAiEndpointsDefaultModelInfo } from "@roo-code/types"
+import type { ApiHandlerOptions } from "../../shared/api"
+
+import { RouterProvider } from "./router-provider"
+import { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from ".."
+import OpenAI from "openai"
+import Anthropic from "@anthropic-ai/sdk"
+import { ApiStream } from "../transform/stream"
+import { convertToOpenAiMessages } from "../transform/openai-format"
+import { calculateApiCostOpenAI } from "../../shared/cost"
+
+export class OVHCloudAIEndpointsHandler extends RouterProvider implements SingleCompletionHandler {
+	constructor(options: ApiHandlerOptions) {
+		super({
+			options,
+			name: "ovhcloud",
+			baseURL: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1",
+			apiKey: options.ovhCloudAiEndpointsApiKey,
+			modelId: options.ovhCloudAiEndpointsModelId,
+			defaultModelId: ovhCloudAiEndpointsDefaultModelId,
+			defaultModelInfo: ovhCloudAiEndpointsDefaultModelInfo,
+		})
+	}
+
+	override async *createMessage(
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		_metadata?: ApiHandlerCreateMessageMetadata,
+	): ApiStream {
+		const { id: modelId, info } = await this.fetchModel()
+
+		const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
+			{ role: "system", content: systemPrompt },
+			...convertToOpenAiMessages(messages),
+		]
+
+		const body: OpenAI.Chat.ChatCompletionCreateParams = {
+			model: modelId,
+			messages: openAiMessages,
+			max_tokens: info.maxTokens,
+			stream: true,
+			stream_options: { include_usage: true },
+		}
+
+		const completion = await this.client.chat.completions.create(body)
+
+		for await (const chunk of completion) {
+			const delta = chunk.choices[0]?.delta
+			if (delta?.content) {
+				yield {
+					type: "text",
+					text: delta.content,
+				}
+			}
+
+			if (chunk.usage) {
+				const usage = chunk.usage as OpenAI.CompletionUsage
+				yield {
+					type: "usage",
+					inputTokens: usage.prompt_tokens || 0,
+					outputTokens: usage.completion_tokens || 0,
+					totalCost: calculateApiCostOpenAI(info, usage.prompt_tokens || 0, usage.completion_tokens || 0),
+				}
+			}
+		}
+	}
+
+	async completePrompt(prompt: string): Promise<string> {
+		const { id: modelId } = await this.fetchModel()
+
+		try {
+			const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
+				model: modelId,
+				messages: [{ role: "user", content: prompt }],
+			}
+
+			if (this.supportsTemperature(modelId)) {
+				requestOptions.temperature = this.options.modelTemperature ?? 0.7
+			}
+
+			const response = await this.client.chat.completions.create(requestOptions)
+			return response.choices[0]?.message.content || ""
+		} catch (error) {
+			if (error instanceof Error) {
+				throw new Error(`OVHCloud AI Endpoints completion error: ${error.message}`)
+			}
+
+			throw error
+		}
+	}
+}

+ 15 - 2
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -2704,6 +2704,7 @@ describe("ClineProvider - Router Models", () => {
 				chutesApiKey: "chutes-key", // kilocode_change
 				litellmApiKey: "litellm-key",
 				litellmBaseUrl: "http://localhost:4000",
+				ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change
 			},
 		} as any)
 
@@ -2734,6 +2735,7 @@ describe("ClineProvider - Router Models", () => {
 		expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
 		expect(getModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) // kilocode_change
 		expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
+		expect(getModels).toHaveBeenCalledWith({ provider: "ovhcloud", apiKey: "ovhcloud-key" }) // kilocode_change
 		expect(getModels).toHaveBeenCalledWith({
 			provider: "litellm",
 			apiKey: "litellm-key",
@@ -2755,6 +2757,7 @@ describe("ClineProvider - Router Models", () => {
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
+				ovhcloud: mockModels, // kilocode_change
 				huggingface: {},
 				"io-intelligence": {},
 			},
@@ -2774,6 +2777,7 @@ describe("ClineProvider - Router Models", () => {
 				chutesApiKey: "chutes-key", // kilocode_change
 				litellmApiKey: "litellm-key",
 				litellmBaseUrl: "http://localhost:4000",
+				ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change
 			},
 		} as any)
 
@@ -2792,6 +2796,7 @@ describe("ClineProvider - Router Models", () => {
 			.mockRejectedValueOnce(new Error("Kilocode-OpenRouter API error")) // kilocode-openrouter fail
 			.mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change
 			.mockResolvedValueOnce(mockModels) // vercel-ai-gateway success
+			.mockResolvedValueOnce(mockModels) // kilocode_change: ovhcloud
 			.mockResolvedValueOnce(mockModels) // deepinfra success
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
 
@@ -2812,6 +2817,7 @@ describe("ClineProvider - Router Models", () => {
 				litellm: {},
 				"kilocode-openrouter": {},
 				"vercel-ai-gateway": mockModels,
+				ovhcloud: mockModels, // kilocode_change
 				huggingface: {},
 				"io-intelligence": {},
 			},
@@ -2874,7 +2880,10 @@ describe("ClineProvider - Router Models", () => {
 				requestyApiKey: "requesty-key",
 				glamaApiKey: "glama-key",
 				unboundApiKey: "unbound-key",
-				chutesApiKey: "chutes-key", // kilocode_change
+				// kilocode_change start
+				ovhCloudAiEndpointsApiKey: "ovhcloud-key",
+				chutesApiKey: "chutes-key",
+				// kilocode_change end
 				// No litellm config
 			},
 		} as any)
@@ -2911,7 +2920,10 @@ describe("ClineProvider - Router Models", () => {
 				requestyApiKey: "requesty-key",
 				glamaApiKey: "glama-key",
 				unboundApiKey: "unbound-key",
-				chutesApiKey: "chutes-key", // kilocode_change
+				// kilocode_change start
+				ovhCloudAiEndpointsApiKey: "ovhcloud-key",
+				chutesApiKey: "chutes-key",
+				// kilocode_change end
 				// No litellm config
 			},
 		} as any)
@@ -2946,6 +2958,7 @@ describe("ClineProvider - Router Models", () => {
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
+				ovhcloud: mockModels, // kilocode_change
 				huggingface: {},
 				"io-intelligence": {},
 			},

+ 22 - 3
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -193,6 +193,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				chutesApiKey: "chutes-key", // kilocode_change
 				litellmApiKey: "litellm-key",
 				litellmBaseUrl: "http://localhost:4000",
+				ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change
 			},
 		})
 	})
@@ -252,6 +253,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				"vercel-ai-gateway": mockModels,
 				huggingface: {},
 				"io-intelligence": {},
+				ovhcloud: mockModels, // kilocode_change
 			},
 		})
 	})
@@ -263,6 +265,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				requestyApiKey: "requesty-key",
 				glamaApiKey: "glama-key",
 				unboundApiKey: "unbound-key",
+				ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change
 				// Missing litellm config
 			},
 		})
@@ -301,7 +304,10 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				requestyApiKey: "requesty-key",
 				glamaApiKey: "glama-key",
 				unboundApiKey: "unbound-key",
-				chutesApiKey: "chutes-key", // kilocode_change
+				// kilocode_change start
+				ovhCloudAiEndpointsApiKey: "ovhcloud-key",
+				chutesApiKey: "chutes-key",
+				// kilocode_change end
 				// Missing litellm config
 			},
 		})
@@ -346,6 +352,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				"vercel-ai-gateway": mockModels,
 				huggingface: {},
 				"io-intelligence": {},
+				ovhcloud: mockModels, // kilocode_change
 			},
 		})
 	})
@@ -368,9 +375,10 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
 			.mockRejectedValueOnce(new Error("Chutes API error")) // chutes // kilocode_change
 			.mockResolvedValueOnce(mockModels) // kilocode-openrouter
-			.mockRejectedValueOnce(new Error("Ollama API error")) // ollama
+			.mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change
 			.mockResolvedValueOnce(mockModels) // vercel-ai-gateway
 			.mockResolvedValueOnce(mockModels) // deepinfra
+			.mockResolvedValueOnce(mockModels) // kilocode_change ovhcloud
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {
@@ -390,6 +398,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				litellm: {},
 				"kilocode-openrouter": mockModels,
 				ollama: {},
+				ovhcloud: mockModels, // kilocode_change
 				lmstudio: {},
 				"vercel-ai-gateway": mockModels,
 				huggingface: {},
@@ -438,9 +447,10 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
 			.mockRejectedValueOnce(new Error("Chutes API error")) // chutes // kilocode_change
 			.mockResolvedValueOnce({}) // kilocode-openrouter - Success
-			.mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change
+			.mockRejectedValueOnce(new Error("Ollama API error")) // ollama
 			.mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway
 			.mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra
+			.mockRejectedValueOnce(new Error("OVHCloud AI Endpoints error")) // ovhcloud // kilocode_change
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {
@@ -498,6 +508,15 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			error: "LiteLLM connection failed",
 			values: { provider: "litellm" },
 		})
+
+		// kilocode_change start
+		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
+			type: "singleRouterModelFetchResponse",
+			success: false,
+			error: "OVHCloud AI Endpoints error",
+			values: { provider: "ovhcloud" },
+		})
+		// kilocode_change end
 	})
 
 	it("prefers config values over message values for LiteLLM", async () => {

+ 7 - 0
src/core/webview/webviewMessageHandler.ts

@@ -803,6 +803,7 @@ export const webviewMessageHandler = async (
 				chutes: {}, // kilocode_change
 				ollama: {},
 				lmstudio: {},
+				ovhcloud: {}, // kilocode_change
 			}
 
 			const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
@@ -856,6 +857,12 @@ export const webviewMessageHandler = async (
 						baseUrl: apiConfiguration.deepInfraBaseUrl,
 					},
 				},
+				// kilocode_change start
+				{
+					key: "ovhcloud",
+					options: { provider: "ovhcloud", apiKey: apiConfiguration.ovhCloudAiEndpointsApiKey },
+				},
+				// kilocode_change end
 			]
 			// kilocode_change end
 

+ 4 - 0
src/shared/ProfileValidator.ts

@@ -94,6 +94,10 @@ export class ProfileValidator {
 				return profile.ioIntelligenceModelId
 			case "deepinfra":
 				return profile.deepInfraModelId
+			// kilocode_change start
+			case "ovhcloud":
+				return profile.ovhCloudAiEndpointsModelId
+			// kilocode_change end
 			case "human-relay":
 			case "fake-ai":
 			default:

+ 17 - 0
src/shared/__tests__/ProfileValidator.spec.ts

@@ -337,6 +337,23 @@ describe("ProfileValidator", () => {
 			expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true)
 		})
 
+		// kilocode_change start
+		it("should extract ovhCloudAiEndpointsModelId for ovhcloud provider", () => {
+			const allowList: OrganizationAllowList = {
+				allowAll: false,
+				providers: {
+					ovhcloud: { allowAll: false, models: ["ovhcloud-model"] },
+				},
+			}
+			const profile: ProviderSettings = {
+				apiProvider: "ovhcloud",
+				ovhCloudAiEndpointsModelId: "ovhcloud-model",
+			}
+
+			expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true)
+		})
+		// kilocode_change end
+
 		it("should handle providers with undefined models list gracefully", () => {
 			const allowList: OrganizationAllowList = {
 				allowAll: false,

+ 1 - 0
src/shared/api.ts

@@ -165,6 +165,7 @@ const dynamicProviderExtras = {
 	glama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
 	ollama: {} as { numCtx?: number }, // kilocode_change
 	lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
+	ovhcloud: {} as { apiKey?: string }, // kilocode_change
 	chutes: {} as { apiKey?: string }, // kilocode_change
 } as const satisfies Record<RouterName, object>
 

+ 4 - 1
webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts

@@ -27,7 +27,10 @@ describe("getModelsByProvider", () => {
 			deepinfra: { "test-model": testModel },
 			"vercel-ai-gateway": { "test-model": testModel },
 			huggingface: { "test-model": testModel },
-			chutes: { "test-model": testModel }, // kilocode_change
+			// kilocode_change start
+			ovhcloud: { "test-model": testModel },
+			chutes: { "test-model": testModel },
+			// kilocode_change end
 		}
 
 		const exceptions = [

+ 9 - 0
webview-ui/src/components/kilocode/hooks/useProviderModels.ts

@@ -49,6 +49,7 @@ import {
 	deepInfraDefaultModelId,
 	cerebrasModels,
 	cerebrasDefaultModelId,
+	ovhCloudAiEndpointsDefaultModelId, // kilocode_change
 } from "@roo-code/types"
 import type { ModelRecord, RouterModels } from "@roo/api"
 import { useRouterModels } from "../../ui/hooks/useRouterModels"
@@ -271,6 +272,14 @@ export const getModelsByProvider = ({
 				defaultModel: deepInfraDefaultModelId,
 			}
 		}
+		// kilocode_change start
+		case "ovhcloud": {
+			return {
+				models: routerModels.ovhcloud,
+				defaultModel: ovhCloudAiEndpointsDefaultModelId,
+			}
+		}
+		// kilocode_change end
 		default:
 			return {
 				models: {},

+ 6 - 0
webview-ui/src/components/kilocode/hooks/useSelectedModel.ts

@@ -16,6 +16,7 @@ export const getModelIdKey = ({
 	| "ollamaModelId"
 	| "lmStudioModelId"
 	| "vsCodeLmModelSelector"
+	| "ovhCloudAiEndpointsModelId" // kilocode_change
 	| "apiModelId"
 > => {
 	switch (provider) {
@@ -49,6 +50,11 @@ export const getModelIdKey = ({
 		case "kilocode": {
 			return "kilocodeModel"
 		}
+		// kilocode_change start
+		case "ovhcloud": {
+			return "ovhCloudAiEndpointsModelId"
+		}
+		// kilocode_change end
 		default: {
 			return "apiModelId"
 		}

+ 2 - 7
webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx

@@ -25,12 +25,8 @@ export const GhostServiceSettingsView = ({
 	...props
 }: GhostServiceSettingsViewProps) => {
 	const { t } = useAppTranslation()
-	const {
-		enableAutoTrigger,
-		autoTriggerDelay,
-		enableQuickInlineTaskKeybinding,
-		enableSmartInlineTaskKeybinding,
-	} = ghostServiceSettings || {}
+	const { enableAutoTrigger, autoTriggerDelay, enableQuickInlineTaskKeybinding, enableSmartInlineTaskKeybinding } =
+		ghostServiceSettings || {}
 	const keybindings = useKeybindings(["kilo-code.ghost.promptCodeSuggestion", "kilo-code.ghost.generateSuggestions"])
 
 	const onEnableAutoTriggerChange = (newValue: boolean) => {
@@ -169,7 +165,6 @@ export const GhostServiceSettingsView = ({
 						</div>
 					</div>
 				</div>
-
 			</Section>
 		</div>
 	)

+ 0 - 2
webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx

@@ -16,7 +16,6 @@ vi.mock("react-i18next", () => ({
 	},
 }))
 
-
 vi.mock("@/i18n/TranslationContext", () => ({
 	useAppTranslation: () => ({
 		t: (key: string) => key,
@@ -118,7 +117,6 @@ describe("GhostServiceSettingsView", () => {
 		expect(screen.getByText(/kilocode:ghost.settings.enableAutoTrigger.label/)).toBeInTheDocument()
 	})
 
-
 	it("toggles auto trigger checkbox correctly", () => {
 		const setCachedStateField = vi.fn()
 		renderComponent({ setCachedStateField })

+ 15 - 0
webview-ui/src/components/settings/ApiOptions.tsx

@@ -39,6 +39,7 @@ import {
 	rooDefaultModelId,
 	vercelAiGatewayDefaultModelId,
 	deepInfraDefaultModelId,
+	ovhCloudAiEndpointsDefaultModelId, // kilocode_change
 } from "@roo-code/types"
 
 import { vscode } from "@src/utils/vscode"
@@ -104,6 +105,7 @@ import {
 	Featherless,
 	VercelAiGateway,
 	DeepInfra,
+	OvhCloudAiEndpoints, // kilocode_change
 } from "./providers"
 
 import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants"
@@ -381,6 +383,7 @@ const ApiOptions = ({
 				fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
 				synthetic: { field: "apiModelId", default: syntheticDefaultModelId }, // kilocode_change
 				featherless: { field: "apiModelId", default: featherlessDefaultModelId },
+				ovhcloud: { field: "ovhCloudAiEndpointsModelId", default: ovhCloudAiEndpointsDefaultModelId }, // kilocode_change
 				"io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId },
 				roo: { field: "apiModelId", default: rooDefaultModelId },
 				"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
@@ -568,6 +571,18 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{/* kilocode_change start */}
+			{selectedProvider === "ovhcloud" && (
+				<OvhCloudAiEndpoints
+					apiConfiguration={apiConfiguration}
+					setApiConfigurationField={setApiConfigurationField}
+					routerModels={routerModels}
+					organizationAllowList={organizationAllowList}
+					modelValidationError={modelValidationError}
+				/>
+			)}
+			{/* kilocode_change end */}
+
 			{selectedProvider === "mistral" && (
 				<Mistral apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}

+ 1 - 0
webview-ui/src/components/settings/ModelPicker.tsx

@@ -44,6 +44,7 @@ type ModelIdKey = keyof Pick<
 	| "deepInfraModelId"
 	| "ioIntelligenceModelId"
 	| "vercelAiGatewayModelId"
+	| "ovhCloudAiEndpointsModelId" // kilocode_change
 >
 
 interface ModelPickerProps {

+ 1 - 0
webview-ui/src/components/settings/constants.ts

@@ -78,6 +78,7 @@ export const PROVIDERS = [
 	{ value: "mistral", label: "Mistral" },
 	{ value: "lmstudio", label: "LM Studio" },
 	{ value: "ollama", label: "Ollama" },
+	{ value: "ovhcloud", label: "OVHcloud AI Endpoints" }, // kilocode_change
 	{ value: "unbound", label: "Unbound" },
 	{ value: "requesty", label: "Requesty" },
 	{ value: "human-relay", label: "Human Relay" },

+ 72 - 0
webview-ui/src/components/settings/providers/OvhCloud.tsx

@@ -0,0 +1,72 @@
+import { useCallback } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { OrganizationAllowList, ovhCloudAiEndpointsDefaultModelId, type ProviderSettings } from "@roo-code/types"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+import { ModelPicker } from "../ModelPicker"
+import { RouterModels } from "@roo/api"
+
+type OvhCloudAiEndpointsProps = {
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
+	routerModels?: RouterModels
+	organizationAllowList: OrganizationAllowList
+	modelValidationError?: string
+}
+
+export const OvhCloudAiEndpoints = ({
+	apiConfiguration,
+	setApiConfigurationField,
+	routerModels,
+	organizationAllowList,
+	modelValidationError,
+}: OvhCloudAiEndpointsProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ProviderSettings, E>(
+			field: K,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.ovhCloudAiEndpointsApiKey || ""}
+				type="password"
+				onInput={handleInputChange("ovhCloudAiEndpointsApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.ovhCloudAiEndpointsApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.ovhCloudAiEndpointsApiKey && (
+				<VSCodeButtonLink href="https://www.ovh.com/manager" appearance="secondary">
+					{t("settings:providers.getOvhCloudAiEndpointsApiKey")}
+				</VSCodeButtonLink>
+			)}
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				setApiConfigurationField={setApiConfigurationField}
+				defaultModelId={ovhCloudAiEndpointsDefaultModelId}
+				models={routerModels?.ovhcloud ?? {}}
+				modelIdKey="ovhCloudAiEndpointsModelId"
+				serviceName="OVHCloud AI Endpoints"
+				serviceUrl="https://endpoints.ai.cloud.ovh.net/catalog"
+				organizationAllowList={organizationAllowList}
+				errorMessage={modelValidationError}
+			/>
+		</>
+	)
+}

+ 1 - 0
webview-ui/src/components/settings/providers/index.ts

@@ -17,6 +17,7 @@ export { Ollama } from "./Ollama"
 export { OpenAI } from "./OpenAI"
 export { OpenAICompatible } from "./OpenAICompatible"
 export { OpenRouter } from "./OpenRouter"
+export { OvhCloudAiEndpoints } from "./OvhCloud" // kilocode_change
 export { QwenCode } from "./QwenCode"
 export { Requesty } from "./Requesty"
 export { SambaNova } from "./SambaNova"

+ 8 - 0
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -63,6 +63,7 @@ import {
 	vercelAiGatewayDefaultModelId,
 	BEDROCK_1M_CONTEXT_MODEL_IDS,
 	deepInfraDefaultModelId,
+	ovhCloudAiEndpointsDefaultModelId, // kilocode_change
 } from "@roo-code/types"
 
 import type { ModelRecord, RouterModels } from "@roo/api"
@@ -444,6 +445,13 @@ function getSelectedModel({
 			const info = routerModels["vercel-ai-gateway"]?.[id]
 			return { id, info }
 		}
+		// kilocode_change start
+		case "ovhcloud": {
+			const id = apiConfiguration.ovhCloudAiEndpointsModelId ?? ovhCloudAiEndpointsDefaultModelId
+			const info = routerModels.ovhcloud[id]
+			return { id, info }
+		}
+		// kilocode_change end
 		// case "anthropic":
 		// case "human-relay":
 		// case "fake-ai":

+ 2 - 0
webview-ui/src/i18n/locales/ar/settings.json

@@ -324,6 +324,8 @@
 		"openAiApiKey": "مفتاح OpenAI",
 		"getHuggingFaceApiKey": "احصل على مفتاح Hugging Face API",
 		"huggingFaceApiKey": "مفتاح Hugging Face API",
+		"getOvhCloudAiEndpointsApiKey": "احصل على مفتاح OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "مفتاح OVHCloud AI Endpoints",
 		"huggingFaceModelId": "معرّف النموذج",
 		"huggingFaceLoading": "جارٍ التحميل...",
 		"huggingFaceModelsCount": "({{count}} نموذج)",

+ 2 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -313,6 +313,8 @@
 		"sambaNovaApiKey": "Clau API de SambaNova",
 		"getHuggingFaceApiKey": "Obtenir clau API de Hugging Face",
 		"huggingFaceApiKey": "Clau API de Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Obtenir clau API de OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Clau API de OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID del model",
 		"huggingFaceLoading": "Carregant...",
 		"huggingFaceModelsCount": "({{count}} models)",

+ 2 - 0
webview-ui/src/i18n/locales/cs/settings.json

@@ -324,6 +324,8 @@
 		"openAiApiKey": "Klíč API OpenAI",
 		"getHuggingFaceApiKey": "Získat klíč API Hugging Face",
 		"huggingFaceApiKey": "Klíč API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Získat klíč API OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Klíč API OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID modelu",
 		"huggingFaceLoading": "Načítání...",
 		"huggingFaceModelsCount": "({{count}} modelů)",

+ 2 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -313,6 +313,8 @@
 		"sambaNovaApiKey": "SambaNova API-Schlüssel",
 		"getHuggingFaceApiKey": "Hugging Face API-Schlüssel erhalten",
 		"huggingFaceApiKey": "Hugging Face API-Schlüssel",
+		"getOvhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API-Schlüssel erhalten",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API-Schlüssel",
 		"huggingFaceModelId": "Modell-ID",
 		"huggingFaceLoading": "Lädt...",
 		"huggingFaceModelsCount": "({{count}} Modelle)",

+ 2 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -318,6 +318,8 @@
 		"sambaNovaApiKey": "SambaNova API Key",
 		"getHuggingFaceApiKey": "Get Hugging Face API Key",
 		"huggingFaceApiKey": "Hugging Face API Key",
+		"getOvhCloudAiEndpointsApiKey": "Get OVHCloud AI Endpoints API Key",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API Key",
 		"huggingFaceModelId": "Model ID",
 		"huggingFaceLoading": "Loading...",
 		"huggingFaceModelsCount": "({{count}} models)",

+ 2 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -313,6 +313,8 @@
 		"sambaNovaApiKey": "Clave API de SambaNova",
 		"getHuggingFaceApiKey": "Obtener clave API de Hugging Face",
 		"huggingFaceApiKey": "Clave API de Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Obtener clave API de OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Clave API de OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID del modelo",
 		"huggingFaceLoading": "Cargando...",
 		"huggingFaceModelsCount": "({{count}} modelos)",

+ 2 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -313,6 +313,8 @@
 		"sambaNovaApiKey": "Clé API SambaNova",
 		"getHuggingFaceApiKey": "Obtenir la clé API Hugging Face",
 		"huggingFaceApiKey": "Clé API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Obtenir la clé API d'OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Clé API d'OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID du modèle",
 		"huggingFaceLoading": "Chargement...",
 		"huggingFaceModelsCount": "({{count}} modèles)",

+ 2 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -313,6 +313,8 @@
 		"sambaNovaApiKey": "SambaNova API कुंजी",
 		"getHuggingFaceApiKey": "Hugging Face API कुंजी प्राप्त करें",
 		"huggingFaceApiKey": "Hugging Face API कुंजी",
+		"getOvhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API कुंजी प्राप्त करें",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API कुंजी",
 		"huggingFaceModelId": "मॉडल ID",
 		"huggingFaceLoading": "लोड हो रहा है...",
 		"huggingFaceModelsCount": "({{count}} मॉडल)",

+ 2 - 0
webview-ui/src/i18n/locales/id/settings.json

@@ -313,6 +313,8 @@
 		"sambaNovaApiKey": "SambaNova API Key",
 		"getHuggingFaceApiKey": "Dapatkan Kunci API Hugging Face",
 		"huggingFaceApiKey": "Kunci API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Dapatkan Kunci API OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Kunci API OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID Model",
 		"getGeminiApiKey": "Dapatkan Gemini API Key",
 		"huggingFaceLoading": "Memuat...",

+ 2 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -314,6 +314,8 @@
 		"sambaNovaApiKey": "Chiave API SambaNova",
 		"getHuggingFaceApiKey": "Ottieni chiave API Hugging Face",
 		"huggingFaceApiKey": "Chiave API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Ottieni chiave API OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Chiave API OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID modello",
 		"huggingFaceLoading": "Caricamento...",
 		"huggingFaceModelsCount": "({{count}} modelli)",

+ 2 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -314,6 +314,8 @@
 		"sambaNovaApiKey": "SambaNova APIキー",
 		"getHuggingFaceApiKey": "Hugging Face APIキーを取得",
 		"huggingFaceApiKey": "Hugging Face APIキー",
+		"getOvhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints APIキーを取得",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints APIキー",
 		"huggingFaceModelId": "モデルID",
 		"huggingFaceLoading": "読み込み中...",
 		"huggingFaceModelsCount": "({{count}}個のモデル)",

+ 2 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -314,6 +314,8 @@
 		"getGeminiApiKey": "Gemini API 키 받기",
 		"getHuggingFaceApiKey": "Hugging Face API 키 받기",
 		"huggingFaceApiKey": "Hugging Face API 키",
+		"getOvhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API 키 받기",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API 키",
 		"huggingFaceModelId": "모델 ID",
 		"huggingFaceLoading": "로딩 중...",
 		"huggingFaceModelsCount": "({{count}}개 모델)",

+ 2 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -314,6 +314,8 @@
 		"getGeminiApiKey": "Gemini API-sleutel ophalen",
 		"getHuggingFaceApiKey": "Hugging Face API-sleutel ophalen",
 		"huggingFaceApiKey": "Hugging Face API-sleutel",
+		"getOvhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API-sleutel ophalen",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API-sleutel",
 		"huggingFaceModelId": "Model ID",
 		"huggingFaceLoading": "Laden...",
 		"huggingFaceModelsCount": "({{count}} modellen)",

+ 2 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -314,6 +314,8 @@
 		"getGeminiApiKey": "Uzyskaj klucz API Gemini",
 		"getHuggingFaceApiKey": "Uzyskaj klucz API Hugging Face",
 		"huggingFaceApiKey": "Klucz API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Uzyskaj klucz API OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Klucz API OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID modelu",
 		"huggingFaceLoading": "Ładowanie...",
 		"huggingFaceModelsCount": "({{count}} modeli)",

+ 2 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -314,6 +314,8 @@
 		"getGeminiApiKey": "Obter chave de API Gemini",
 		"getHuggingFaceApiKey": "Obter chave de API Hugging Face",
 		"huggingFaceApiKey": "Chave de API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Obter chave de API OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Chave de API OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID do modelo",
 		"huggingFaceLoading": "Carregando...",
 		"huggingFaceModelsCount": "({{count}} modelos)",

+ 2 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -314,6 +314,8 @@
 		"getGeminiApiKey": "Получить Gemini API-ключ",
 		"getHuggingFaceApiKey": "Получить Hugging Face API-ключ",
 		"huggingFaceApiKey": "Hugging Face API-ключ",
+		"getOvhCloudAiEndpointsApiKey": "Получить OVHCloud AI Endpoints API-ключ",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API-ключ",
 		"huggingFaceModelId": "ID модели",
 		"huggingFaceLoading": "Загрузка...",
 		"huggingFaceModelsCount": "({{count}} моделей)",

+ 2 - 0
webview-ui/src/i18n/locales/th/settings.json

@@ -325,6 +325,8 @@
 		"openAiApiKey": "คีย์ API ของ OpenAI",
 		"getHuggingFaceApiKey": "รับคีย์ API ของ Hugging Face",
 		"huggingFaceApiKey": "คีย์ API ของ Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "รับคีย์ API ของ OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "คีย์ API ของ OVHCloud AI Endpoints",
 		"huggingFaceModelId": "รหัสโมเดล",
 		"huggingFaceLoading": "กำลังโหลด...",
 		"huggingFaceModelsCount": "({{count}} โมเดล)",

+ 2 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -314,6 +314,8 @@
 		"sambaNovaApiKey": "SambaNova API Anahtarı",
 		"getHuggingFaceApiKey": "Hugging Face API Anahtarı Al",
 		"huggingFaceApiKey": "Hugging Face API Anahtarı",
+		"getOvhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API Anahtarı Al",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API Anahtarı",
 		"huggingFaceModelId": "Model ID",
 		"huggingFaceLoading": "Yükleniyor...",
 		"huggingFaceModelsCount": "({{count}} model)",

+ 2 - 0
webview-ui/src/i18n/locales/uk/settings.json

@@ -325,6 +325,8 @@
 		"openAiApiKey": "Ключ API OpenAI",
 		"getHuggingFaceApiKey": "Отримати ключ API Hugging Face",
 		"huggingFaceApiKey": "Ключ API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Отримати ключ API OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Ключ API OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID моделі",
 		"huggingFaceLoading": "Завантаження...",
 		"huggingFaceModelsCount": "({{count}} моделей)",

+ 2 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -313,6 +313,8 @@
 		"sambaNovaApiKey": "Khóa API SambaNova",
 		"getHuggingFaceApiKey": "Lấy Khóa API Hugging Face",
 		"huggingFaceApiKey": "Khóa API Hugging Face",
+		"getOvhCloudAiEndpointsApiKey": "Lấy Khóa API OVHCloud AI Endpoints",
+		"ovhCloudAiEndpointsApiKey": "Khóa API OVHCloud AI Endpoints",
 		"huggingFaceModelId": "ID Mô hình",
 		"huggingFaceLoading": "Đang tải...",
 		"huggingFaceModelsCount": "({{count}} mô hình)",

+ 2 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -314,6 +314,8 @@
 		"sambaNovaApiKey": "SambaNova API 密钥",
 		"getHuggingFaceApiKey": "获取 Hugging Face API 密钥",
 		"huggingFaceApiKey": "Hugging Face API 密钥",
+		"getOvhCloudAiEndpointsApiKey": "获取 OVHCloud AI Endpoints API 密钥",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API 密钥",
 		"huggingFaceModelId": "模型 ID",
 		"huggingFaceLoading": "加载中...",
 		"huggingFaceModelsCount": "({{count}} 个模型)",

+ 2 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -314,6 +314,8 @@
 		"sambaNovaApiKey": "SambaNova API 金鑰",
 		"getHuggingFaceApiKey": "取得 Hugging Face API 金鑰",
 		"huggingFaceApiKey": "Hugging Face API 金鑰",
+		"getOvhCloudAiEndpointsApiKey": "取得 OVHCloud AI Endpoints API 金鑰",
+		"ovhCloudAiEndpointsApiKey": "OVHCloud AI Endpoints API 金鑰",
 		"huggingFaceModelId": "模型 ID",
 		"huggingFaceLoading": "載入中...",
 		"huggingFaceModelsCount": "({{count}} 個模型)",

+ 4 - 1
webview-ui/src/utils/__tests__/validate.test.ts

@@ -61,7 +61,10 @@ describe("Model Validation Functions", () => {
 		"io-intelligence": {},
 		"vercel-ai-gateway": {},
 		huggingface: {},
-		chutes: {}, // kilocode_change
+		// kilocode_change start
+		ovhcloud: {},
+		chutes: {},
+		// kilocode_change end
 	}
 
 	const allowAllOrganization: OrganizationAllowList = {

+ 7 - 0
webview-ui/src/utils/validate.ts

@@ -175,6 +175,13 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
 				return i18next.t("settings:validation.apiKey")
 			}
 			break
+		// kilocode_change start
+		case "ovhcloud":
+			if (!apiConfiguration.ovhCloudAiEndpointsApiKey) {
+				return i18next.t("settings:validation.apiKey")
+			}
+			break
+		// kilocode_change end
 	}
 
 	return undefined