Skip to content

Commit 5971caa

Browse files
authored
feat(openai-codex): persist and send Fast priority mode (Zoo-Code-Org#1063)
* feat(openai-codex): persist and send Fast priority mode * refactor(openai-codex): align service tier types
1 parent cd29243 commit 5971caa

6 files changed

Lines changed: 228 additions & 4 deletions

File tree

packages/types/src/__tests__/provider-settings.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,49 @@
1-
import { ANTHROPIC_API_PROTOCOL, getApiProtocol, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js"
1+
import { ANTHROPIC_API_PROTOCOL, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js"
2+
import {
3+
getApiProtocol,
4+
OPEN_AI_CODEX_SERVICE_TIER_KEY,
5+
PROVIDER_SETTINGS_KEYS,
6+
providerSettingsSchema,
7+
providerSettingsSchemaDiscriminated,
8+
} from "../provider-settings.js"
9+
import { OpenAiCodexServiceTier, OpenAiServiceTier } from "../model.js"
10+
11+
describe("OpenAI Codex provider settings", () => {
12+
it("preserves the Fast preference in general and provider-specific schemas", () => {
13+
const settings = {
14+
apiProvider: providerIdentifiers.openaiCodex,
15+
apiModelId: "gpt-5.6-sol",
16+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority,
17+
}
18+
19+
expect(providerSettingsSchema.parse(settings)).toEqual(settings)
20+
expect(providerSettingsSchemaDiscriminated.parse(settings)).toEqual(settings)
21+
expect(PROVIDER_SETTINGS_KEYS).toContain(OPEN_AI_CODEX_SERVICE_TIER_KEY)
22+
})
23+
24+
it.each([undefined, OpenAiCodexServiceTier.Default])(
25+
"accepts %s as the Standard preference",
26+
(openAiCodexServiceTier) => {
27+
const standardSettings = {
28+
apiProvider: providerIdentifiers.openaiCodex,
29+
apiModelId: "gpt-5.6-sol",
30+
...(openAiCodexServiceTier ? { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier } : {}),
31+
}
32+
33+
expect(providerSettingsSchemaDiscriminated.parse(standardSettings)).toEqual(standardSettings)
34+
},
35+
)
36+
37+
it("rejects unsupported service tiers", () => {
38+
expect(
39+
providerSettingsSchemaDiscriminated.safeParse({
40+
apiProvider: providerIdentifiers.openaiCodex,
41+
apiModelId: "gpt-5.6-sol",
42+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiServiceTier.Flex,
43+
}).success,
44+
).toBe(false)
45+
})
46+
})
247

348
describe("getApiProtocol", () => {
449
it("preserves API protocol wire values", () => {

packages/types/src/model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ export const serviceTiers = [OpenAiServiceTier.Default, OpenAiServiceTier.Flex,
7070
export const serviceTierSchema = z.enum(serviceTiers)
7171
export type ServiceTier = z.infer<typeof serviceTierSchema>
7272

73+
/**
74+
* Service tiers for Codex requests authenticated through a ChatGPT subscription.
75+
*/
76+
export const OpenAiCodexServiceTier = {
77+
Default: "default",
78+
Priority: "priority",
79+
} as const
80+
81+
export const openAiCodexServiceTiers = [OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.Priority] as const
82+
export const openAiCodexServiceTierSchema = z.enum(openAiCodexServiceTiers)
83+
export type OpenAiCodexServiceTier = z.infer<typeof openAiCodexServiceTierSchema>
84+
7385
/**
7486
* ModelParameter
7587
*/

packages/types/src/provider-settings.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { z } from "zod"
22

3-
import { modelInfoSchema, reasoningEffortSettingSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js"
3+
import {
4+
modelInfoSchema,
5+
openAiCodexServiceTierSchema,
6+
reasoningEffortSettingSchema,
7+
verbosityLevelsSchema,
8+
serviceTierSchema,
9+
} from "./model.js"
410
import { codebaseIndexProviderSchema } from "./codebase-index.js"
511
import {
612
providerIdentifiers,
@@ -38,6 +44,7 @@ import {
3844
*/
3945

4046
export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
47+
export const OPEN_AI_CODEX_SERVICE_TIER_KEY = "openAiCodexServiceTier"
4148

4249
/**
4350
* DynamicProvider
@@ -279,7 +286,8 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({
279286
})
280287

281288
const openAiCodexSchema = apiModelIdProviderModelSchema.extend({
282-
// No additional settings needed - uses OAuth authentication
289+
// Codex "Fast" mode maps to the Responses API priority service tier.
290+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTierSchema.optional(),
283291
})
284292

285293
const openAiNativeSchema = apiModelIdProviderModelSchema.extend({

src/api/providers/__tests__/openai-codex.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ vitest.mock("@roo-code/telemetry", () => ({
99
}))
1010

1111
import { Anthropic } from "@anthropic-ai/sdk"
12+
import { OPEN_AI_CODEX_SERVICE_TIER_KEY, OpenAiCodexServiceTier, SERVICE_TIER_KEY } from "@roo-code/types"
1213
import { OpenAiCodexHandler, transformLunaResponsesLiteBody } from "../openai-codex"
1314
import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth"
1415

@@ -76,6 +77,84 @@ describe("OpenAiCodexHandler.getModel", () => {
7677
})
7778

7879
describe("OpenAiCodexHandler.createMessage", () => {
80+
afterEach(() => {
81+
vitest.restoreAllMocks()
82+
vitest.unstubAllGlobals()
83+
})
84+
85+
it("sends the priority service tier in streaming SDK requests when Fast is selected", async () => {
86+
const handler = new OpenAiCodexHandler({
87+
apiModelId: "gpt-5.6-sol",
88+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority,
89+
})
90+
vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
91+
vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
92+
const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream())
93+
Reflect.set(handler, "client", { responses: { create: mockCreate } })
94+
95+
await drainStream(handler.createMessage("System prompt", []))
96+
97+
const [body] = mockCreate.mock.calls[0]
98+
expect(body).toMatchObject({
99+
stream: true,
100+
[SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority,
101+
})
102+
})
103+
104+
it.each([
105+
["an absent preference", {}],
106+
[
107+
"an explicit Standard preference from an older profile",
108+
{ [OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Default },
109+
],
110+
])("omits the service tier in streaming SDK requests for %s", async (_description, serviceTierOptions) => {
111+
const handler = new OpenAiCodexHandler({
112+
apiModelId: "gpt-5.6-sol",
113+
...serviceTierOptions,
114+
} as ConstructorParameters<typeof OpenAiCodexHandler>[0])
115+
vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
116+
vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
117+
const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream())
118+
Reflect.set(handler, "client", { responses: { create: mockCreate } })
119+
120+
await drainStream(handler.createMessage("System prompt", []))
121+
122+
expect(mockCreate.mock.calls[0][0]).not.toHaveProperty(SERVICE_TIER_KEY)
123+
})
124+
125+
it("preserves the priority service tier in the manual streaming fallback", async () => {
126+
const handler = new OpenAiCodexHandler({
127+
apiModelId: "gpt-5.6-sol",
128+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority,
129+
})
130+
vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
131+
vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
132+
Reflect.set(handler, "client", {
133+
responses: { create: vitest.fn().mockRejectedValue(new Error("SDK unavailable")) },
134+
})
135+
const mockFetch = vitest.fn().mockResolvedValue({
136+
ok: true,
137+
body: new ReadableStream({
138+
start(controller) {
139+
controller.enqueue(
140+
new TextEncoder().encode(
141+
'data: {"type":"response.completed","response":{"output":[],"usage":{"input_tokens":1,"output_tokens":1}}}\n\n',
142+
),
143+
)
144+
controller.close()
145+
},
146+
}),
147+
})
148+
vitest.stubGlobal("fetch", mockFetch)
149+
150+
await drainStream(handler.createMessage("System prompt", []))
151+
152+
expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchObject({
153+
stream: true,
154+
[SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority,
155+
})
156+
})
157+
79158
it("should skip URL-sourced images in formatFullConversation (only base64 emits input_image)", async () => {
80159
const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.1-codex" })
81160

@@ -176,6 +255,40 @@ describe("OpenAiCodexHandler.createMessage", () => {
176255
})
177256
})
178257

258+
describe("OpenAiCodexHandler.completePrompt service tier", () => {
259+
afterEach(() => {
260+
vitest.restoreAllMocks()
261+
vitest.unstubAllGlobals()
262+
})
263+
264+
it.each<[string, OpenAiCodexServiceTier | undefined, typeof OpenAiCodexServiceTier.Priority | undefined]>([
265+
["Fast", OpenAiCodexServiceTier.Priority, OpenAiCodexServiceTier.Priority],
266+
["Standard", undefined, undefined],
267+
])("uses the %s preference in non-streaming requests", async (_mode, configuredTier, expectedTier) => {
268+
const handler = new OpenAiCodexHandler({
269+
apiModelId: "gpt-5.6-sol",
270+
...(configuredTier ? { [OPEN_AI_CODEX_SERVICE_TIER_KEY]: configuredTier } : {}),
271+
})
272+
vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
273+
vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
274+
const mockFetch = vitest.fn().mockResolvedValue({
275+
ok: true,
276+
json: vitest.fn().mockResolvedValue({ text: "Complete" }),
277+
})
278+
vitest.stubGlobal("fetch", mockFetch)
279+
280+
await expect(handler.completePrompt("Hello")).resolves.toBe("Complete")
281+
282+
const body = JSON.parse(mockFetch.mock.calls[0][1].body)
283+
expect(body.stream).toBe(false)
284+
if (expectedTier) {
285+
expect(body[SERVICE_TIER_KEY]).toBe(expectedTier)
286+
} else {
287+
expect(body).not.toHaveProperty(SERVICE_TIER_KEY)
288+
}
289+
})
290+
})
291+
179292
describe("transformLunaResponsesLiteBody", () => {
180293
it("creates the exact Responses Lite body while preserving unrelated fields and reasoning", () => {
181294
const tools = [{ type: "function", name: "read_file", parameters: { type: "object" } }]

src/api/providers/openai-codex.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import OpenAI from "openai"
55

66
import {
77
type ModelInfo,
8+
OPEN_AI_CODEX_SERVICE_TIER_KEY,
9+
OpenAiCodexServiceTier,
810
openAiCodexDefaultModelId,
911
OpenAiCodexModelId,
1012
openAiCodexModels,
13+
SERVICE_TIER_KEY,
1114
type ReasoningEffort,
1215
type ReasoningEffortExtended,
1316
ApiProviderError,
@@ -29,6 +32,8 @@ import { t } from "../../i18n"
2932

3033
export type OpenAiCodexModel = ReturnType<OpenAiCodexHandler["getModel"]>
3134

35+
type OpenAiCodexRequestServiceTier = typeof OpenAiCodexServiceTier.Priority
36+
3237
/**
3338
* OpenAI Codex base URL for API requests
3439
* 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"
3742
const LUNA_MODEL_ID = "gpt-5.6-luna"
3843
const LUNA_CODEX_VERSION = "0.144.0"
3944

45+
const getOpenAiCodexServiceTier = (options: ApiHandlerOptions): OpenAiCodexRequestServiceTier | undefined =>
46+
options[OPEN_AI_CODEX_SERVICE_TIER_KEY] === OpenAiCodexServiceTier.Priority
47+
? OpenAiCodexServiceTier.Priority
48+
: undefined
49+
4050
function stripInputImageDetail(value: any): any {
4151
if (Array.isArray(value)) {
4252
return value.map(stripInputImageDetail)
@@ -365,6 +375,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
365375
model: string
366376
input: Array<{ role: "user" | "assistant"; content: any[] } | { type: string; content: string }>
367377
stream: boolean
378+
[SERVICE_TIER_KEY]?: OpenAiCodexRequestServiceTier
368379
reasoning?: { effort?: ReasoningEffortExtended; summary?: "auto" }
369380
temperature?: number
370381
store?: boolean
@@ -383,12 +394,14 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
383394

384395
// Per the implementation guide: Codex backend may reject max_output_tokens
385396
// and prompt_cache_retention, so we omit them
397+
const serviceTier = getOpenAiCodexServiceTier(this.options)
386398
const body: ResponsesRequestBody = {
387399
model: model.id,
388400
input: formattedInput,
389401
stream: true,
390402
store: false,
391403
instructions: systemPrompt,
404+
...(serviceTier ? { [SERVICE_TIER_KEY]: serviceTier } : {}),
392405
// Only include encrypted reasoning content when reasoning effort is set
393406
...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}),
394407
...(reasoningEffort
@@ -1261,6 +1274,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
12611274
}
12621275

12631276
const reasoningEffort = this.getReasoningEffort(model)
1277+
const serviceTier = getOpenAiCodexServiceTier(this.options)
12641278

12651279
const baseRequestBody: any = {
12661280
model: model.id,
@@ -1272,6 +1286,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
12721286
],
12731287
stream: false,
12741288
store: false,
1289+
...(serviceTier ? { [SERVICE_TIER_KEY]: serviceTier } : {}),
12751290
...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}),
12761291
}
12771292

src/core/config/__tests__/ProviderSettingsManager.spec.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
import { ExtensionContext } from "vscode"
44

5-
import type { ProviderSettings } from "@roo-code/types"
5+
import {
6+
OPEN_AI_CODEX_SERVICE_TIER_KEY,
7+
OpenAiCodexServiceTier,
8+
providerIdentifiers,
9+
type ProviderSettings,
10+
} from "@roo-code/types"
611

712
import { ProviderSettingsManager, ProviderProfiles, SyncCloudProfilesResult } from "../ProviderSettingsManager"
813

@@ -451,6 +456,32 @@ describe("ProviderSettingsManager", () => {
451456
expect(storedConfig).toEqual(expectedConfig)
452457
})
453458

459+
it.each([OpenAiCodexServiceTier.Default, OpenAiCodexServiceTier.Priority] as const)(
460+
"should persist the OpenAI Codex %s speed preference",
461+
async (openAiCodexServiceTier) => {
462+
mockSecrets.get.mockResolvedValue(
463+
JSON.stringify({
464+
currentApiConfigName: "default",
465+
apiConfigs: { default: {} },
466+
modeApiConfigs: {},
467+
}),
468+
)
469+
470+
await providerSettingsManager.saveConfig("codex", {
471+
apiProvider: providerIdentifiers.openaiCodex,
472+
apiModelId: "gpt-5.6-sol",
473+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier,
474+
})
475+
476+
const storedProfiles = JSON.parse(mockSecrets.store.mock.calls.at(-1)?.[1])
477+
expect(storedProfiles.apiConfigs.codex).toMatchObject({
478+
apiProvider: providerIdentifiers.openaiCodex,
479+
apiModelId: "gpt-5.6-sol",
480+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: openAiCodexServiceTier,
481+
})
482+
},
483+
)
484+
454485
it("should only save provider relevant settings", async () => {
455486
mockSecrets.get.mockResolvedValue(
456487
JSON.stringify({

0 commit comments

Comments
 (0)