Kaynağa Gözat

Merge pull request #2199 from Kilo-Org/feat/deepinfra

feat(provider): add DeepInfra with dynamic model fetching & prompt-caching
Christiaan Arnoldus 5 ay önce
ebeveyn
işleme
6e524083fa

+ 5 - 0
.changeset/pretty-hornets-brake.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": minor
+---
+
+Thanks @Thachnh! - Added DeepInfra provider with dynamic model fetching and prompt caching

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

@@ -207,6 +207,7 @@ export const SECRET_STATE_KEYS = [
 	"codeIndexQdrantApiKey",
 	// kilocode_change start
 	"kilocodeToken",
+	"deepInfraApiKey",
 	// kilocode_change end
 	"codebaseIndexOpenAiCompatibleApiKey",
 	"codebaseIndexGeminiApiKey",

+ 12 - 0
packages/types/src/provider-settings.ts

@@ -57,6 +57,7 @@ export const providerNames = [
 	"litellm",
 	// kilocode_change start
 	"kilocode",
+	"deepinfra",
 	"gemini-cli",
 	"virtual-quota-fallback",
 	"qwen-code",
@@ -320,6 +321,12 @@ const kilocodeSchema = baseProviderSettingsSchema.extend({
 	openRouterProviderSort: openRouterProviderSortSchema.optional(),
 })
 
+const deepInfraSchema = apiModelIdProviderModelSchema.extend({
+	deepInfraBaseUrl: z.string().optional(),
+	deepInfraApiKey: z.string().optional(),
+	deepInfraModelId: z.string().optional(),
+})
+
 export const virtualQuotaFallbackProfileDataSchema = z.object({
 	profileName: z.string().optional(),
 	profileId: z.string().optional(),
@@ -393,6 +400,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	fakeAiSchema.merge(z.object({ apiProvider: z.literal("fake-ai") })),
 	xaiSchema.merge(z.object({ apiProvider: z.literal("xai") })),
 	// kilocode_change start
+	deepInfraSchema.merge(z.object({ apiProvider: z.literal("deepinfra") })),
 	geminiCliSchema.merge(z.object({ apiProvider: z.literal("gemini-cli") })),
 	kilocodeSchema.merge(z.object({ apiProvider: z.literal("kilocode") })),
 	virtualQuotaFallbackSchema.merge(z.object({ apiProvider: z.literal("virtual-quota-fallback") })),
@@ -430,6 +438,7 @@ export const providerSettingsSchema = z.object({
 	...kilocodeSchema.shape,
 	...virtualQuotaFallbackSchema.shape,
 	...qwenCodeSchema.shape,
+	...deepInfraSchema.shape,
 	// kilocode_change end
 	...openAiNativeSchema.shape,
 	...mistralSchema.shape,
@@ -478,6 +487,7 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
 	"litellmModelId",
 	"huggingFaceModelId",
 	"ioIntelligenceModelId",
+	"deepInfraModelId", // kilocode_change
 ]
 
 export const getModelId = (settings: ProviderSettings): string | undefined => {
@@ -598,6 +608,7 @@ export const MODELS_BY_PROVIDER: Record<
 	kilocode: { id: "kilocode", label: "Kilocode", models: [] },
 	"virtual-quota-fallback": { id: "virtual-quota-fallback", label: "Virtual Quota Fallback", models: [] },
 	"qwen-code": { id: "qwen-code", label: "Qwen Code", models: [] },
+	deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
 	// kilocode_change end
 }
 
@@ -611,6 +622,7 @@ export const dynamicProviders = [
 	// kilocode_change start
 	"kilocode",
 	"virtual-quota-fallback",
+	"deepinfra",
 	// kilocode_change end
 ] as const satisfies readonly ProviderName[]
 

+ 16 - 0
packages/types/src/providers/deepinfra.ts

@@ -0,0 +1,16 @@
+// kilocode_change: provider added
+
+import type { ModelInfo } from "../model.js"
+
+// Default fallback values for DeepInfra when model metadata is not yet loaded.
+export const deepInfraDefaultModelId = "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo"
+
+export const deepInfraDefaultModelInfo: ModelInfo = {
+	maxTokens: 16384,
+	contextWindow: 262144,
+	supportsImages: false,
+	supportsPromptCache: false,
+	inputPrice: 0.3,
+	outputPrice: 1.2,
+	description: "Qwen 3 Coder 480B A35B Instruct Turbo model, 256K context.",
+}

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

@@ -8,7 +8,11 @@ export * from "./doubao.js"
 export * from "./featherless.js"
 export * from "./fireworks.js"
 export * from "./gemini.js"
-export * from "./gemini-cli.js" // kilocode_change
+// kilocode_change start
+export * from "./gemini-cli.js"
+export * from "./qwen-code.js"
+export * from "./deepinfra.js"
+// kilocode_change end
 export * from "./glama.js"
 export * from "./groq.js"
 export * from "./huggingface.js"
@@ -21,7 +25,6 @@ export * from "./ollama.js"
 export * from "./openai.js"
 export * from "./openrouter.js"
 export * from "./requesty.js"
-export * from "./qwen-code.js" // kilocode_change
 export * from "./roo.js"
 export * from "./sambanova.js"
 export * from "./unbound.js"

+ 3 - 0
src/api/index.ts

@@ -34,6 +34,7 @@ import {
 	VirtualQuotaFallbackHandler,
 	GeminiCliHandler,
 	QwenCodeHandler,
+	DeepInfraHandler,
 	// kilocode_change end
 	ClaudeCodeHandler,
 	SambaNovaHandler,
@@ -98,6 +99,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new VirtualQuotaFallbackHandler(options)
 		case "qwen-code":
 			return new QwenCodeHandler(options)
+		case "deepinfra":
+			return new DeepInfraHandler(options)
 		// kilocode_change end
 		case "anthropic":
 			return new AnthropicHandler(options)

+ 151 - 0
src/api/providers/deepinfra.ts

@@ -0,0 +1,151 @@
+// kilocode_change - provider added
+
+import { Anthropic } from "@anthropic-ai/sdk" // for message param types
+import OpenAI from "openai"
+
+import { deepInfraDefaultModelId, deepInfraDefaultModelInfo } from "@roo-code/types"
+
+import type { ApiHandlerOptions } from "../../shared/api"
+import { calculateApiCostOpenAI } from "../../shared/cost"
+
+import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
+import { convertToOpenAiMessages } from "../transform/openai-format"
+
+import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
+import { RouterProvider } from "./router-provider"
+import { getModelParams } from "../transform/model-params"
+import { getModels } from "./fetchers/modelCache"
+
+/**
+ * DeepInfra provider handler (OpenAI compatible)
+ */
+export class DeepInfraHandler extends RouterProvider implements SingleCompletionHandler {
+	constructor(options: ApiHandlerOptions) {
+		super({
+			options: {
+				...options,
+				openAiHeaders: {
+					"X-Deepinfra-Source": "kilocode",
+					"X-Deepinfra-Version": `2025-08-25`,
+				},
+			},
+			name: "deepinfra",
+			baseURL: `${options.deepInfraBaseUrl || "https://api.deepinfra.com/v1/openai"}`,
+			apiKey: options.deepInfraApiKey || "not-provided",
+			modelId: options.deepInfraModelId,
+			defaultModelId: deepInfraDefaultModelId,
+			defaultModelInfo: deepInfraDefaultModelInfo,
+		})
+	}
+
+	public override async fetchModel() {
+		this.models = await getModels({ provider: this.name, apiKey: this.client.apiKey, baseUrl: this.client.baseURL })
+		return this.getModel()
+	}
+
+	override getModel() {
+		const id = this.options.deepInfraModelId ?? deepInfraDefaultModelId
+		const info = this.models[id] ?? deepInfraDefaultModelInfo
+
+		const params = getModelParams({
+			format: "openai",
+			modelId: id,
+			model: info,
+			settings: this.options,
+		})
+
+		return { id, info, ...params }
+	}
+
+	override async *createMessage(
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		_metadata?: ApiHandlerCreateMessageMetadata,
+	): ApiStream {
+		const { id: modelId, info, reasoningEffort: reasoning_effort } = await this.fetchModel()
+		let prompt_cache_key = undefined
+		if (info.supportsPromptCache && _metadata?.taskId) {
+			prompt_cache_key = _metadata.taskId
+		}
+
+		const requestOptions = {
+			model: modelId,
+			messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
+			stream: true,
+			stream_options: { include_usage: true },
+			reasoning_effort,
+			prompt_cache_key,
+		} as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
+
+		if (this.supportsTemperature(modelId)) {
+			requestOptions.temperature = this.options.modelTemperature ?? 0
+		}
+
+		// If includeMaxTokens is enabled, set a cap using model info
+		if (this.options.includeMaxTokens === true && info.maxTokens) {
+			// Prefer modern OpenAI param when available in SDK
+			;(requestOptions as any).max_completion_tokens = this.options.modelMaxTokens || info.maxTokens
+		}
+
+		const { data: stream } = await this.client.chat.completions.create(requestOptions).withResponse()
+
+		let lastUsage: OpenAI.CompletionUsage | undefined
+		for await (const chunk of stream) {
+			const delta = chunk.choices[0]?.delta
+
+			if (delta?.content) {
+				yield { type: "text", text: delta.content }
+			}
+
+			if (delta && "reasoning_content" in delta && delta.reasoning_content) {
+				yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" }
+			}
+
+			if (chunk.usage) {
+				lastUsage = chunk.usage
+			}
+		}
+
+		if (lastUsage) {
+			yield this.processUsageMetrics(lastUsage, info)
+		}
+	}
+
+	async completePrompt(prompt: string): Promise<string> {
+		const { id: modelId, info } = await this.fetchModel()
+
+		const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
+			model: modelId,
+			messages: [{ role: "user", content: prompt }],
+		}
+		if (this.supportsTemperature(modelId)) {
+			requestOptions.temperature = this.options.modelTemperature ?? 0
+		}
+		if (this.options.includeMaxTokens === true && info.maxTokens) {
+			;(requestOptions as any).max_completion_tokens = this.options.modelMaxTokens || info.maxTokens
+		}
+
+		const resp = await this.client.chat.completions.create(requestOptions)
+		return resp.choices[0]?.message?.content || ""
+	}
+
+	protected processUsageMetrics(usage: any, modelInfo?: any): ApiStreamUsageChunk {
+		const inputTokens = usage?.prompt_tokens || 0
+		const outputTokens = usage?.completion_tokens || 0
+		const cacheWriteTokens = usage?.prompt_tokens_details?.cache_write_tokens || 0
+		const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0
+
+		const totalCost = modelInfo
+			? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
+			: 0
+
+		return {
+			type: "usage",
+			inputTokens,
+			outputTokens,
+			cacheWriteTokens: cacheWriteTokens || undefined,
+			cacheReadTokens: cacheReadTokens || undefined,
+			totalCost,
+		}
+	}
+}

+ 73 - 0
src/api/providers/fetchers/deepinfra.ts

@@ -0,0 +1,73 @@
+import axios from "axios"
+import { z } from "zod"
+
+import { type ModelInfo } from "@roo-code/types"
+
+import { DEFAULT_HEADERS } from "../constants"
+
+// DeepInfra models endpoint follows OpenAI /models shape with an added metadata object.
+// Use only the supported fields and infer capabilities from tags.
+
+const DeepInfraModelSchema = z.object({
+	id: z.string(),
+	object: z.literal("model"),
+	owned_by: z.string().optional(),
+	created: z.number().optional(),
+	root: z.string().optional(),
+	metadata: z
+		.object({
+			description: z.string().optional(),
+			context_length: z.number().optional(),
+			max_tokens: z.number().optional(),
+			tags: z.array(z.string()).optional(), // e.g., ["vision", "prompt_cache"]
+			pricing: z
+				.object({
+					input_tokens: z.number().optional(),
+					output_tokens: z.number().optional(),
+					cache_read_tokens: z.number().optional(),
+				})
+				.optional(),
+		})
+		.optional(),
+})
+
+const DeepInfraModelsResponseSchema = z.object({ data: z.array(DeepInfraModelSchema) })
+
+export async function getDeepInfraModels(
+	apiKey?: string,
+	baseUrl: string = "https://api.deepinfra.com/v1/openai",
+): Promise<Record<string, ModelInfo>> {
+	const headers: Record<string, string> = { ...DEFAULT_HEADERS }
+	if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
+
+	const url = `${baseUrl.replace(/\/$/, "")}/models`
+	const models: Record<string, ModelInfo> = {}
+
+	const response = await axios.get(url, { headers })
+	const parsed = DeepInfraModelsResponseSchema.safeParse(response.data)
+	const data = parsed.success ? parsed.data.data : response.data?.data || []
+
+	for (const m of data as Array<z.infer<typeof DeepInfraModelSchema>>) {
+		const meta = m.metadata || {}
+		const tags = meta.tags || []
+
+		const contextWindow = typeof meta.context_length === "number" ? meta.context_length : 8192
+		const maxTokens = typeof meta.max_tokens === "number" ? meta.max_tokens : Math.ceil(contextWindow * 0.2)
+
+		const info: ModelInfo = {
+			maxTokens,
+			contextWindow,
+			supportsImages: tags.includes("vision"),
+			supportsPromptCache: tags.includes("prompt_cache"),
+			supportsReasoningEffort: tags.includes("reasoning_effort"),
+			inputPrice: meta.pricing?.input_tokens,
+			outputPrice: meta.pricing?.output_tokens,
+			cacheReadsPrice: meta.pricing?.cache_read_tokens,
+			description: meta.description,
+		}
+
+		models[m.id] = info
+	}
+
+	return models
+}

+ 4 - 0
src/api/providers/fetchers/modelCache.ts

@@ -19,6 +19,7 @@ import { getKiloBaseUriFromToken } from "../../../shared/kilocode/token"
 import { getOllamaModels } from "./ollama"
 import { getLMStudioModels } from "./lmstudio"
 import { getIOIntelligenceModels } from "./io-intelligence"
+import { getDeepInfraModels } from "./deepinfra" // kilocode_change
 const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
 
 export /*kilocode_change*/ async function writeModels(router: RouterName, data: ModelRecord) {
@@ -89,6 +90,9 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
 					headers: options.kilocodeToken ? { Authorization: `Bearer ${options.kilocodeToken}` } : undefined,
 				})
 				break
+			case "deepinfra":
+				models = await getDeepInfraModels(options.apiKey, options.baseUrl)
+				break
 			case "cerebras":
 				models = cerebrasModels
 				break

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

@@ -26,6 +26,7 @@ export { SambaNovaHandler } from "./sambanova"
 export { UnboundHandler } from "./unbound"
 export { VertexHandler } from "./vertex"
 // kilocode_change start
+export { DeepInfraHandler } from "./deepinfra"
 export { GeminiCliHandler } from "./gemini-cli"
 export { QwenCodeHandler } from "./qwen-code"
 export { VirtualQuotaFallbackHandler } from "./virtual-quota-fallback"

+ 4 - 0
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -2728,6 +2728,7 @@ describe("ClineProvider - Router Models", () => {
 				"kilocode-openrouter": mockModels,
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
+				deepinfra: mockModels, // kilocode_change
 			},
 		})
 	})
@@ -2760,6 +2761,7 @@ describe("ClineProvider - Router Models", () => {
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
 			.mockRejectedValueOnce(new Error("Kilocode-OpenRouter API error")) // kilocode-openrouter fail
 			.mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change
+			.mockRejectedValueOnce(new Error("DeepInfra API error")) // kilocode_change
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
 
 		await messageHandler({ type: "requestRouterModels" })
@@ -2776,6 +2778,7 @@ describe("ClineProvider - Router Models", () => {
 				lmstudio: {},
 				litellm: {},
 				"kilocode-openrouter": {},
+				deepinfra: {}, // kilocode_change
 			},
 		})
 
@@ -2892,6 +2895,7 @@ describe("ClineProvider - Router Models", () => {
 				unbound: mockModels,
 				litellm: {},
 				"kilocode-openrouter": mockModels,
+				deepinfra: mockModels, // kilocode_change
 				ollama: mockModels, // kilocode_change
 				lmstudio: {},
 			},

+ 5 - 0
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -195,6 +195,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				litellm: mockModels,
 				"kilocode-openrouter": mockModels,
 				ollama: mockModels, // kilocode_change
+				deepinfra: mockModels, // kilocode_change
 				lmstudio: {},
 			},
 		})
@@ -283,6 +284,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				litellm: {},
 				"kilocode-openrouter": mockModels,
 				ollama: mockModels, // kilocode_change
+				deepinfra: mockModels, // kilocode_change
 				lmstudio: {},
 			},
 		})
@@ -306,6 +308,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
 			.mockResolvedValueOnce(mockModels) // kilocode-openrouter
 			.mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change
+			.mockResolvedValueOnce(mockModels) // kilocode_change deepinfra
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {
@@ -323,6 +326,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 				litellm: {},
 				"kilocode-openrouter": mockModels,
 				ollama: {},
+				deepinfra: mockModels,
 				lmstudio: {},
 			},
 		})
@@ -359,6 +363,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 			.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
 			.mockResolvedValueOnce({}) // kilocode-openrouter - Success
 			.mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change
+			.mockRejectedValueOnce({}) // kilocode_change deepinfra
 			.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
 
 		await webviewMessageHandler(mockClineProvider, {

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

@@ -571,6 +571,7 @@ export const webviewMessageHandler = async (
 				"kilocode-openrouter": {}, // kilocode_change
 				ollama: {},
 				lmstudio: {},
+				deepinfra: {}, // kilocode_change
 			}
 
 			const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
@@ -613,6 +614,7 @@ export const webviewMessageHandler = async (
 					},
 				},
 				{ key: "ollama", options: { provider: "ollama", baseUrl: apiConfiguration.ollamaBaseUrl } },
+				{ key: "deepinfra", options: { provider: "deepinfra", apiKey: apiConfiguration.deepInfraApiKey } },
 			]
 			// kilocode_change end
 

+ 4 - 0
src/shared/ProfileValidator.ts

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

+ 2 - 0
src/shared/api.ts

@@ -132,6 +132,7 @@ const routerNames = [
 	"ollama",
 	"lmstudio",
 	"io-intelligence",
+	"deepinfra", // kilocode_change
 ] as const
 
 export type RouterName = (typeof routerNames)[number]
@@ -257,4 +258,5 @@ export type GetModelsOptions =
 	| { provider: "cerebras"; cerebrasApiKey?: string } // kilocode_change
 	| { provider: "ollama"; baseUrl?: string }
 	| { provider: "lmstudio"; baseUrl?: string }
+	| { provider: "deepinfra"; apiKey?: string; baseUrl?: string }
 	| { provider: "io-intelligence"; apiKey: string }

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

@@ -24,6 +24,7 @@ describe("getModelsByProvider", () => {
 			ollama: { "test-model": testModel },
 			lmstudio: { "test-model": testModel },
 			"io-intelligence": { "test-model": testModel },
+			deepinfra: { "test-model": testModel },
 		}
 
 		const exceptions = [

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

@@ -44,6 +44,7 @@ import {
 	sambaNovaDefaultModelId,
 	featherlessModels,
 	featherlessDefaultModelId,
+	deepInfraDefaultModelId,
 } from "@roo-code/types"
 import { cerebrasModels, cerebrasDefaultModelId } from "@roo/api"
 import type { ModelRecord, RouterModels } from "@roo/api"
@@ -246,6 +247,12 @@ export const getModelsByProvider = ({
 				defaultModel: featherlessDefaultModelId,
 			}
 		}
+		case "deepinfra": {
+			return {
+				models: routerModels.deepinfra,
+				defaultModel: deepInfraDefaultModelId,
+			}
+		}
 		default:
 			return {
 				models: {},

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

@@ -36,6 +36,7 @@ import {
 	ioIntelligenceDefaultModelId,
 	qwenCodeDefaultModelId,
 	rooDefaultModelId,
+	deepInfraDefaultModelId, // kilocode_change
 } from "@roo-code/types"
 
 import { vscode } from "@src/utils/vscode"
@@ -94,6 +95,7 @@ import {
 	GeminiCli,
 	VirtualQuotaFallbackProvider,
 	QwenCode,
+	DeepInfra,
 	// kilocode_change end
 	ZAi,
 	Fireworks,
@@ -206,6 +208,7 @@ const ApiOptions = ({
 		openRouterBaseUrl: apiConfiguration?.openRouterBaseUrl,
 		openRouterApiKey: apiConfiguration?.openRouterApiKey,
 		kilocodeOrganizationId: apiConfiguration?.kilocodeOrganizationId ?? "personal",
+		deepInfraApiKey: apiConfiguration?.deepInfraApiKey,
 	})
 
 	//const { data: openRouterModelProviders } = useOpenRouterModelProviders(
@@ -371,6 +374,7 @@ const ApiOptions = ({
 				kilocode: { field: "kilocodeModel", default: kilocodeDefaultModel },
 				"gemini-cli": { field: "apiModelId", default: geminiCliDefaultModelId },
 				"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
+				deepinfra: { field: "deepInfraModelId", default: deepInfraDefaultModelId },
 				// kilocode_change end
 			}
 
@@ -667,6 +671,21 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{
+				// kilocode_change start
+				selectedProvider === "deepinfra" && (
+					<DeepInfra
+						apiConfiguration={apiConfiguration}
+						setApiConfigurationField={setApiConfigurationField}
+						routerModels={routerModels}
+						refetchRouterModels={() => refetchRouterModels()}
+						organizationAllowList={organizationAllowList}
+						modelValidationError={modelValidationError}
+					/>
+				)
+				// kilocode_change end
+			}
+
 			{selectedProvider === "human-relay" && (
 				<>
 					<div className="text-sm text-vscode-descriptionForeground">

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

@@ -38,7 +38,10 @@ type ModelIdKey = keyof Pick<
 	| "requestyModelId"
 	| "openAiModelId"
 	| "litellmModelId"
+	// kilocode_change start
 	| "kilocodeModel"
+	| "deepInfraModelId"
+	// kilocode_change end
 	| "ioIntelligenceModelId"
 >
 

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

@@ -88,7 +88,10 @@ export const PROVIDERS = [
 	{ value: "fireworks", label: "Fireworks AI" },
 	{ value: "featherless", label: "Featherless AI" },
 	{ value: "io-intelligence", label: "IO Intelligence" },
-	// { value: "roo", label: "Roo Code Cloud" }, // kilocode_change
+	// kilocode_change start
+	{ value: "deepinfra", label: "Deep Infra" },
+	// { value: "roo", label: "Roo Code Cloud" },
+	// kilocode_change end
 ].sort((a, b) => a.label.localeCompare(b.label))
 
 PROVIDERS.unshift({ value: "kilocode", label: "Kilo Code" }) // kilocode_change

+ 97 - 0
webview-ui/src/components/settings/providers/DeepInfra.tsx

@@ -0,0 +1,97 @@
+// kilocode_change - provider added
+
+import { useCallback, useEffect, useState } from "react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+
+import { type ProviderSettings, deepInfraDefaultModelId } from "@roo-code/types"
+
+import type { OrganizationAllowList } from "@roo/cloud"
+import type { RouterModels } from "@roo/api"
+
+import { vscode } from "@src/utils/vscode"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Button } from "@src/components/ui"
+
+import { inputEventTransform } from "../transforms"
+import { ModelPicker } from "../ModelPicker"
+
+type DeepInfraProps = {
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
+	routerModels?: RouterModels
+	refetchRouterModels: () => void
+	organizationAllowList: OrganizationAllowList
+	modelValidationError?: string
+}
+
+export const DeepInfra = ({
+	apiConfiguration,
+	setApiConfigurationField,
+	routerModels,
+	refetchRouterModels,
+	organizationAllowList,
+	modelValidationError,
+}: DeepInfraProps) => {
+	const { t } = useAppTranslation()
+
+	const [didRefetch, setDidRefetch] = useState<boolean>()
+
+	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],
+	)
+
+	useEffect(() => {
+		// When base URL or API key changes, trigger a silent refresh of models
+		// The outer ApiOptions debounces and sends requestRouterModels; this keeps UI responsive
+	}, [apiConfiguration.deepInfraBaseUrl, apiConfiguration.deepInfraApiKey])
+
+	return (
+		<>
+			<VSCodeTextField
+				value={apiConfiguration?.deepInfraApiKey || ""}
+				type="password"
+				onInput={handleInputChange("deepInfraApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.apiKey")}</label>
+			</VSCodeTextField>
+
+			<Button
+				variant="outline"
+				onClick={() => {
+					vscode.postMessage({ type: "flushRouterModels", text: "deepinfra" })
+					refetchRouterModels()
+					setDidRefetch(true)
+				}}>
+				<div className="flex items-center gap-2">
+					<span className="codicon codicon-refresh" />
+					{t("settings:providers.refreshModels.label")}
+				</div>
+			</Button>
+			{didRefetch && (
+				<div className="flex items-center text-vscode-errorForeground">
+					{t("settings:providers.refreshModels.hint")}
+				</div>
+			)}
+
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				setApiConfigurationField={setApiConfigurationField}
+				defaultModelId={deepInfraDefaultModelId}
+				models={routerModels?.deepinfra ?? {}}
+				modelIdKey="deepInfraModelId"
+				serviceName="Deep Infra"
+				serviceUrl="https://deepinfra.com/models"
+				organizationAllowList={organizationAllowList}
+				errorMessage={modelValidationError}
+			/>
+		</>
+	)
+}

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

@@ -27,6 +27,7 @@ export { XAI } from "./XAI"
 export { GeminiCli } from "./GeminiCli"
 export { VirtualQuotaFallbackProvider } from "./VirtualQuotaFallbackProvider"
 export { QwenCode } from "./QwenCode"
+export { DeepInfra } from "./DeepInfra"
 // kilocode_change end
 export { ZAi } from "./ZAi"
 export { LiteLLM } from "./LiteLLM"

+ 1 - 0
webview-ui/src/components/ui/hooks/useRouterModels.ts

@@ -42,6 +42,7 @@ type RouterModelsQueryKey = {
 	lmStudioBaseUrl?: string
 	ollamaBaseUrl?: string
 	kilocodeOrganizationId?: string
+	deepInfraApiKey?: string
 	// Requesty, Unbound, etc should perhaps also be here, but they already have their own hacks for reloading
 }
 

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

@@ -19,6 +19,7 @@ import {
 	geminiCliModels,
 	qwenCodeModels,
 	qwenCodeDefaultModelId,
+	deepInfraDefaultModelId,
 	// kilocode_change end
 	mistralDefaultModelId,
 	mistralModels,
@@ -183,6 +184,11 @@ function getSelectedModel({
 			const info = routerModels.litellm[id]
 			return { id, info }
 		}
+		case "deepinfra": {
+			const id = apiConfiguration.deepInfraModelId ?? deepInfraDefaultModelId
+			const info = routerModels.deepinfra[id]
+			return { id, info }
+		}
 		case "xai": {
 			const id = apiConfiguration.apiModelId ?? xaiDefaultModelId
 			const info = xaiModels[id as keyof typeof xaiModels]

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

@@ -59,6 +59,7 @@ describe("Model Validation Functions", () => {
 		ollama: {},
 		lmstudio: {},
 		"io-intelligence": {},
+		deepinfra: {}, // kilocode_change
 	}
 
 	const allowAllOrganization: OrganizationAllowList = {

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

@@ -74,6 +74,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
 			}
 			break
 		// kilocode_change start
+		case "deepinfra":
+			if (!apiConfiguration.deepInfraApiKey) {
+				return i18next.t("settings:validation.apiKey")
+			}
+			break
 		case "gemini-cli":
 			// OAuth-based provider, no API key validation needed
 			break
@@ -214,6 +219,10 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: str
 			return apiConfiguration.huggingFaceModelId
 		case "io-intelligence":
 			return apiConfiguration.ioIntelligenceModelId
+		// kilocode_change start
+		case "deepinfra":
+			return apiConfiguration.deepInfraModelId
+		// kilocode_change end
 		default:
 			return apiConfiguration.apiModelId
 	}
@@ -284,6 +293,11 @@ export function validateModelId(apiConfiguration: ProviderSettings, routerModels
 		case "litellm":
 			modelId = apiConfiguration.litellmModelId
 			break
+		// kilocode_change start
+		case "deepinfra":
+			modelId = apiConfiguration.deepInfraModelId
+			break
+		// kilocode_change end
 		case "io-intelligence":
 			modelId = apiConfiguration.ioIntelligenceModelId
 			break