Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/dynamic-openai-model-fetching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"kilo-code": patch
"@kilocode/types": patch
---

Implement dynamic model fetching for OpenAI-compatible providers
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ export type ExtensionState = Pick<
debug?: boolean
speechToTextStatus?: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled" } // kilocode_change: Speech-to-text availability status with failure reason
appendSystemPrompt?: string // kilocode_change: Custom text to append to system prompt (CLI only)
openAiModels?: string[]
}

export interface Command {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ModelInfo } from "@roo-code/types"
import { RouterModels } from "@roo/api"
import { getModelsByProvider } from "../useProviderModels"

describe("PR #5562: Dynamic OpenAI model fetching on front page", () => {
const testModel: ModelInfo = {
maxTokens: 4096,
contextWindow: 8192,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.2,
description: "Test model",
}

const routerModels: RouterModels = {
openrouter: { "test-model": testModel },
requesty: { "test-model": testModel },
glama: { "test-model": testModel },
unbound: { "test-model": testModel },
litellm: { "test-model": testModel },
kilocode: { "test-model": testModel },
"nano-gpt": { "test-model": testModel },
ollama: { "test-model": testModel },
lmstudio: { "test-model": testModel },
"io-intelligence": { "test-model": testModel },
deepinfra: { "test-model": testModel },
"vercel-ai-gateway": { "test-model": testModel },
huggingface: { "test-model": testModel },
gemini: { "test-model": testModel },
ovhcloud: { "test-model": testModel },
chutes: { "test-model": testModel },
"sap-ai-core": { "test-model": testModel },
synthetic: { "test-model": testModel },
inception: { "test-model": testModel },
roo: { "test-model": testModel },
}

const baseArgs = {
routerModels,
kilocodeDefaultModel: "test-model",
options: { isChina: false },
}

it("returns dynamically fetched models when openAiModels is provided", () => {
const result = getModelsByProvider({
...baseArgs,
provider: "openai",
openAiModels: ["gpt-4o", "gpt-4o-mini", "o1-preview"],
})

expect(Object.keys(result.models)).toEqual(["gpt-4o", "gpt-4o-mini", "o1-preview"])
expect(result.defaultModel).toBe("gpt-4o")
// Each model should have sane defaults (128K context, supports images)
expect(result.models["gpt-4o"].contextWindow).toBe(128_000)
expect(result.models["gpt-4o"].supportsImages).toBe(true)
})
Comment on lines +45 to +57
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if openai-responses is handled in getModelsByProvider
rg -n 'openai-responses' webview-ui/src/components/kilocode/hooks/useProviderModels.ts

Repository: jeremylongshore/kilocode

Length of output: 50


🏁 Script executed:

# Also search for openai-responses more broadly to understand its usage
rg -n 'openai-responses' webview-ui/src/components/kilocode/hooks/ -A 2 -B 2

Repository: jeremylongshore/kilocode

Length of output: 678


🏁 Script executed:

# Check the switch statement in getModelsByProvider to see all cases
fd 'useProviderModels.ts' webview-ui/src/components/kilocode/hooks/ --exec cat -n {} \;

Repository: jeremylongshore/kilocode

Length of output: 11631


🏁 Script executed:

# Check the ExtensionStateContext to see where model fetching is triggered
rg -n 'ExtensionStateContext' webview-ui/src/components/kilocode/ -l

Repository: jeremylongshore/kilocode

Length of output: 1031


🏁 Script executed:

# Search for ExtensionStateContext to find where model fetching is triggered
rg -n 'openai-responses' webview-ui/src/context/ -A 3 -B 3

Repository: jeremylongshore/kilocode

Length of output: 699


🏁 Script executed:

# Also check the ExtensionStateContext file itself for model fetching logic
fd 'ExtensionStateContext' webview-ui/src/ --exec cat -n {} \; | head -600

Repository: jeremylongshore/kilocode

Length of output: 29014


🏁 Script executed:

# Search more broadly for where openAiModels is fetched or populated
rg -n 'openAiModels' webview-ui/src/context/ -A 2 -B 2

Repository: jeremylongshore/kilocode

Length of output: 1261


🏁 Script executed:

# Check if openai-responses is used as a provider anywhere in the codebase
rg -n '"openai-responses"' webview-ui/src --type ts --type tsx -B 2 -A 2

Repository: jeremylongshore/kilocode

Length of output: 95


🏁 Script executed:

# Also check if there's any mention of openai-responses in provider type definitions
rg -n 'openai-responses' --type ts --type tsx -l

Repository: jeremylongshore/kilocode

Length of output: 95


🏁 Script executed:

# Check if openai-responses is used as a provider anywhere in the codebase
rg -n '"openai-responses"' webview-ui/src -B 2 -A 2

Repository: jeremylongshore/kilocode

Length of output: 3569


🏁 Script executed:

# Check if openai-responses is defined in type definitions
fd 'types' webview-ui/src -type d | head -5

Repository: jeremylongshore/kilocode

Length of output: 238


🏁 Script executed:

# Search for ProviderName type definition to see all valid providers
rg -n 'type ProviderName' webview-ui/src -A 30

Repository: jeremylongshore/kilocode

Length of output: 14398


🏁 Script executed:

# Check the complete case for openai-responses in useSelectedModel.ts
sed -n '349,365p' webview-ui/src/components/ui/hooks/useSelectedModel.ts

Repository: jeremylongshore/kilocode

Length of output: 817


🏁 Script executed:

# Check if openAiModels state is used anywhere in useSelectedModel or elsewhere
rg -n 'openAiModels' webview-ui/src/components -B 2 -A 2

Repository: jeremylongshore/kilocode

Length of output: 6925


Add missing case for openai-responses in getModelsByProvider.

The ExtensionStateContext (line 573) triggers model fetching for both "openai" and "openai-responses" providers, but getModelsByProvider only has a case "openai" branch (lines 186-197). When apiProvider is "openai-responses", the function falls through to the default case and returns empty models and defaultModel, silently discarding the dynamically fetched models. Either add a matching case for "openai-responses" that handles the openAiModels parameter, or remove "openai-responses" from the fetch trigger in ExtensionStateContext.

🤖 Prompt for AI Agents
In
`@webview-ui/src/components/kilocode/hooks/__tests__/dynamic-openai-models.spec.ts`
around lines 45 - 57, getModelsByProvider is missing handling for the
"openai-responses" provider so dynamically fetched models passed via
openAiModels get discarded; add a case "openai-responses" in getModelsByProvider
that mirrors the existing "openai" branch (using the openAiModels argument to
populate result.models and result.defaultModel with the same defaults like
contextWindow and supportsImages) or alternatively remove "openai-responses"
from the fetch trigger in ExtensionStateContext so both locations stay
consistent; update getModelsByProvider (and ensure any tests referencing
getModelsByProvider still pass) to reference the openAiModels parameter when
apiProvider === "openai-responses".


it("returns empty models when openAiModels is not provided", () => {
const result = getModelsByProvider({
...baseArgs,
provider: "openai",
})

expect(Object.keys(result.models)).toHaveLength(0)
expect(result.defaultModel).toBe("")
})

it("handles empty openAiModels array gracefully", () => {
const result = getModelsByProvider({
...baseArgs,
provider: "openai",
openAiModels: [],
})

// Empty array is truthy but has no models
expect(Object.keys(result.models)).toHaveLength(0)
expect(result.defaultModel).toBe("")
})
})
13 changes: 11 additions & 2 deletions webview-ui/src/components/kilocode/hooks/useProviderModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
internationalZAiDefaultModelId,
mainlandZAiModels,
mainlandZAiDefaultModelId,
openAiModelInfoSaneDefaults,
} from "@roo-code/types"
import type { ModelRecord, RouterModels } from "@roo/api"
import { useRouterModels } from "../../ui/hooks/useRouterModels"
Expand All @@ -73,11 +74,13 @@ export const getModelsByProvider = ({
provider,
routerModels,
kilocodeDefaultModel,
openAiModels,
options = { isChina: false },
}: {
provider: ProviderName
routerModels: RouterModels
kilocodeDefaultModel: string
openAiModels?: string[]
options: { isChina?: boolean }
}): { models: ModelRecord; defaultModel: string } => {
switch (provider) {
Expand Down Expand Up @@ -181,7 +184,12 @@ export const getModelsByProvider = ({
}
}
case "openai": {
// TODO(catrielmuller): Support the fetch here
if (openAiModels) {
return {
models: Object.fromEntries(openAiModels.map((model) => [model, openAiModelInfoSaneDefaults])),
defaultModel: openAiModels[0] || "",
}
}
Comment on lines +187 to +192
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The if (openAiModels) check is truthy for an empty array. This makes the logic inside the if block redundant for that case, as it produces the same result as the return statement on line 193. It's better to explicitly check if the array has elements for clarity and to avoid the || "" fallback.

            if (openAiModels && openAiModels.length > 0) {
				return {
					models: Object.fromEntries(openAiModels.map((model) => [model, openAiModelInfoSaneDefaults])),
					defaultModel: openAiModels[0],
				}
			}

return {
models: {},
defaultModel: "",
Expand Down Expand Up @@ -351,7 +359,7 @@ export const getOptionsForProvider = (provider: ProviderName, apiConfiguration?:
export const useProviderModels = (apiConfiguration?: ProviderSettings) => {
const provider = apiConfiguration?.apiProvider || "anthropic"

const { kilocodeDefaultModel } = useExtensionState()
const { kilocodeDefaultModel, openAiModels } = useExtensionState()

const routerModels = useRouterModels({
openRouterBaseUrl: apiConfiguration?.openRouterBaseUrl,
Expand All @@ -375,6 +383,7 @@ export const useProviderModels = (apiConfiguration?: ProviderSettings) => {
provider,
routerModels: routerModels.data,
kilocodeDefaultModel,
openAiModels,
options,
})
: FALLBACK_MODELS
Expand Down
39 changes: 39 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useDebounce } from "react-use"

import {
type ProviderSettings,
Expand Down Expand Up @@ -220,6 +221,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setIncludeCurrentTime: (value: boolean) => void
includeCurrentCost?: boolean
setIncludeCurrentCost: (value: boolean) => void
openAiModels?: string[]
}

export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
Expand Down Expand Up @@ -360,6 +362,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
openRouterImageGenerationSelectedModel: "",
includeCurrentTime: true,
includeCurrentCost: true,
openAiModels: [],
})

const [didHydrateState, setDidHydrateState] = useState(false)
Expand Down Expand Up @@ -518,6 +521,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setExtensionRouterModels(message.routerModels)
break
}
case "openAiModels": {
setState((prevState) => ({ ...prevState, openAiModels: message.openAiModels ?? [] }))
break
}
case "marketplaceData": {
if (message.marketplaceItems !== undefined) {
setMarketplaceItems(message.marketplaceItems)
Expand Down Expand Up @@ -554,6 +561,38 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setPrevCloudIsAuthenticated(currentAuth)
}, [state.cloudIsAuthenticated, prevCloudIsAuthenticated, state.apiConfiguration?.apiProvider])

// Fetch OpenAI models on startup or when configuration changes
useDebounce(
() => {
if (!didHydrateState) {
return
}

const { apiProvider, openAiBaseUrl, openAiApiKey, openAiHeaders } = state.apiConfiguration || {}

if (apiProvider === "openai" || apiProvider === "openai-responses") {
if (openAiBaseUrl) {
vscode.postMessage({
type: "requestOpenAiModels",
values: {
baseUrl: openAiBaseUrl,
apiKey: openAiApiKey,
openAiHeaders,
},
})
}
}
},
500,
[
didHydrateState,
state.apiConfiguration?.apiProvider,
state.apiConfiguration?.openAiBaseUrl,
state.apiConfiguration?.openAiApiKey,
state.apiConfiguration?.openAiHeaders,
],
)
Comment on lines +565 to +594
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of the useDebounce hook for fetching OpenAI models doesn't clear the openAiModels state when the user switches to a different provider or removes the openAiBaseUrl. This can lead to stale data being displayed in the UI, which can be confusing. To improve the user experience, the model list should be cleared in these scenarios.

	useDebounce(
		() => {
			if (!didHydrateState) {
				return
			}

			const { apiProvider, openAiBaseUrl, openAiApiKey, openAiHeaders } = state.apiConfiguration || {}

			if (apiProvider === "openai" || apiProvider === "openai-responses") {
				if (openAiBaseUrl) {
					vscode.postMessage({
						type: "requestOpenAiModels",
						values: {
							baseUrl: openAiBaseUrl,
							apiKey: openAiApiKey,
							openAiHeaders,
						},
					})
				} else if (state.openAiModels?.length) {
					// Clear models if base URL is removed for the relevant provider
					setState((prevState) => ({ ...prevState, openAiModels: [] }))
				}
			} else if (state.openAiModels?.length) {
				// Clear models if provider is not OpenAI-compatible
				setState((prevState) => ({ ...prevState, openAiModels: [] }))
			}
		},
		500,
		[
			didHydrateState,
			state.apiConfiguration?.apiProvider,
			state.apiConfiguration?.openAiBaseUrl,
			state.apiConfiguration?.openAiApiKey,
			state.apiConfiguration?.openAiHeaders,
		],
	)


const contextValue: ExtensionStateContextType = {
...state,
reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true,
Expand Down