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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion packages/types/src/__tests__/provider-settings.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
23 changes: 20 additions & 3 deletions packages/types/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,29 @@ export const verbosityLevelsSchema = z.enum(verbosityLevels)

export type VerbosityLevel = z.infer<typeof verbosityLevelsSchema>

/** 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 {
Comment thread
WebMad marked this conversation as resolved.
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<typeof serviceTierSchema>
export type ServiceTier = `${OpenAiServiceTier}`

/**
* Service tiers for Codex requests authenticated through a ChatGPT subscription.
*/
export enum OpenAiCodexServiceTier {
Default = "default",
Priority = "priority",
}

/**
* ModelParameter
Expand Down
14 changes: 12 additions & 2 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -36,6 +42,7 @@ import {
*/

export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
export const OPEN_AI_CODEX_SERVICE_TIER_KEY = "openAiCodexServiceTier"

/**
* DynamicProvider
Expand Down Expand Up @@ -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({
Expand Down
17 changes: 9 additions & 8 deletions src/api/providers/__tests__/bedrock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
BEDROCK_1M_CONTEXT_MODEL_IDS,
BEDROCK_SERVICE_TIER_MODEL_IDS,
bedrockModels,
SERVICE_TIER_KEY,
ApiProviderError,
} from "@roo-code/types"

Expand Down Expand Up @@ -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()
}
})

Expand All @@ -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()
}
})

Expand Down Expand Up @@ -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()
}
})

Expand All @@ -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()
}
})
})
Expand Down
113 changes: 113 additions & 0 deletions src/api/providers/__tests__/openai-codex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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<typeof OpenAiCodexHandler>[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" })

Expand Down Expand Up @@ -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" } }]
Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/__tests__/openai-native-usage.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
Loading
Loading