From 82da39f659aa620d30f77f929b030c6dd218331c Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Tue, 28 Jul 2026 20:28:19 +0300 Subject: [PATCH 1/6] feat(openai-codex): add Fast priority mode --- .../src/__tests__/provider-settings.test.ts | 41 ++++++- packages/types/src/provider-settings.ts | 3 +- .../providers/__tests__/openai-codex.spec.ts | 106 ++++++++++++++++++ src/api/providers/openai-codex.ts | 11 ++ .../__tests__/ProviderSettingsManager.spec.ts | 26 +++++ .../settings/providers/OpenAICodex.tsx | 35 +++++- .../providers/__tests__/OpenAICodex.spec.tsx | 83 ++++++++++++++ webview-ui/src/i18n/locales/ca/settings.json | 6 + webview-ui/src/i18n/locales/de/settings.json | 6 + webview-ui/src/i18n/locales/en/settings.json | 6 + webview-ui/src/i18n/locales/es/settings.json | 6 + webview-ui/src/i18n/locales/fr/settings.json | 6 + webview-ui/src/i18n/locales/hi/settings.json | 6 + webview-ui/src/i18n/locales/id/settings.json | 6 + webview-ui/src/i18n/locales/it/settings.json | 6 + webview-ui/src/i18n/locales/ja/settings.json | 6 + webview-ui/src/i18n/locales/ko/settings.json | 6 + webview-ui/src/i18n/locales/nl/settings.json | 6 + webview-ui/src/i18n/locales/pl/settings.json | 6 + .../src/i18n/locales/pt-BR/settings.json | 6 + webview-ui/src/i18n/locales/ru/settings.json | 6 + webview-ui/src/i18n/locales/tr/settings.json | 6 + webview-ui/src/i18n/locales/vi/settings.json | 6 + .../src/i18n/locales/zh-CN/settings.json | 6 + .../src/i18n/locales/zh-TW/settings.json | 6 + 25 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 724fc20f34..f2da349870 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -1,4 +1,43 @@ -import { getApiProtocol } from "../provider-settings.js" +import { + getApiProtocol, + PROVIDER_SETTINGS_KEYS, + providerSettingsSchema, + providerSettingsSchemaDiscriminated, +} from "../provider-settings.js" + +describe("OpenAI Codex provider settings", () => { + it("preserves the Fast preference in general and provider-specific schemas", () => { + const settings = { + apiProvider: "openai-codex" as const, + apiModelId: "gpt-5.6-sol", + openAiCodexServiceTier: "priority" as const, + } + + expect(providerSettingsSchema.parse(settings)).toEqual(settings) + expect(providerSettingsSchemaDiscriminated.parse(settings)).toEqual(settings) + expect(PROVIDER_SETTINGS_KEYS).toContain("openAiCodexServiceTier") + }) + + it.each([undefined, "default"])("accepts %s as the Standard preference", (openAiCodexServiceTier) => { + const standardSettings = { + apiProvider: "openai-codex" as const, + apiModelId: "gpt-5.6-sol", + ...(openAiCodexServiceTier ? { openAiCodexServiceTier } : {}), + } + + expect(providerSettingsSchemaDiscriminated.parse(standardSettings)).toEqual(standardSettings) + }) + + it("rejects unsupported service tiers", () => { + expect( + providerSettingsSchemaDiscriminated.safeParse({ + apiProvider: "openai-codex", + apiModelId: "gpt-5.6-sol", + openAiCodexServiceTier: "flex", + }).success, + ).toBe(false) + }) +}) describe("getApiProtocol", () => { describe("Anthropic-style providers", () => { diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 3898c65bcc..459dabd79a 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -277,7 +277,8 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({ }) const openAiCodexSchema = apiModelIdProviderModelSchema.extend({ - // No additional settings needed - uses OAuth authentication + // Codex "Fast" mode maps to the Responses API priority service tier. + openAiCodexServiceTier: z.enum(["default", "priority"]).optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index c4249f4183..1d875e303e 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -76,6 +76,78 @@ describe("OpenAiCodexHandler.getModel", () => { }) describe("OpenAiCodexHandler.createMessage", () => { + afterEach(() => { + vitest.restoreAllMocks() + vitest.unstubAllGlobals() + }) + + it("sends the priority service tier in streaming SDK requests when Fast is selected", async () => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + openAiCodexServiceTier: "priority", + }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) + Reflect.set(handler, "client", { responses: { create: mockCreate } }) + + await drainStream(handler.createMessage("System prompt", [])) + + const [body] = mockCreate.mock.calls[0] + expect(body).toMatchObject({ stream: true, service_tier: "priority" }) + }) + + it.each([ + ["an absent preference", {}], + ["an explicit Standard preference from an older profile", { openAiCodexServiceTier: "default" }], + ])("omits the service tier in streaming SDK requests for %s", async (_description, serviceTierOptions) => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + ...serviceTierOptions, + } as ConstructorParameters[0]) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) + Reflect.set(handler, "client", { responses: { create: mockCreate } }) + + await drainStream(handler.createMessage("System prompt", [])) + + expect(mockCreate.mock.calls[0][0]).not.toHaveProperty("service_tier") + }) + + it("preserves the priority service tier in the manual streaming fallback", async () => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + openAiCodexServiceTier: "priority", + }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + Reflect.set(handler, "client", { + responses: { create: vitest.fn().mockRejectedValue(new Error("SDK unavailable")) }, + }) + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"response.completed","response":{"output":[],"usage":{"input_tokens":1,"output_tokens":1}}}\n\n', + ), + ) + controller.close() + }, + }), + }) + vitest.stubGlobal("fetch", mockFetch) + + await drainStream(handler.createMessage("System prompt", [])) + + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchObject({ + stream: true, + service_tier: "priority", + }) + }) + it("should skip URL-sourced images in formatFullConversation (only base64 emits input_image)", async () => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.1-codex" }) @@ -176,6 +248,40 @@ describe("OpenAiCodexHandler.createMessage", () => { }) }) +describe("OpenAiCodexHandler.completePrompt service tier", () => { + afterEach(() => { + vitest.restoreAllMocks() + vitest.unstubAllGlobals() + }) + + it.each([ + ["Fast", "priority", "priority"], + ["Standard", undefined, undefined], + ])("uses the %s preference in non-streaming requests", async (_mode, configuredTier, expectedTier) => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + ...(configuredTier ? { openAiCodexServiceTier: configuredTier as "priority" } : {}), + }) + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + json: vitest.fn().mockResolvedValue({ text: "Complete" }), + }) + vitest.stubGlobal("fetch", mockFetch) + + await expect(handler.completePrompt("Hello")).resolves.toBe("Complete") + + const body = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(body.stream).toBe(false) + if (expectedTier) { + expect(body.service_tier).toBe(expectedTier) + } else { + expect(body).not.toHaveProperty("service_tier") + } + }) +}) + describe("transformLunaResponsesLiteBody", () => { it("creates the exact Responses Lite body while preserving unrelated fields and reasoning", () => { const tools = [{ type: "function", name: "read_file", parameters: { type: "object" } }] diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 27ddbe19e2..3f2f01aa17 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -29,6 +29,8 @@ import { t } from "../../i18n" export type OpenAiCodexModel = ReturnType +type OpenAiCodexRequestServiceTier = "priority" + /** * OpenAI Codex base URL for API requests * Per the implementation guide: requests are routed to chatgpt.com/backend-api/codex @@ -37,6 +39,10 @@ const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api/codex" const LUNA_MODEL_ID = "gpt-5.6-luna" const LUNA_CODEX_VERSION = "0.144.0" +function getOpenAiCodexServiceTier(options: ApiHandlerOptions): OpenAiCodexRequestServiceTier | undefined { + return options.openAiCodexServiceTier === "priority" ? "priority" : undefined +} + function stripInputImageDetail(value: any): any { if (Array.isArray(value)) { return value.map(stripInputImageDetail) @@ -365,6 +371,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion model: string input: Array<{ role: "user" | "assistant"; content: any[] } | { type: string; content: string }> stream: boolean + service_tier?: OpenAiCodexRequestServiceTier reasoning?: { effort?: ReasoningEffortExtended; summary?: "auto" } temperature?: number store?: boolean @@ -383,12 +390,14 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Per the implementation guide: Codex backend may reject max_output_tokens // and prompt_cache_retention, so we omit them + const serviceTier = getOpenAiCodexServiceTier(this.options) const body: ResponsesRequestBody = { model: model.id, input: formattedInput, stream: true, store: false, instructions: systemPrompt, + ...(serviceTier ? { service_tier: serviceTier } : {}), // Only include encrypted reasoning content when reasoning effort is set ...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}), ...(reasoningEffort @@ -1261,6 +1270,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } const reasoningEffort = this.getReasoningEffort(model) + const serviceTier = getOpenAiCodexServiceTier(this.options) const baseRequestBody: any = { model: model.id, @@ -1272,6 +1282,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion ], stream: false, store: false, + ...(serviceTier ? { service_tier: serviceTier } : {}), ...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}), } diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index c6bd19c0b1..3ca503601f 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -451,6 +451,32 @@ describe("ProviderSettingsManager", () => { expect(storedConfig).toEqual(expectedConfig) }) + it.each(["default", "priority"] as const)( + "should persist the OpenAI Codex %s speed preference", + async (openAiCodexServiceTier) => { + mockSecrets.get.mockResolvedValue( + JSON.stringify({ + currentApiConfigName: "default", + apiConfigs: { default: {} }, + modeApiConfigs: {}, + }), + ) + + await providerSettingsManager.saveConfig("codex", { + apiProvider: "openai-codex", + apiModelId: "gpt-5.6-sol", + openAiCodexServiceTier, + }) + + const storedProfiles = JSON.parse(mockSecrets.store.mock.calls.at(-1)?.[1]) + expect(storedProfiles.apiConfigs.codex).toMatchObject({ + apiProvider: "openai-codex", + apiModelId: "gpt-5.6-sol", + openAiCodexServiceTier, + }) + }, + ) + it("should only save provider relevant settings", async () => { mockSecrets.get.mockResolvedValue( JSON.stringify({ diff --git a/webview-ui/src/components/settings/providers/OpenAICodex.tsx b/webview-ui/src/components/settings/providers/OpenAICodex.tsx index 755b272702..686ac36de5 100644 --- a/webview-ui/src/components/settings/providers/OpenAICodex.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICodex.tsx @@ -3,7 +3,15 @@ import React from "react" import { type ProviderSettings, openAiCodexDefaultModelId, openAiCodexModels } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { Button } from "@src/components/ui" +import { + Button, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + StandardTooltip, +} from "@src/components/ui" import { vscode } from "@src/utils/vscode" import { ModelPicker } from "../ModelPicker" @@ -66,6 +74,31 @@ export const OpenAICodex: React.FC = ({ simplifySettings={simplifySettings} hidePricing /> + +
+
+ + + + +
+ +
) } diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx new file mode 100644 index 0000000000..4819ed175c --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx @@ -0,0 +1,83 @@ +import React from "react" + +import type { ProviderSettings } from "@roo-code/types" + +import { fireEvent, render, screen } from "@/utils/test-utils" + +import { OpenAICodex } from "../OpenAICodex" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => + ({ + "settings:openAiCodexSpeed.label": "Speed", + "settings:openAiCodexSpeed.tooltip": + "Fast uses Codex priority processing for about 1.5x speed and consumes more subscription quota.", + "settings:openAiCodexSpeed.standard": "Standard", + "settings:openAiCodexSpeed.fast": "Fast (1.5x speed, increased usage)", + })[key] ?? key, + }), +})) + +vi.mock("@src/components/ui", () => ({ + Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( + + ), + Select: ({ children, value, onValueChange }: any) => ( + + ), + SelectContent: ({ children }: any) => <>{children}, + SelectItem: ({ children, value }: any) => , + SelectTrigger: ({ children }: any) => <>{children}, + SelectValue: () => null, + StandardTooltip: ({ children, content }: any) => {children}, +})) + +vi.mock("../../ModelPicker", () => ({ + ModelPicker: () =>
, +})) + +vi.mock("../OpenAICodexRateLimitDashboard", () => ({ + OpenAICodexRateLimitDashboard: () => null, +})) + +vi.mock("@src/utils/vscode", () => ({ + vscode: { postMessage: vi.fn() }, +})) + +describe("OpenAICodex speed selector", () => { + const renderSelector = (apiConfiguration: ProviderSettings, setApiConfigurationField = vi.fn()) => { + render() + return { setApiConfigurationField, selector: screen.getByRole("combobox", { name: "Speed" }) } + } + + it("defaults to Standard and clearly explains the Fast quota trade-off", () => { + const { selector } = renderSelector({ apiProvider: "openai-codex" }) + + expect(selector).toHaveValue("default") + expect(screen.getByRole("option", { name: "Standard" })).toBeInTheDocument() + expect(screen.getByRole("option", { name: "Fast (1.5x speed, increased usage)" })).toBeInTheDocument() + expect( + screen.getByTitle( + "Fast uses Codex priority processing for about 1.5x speed and consumes more subscription quota.", + ), + ).toBeInTheDocument() + }) + + it("selects Fast from a saved preference and persists changes through the settings callback", () => { + const { selector, setApiConfigurationField } = renderSelector({ + apiProvider: "openai-codex", + openAiCodexServiceTier: "priority", + }) + + expect(selector).toHaveValue("priority") + + fireEvent.change(selector, { target: { value: "default" } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiCodexServiceTier", "default") + + fireEvent.change(selector, { target: { value: "priority" } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiCodexServiceTier", "priority") + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index f861b8734c..56ad943f56 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Limitar el nombre màxim de tokens en la resposta", "maxOutputTokensLabel": "Tokens màxims de sortida", "maxTokensGenerateDescription": "Tokens màxims a generar en la resposta", + "openAiCodexSpeed": { + "label": "Velocitat", + "tooltip": "El mode ràpid utilitza el processament prioritari de Codex per oferir aproximadament 1,5 vegades més velocitat i consumeix més quota de subscripció.", + "standard": "Estàndard", + "fast": "Ràpid (1,5 vegades més velocitat, més consum)" + }, "serviceTier": { "label": "Nivell de servei", "tooltip": "Per a un processament més ràpid de les sol·licituds de l'API, proveu el nivell de servei de processament prioritari. Per a preus més baixos amb una latència més alta, proveu el nivell de processament flexible.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index e1ff092237..8af5a6ade7 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Begrenze die maximale Anzahl von Tokens in der Antwort", "maxOutputTokensLabel": "Maximale Ausgabe-Tokens", "maxTokensGenerateDescription": "Maximale Tokens, die in der Antwort generiert werden", + "openAiCodexSpeed": { + "label": "Geschwindigkeit", + "tooltip": "Der schnelle Modus nutzt die priorisierte Codex-Verarbeitung für etwa 1,5-fache Geschwindigkeit und verbraucht mehr von deinem Abonnementkontingent.", + "standard": "Standard", + "fast": "Schnell (1,5-fache Geschwindigkeit, höherer Verbrauch)" + }, "serviceTier": { "label": "Service-Stufe", "tooltip": "Für eine schnellere Verarbeitung von API-Anfragen, probiere die Prioritäts-Verarbeitungsstufe. Für niedrigere Preise bei höherer Latenz, probiere die Flex-Verarbeitungsstufe.", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 37d33f8172..3d84065849 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1126,6 +1126,12 @@ "limitMaxTokensDescription": "Limit the maximum number of tokens in the response", "maxOutputTokensLabel": "Max output tokens", "maxTokensGenerateDescription": "Maximum tokens to generate in response", + "openAiCodexSpeed": { + "label": "Speed", + "tooltip": "Fast uses Codex priority processing for about 1.5x speed and consumes more subscription quota.", + "standard": "Standard", + "fast": "Fast (1.5x speed, increased usage)" + }, "serviceTier": { "label": "Service tier", "tooltip": "For faster processing of API requests, try the priority processing service tier. For lower prices with higher latency, try the flex processing tier.", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 4a5ce59d79..3898e90f54 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Limitar el número máximo de tokens en la respuesta", "maxOutputTokensLabel": "Tokens máximos de salida", "maxTokensGenerateDescription": "Tokens máximos a generar en la respuesta", + "openAiCodexSpeed": { + "label": "Velocidad", + "tooltip": "El modo rápido usa el procesamiento prioritario de Codex para ofrecer aproximadamente 1,5 veces más velocidad y consume más cuota de suscripción.", + "standard": "Estándar", + "fast": "Rápido (1,5 veces más velocidad, mayor uso)" + }, "serviceTier": { "label": "Nivel de servicio", "tooltip": "Para un procesamiento más rápido de las solicitudes de API, prueba el nivel de servicio de procesamiento prioritario. Para precios más bajos con mayor latencia, prueba el nivel de procesamiento flexible.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 03c64cc1e9..e5c3293e72 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Limiter le nombre maximum de tokens dans la réponse", "maxOutputTokensLabel": "Tokens de sortie maximum", "maxTokensGenerateDescription": "Tokens maximum à générer dans la réponse", + "openAiCodexSpeed": { + "label": "Vitesse", + "tooltip": "Le mode rapide utilise le traitement prioritaire de Codex pour une vitesse environ 1,5 fois supérieure et consomme davantage de quota d'abonnement.", + "standard": "Standard", + "fast": "Rapide (vitesse 1,5 fois supérieure, utilisation accrue)" + }, "serviceTier": { "label": "Niveau de service", "tooltip": "Pour un traitement plus rapide des demandes d'API, essayez le niveau de service de traitement prioritaire. Pour des prix plus bas avec une latence plus élevée, essayez le niveau de traitement flexible.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2f6c6fc542..3f84094af8 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "प्रतिक्रिया में टोकन की अधिकतम संख्या सीमित करें", "maxOutputTokensLabel": "अधिकतम आउटपुट टोकन", "maxTokensGenerateDescription": "प्रतिक्रिया में उत्पन्न करने के लिए अधिकतम टोकन", + "openAiCodexSpeed": { + "label": "गति", + "tooltip": "तेज़ मोड लगभग 1.5 गुना गति के लिए Codex की प्राथमिकता प्रोसेसिंग का उपयोग करता है और अधिक सदस्यता कोटा खर्च करता है।", + "standard": "मानक", + "fast": "तेज़ (1.5 गुना गति, अधिक उपयोग)" + }, "serviceTier": { "label": "सेवा स्तर", "tooltip": "API अनुरोधों के तेज़ प्रसंस्करण के लिए, प्राथमिकता प्रसंस्करण सेवा स्तर का प्रयास करें। उच्च विलंबता के साथ कम कीमतों के लिए, फ्लेक्स प्रसंस्करण स्तर का प्रयास करें।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index f98af2bc28..47eb9cd54c 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Batasi jumlah maksimum token dalam respons", "maxOutputTokensLabel": "Token output maksimum", "maxTokensGenerateDescription": "Token maksimum untuk dihasilkan dalam respons", + "openAiCodexSpeed": { + "label": "Kecepatan", + "tooltip": "Mode Cepat menggunakan pemrosesan prioritas Codex untuk kecepatan sekitar 1,5x dan memakai lebih banyak kuota langganan.", + "standard": "Standar", + "fast": "Cepat (kecepatan 1,5x, penggunaan meningkat)" + }, "serviceTier": { "label": "Tingkat layanan", "tooltip": "Untuk pemrosesan permintaan API yang lebih cepat, coba tingkat layanan pemrosesan prioritas. Untuk harga lebih rendah dengan latensi lebih tinggi, coba tingkat pemrosesan fleksibel.", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index ea66a2ad23..12d712e541 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Limita il numero massimo di token nella risposta", "maxOutputTokensLabel": "Token di output massimi", "maxTokensGenerateDescription": "Token massimi da generare nella risposta", + "openAiCodexSpeed": { + "label": "Velocità", + "tooltip": "La modalità veloce usa l'elaborazione prioritaria di Codex per una velocità circa 1,5 volte superiore e consuma più quota dell'abbonamento.", + "standard": "Standard", + "fast": "Veloce (velocità 1,5 volte superiore, utilizzo maggiore)" + }, "serviceTier": { "label": "Livello di servizio", "tooltip": "Per un'elaborazione più rapida delle richieste API, prova il livello di servizio di elaborazione prioritaria. Per prezzi più bassi con una latenza maggiore, prova il livello di elaborazione flessibile.", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index eed8315620..20f1a318ff 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "レスポンスの最大トークン数を制限する", "maxOutputTokensLabel": "最大出力トークン", "maxTokensGenerateDescription": "レスポンスで生成する最大トークン数", + "openAiCodexSpeed": { + "label": "速度", + "tooltip": "高速モードは Codex の優先処理を使用して約1.5倍の速度を実現し、サブスクリプションの利用枠をより多く消費します。", + "standard": "標準", + "fast": "高速(約1.5倍、使用量増加)" + }, "serviceTier": { "label": "サービスティア", "tooltip": "APIリクエストをより速く処理するには、優先処理サービスティアをお試しください。低価格でレイテンシが高い場合は、フレックス処理ティアをお試しください。", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index ae8fd3b9ef..073b0635b0 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "응답에서 최대 토큰 수 제한", "maxOutputTokensLabel": "최대 출력 토큰", "maxTokensGenerateDescription": "응답에서 생성할 최대 토큰 수", + "openAiCodexSpeed": { + "label": "속도", + "tooltip": "빠른 모드는 Codex 우선 처리를 사용하여 약 1.5배 빠른 속도를 제공하며 구독 할당량을 더 많이 사용합니다.", + "standard": "표준", + "fast": "빠름(1.5배 속도, 사용량 증가)" + }, "serviceTier": { "label": "서비스 등급", "tooltip": "API 요청을 더 빠르게 처리하려면 우선 처리 서비스 등급을 사용해 보세요. 더 낮은 가격에 더 높은 지연 시간을 원하시면 플렉스 처리 등급을 사용해 보세요.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 45b92345d9..7e19c938a2 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Beperk het maximale aantal tokens in het antwoord", "maxOutputTokensLabel": "Maximale output tokens", "maxTokensGenerateDescription": "Maximale tokens om te genereren in het antwoord", + "openAiCodexSpeed": { + "label": "Snelheid", + "tooltip": "De snelle modus gebruikt prioriteitsverwerking van Codex voor ongeveer 1,5 keer hogere snelheid en verbruikt meer abonnementquotum.", + "standard": "Standaard", + "fast": "Snel (1,5 keer hogere snelheid, meer verbruik)" + }, "serviceTier": { "label": "Serviceniveau", "tooltip": "Voor snellere verwerking van API-verzoeken, probeer het prioriteitsverwerkingsniveau. Voor lagere prijzen met hogere latentie, probeer het flexverwerkingsniveau.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index bf35f445dd..3499d5d49a 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Ogranicz maksymalną liczbę tokenów w odpowiedzi", "maxOutputTokensLabel": "Maksymalne tokeny wyjściowe", "maxTokensGenerateDescription": "Maksymalne tokeny do wygenerowania w odpowiedzi", + "openAiCodexSpeed": { + "label": "Szybkość", + "tooltip": "Tryb szybki korzysta z priorytetowego przetwarzania Codex, zapewniając około 1,5 raza większą szybkość i zużywając więcej limitu subskrypcji.", + "standard": "Standardowy", + "fast": "Szybki (1,5 raza szybciej, większe użycie)" + }, "serviceTier": { "label": "Poziom usług", "tooltip": "Aby szybciej przetwarzać żądania API, wypróbuj priorytetowy poziom usług. Aby uzyskać niższe ceny przy wyższej latencji, wypróbuj elastyczny poziom usług.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 99d6013a89..4b6922e8e0 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Limitar o número máximo de tokens na resposta", "maxOutputTokensLabel": "Tokens máximos de saída", "maxTokensGenerateDescription": "Tokens máximos para gerar na resposta", + "openAiCodexSpeed": { + "label": "Velocidade", + "tooltip": "O modo rápido usa o processamento prioritário do Codex para oferecer cerca de 1,5 vez mais velocidade e consome mais cota da assinatura.", + "standard": "Padrão", + "fast": "Rápido (1,5 vez mais velocidade, maior uso)" + }, "serviceTier": { "label": "Nível de serviço", "tooltip": "Para um processamento mais rápido das solicitações de API, experimente o nível de serviço de processamento prioritário. Para preços mais baixos com maior latência, experimente o nível de processamento flexível.", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index a29da12326..8a63bcbd6e 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Ограничить максимальное количество токенов в ответе", "maxOutputTokensLabel": "Максимальные выходные токены", "maxTokensGenerateDescription": "Максимальные токены для генерации в ответе", + "openAiCodexSpeed": { + "label": "Скорость", + "tooltip": "Быстрый режим использует приоритетную обработку Codex, обеспечивая примерно 1,5-кратную скорость и расходуя больше квоты подписки.", + "standard": "Стандартный", + "fast": "Быстрый (скорость 1,5×, повышенный расход)" + }, "serviceTier": { "label": "Уровень обслуживания", "tooltip": "Для более быстрой обработки запросов API попробуйте уровень обслуживания с приоритетной обработкой. Для более низких цен с более высокой задержкой попробуйте уровень гибкой обработки.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 13c0b9c37c..2f683e9b37 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Yanıttaki maksimum token sayısını sınırla", "maxOutputTokensLabel": "Maksimum çıktı tokenları", "maxTokensGenerateDescription": "Yanıtta oluşturulacak maksimum token sayısı", + "openAiCodexSpeed": { + "label": "Hız", + "tooltip": "Hızlı mod, yaklaşık 1,5 kat hız için Codex öncelikli işlemeyi kullanır ve daha fazla abonelik kotası tüketir.", + "standard": "Standart", + "fast": "Hızlı (1,5 kat hız, daha fazla kullanım)" + }, "serviceTier": { "label": "Hizmet seviyesi", "tooltip": "Daha hızlı API isteği işleme için öncelikli işleme hizmeti seviyesini deneyin. Daha düşük gecikme süresiyle daha düşük fiyatlar için esnek işleme seviyesini deneyin.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index b59313d47a..89015b24e5 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "Giới hạn số lượng token tối đa trong phản hồi", "maxOutputTokensLabel": "Token đầu ra tối đa", "maxTokensGenerateDescription": "Token tối đa để tạo trong phản hồi", + "openAiCodexSpeed": { + "label": "Tốc độ", + "tooltip": "Chế độ Nhanh sử dụng xử lý ưu tiên của Codex để đạt tốc độ nhanh hơn khoảng 1,5 lần và tiêu tốn nhiều hạn mức đăng ký hơn.", + "standard": "Tiêu chuẩn", + "fast": "Nhanh (tốc độ 1,5 lần, mức sử dụng cao hơn)" + }, "serviceTier": { "label": "Cấp độ dịch vụ", "tooltip": "Để xử lý các yêu cầu API nhanh hơn, hãy thử cấp độ dịch vụ xử lý ưu tiên. Để có giá thấp hơn với độ trễ cao hơn, hãy thử cấp độ xử lý linh hoạt.", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 1b408875d4..7d7fbb3769 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1046,6 +1046,12 @@ "limitMaxTokensDescription": "限制响应中的最大 Token 数量", "maxOutputTokensLabel": "最大输出 Token 数", "maxTokensGenerateDescription": "响应中生成的最大 Token 数", + "openAiCodexSpeed": { + "label": "速度", + "tooltip": "快速模式使用 Codex 优先处理,速度约为 1.5 倍,但会消耗更多订阅配额。", + "standard": "标准", + "fast": "快速(1.5 倍速度,用量增加)" + }, "serviceTier": { "label": "服务等级", "tooltip": "为加快API请求处理速度,请尝试优先处理服务等级。为获得更低价格但延迟较高,请尝试灵活处理等级。", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 481bab5301..8556e8b2f4 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1073,6 +1073,12 @@ "limitMaxTokensDescription": "限制回應中的最大 Token 數量", "maxOutputTokensLabel": "最大輸出 Token 數", "maxTokensGenerateDescription": "回應中產生的最大 Token 數", + "openAiCodexSpeed": { + "label": "速度", + "tooltip": "快速模式使用 Codex 優先處理,速度約為 1.5 倍,但會消耗更多訂閱配額。", + "standard": "標準", + "fast": "快速(1.5 倍速度,用量增加)" + }, "serviceTier": { "label": "服務層級", "tooltip": "若需更快的 API 請求處理,請嘗試優先處理服務層級。若需較低價格但延遲較高,請嘗試彈性處理層級。", From 42de997a47a82af1e347a3ec16140f6d7c4b0375 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Tue, 28 Jul 2026 21:05:45 +0300 Subject: [PATCH 2/6] refactor(openai): centralize service tier values --- .../src/__tests__/provider-settings.test.ts | 29 ++++++++------- packages/types/src/model.ts | 10 ++++-- packages/types/src/provider-settings.ts | 10 ++++-- .../providers/__tests__/openai-codex.spec.ts | 20 ++++++----- .../__tests__/openai-native-usage.spec.ts | 4 +-- .../providers/__tests__/openai-native.spec.ts | 4 +-- src/api/providers/openai-codex.ts | 8 ++--- src/api/providers/openai-native.ts | 7 ++-- .../__tests__/ProviderSettingsManager.spec.ts | 8 ++--- src/shared/cost.ts | 5 ++- src/utils/__tests__/cost.spec.ts | 6 ++-- .../src/components/settings/ModelInfoView.tsx | 36 ++++++++++--------- .../components/settings/providers/OpenAI.tsx | 16 +++++---- .../settings/providers/OpenAICodex.tsx | 18 ++++++---- .../providers/__tests__/OpenAICodex.spec.tsx | 20 +++++------ 15 files changed, 116 insertions(+), 85 deletions(-) diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index f2da349870..44f97198dd 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -4,13 +4,15 @@ import { providerSettingsSchema, providerSettingsSchemaDiscriminated, } from "../provider-settings.js" +import { OpenAiServiceTier } from "../model.js" +import { providerIdentifiers } from "../provider-identifiers.js" describe("OpenAI Codex provider settings", () => { it("preserves the Fast preference in general and provider-specific schemas", () => { const settings = { - apiProvider: "openai-codex" as const, + apiProvider: providerIdentifiers.openaiCodex, apiModelId: "gpt-5.6-sol", - openAiCodexServiceTier: "priority" as const, + openAiCodexServiceTier: OpenAiServiceTier.Priority, } expect(providerSettingsSchema.parse(settings)).toEqual(settings) @@ -18,22 +20,25 @@ describe("OpenAI Codex provider settings", () => { expect(PROVIDER_SETTINGS_KEYS).toContain("openAiCodexServiceTier") }) - it.each([undefined, "default"])("accepts %s as the Standard preference", (openAiCodexServiceTier) => { - const standardSettings = { - apiProvider: "openai-codex" as const, - apiModelId: "gpt-5.6-sol", - ...(openAiCodexServiceTier ? { openAiCodexServiceTier } : {}), - } + it.each([undefined, OpenAiServiceTier.Default])( + "accepts %s as the Standard preference", + (openAiCodexServiceTier) => { + const standardSettings = { + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + ...(openAiCodexServiceTier ? { openAiCodexServiceTier } : {}), + } - expect(providerSettingsSchemaDiscriminated.parse(standardSettings)).toEqual(standardSettings) - }) + expect(providerSettingsSchemaDiscriminated.parse(standardSettings)).toEqual(standardSettings) + }, + ) it("rejects unsupported service tiers", () => { expect( providerSettingsSchemaDiscriminated.safeParse({ - apiProvider: "openai-codex", + apiProvider: providerIdentifiers.openaiCodex, apiModelId: "gpt-5.6-sol", - openAiCodexServiceTier: "flex", + openAiCodexServiceTier: OpenAiServiceTier.Flex, }).success, ).toBe(false) }) diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 257a30f3e7..7f15eef926 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -57,9 +57,15 @@ export type VerbosityLevel = z.infer /** * Service tiers (OpenAI Responses API) */ -export const serviceTiers = ["default", "flex", "priority"] as const +export enum OpenAiServiceTier { + Default = "default", + Flex = "flex", + Priority = "priority", +} + +export const serviceTiers = Object.values(OpenAiServiceTier) as [`${OpenAiServiceTier}`, ...`${OpenAiServiceTier}`[]] export const serviceTierSchema = z.enum(serviceTiers) -export type ServiceTier = z.infer +export type ServiceTier = `${OpenAiServiceTier}` /** * ModelParameter diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 459dabd79a..decd981bc5 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -1,6 +1,12 @@ import { z } from "zod" -import { modelInfoSchema, reasoningEffortSettingSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js" +import { + modelInfoSchema, + OpenAiServiceTier, + reasoningEffortSettingSchema, + verbosityLevelsSchema, + serviceTierSchema, +} from "./model.js" import { codebaseIndexProviderSchema } from "./codebase-index.js" import { providerIdentifiers, @@ -278,7 +284,7 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({ const openAiCodexSchema = apiModelIdProviderModelSchema.extend({ // Codex "Fast" mode maps to the Responses API priority service tier. - openAiCodexServiceTier: z.enum(["default", "priority"]).optional(), + openAiCodexServiceTier: z.enum([OpenAiServiceTier.Default, OpenAiServiceTier.Priority]).optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index 1d875e303e..ef004ffb70 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -9,6 +9,7 @@ vitest.mock("@roo-code/telemetry", () => ({ })) import { Anthropic } from "@anthropic-ai/sdk" +import { OpenAiServiceTier } from "@roo-code/types" import { OpenAiCodexHandler, transformLunaResponsesLiteBody } from "../openai-codex" import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" @@ -84,7 +85,7 @@ describe("OpenAiCodexHandler.createMessage", () => { it("sends the priority service tier in streaming SDK requests when Fast is selected", async () => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-sol", - openAiCodexServiceTier: "priority", + openAiCodexServiceTier: OpenAiServiceTier.Priority, }) vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") @@ -94,12 +95,15 @@ describe("OpenAiCodexHandler.createMessage", () => { await drainStream(handler.createMessage("System prompt", [])) const [body] = mockCreate.mock.calls[0] - expect(body).toMatchObject({ stream: true, service_tier: "priority" }) + expect(body).toMatchObject({ stream: true, service_tier: OpenAiServiceTier.Priority }) }) it.each([ ["an absent preference", {}], - ["an explicit Standard preference from an older profile", { openAiCodexServiceTier: "default" }], + [ + "an explicit Standard preference from an older profile", + { openAiCodexServiceTier: OpenAiServiceTier.Default }, + ], ])("omits the service tier in streaming SDK requests for %s", async (_description, serviceTierOptions) => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-sol", @@ -118,7 +122,7 @@ describe("OpenAiCodexHandler.createMessage", () => { it("preserves the priority service tier in the manual streaming fallback", async () => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-sol", - openAiCodexServiceTier: "priority", + openAiCodexServiceTier: OpenAiServiceTier.Priority, }) vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") @@ -144,7 +148,7 @@ describe("OpenAiCodexHandler.createMessage", () => { expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchObject({ stream: true, - service_tier: "priority", + service_tier: OpenAiServiceTier.Priority, }) }) @@ -254,13 +258,13 @@ describe("OpenAiCodexHandler.completePrompt service tier", () => { vitest.unstubAllGlobals() }) - it.each([ - ["Fast", "priority", "priority"], + it.each<[string, OpenAiServiceTier.Priority | undefined, OpenAiServiceTier.Priority | undefined]>([ + ["Fast", OpenAiServiceTier.Priority, OpenAiServiceTier.Priority], ["Standard", undefined, undefined], ])("uses the %s preference in non-streaming requests", async (_mode, configuredTier, expectedTier) => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-sol", - ...(configuredTier ? { openAiCodexServiceTier: configuredTier as "priority" } : {}), + ...(configuredTier ? { openAiCodexServiceTier: configuredTier } : {}), }) vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") diff --git a/src/api/providers/__tests__/openai-native-usage.spec.ts b/src/api/providers/__tests__/openai-native-usage.spec.ts index a266642e7a..184f04ec80 100644 --- a/src/api/providers/__tests__/openai-native-usage.spec.ts +++ b/src/api/providers/__tests__/openai-native-usage.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from "vitest" import { OpenAiNativeHandler } from "../openai-native" -import { openAiNativeModels } from "@roo-code/types" +import { OpenAiServiceTier, openAiNativeModels } from "@roo-code/types" describe("OpenAiNativeHandler - normalizeUsage", () => { let handler: OpenAiNativeHandler @@ -468,7 +468,7 @@ describe("OpenAiNativeHandler - normalizeUsage", () => { it("should not apply GPT-5.4 long-context pricing to priority tier", () => { handler = new OpenAiNativeHandler({ openAiNativeApiKey: "test-key", - openAiNativeServiceTier: "priority", + openAiNativeServiceTier: OpenAiServiceTier.Priority, }) const usage = { diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 1acb4101be..4ef4c7cd27 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -13,7 +13,7 @@ vitest.mock("@roo-code/telemetry", () => ({ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { ApiProviderError } from "@roo-code/types" +import { ApiProviderError, OpenAiServiceTier } from "@roo-code/types" import { OpenAiNativeHandler } from "../openai-native" import { ApiHandlerOptions } from "../../../shared/api" @@ -332,7 +332,7 @@ describe("OpenAiNativeHandler", () => { expect(modelInfo.info.longContextPricing).toBeUndefined() expect(modelInfo.info.tiers).toEqual([ expect.objectContaining({ - name: "flex", + name: OpenAiServiceTier.Flex, outputPrice: 0.625, }), ]) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 3f2f01aa17..e93e26e703 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -5,6 +5,7 @@ import OpenAI from "openai" import { type ModelInfo, + OpenAiServiceTier, openAiCodexDefaultModelId, OpenAiCodexModelId, openAiCodexModels, @@ -29,7 +30,7 @@ import { t } from "../../i18n" export type OpenAiCodexModel = ReturnType -type OpenAiCodexRequestServiceTier = "priority" +type OpenAiCodexRequestServiceTier = OpenAiServiceTier.Priority /** * OpenAI Codex base URL for API requests @@ -39,9 +40,8 @@ const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api/codex" const LUNA_MODEL_ID = "gpt-5.6-luna" const LUNA_CODEX_VERSION = "0.144.0" -function getOpenAiCodexServiceTier(options: ApiHandlerOptions): OpenAiCodexRequestServiceTier | undefined { - return options.openAiCodexServiceTier === "priority" ? "priority" : undefined -} +const getOpenAiCodexServiceTier = (options: ApiHandlerOptions): OpenAiCodexRequestServiceTier | undefined => + options.openAiCodexServiceTier === OpenAiServiceTier.Priority ? OpenAiServiceTier.Priority : undefined function stripInputImageDetail(value: any): any { if (Array.isArray(value)) { diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index a1ce2d89d0..3f30548211 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -13,6 +13,7 @@ import { type ReasoningEffort, type VerbosityLevel, type ReasoningEffortExtended, + OpenAiServiceTier, type ServiceTier, ApiProviderError, } from "@roo-code/types" @@ -369,7 +370,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio ...(model.maxTokens ? { max_output_tokens: model.maxTokens } : {}), // Include tier when selected and supported by the model, or when explicitly "default" ...(requestedTier && - (requestedTier === "default" || allowedTierNames.has(requestedTier)) && { + (requestedTier === OpenAiServiceTier.Default || allowedTierNames.has(requestedTier)) && { service_tier: requestedTier, }), // Enable extended prompt cache retention for models that support it. @@ -1418,7 +1419,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio * If no tier or no overrides exist, the original ModelInfo is returned. */ private applyServiceTierPricing(info: ModelInfo, tier?: ServiceTier): ModelInfo { - if (!tier || tier === "default") return info + if (!tier || tier === OpenAiServiceTier.Default) return info // Find the tier with matching name in the tiers array const tierInfo = info.tiers?.find((t) => t.name === tier) @@ -1512,7 +1513,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Include service tier if selected and supported const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || []) - if (requestedTier && (requestedTier === "default" || allowedTierNames.has(requestedTier))) { + if (requestedTier && (requestedTier === OpenAiServiceTier.Default || allowedTierNames.has(requestedTier))) { requestBody.service_tier = requestedTier } diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index 3ca503601f..28d1dcbbad 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -2,7 +2,7 @@ import { ExtensionContext } from "vscode" -import type { ProviderSettings } from "@roo-code/types" +import { OpenAiServiceTier, providerIdentifiers, type ProviderSettings } from "@roo-code/types" import { ProviderSettingsManager, ProviderProfiles, SyncCloudProfilesResult } from "../ProviderSettingsManager" @@ -451,7 +451,7 @@ describe("ProviderSettingsManager", () => { expect(storedConfig).toEqual(expectedConfig) }) - it.each(["default", "priority"] as const)( + it.each([OpenAiServiceTier.Default, OpenAiServiceTier.Priority] as const)( "should persist the OpenAI Codex %s speed preference", async (openAiCodexServiceTier) => { mockSecrets.get.mockResolvedValue( @@ -463,14 +463,14 @@ describe("ProviderSettingsManager", () => { ) await providerSettingsManager.saveConfig("codex", { - apiProvider: "openai-codex", + apiProvider: providerIdentifiers.openaiCodex, apiModelId: "gpt-5.6-sol", openAiCodexServiceTier, }) const storedProfiles = JSON.parse(mockSecrets.store.mock.calls.at(-1)?.[1]) expect(storedProfiles.apiConfigs.codex).toMatchObject({ - apiProvider: "openai-codex", + apiProvider: providerIdentifiers.openaiCodex, apiModelId: "gpt-5.6-sol", openAiCodexServiceTier, }) diff --git a/src/shared/cost.ts b/src/shared/cost.ts index 8954904fda..754a2d84df 100644 --- a/src/shared/cost.ts +++ b/src/shared/cost.ts @@ -1,5 +1,4 @@ -import type { ModelInfo } from "@roo-code/types" -import type { ServiceTier } from "@roo-code/types" +import { OpenAiServiceTier, type ModelInfo, type ServiceTier } from "@roo-code/types" export interface ApiCostResult { totalInputTokens: number @@ -13,7 +12,7 @@ function applyLongContextPricing(modelInfo: ModelInfo, totalInputTokens: number, return modelInfo } - const effectiveServiceTier = serviceTier ?? "default" + const effectiveServiceTier = serviceTier ?? OpenAiServiceTier.Default if (pricing.appliesToServiceTiers && !pricing.appliesToServiceTiers.includes(effectiveServiceTier)) { return modelInfo } diff --git a/src/utils/__tests__/cost.spec.ts b/src/utils/__tests__/cost.spec.ts index 6f0b594c8d..b369a0405b 100644 --- a/src/utils/__tests__/cost.spec.ts +++ b/src/utils/__tests__/cost.spec.ts @@ -1,6 +1,6 @@ // npx vitest utils/__tests__/cost.spec.ts -import type { ModelInfo } from "@roo-code/types" +import { OpenAiServiceTier, type ModelInfo } from "@roo-code/types" import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../../shared/cost" @@ -283,7 +283,7 @@ describe("Cost Utility", () => { thresholdTokens: 272_000, inputPriceMultiplier: 2, outputPriceMultiplier: 1.5, - appliesToServiceTiers: ["default", "flex"], + appliesToServiceTiers: [OpenAiServiceTier.Default, OpenAiServiceTier.Flex], }, } @@ -293,7 +293,7 @@ describe("Cost Utility", () => { 1_000, undefined, 100_000, - "priority", + OpenAiServiceTier.Priority, ) // Input cost: (5.0 / 1_000_000) * (300000 - 100000) = 1.0 diff --git a/webview-ui/src/components/settings/ModelInfoView.tsx b/webview-ui/src/components/settings/ModelInfoView.tsx index e043f68f83..bbf9fa39b2 100644 --- a/webview-ui/src/components/settings/ModelInfoView.tsx +++ b/webview-ui/src/components/settings/ModelInfoView.tsx @@ -1,6 +1,6 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import type { ModelInfo } from "@roo-code/types" +import { OpenAiServiceTier, providerIdentifiers, type ModelInfo } from "@roo-code/types" import { formatPrice } from "@src/utils/formatPrice" import { cn } from "@src/lib/utils" @@ -29,8 +29,10 @@ export const ModelInfoView = ({ // Show tiered pricing table for OpenAI Native when model supports non-standard tiers const allowedTierNames = - modelInfo?.tiers?.filter((t) => t.name === "flex" || t.name === "priority")?.map((t) => t.name) ?? [] - const shouldShowTierPricingTable = apiProvider === "openai-native" && allowedTierNames.length > 0 + modelInfo?.tiers + ?.filter((t) => t.name === OpenAiServiceTier.Flex || t.name === OpenAiServiceTier.Priority) + ?.map((t) => t.name) ?? [] + const shouldShowTierPricingTable = apiProvider === providerIdentifiers.openaiNative && allowedTierNames.length > 0 const fmt = (n?: number) => (typeof n === "number" ? `${formatPrice(n)}` : "—") const baseInfoItems = [ @@ -144,48 +146,48 @@ export const ModelInfoView = ({ {fmt(modelInfo?.outputPrice)} {fmt(modelInfo?.cacheReadsPrice)} - {allowedTierNames.includes("flex") && ( + {allowedTierNames.includes(OpenAiServiceTier.Flex) && ( {t("settings:serviceTier.flex")} {fmt( - modelInfo?.tiers?.find((t) => t.name === "flex")?.inputPrice ?? - modelInfo?.inputPrice, + modelInfo?.tiers?.find((t) => t.name === OpenAiServiceTier.Flex) + ?.inputPrice ?? modelInfo?.inputPrice, )} {fmt( - modelInfo?.tiers?.find((t) => t.name === "flex")?.outputPrice ?? - modelInfo?.outputPrice, + modelInfo?.tiers?.find((t) => t.name === OpenAiServiceTier.Flex) + ?.outputPrice ?? modelInfo?.outputPrice, )} {fmt( - modelInfo?.tiers?.find((t) => t.name === "flex")?.cacheReadsPrice ?? - modelInfo?.cacheReadsPrice, + modelInfo?.tiers?.find((t) => t.name === OpenAiServiceTier.Flex) + ?.cacheReadsPrice ?? modelInfo?.cacheReadsPrice, )} )} - {allowedTierNames.includes("priority") && ( + {allowedTierNames.includes(OpenAiServiceTier.Priority) && ( {t("settings:serviceTier.priority")} {fmt( - modelInfo?.tiers?.find((t) => t.name === "priority")?.inputPrice ?? - modelInfo?.inputPrice, + modelInfo?.tiers?.find((t) => t.name === OpenAiServiceTier.Priority) + ?.inputPrice ?? modelInfo?.inputPrice, )} {fmt( - modelInfo?.tiers?.find((t) => t.name === "priority")?.outputPrice ?? - modelInfo?.outputPrice, + modelInfo?.tiers?.find((t) => t.name === OpenAiServiceTier.Priority) + ?.outputPrice ?? modelInfo?.outputPrice, )} {fmt( - modelInfo?.tiers?.find((t) => t.name === "priority")?.cacheReadsPrice ?? - modelInfo?.cacheReadsPrice, + modelInfo?.tiers?.find((t) => t.name === OpenAiServiceTier.Priority) + ?.cacheReadsPrice ?? modelInfo?.cacheReadsPrice, )} diff --git a/webview-ui/src/components/settings/providers/OpenAI.tsx b/webview-ui/src/components/settings/providers/OpenAI.tsx index 96fd6c89be..460019e75a 100644 --- a/webview-ui/src/components/settings/providers/OpenAI.tsx +++ b/webview-ui/src/components/settings/providers/OpenAI.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react" import { Checkbox } from "vscrui" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import type { ModelInfo, ProviderSettings } from "@roo-code/types" +import { OpenAiServiceTier, type ModelInfo, type ProviderSettings } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" @@ -78,7 +78,7 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedMod {(() => { const allowedTiers = (selectedModelInfo?.tiers?.map((t) => t.name).filter(Boolean) || []).filter( - (t) => t === "flex" || t === "priority", + (t) => t === OpenAiServiceTier.Flex || t === OpenAiServiceTier.Priority, ) if (allowedTiers.length === 0) return null @@ -92,7 +92,7 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedMod
diff --git a/webview-ui/src/components/settings/providers/OpenAICodex.tsx b/webview-ui/src/components/settings/providers/OpenAICodex.tsx index 686ac36de5..283a51c6a1 100644 --- a/webview-ui/src/components/settings/providers/OpenAICodex.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICodex.tsx @@ -1,6 +1,6 @@ import React from "react" -import { type ProviderSettings, openAiCodexDefaultModelId, openAiCodexModels } from "@roo-code/types" +import { OpenAiServiceTier, type ProviderSettings, openAiCodexDefaultModelId, openAiCodexModels } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { @@ -17,6 +17,8 @@ import { vscode } from "@src/utils/vscode" import { ModelPicker } from "../ModelPicker" import { OpenAICodexRateLimitDashboard } from "./OpenAICodexRateLimitDashboard" +const OPEN_AI_CODEX_SERVICE_TIER_KEY = "openAiCodexServiceTier" + interface OpenAICodexProps { apiConfiguration: ProviderSettings setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void @@ -83,19 +85,23 @@ export const OpenAICodex: React.FC = ({ diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx index 4819ed175c..6df99c443d 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx @@ -1,6 +1,6 @@ import React from "react" -import type { ProviderSettings } from "@roo-code/types" +import { OpenAiServiceTier, providerIdentifiers, type ProviderSettings } from "@roo-code/types" import { fireEvent, render, screen } from "@/utils/test-utils" @@ -54,9 +54,9 @@ describe("OpenAICodex speed selector", () => { } it("defaults to Standard and clearly explains the Fast quota trade-off", () => { - const { selector } = renderSelector({ apiProvider: "openai-codex" }) + const { selector } = renderSelector({ apiProvider: providerIdentifiers.openaiCodex }) - expect(selector).toHaveValue("default") + expect(selector).toHaveValue(OpenAiServiceTier.Default) expect(screen.getByRole("option", { name: "Standard" })).toBeInTheDocument() expect(screen.getByRole("option", { name: "Fast (1.5x speed, increased usage)" })).toBeInTheDocument() expect( @@ -68,16 +68,16 @@ describe("OpenAICodex speed selector", () => { it("selects Fast from a saved preference and persists changes through the settings callback", () => { const { selector, setApiConfigurationField } = renderSelector({ - apiProvider: "openai-codex", - openAiCodexServiceTier: "priority", + apiProvider: providerIdentifiers.openaiCodex, + openAiCodexServiceTier: OpenAiServiceTier.Priority, }) - expect(selector).toHaveValue("priority") + expect(selector).toHaveValue(OpenAiServiceTier.Priority) - fireEvent.change(selector, { target: { value: "default" } }) - expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiCodexServiceTier", "default") + fireEvent.change(selector, { target: { value: OpenAiServiceTier.Default } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiCodexServiceTier", OpenAiServiceTier.Default) - fireEvent.change(selector, { target: { value: "priority" } }) - expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiCodexServiceTier", "priority") + fireEvent.change(selector, { target: { value: OpenAiServiceTier.Priority } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiCodexServiceTier", OpenAiServiceTier.Priority) }) }) From d481f51ab1a49543f87e1d825f7ad261892de1bf Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Tue, 28 Jul 2026 21:59:29 +0300 Subject: [PATCH 3/6] test(openai): cover service tier branches --- .../providers/__tests__/openai-native.spec.ts | 66 +++++++++++++ .../settings/__tests__/ModelInfoView.spec.tsx | 97 +++++++++++++++++++ .../providers/__tests__/OpenAI.spec.tsx | 92 ++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx create mode 100644 webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 4ef4c7cd27..bd1f0afc3d 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -122,6 +122,29 @@ describe("OpenAiNativeHandler", () => { }) describe("createMessage", () => { + it.each([OpenAiServiceTier.Default, OpenAiServiceTier.Priority])( + "should include the selected %s service tier", + async (serviceTier) => { + mockResponsesCreate.mockResolvedValue({ + async *[Symbol.asyncIterator]() {}, + }) + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-sol", + openAiNativeServiceTier: serviceTier, + }) + + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + void chunk + } + + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ service_tier: serviceTier }), + expect.any(Object), + ) + }, + ) + it("should handle streaming responses via Responses API", async () => { // Mock fetch for Responses API fallback const mockFetch = vitest.fn().mockResolvedValue({ @@ -221,6 +244,28 @@ describe("OpenAiNativeHandler", () => { ) }) + it.each([OpenAiServiceTier.Default, OpenAiServiceTier.Priority])( + "should include the selected %s service tier", + async (serviceTier) => { + mockResponsesCreate.mockResolvedValue({ output: [] }) + handler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.6-sol", + openAiNativeServiceTier: serviceTier, + }) + + await handler.completePrompt("Test prompt") + + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ + stream: false, + service_tier: serviceTier, + }), + expect.any(Object), + ) + }, + ) + it("should handle SDK errors in completePrompt", async () => { // Mock SDK to throw an error mockResponsesCreate.mockRejectedValue(new Error("API Error")) @@ -338,6 +383,27 @@ describe("OpenAiNativeHandler", () => { ]) }) + it("should retain standard pricing for an explicitly selected default tier", () => { + const defaultTierHandler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-5.4", + openAiNativeServiceTier: OpenAiServiceTier.Default, + }) + const model = defaultTierHandler.getModel() + const normalizeUsage = Reflect.get(defaultTierHandler, "normalizeUsage") + + const result = Reflect.apply(normalizeUsage, defaultTierHandler, [ + { + input_tokens: 100_000, + output_tokens: 1_000, + cache_read_input_tokens: 20_000, + }, + model, + ]) as { totalCost: number } + + expect(result.totalCost).toBeCloseTo(0.22, 6) + }) + it("should return GPT-5.3 Chat model info when selected", () => { const chatHandler = new OpenAiNativeHandler({ ...mockOptions, diff --git a/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx new file mode 100644 index 0000000000..2da94ce028 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ModelInfoView.spec.tsx @@ -0,0 +1,97 @@ +import { OpenAiServiceTier, providerIdentifiers, type ModelInfo } from "@roo-code/types" + +import { render, screen, within } from "@/utils/test-utils" + +import { ModelInfoView } from "../ModelInfoView" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => + ({ + "settings:serviceTier.pricingTableTitle": "Service tier pricing", + "settings:serviceTier.columns.tier": "Tier", + "settings:serviceTier.columns.input": "Input", + "settings:serviceTier.columns.output": "Output", + "settings:serviceTier.columns.cacheReads": "Cache reads", + "settings:serviceTier.standard": "Standard", + "settings:serviceTier.flex": "Flex", + "settings:serviceTier.priority": "Priority", + })[key] ?? key, + }), +})) + +const baseModelInfo: ModelInfo = { + contextWindow: 128_000, + supportsPromptCache: true, + inputPrice: 10, + outputPrice: 20, + cacheReadsPrice: 3, +} + +const defaultProps = { + selectedModelId: "gpt-test", + isDescriptionExpanded: false, + setIsDescriptionExpanded: vi.fn(), +} + +const getPricingRowValues = (tier: string) => { + const row = screen.getByRole("cell", { name: tier }).closest("tr") + expect(row).not.toBeNull() + return within(row!) + .getAllByRole("cell") + .map((cell) => cell.textContent) +} + +describe("ModelInfoView service tier pricing", () => { + it("shows OpenAI Native tier prices with per-field fallback to Standard pricing", () => { + const modelInfo: ModelInfo = { + ...baseModelInfo, + tiers: [ + { name: OpenAiServiceTier.Default, contextWindow: 128_000 }, + { + name: OpenAiServiceTier.Flex, + contextWindow: 128_000, + inputPrice: 4, + cacheReadsPrice: 1, + }, + { + name: OpenAiServiceTier.Priority, + contextWindow: 128_000, + outputPrice: 40, + }, + ], + } + + render() + + expect(screen.getByText("Service tier pricing")).toBeInTheDocument() + expect(getPricingRowValues("Standard")).toEqual(["Standard", "$10.00", "$20.00", "$3.00"]) + expect(getPricingRowValues("Flex")).toEqual(["Flex", "$4.00", "$20.00", "$1.00"]) + expect(getPricingRowValues("Priority")).toEqual(["Priority", "$10.00", "$40.00", "$3.00"]) + }) + + it("only shows the tier pricing table for OpenAI Native models with a non-standard tier", () => { + const tieredModelInfo: ModelInfo = { + ...baseModelInfo, + tiers: [{ name: OpenAiServiceTier.Flex, contextWindow: 128_000 }], + } + const { rerender } = render( + , + ) + + expect(screen.queryByText("Service tier pricing")).not.toBeInTheDocument() + + rerender( + , + ) + + expect(screen.queryByText("Service tier pricing")).not.toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx new file mode 100644 index 0000000000..239c76932f --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAI.spec.tsx @@ -0,0 +1,92 @@ +import React from "react" + +import { OpenAiServiceTier, providerIdentifiers, type ModelInfo, type ProviderSettings } from "@roo-code/types" + +import { fireEvent, render, screen } from "@/utils/test-utils" + +import { OpenAI } from "../OpenAI" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("vscrui", () => ({ + Checkbox: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock("@src/components/ui", () => ({ + Select: ({ children, value, onValueChange }: any) => ( + + ), + SelectContent: ({ children }: any) => <>{children}, + SelectItem: ({ children, value }: any) => , + SelectTrigger: () => null, + SelectValue: () => null, + StandardTooltip: ({ children, content }: any) => {children}, +})) + +const baseModelInfo: ModelInfo = { + contextWindow: 128_000, + supportsPromptCache: true, +} + +describe("OpenAI service tier selector", () => { + it("shows supported service tiers and persists the selected tier", () => { + const setApiConfigurationField = vi.fn() + const selectedModelInfo: ModelInfo = { + ...baseModelInfo, + tiers: [ + { name: OpenAiServiceTier.Default, contextWindow: 128_000 }, + { contextWindow: 128_000 }, + { name: OpenAiServiceTier.Flex, contextWindow: 128_000 }, + { name: OpenAiServiceTier.Priority, contextWindow: 128_000 }, + ], + } + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.openaiNative, + openAiNativeApiKey: "test-api-key", + } + + render( + , + ) + + const selector = screen.getByRole("combobox", { name: "Service tier" }) + expect(selector).toHaveValue(OpenAiServiceTier.Default) + expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([ + "Standard", + "Flex", + "Priority", + ]) + + fireEvent.change(selector, { target: { value: OpenAiServiceTier.Flex } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiNativeServiceTier", OpenAiServiceTier.Flex) + + fireEvent.change(selector, { target: { value: OpenAiServiceTier.Priority } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiNativeServiceTier", OpenAiServiceTier.Priority) + }) + + it("hides the selector when the model only exposes the default tier", () => { + render( + , + ) + + expect(screen.queryByTestId("openai-service-tier")).not.toBeInTheDocument() + }) +}) From dd9c9cdbfafc57dd6fe68a2b7f05f237ac4c63b1 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Tue, 28 Jul 2026 22:26:33 +0300 Subject: [PATCH 4/6] refactor(openai): split Codex service tiers --- .../src/__tests__/provider-settings.test.ts | 6 ++--- packages/types/src/model.ts | 10 ++++++++- packages/types/src/provider-settings.ts | 4 ++-- .../providers/__tests__/openai-codex.spec.ts | 16 +++++++------- src/api/providers/openai-codex.ts | 6 ++--- .../__tests__/ProviderSettingsManager.spec.ts | 4 ++-- .../settings/providers/OpenAICodex.tsx | 13 +++++++---- .../providers/__tests__/OpenAICodex.spec.tsx | 22 ++++++++++++------- 8 files changed, 50 insertions(+), 31 deletions(-) diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 44f97198dd..141590b447 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -4,7 +4,7 @@ import { providerSettingsSchema, providerSettingsSchemaDiscriminated, } from "../provider-settings.js" -import { OpenAiServiceTier } from "../model.js" +import { OpenAiCodexServiceTier, OpenAiServiceTier } from "../model.js" import { providerIdentifiers } from "../provider-identifiers.js" describe("OpenAI Codex provider settings", () => { @@ -12,7 +12,7 @@ describe("OpenAI Codex provider settings", () => { const settings = { apiProvider: providerIdentifiers.openaiCodex, apiModelId: "gpt-5.6-sol", - openAiCodexServiceTier: OpenAiServiceTier.Priority, + openAiCodexServiceTier: OpenAiCodexServiceTier.Priority, } expect(providerSettingsSchema.parse(settings)).toEqual(settings) @@ -20,7 +20,7 @@ describe("OpenAI Codex provider settings", () => { expect(PROVIDER_SETTINGS_KEYS).toContain("openAiCodexServiceTier") }) - it.each([undefined, OpenAiServiceTier.Default])( + it.each([undefined, OpenAiCodexServiceTier.Default])( "accepts %s as the Standard preference", (openAiCodexServiceTier) => { const standardSettings = { diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 7f15eef926..8daf62c2df 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -55,7 +55,7 @@ export const verbosityLevelsSchema = z.enum(verbosityLevels) export type VerbosityLevel = z.infer /** - * Service tiers (OpenAI Responses API) + * Service tiers for the public OpenAI Responses API. */ export enum OpenAiServiceTier { Default = "default", @@ -67,6 +67,14 @@ export const serviceTiers = Object.values(OpenAiServiceTier) as [`${OpenAiServic export const serviceTierSchema = z.enum(serviceTiers) export type ServiceTier = `${OpenAiServiceTier}` +/** + * Service tiers for Codex requests authenticated through a ChatGPT subscription. + */ +export enum OpenAiCodexServiceTier { + Default = "default", + Priority = "priority", +} + /** * ModelParameter */ diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index decd981bc5..8716fcab4d 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { modelInfoSchema, - OpenAiServiceTier, + OpenAiCodexServiceTier, reasoningEffortSettingSchema, verbosityLevelsSchema, serviceTierSchema, @@ -284,7 +284,7 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({ const openAiCodexSchema = apiModelIdProviderModelSchema.extend({ // Codex "Fast" mode maps to the Responses API priority service tier. - openAiCodexServiceTier: z.enum([OpenAiServiceTier.Default, OpenAiServiceTier.Priority]).optional(), + openAiCodexServiceTier: z.enum([OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.Priority]).optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index ef004ffb70..27390b14a7 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -9,7 +9,7 @@ vitest.mock("@roo-code/telemetry", () => ({ })) import { Anthropic } from "@anthropic-ai/sdk" -import { OpenAiServiceTier } from "@roo-code/types" +import { OpenAiCodexServiceTier } from "@roo-code/types" import { OpenAiCodexHandler, transformLunaResponsesLiteBody } from "../openai-codex" import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" @@ -85,7 +85,7 @@ describe("OpenAiCodexHandler.createMessage", () => { it("sends the priority service tier in streaming SDK requests when Fast is selected", async () => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-sol", - openAiCodexServiceTier: OpenAiServiceTier.Priority, + openAiCodexServiceTier: OpenAiCodexServiceTier.Priority, }) vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") @@ -95,14 +95,14 @@ describe("OpenAiCodexHandler.createMessage", () => { await drainStream(handler.createMessage("System prompt", [])) const [body] = mockCreate.mock.calls[0] - expect(body).toMatchObject({ stream: true, service_tier: OpenAiServiceTier.Priority }) + expect(body).toMatchObject({ stream: true, service_tier: OpenAiCodexServiceTier.Priority }) }) it.each([ ["an absent preference", {}], [ "an explicit Standard preference from an older profile", - { openAiCodexServiceTier: OpenAiServiceTier.Default }, + { openAiCodexServiceTier: OpenAiCodexServiceTier.Default }, ], ])("omits the service tier in streaming SDK requests for %s", async (_description, serviceTierOptions) => { const handler = new OpenAiCodexHandler({ @@ -122,7 +122,7 @@ describe("OpenAiCodexHandler.createMessage", () => { it("preserves the priority service tier in the manual streaming fallback", async () => { const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.6-sol", - openAiCodexServiceTier: OpenAiServiceTier.Priority, + openAiCodexServiceTier: OpenAiCodexServiceTier.Priority, }) vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") @@ -148,7 +148,7 @@ describe("OpenAiCodexHandler.createMessage", () => { expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchObject({ stream: true, - service_tier: OpenAiServiceTier.Priority, + service_tier: OpenAiCodexServiceTier.Priority, }) }) @@ -258,8 +258,8 @@ describe("OpenAiCodexHandler.completePrompt service tier", () => { vitest.unstubAllGlobals() }) - it.each<[string, OpenAiServiceTier.Priority | undefined, OpenAiServiceTier.Priority | undefined]>([ - ["Fast", OpenAiServiceTier.Priority, OpenAiServiceTier.Priority], + it.each<[string, OpenAiCodexServiceTier.Priority | undefined, OpenAiCodexServiceTier.Priority | undefined]>([ + ["Fast", OpenAiCodexServiceTier.Priority, OpenAiCodexServiceTier.Priority], ["Standard", undefined, undefined], ])("uses the %s preference in non-streaming requests", async (_mode, configuredTier, expectedTier) => { const handler = new OpenAiCodexHandler({ diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index e93e26e703..84d2c551d7 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -5,7 +5,7 @@ import OpenAI from "openai" import { type ModelInfo, - OpenAiServiceTier, + OpenAiCodexServiceTier, openAiCodexDefaultModelId, OpenAiCodexModelId, openAiCodexModels, @@ -30,7 +30,7 @@ import { t } from "../../i18n" export type OpenAiCodexModel = ReturnType -type OpenAiCodexRequestServiceTier = OpenAiServiceTier.Priority +type OpenAiCodexRequestServiceTier = OpenAiCodexServiceTier.Priority /** * OpenAI Codex base URL for API requests @@ -41,7 +41,7 @@ const LUNA_MODEL_ID = "gpt-5.6-luna" const LUNA_CODEX_VERSION = "0.144.0" const getOpenAiCodexServiceTier = (options: ApiHandlerOptions): OpenAiCodexRequestServiceTier | undefined => - options.openAiCodexServiceTier === OpenAiServiceTier.Priority ? OpenAiServiceTier.Priority : undefined + options.openAiCodexServiceTier === OpenAiCodexServiceTier.Priority ? OpenAiCodexServiceTier.Priority : undefined function stripInputImageDetail(value: any): any { if (Array.isArray(value)) { diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index 28d1dcbbad..d56d79c77a 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -2,7 +2,7 @@ import { ExtensionContext } from "vscode" -import { OpenAiServiceTier, providerIdentifiers, type ProviderSettings } from "@roo-code/types" +import { OpenAiCodexServiceTier, providerIdentifiers, type ProviderSettings } from "@roo-code/types" import { ProviderSettingsManager, ProviderProfiles, SyncCloudProfilesResult } from "../ProviderSettingsManager" @@ -451,7 +451,7 @@ describe("ProviderSettingsManager", () => { expect(storedConfig).toEqual(expectedConfig) }) - it.each([OpenAiServiceTier.Default, OpenAiServiceTier.Priority] as const)( + it.each([OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.Priority] as const)( "should persist the OpenAI Codex %s speed preference", async (openAiCodexServiceTier) => { mockSecrets.get.mockResolvedValue( diff --git a/webview-ui/src/components/settings/providers/OpenAICodex.tsx b/webview-ui/src/components/settings/providers/OpenAICodex.tsx index 283a51c6a1..26cbdd59a7 100644 --- a/webview-ui/src/components/settings/providers/OpenAICodex.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICodex.tsx @@ -1,6 +1,11 @@ import React from "react" -import { OpenAiServiceTier, type ProviderSettings, openAiCodexDefaultModelId, openAiCodexModels } from "@roo-code/types" +import { + OpenAiCodexServiceTier, + type ProviderSettings, + openAiCodexDefaultModelId, + openAiCodexModels, +} from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { @@ -85,7 +90,7 @@ export const OpenAICodex: React.FC = ({