diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 724fc20f34..1cf12b7236 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -1,4 +1,49 @@ -import { getApiProtocol } from "../provider-settings.js" +import { + getApiProtocol, + OPEN_AI_CODEX_SERVICE_TIER_KEY, + PROVIDER_SETTINGS_KEYS, + providerSettingsSchema, + providerSettingsSchemaDiscriminated, +} from "../provider-settings.js" +import { OpenAiCodexServiceTier, 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: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority, + } + + expect(providerSettingsSchema.parse(settings)).toEqual(settings) + expect(providerSettingsSchemaDiscriminated.parse(settings)).toEqual(settings) + expect(PROVIDER_SETTINGS_KEYS).toContain(OPEN_AI_CODEX_SERVICE_TIER_KEY) + }) + + it.each([undefined, OpenAiCodexServiceTier.Default])( + "accepts %s as the Standard preference", + (openAiCodexServiceTier) => { + const standardSettings = { + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + ...(openAiCodexServiceTier ? { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier } : {}), + } + + expect(providerSettingsSchemaDiscriminated.parse(standardSettings)).toEqual(standardSettings) + }, + ) + + it("rejects unsupported service tiers", () => { + expect( + providerSettingsSchemaDiscriminated.safeParse({ + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiServiceTier.Flex, + }).success, + ).toBe(false) + }) +}) describe("getApiProtocol", () => { describe("Anthropic-style providers", () => { diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 257a30f3e7..ba7ef4745d 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -54,12 +54,29 @@ export const verbosityLevelsSchema = z.enum(verbosityLevels) export type VerbosityLevel = z.infer +/** Serialized service tier field used in provider request payloads and responses. */ +export const SERVICE_TIER_KEY = "service_tier" + /** - * Service tiers (OpenAI Responses API) + * Service tiers for the public 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}` + +/** + * 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 3898c65bcc..0ae54dc7fc 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, + OpenAiCodexServiceTier, + reasoningEffortSettingSchema, + verbosityLevelsSchema, + serviceTierSchema, +} from "./model.js" import { codebaseIndexProviderSchema } from "./codebase-index.js" import { providerIdentifiers, @@ -36,6 +42,7 @@ import { */ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 +export const OPEN_AI_CODEX_SERVICE_TIER_KEY = "openAiCodexServiceTier" /** * DynamicProvider @@ -277,7 +284,10 @@ 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. + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: z + .enum([OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.Priority]) + .optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index 71c2d1cd0c..b025f33f02 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -58,6 +58,7 @@ import { BEDROCK_1M_CONTEXT_MODEL_IDS, BEDROCK_SERVICE_TIER_MODEL_IDS, bedrockModels, + SERVICE_TIER_KEY, ApiProviderError, } from "@roo-code/types" @@ -1233,10 +1234,10 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // service_tier should be at the top level of the payload - expect(commandArg.service_tier).toBe("PRIORITY") + expect(commandArg[SERVICE_TIER_KEY]).toBe("PRIORITY") // service_tier should NOT be in additionalModelRequestFields if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) @@ -1263,10 +1264,10 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // service_tier should be at the top level of the payload - expect(commandArg.service_tier).toBe("FLEX") + expect(commandArg[SERVICE_TIER_KEY]).toBe("FLEX") // service_tier should NOT be in additionalModelRequestFields if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) @@ -1294,9 +1295,9 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // Service tier should NOT be included for unsupported models (at top level or in additionalModelRequestFields) - expect(commandArg.service_tier).toBeUndefined() + expect(commandArg[SERVICE_TIER_KEY]).toBeUndefined() if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) @@ -1323,9 +1324,9 @@ describe("AwsBedrockHandler", () => { const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any // Service tier should NOT be included when not specified (at top level or in additionalModelRequestFields) - expect(commandArg.service_tier).toBeUndefined() + expect(commandArg[SERVICE_TIER_KEY]).toBeUndefined() if (commandArg.additionalModelRequestFields) { - expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined() + expect(commandArg.additionalModelRequestFields[SERVICE_TIER_KEY]).toBeUndefined() } }) }) diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index c4249f4183..359f62ae2a 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 { OPEN_AI_CODEX_SERVICE_TIER_KEY, OpenAiCodexServiceTier, SERVICE_TIER_KEY } from "@roo-code/types" import { OpenAiCodexHandler, transformLunaResponsesLiteBody } from "../openai-codex" import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" @@ -76,6 +77,84 @@ 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", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: 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_KEY]: OpenAiCodexServiceTier.Priority, + }) + }) + + it.each([ + ["an absent preference", {}], + [ + "an explicit Standard preference from an older profile", + { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: 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_KEY) + }) + + it("preserves the priority service tier in the manual streaming fallback", async () => { + const handler = new OpenAiCodexHandler({ + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: 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_KEY]: OpenAiCodexServiceTier.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 +255,40 @@ describe("OpenAiCodexHandler.createMessage", () => { }) }) +describe("OpenAiCodexHandler.completePrompt service tier", () => { + afterEach(() => { + vitest.restoreAllMocks() + vitest.unstubAllGlobals() + }) + + 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({ + apiModelId: "gpt-5.6-sol", + ...(configuredTier ? { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: configuredTier } : {}), + }) + 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_KEY]).toBe(expectedTier) + } else { + expect(body).not.toHaveProperty(SERVICE_TIER_KEY) + } + }) +}) + 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/__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..63c2e4e396 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, SERVICE_TIER_KEY } from "@roo-code/types" import { OpenAiNativeHandler } from "../openai-native" import { ApiHandlerOptions } from "../../../shared/api" @@ -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_KEY]: 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_KEY]: serviceTier, + }), + expect.any(Object), + ) + }, + ) + it("should handle SDK errors in completePrompt", async () => { // Mock SDK to throw an error mockResponsesCreate.mockRejectedValue(new Error("API Error")) @@ -332,12 +377,33 @@ describe("OpenAiNativeHandler", () => { expect(modelInfo.info.longContextPricing).toBeUndefined() expect(modelInfo.info.tiers).toEqual([ expect.objectContaining({ - name: "flex", + name: OpenAiServiceTier.Flex, outputPrice: 0.625, }), ]) }) + 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/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 645ad8d354..0d39e843c4 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -33,6 +33,7 @@ import { BEDROCK_GLOBAL_INFERENCE_MODEL_IDS, BEDROCK_SERVICE_TIER_MODEL_IDS, BEDROCK_SERVICE_TIER_PRICING, + SERVICE_TIER_KEY, ApiProviderError, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -99,7 +100,7 @@ interface BedrockPayload { // AWS Bedrock service tiers (STANDARD, FLEX, PRIORITY) are specified at the top level // https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html type BedrockPayloadWithServiceTier = BedrockPayload & { - service_tier?: BedrockServiceTier + [SERVICE_TIER_KEY]?: BedrockServiceTier } // Define specific types for content block events to avoid 'as any' usage @@ -553,7 +554,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH ...(thinkingEnabled && { anthropic_version: "bedrock-2023-05-31" }), toolConfig, // Add service_tier as a top-level parameter (not inside additionalModelRequestFields) - ...(useServiceTier && { service_tier: this.options.awsBedrockServiceTier }), + ...(useServiceTier && { [SERVICE_TIER_KEY]: this.options.awsBedrockServiceTier }), } // Create AbortController with 10 minute timeout diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 27ddbe19e2..2619b5114a 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -5,9 +5,12 @@ import OpenAI from "openai" import { type ModelInfo, + OPEN_AI_CODEX_SERVICE_TIER_KEY, + OpenAiCodexServiceTier, openAiCodexDefaultModelId, OpenAiCodexModelId, openAiCodexModels, + SERVICE_TIER_KEY, type ReasoningEffort, type ReasoningEffortExtended, ApiProviderError, @@ -29,6 +32,8 @@ import { t } from "../../i18n" export type OpenAiCodexModel = ReturnType +type OpenAiCodexRequestServiceTier = OpenAiCodexServiceTier.Priority + /** * OpenAI Codex base URL for API requests * Per the implementation guide: requests are routed to chatgpt.com/backend-api/codex @@ -37,6 +42,11 @@ 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" +const getOpenAiCodexServiceTier = (options: ApiHandlerOptions): OpenAiCodexRequestServiceTier | undefined => + options[OPEN_AI_CODEX_SERVICE_TIER_KEY] === OpenAiCodexServiceTier.Priority + ? OpenAiCodexServiceTier.Priority + : undefined + function stripInputImageDetail(value: any): any { if (Array.isArray(value)) { return value.map(stripInputImageDetail) @@ -365,6 +375,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_KEY]?: OpenAiCodexRequestServiceTier reasoning?: { effort?: ReasoningEffortExtended; summary?: "auto" } temperature?: number store?: boolean @@ -383,12 +394,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_KEY]: serviceTier } : {}), // Only include encrypted reasoning content when reasoning effort is set ...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}), ...(reasoningEffort @@ -1261,6 +1274,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 +1286,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion ], stream: false, store: false, + ...(serviceTier ? { [SERVICE_TIER_KEY]: serviceTier } : {}), ...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}), } diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index a1ce2d89d0..e3ca443b44 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -13,6 +13,8 @@ import { type ReasoningEffort, type VerbosityLevel, type ReasoningEffortExtended, + OpenAiServiceTier, + SERVICE_TIER_KEY, type ServiceTier, ApiProviderError, } from "@roo-code/types" @@ -318,7 +320,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio max_output_tokens?: number store?: boolean instructions?: string - service_tier?: ServiceTier + [SERVICE_TIER_KEY]?: ServiceTier include?: string[] /** Prompt cache retention policy: "in_memory" (default) or "24h" for extended caching */ prompt_cache_retention?: "in_memory" | "24h" @@ -369,8 +371,8 @@ 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)) && { - service_tier: requestedTier, + (requestedTier === OpenAiServiceTier.Default || allowedTierNames.has(requestedTier)) && { + [SERVICE_TIER_KEY]: requestedTier, }), // Enable extended prompt cache retention for models that support it. // This uses the OpenAI Responses API `prompt_cache_retention` parameter. @@ -705,8 +707,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio const parsed = JSON.parse(data) // Capture resolved service tier if present - if (parsed.response?.service_tier) { - this.lastServiceTier = parsed.response.service_tier as ServiceTier + if (parsed.response?.[SERVICE_TIER_KEY]) { + this.lastServiceTier = parsed.response[SERVICE_TIER_KEY] as ServiceTier } // Capture complete output array (includes reasoning items with encrypted_content) if (parsed.response?.output && Array.isArray(parsed.response.output)) { @@ -1016,8 +1018,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } } else if (parsed.type === "response.completed" || parsed.type === "response.done") { // Capture resolved service tier if present - if (parsed.response?.service_tier) { - this.lastServiceTier = parsed.response.service_tier as ServiceTier + if (parsed.response?.[SERVICE_TIER_KEY]) { + this.lastServiceTier = parsed.response[SERVICE_TIER_KEY] as ServiceTier } // Capture top-level response id if (parsed.response?.id) { @@ -1146,8 +1148,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio */ private async *processEvent(event: any, model: OpenAiNativeModel): ApiStream { // Capture resolved service tier when available - if (event?.response?.service_tier) { - this.lastServiceTier = event.response.service_tier as ServiceTier + if (event?.response?.[SERVICE_TIER_KEY]) { + this.lastServiceTier = event.response[SERVICE_TIER_KEY] as ServiceTier } // Capture complete output array (includes reasoning items with encrypted_content) if (event?.response?.output && Array.isArray(event.response.output)) { @@ -1418,7 +1420,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,8 +1514,8 @@ 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))) { - requestBody.service_tier = requestedTier + if (requestedTier && (requestedTier === OpenAiServiceTier.Default || allowedTierNames.has(requestedTier))) { + requestBody[SERVICE_TIER_KEY] = requestedTier } // Add reasoning if supported diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index c6bd19c0b1..491ef3b18a 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -2,7 +2,12 @@ import { ExtensionContext } from "vscode" -import type { ProviderSettings } from "@roo-code/types" +import { + OPEN_AI_CODEX_SERVICE_TIER_KEY, + OpenAiCodexServiceTier, + providerIdentifiers, + type ProviderSettings, +} from "@roo-code/types" import { ProviderSettingsManager, ProviderProfiles, SyncCloudProfilesResult } from "../ProviderSettingsManager" @@ -451,6 +456,32 @@ describe("ProviderSettingsManager", () => { expect(storedConfig).toEqual(expectedConfig) }) + it.each([OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.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: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier, + }) + + const storedProfiles = JSON.parse(mockSecrets.store.mock.calls.at(-1)?.[1]) + expect(storedProfiles.apiConfigs.codex).toMatchObject({ + apiProvider: providerIdentifiers.openaiCodex, + apiModelId: "gpt-5.6-sol", + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier, + }) + }, + ) + it("should only save provider relevant settings", async () => { mockSecrets.get.mockResolvedValue( JSON.stringify({ diff --git a/src/core/task/__tests__/Task.throttle.test.ts b/src/core/task/__tests__/Task.throttle.test.ts index 34d78a4ef9..6f31057828 100644 --- a/src/core/task/__tests__/Task.throttle.test.ts +++ b/src/core/task/__tests__/Task.throttle.test.ts @@ -64,11 +64,16 @@ describe("Task token usage throttling", () => { let mockProvider: any let mockApiConfiguration: ProviderSettings let task: Task + let consoleLogSpy: ReturnType beforeEach(() => { // Reset all mocks vi.clearAllMocks() vi.useFakeTimers() + // Task.dispose() logs during afterEach. Under the full coverage suite, forwarding + // those teardown logs can race with Vitest worker shutdown and leave + // onUserConsoleLog RPC calls pending. + consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}) // Mock provider mockProvider = { @@ -97,10 +102,11 @@ describe("Task token usage throttling", () => { }) afterEach(() => { - vi.useRealTimers() if (task && !task.abort) { task.dispose() } + consoleLogSpy.mockRestore() + vi.useRealTimers() }) test("should emit TaskTokenUsageUpdated immediately on first change", async () => { 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/__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/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 755b272702..443335a79c 100644 --- a/webview-ui/src/components/settings/providers/OpenAICodex.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICodex.tsx @@ -1,9 +1,23 @@ import React from "react" -import { type ProviderSettings, openAiCodexDefaultModelId, openAiCodexModels } from "@roo-code/types" +import { + OPEN_AI_CODEX_SERVICE_TIER_KEY, + OpenAiCodexServiceTier, + 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 +80,35 @@ export const OpenAICodex: React.FC = ({ simplifySettings={simplifySettings} hidePricing /> + +
+
+ + + + +
+ +
) } 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() + }) +}) 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..60036c7844 --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICodex.spec.tsx @@ -0,0 +1,94 @@ +import React from "react" + +import { + OPEN_AI_CODEX_SERVICE_TIER_KEY, + OpenAiCodexServiceTier, + providerIdentifiers, + 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: providerIdentifiers.openaiCodex }) + + expect(selector).toHaveValue(OpenAiCodexServiceTier.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: providerIdentifiers.openaiCodex, + [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority, + }) + + expect(selector).toHaveValue(OpenAiCodexServiceTier.Priority) + + fireEvent.change(selector, { target: { value: OpenAiCodexServiceTier.Default } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith( + OPEN_AI_CODEX_SERVICE_TIER_KEY, + OpenAiCodexServiceTier.Default, + ) + + fireEvent.change(selector, { target: { value: OpenAiCodexServiceTier.Priority } }) + expect(setApiConfigurationField).toHaveBeenLastCalledWith( + OPEN_AI_CODEX_SERVICE_TIER_KEY, + 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 請求處理,請嘗試優先處理服務層級。若需較低價格但延遲較高,請嘗試彈性處理層級。",