diff --git a/packages/types/src/providers/openai-codex.ts b/packages/types/src/providers/openai-codex.ts index e9cf5e170c2..051ef4f138e 100644 --- a/packages/types/src/providers/openai-codex.ts +++ b/packages/types/src/providers/openai-codex.ts @@ -41,6 +41,23 @@ export const openAiCodexModels = { supportsTemperature: false, description: "GPT-5.1 Codex Max: Maximum capability coding model via ChatGPT subscription", }, + "gpt-5.1-codex": { + maxTokens: 128000, + contextWindow: 400000, + supportsNativeTools: true, + defaultToolProtocol: "native", + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["low", "medium", "high"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsTemperature: false, + description: "GPT-5.1 Codex: GPT-5.1 optimized for agentic coding via ChatGPT subscription", + }, "gpt-5.2-codex": { maxTokens: 128000, contextWindow: 400000, @@ -57,6 +74,76 @@ export const openAiCodexModels = { supportsTemperature: false, description: "GPT-5.2 Codex: OpenAI's flagship coding model via ChatGPT subscription", }, + "gpt-5.1": { + maxTokens: 128000, + contextWindow: 400000, + supportsNativeTools: true, + defaultToolProtocol: "native", + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsVerbosity: true, + supportsTemperature: false, + description: "GPT-5.1: General GPT-5.1 model via ChatGPT subscription", + }, + "gpt-5": { + maxTokens: 128000, + contextWindow: 400000, + supportsNativeTools: true, + defaultToolProtocol: "native", + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["minimal", "low", "medium", "high"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsVerbosity: true, + supportsTemperature: false, + description: "GPT-5: General GPT-5 model via ChatGPT subscription", + }, + "gpt-5-codex": { + maxTokens: 128000, + contextWindow: 400000, + supportsNativeTools: true, + defaultToolProtocol: "native", + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["low", "medium", "high"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsTemperature: false, + description: "GPT-5 Codex: GPT-5 optimized for agentic coding via ChatGPT subscription", + }, + "gpt-5-codex-mini": { + maxTokens: 128000, + contextWindow: 400000, + supportsNativeTools: true, + defaultToolProtocol: "native", + includedTools: ["apply_patch"], + excludedTools: ["apply_diff", "write_to_file"], + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["low", "medium", "high"], + reasoningEffort: "medium", + // Subscription-based: no per-token costs + inputPrice: 0, + outputPrice: 0, + supportsTemperature: false, + description: "GPT-5 Codex Mini: Faster coding model via ChatGPT subscription", + }, "gpt-5.1-codex-mini": { maxTokens: 128000, contextWindow: 400000, diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts new file mode 100644 index 00000000000..f35d6e61ee7 --- /dev/null +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -0,0 +1,26 @@ +// npx vitest run api/providers/__tests__/openai-codex.spec.ts + +import { OpenAiCodexHandler } from "../openai-codex" + +describe("OpenAiCodexHandler.getModel", () => { + it.each(["gpt-5.1", "gpt-5", "gpt-5.1-codex", "gpt-5-codex", "gpt-5-codex-mini"])( + "should return specified model when a valid model id is provided: %s", + (apiModelId) => { + const handler = new OpenAiCodexHandler({ apiModelId }) + const model = handler.getModel() + + expect(model.id).toBe(apiModelId) + expect(model.info).toBeDefined() + // Default reasoning effort for GPT-5 family + expect(model.info.reasoningEffort).toBe("medium") + }, + ) + + it("should fall back to default model when an invalid model id is provided", () => { + const handler = new OpenAiCodexHandler({ apiModelId: "not-a-real-model" }) + const model = handler.getModel() + + expect(model.id).toBe("gpt-5.2-codex") + expect(model.info).toBeDefined() + }) +}) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 1012b73263f..d10e4cb3dc5 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -311,6 +311,7 @@ const ApiOptions = ({ // To address that we set the modelId to the default value for th // provider if it's not already set. const validateAndResetModel = ( + provider: ProviderName, modelId: string | undefined, field: keyof ProviderSettings, defaultValue?: string, @@ -318,11 +319,32 @@ const ApiOptions = ({ // in case we haven't set a default value for a provider if (!defaultValue) return - // only set default if no model is set, but don't reset invalid models - // let users see and decide what to do with invalid model selections - const shouldSetDefault = !modelId + // 1) If nothing is set, initialize to the provider default. + if (!modelId) { + setApiConfigurationField(field, defaultValue, false) + return + } + + // 2) If something *is* set, ensure it's valid for the newly selected provider. + // + // Without this, switching providers can leave the UI showing a model from the + // previously selected provider (including model IDs that don't exist for the + // newly selected provider). + // + // Note: We only validate providers with static model lists. + const staticModels = MODELS_BY_PROVIDER[provider] + if (!staticModels) { + return + } + + // Bedrock has a special “custom-arn” pseudo-model that isn't part of MODELS_BY_PROVIDER. + if (provider === "bedrock" && modelId === "custom-arn") { + return + } - if (shouldSetDefault) { + const filteredModels = filterModels(staticModels, provider, organizationAllowList) + const isValidModel = !!filteredModels && Object.prototype.hasOwnProperty.call(filteredModels, modelId) + if (!isValidModel) { setApiConfigurationField(field, defaultValue, false) } } @@ -381,13 +403,14 @@ const ApiOptions = ({ const config = PROVIDER_MODEL_CONFIG[value] if (config) { validateAndResetModel( + value, apiConfiguration[config.field] as string | undefined, config.field, config.default, ) } }, - [setApiConfigurationField, apiConfiguration], + [setApiConfigurationField, apiConfiguration, organizationAllowList], ) const modelValidationError = useMemo(() => { diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index c835e3062df..c8bcc72c0af 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -4,6 +4,7 @@ import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { type ModelInfo, type ProviderSettings, openAiModelInfoSaneDefaults } from "@roo-code/types" +import { openAiCodexDefaultModelId } from "@roo-code/types" import * as ExtensionStateContext from "@src/context/ExtensionStateContext" const { ExtensionStateContextProvider } = ExtensionStateContext @@ -297,6 +298,31 @@ const renderApiOptions = (props: Partial = {}) => { } describe("ApiOptions", () => { + it("resets model to provider default when switching to openai-codex with an invalid prior apiModelId", () => { + const mockSetApiConfigurationField = vi.fn() + + renderApiOptions({ + apiConfiguration: { + apiProvider: "anthropic", + // Simulate a previously-selected model ID from another provider. + // When switching to OpenAI - ChatGPT Plus/Pro, this is invalid and should be reset. + apiModelId: "claude-3-5-sonnet-20241022", + }, + setApiConfigurationField: mockSetApiConfigurationField, + }) + + const providerSelectContainer = screen.getByTestId("provider-select") + const providerSelect = providerSelectContainer.querySelector("select") as HTMLSelectElement + expect(providerSelect).toBeInTheDocument() + + fireEvent.change(providerSelect, { target: { value: "openai-codex" } }) + + // Provider is updated + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiProvider", "openai-codex") + // Model is reset to the provider default since the previous value is invalid for this provider + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiModelId", openAiCodexDefaultModelId, false) + }) + it("shows diff settings, temperature and rate limit controls by default", () => { renderApiOptions({ apiConfiguration: {