From 1980d8b7b327ca96edae6d80e26f42f2cff53039 Mon Sep 17 00:00:00 2001 From: icebear0828 Date: Mon, 30 Mar 2026 21:25:10 -0500 Subject: [PATCH] fix: map service_tier 'fast' to 'priority' for upstream Codex backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Codex backend rejects `service_tier: "fast"` in the request body ("Unsupported service_tier: fast"). The Desktop app (codex-rs) maps `Fast` to `"priority"` before sending — align proxy behavior to match. Previously cc1b29d stripped service_tier entirely as a workaround, making -fast/-flex suffixes non-functional. Now the field is preserved in both HTTP and WebSocket paths with the correct mapping. --- src/proxy/__tests__/codex-api-headers.test.ts | 68 ++++++++++++++++++- src/proxy/codex-api.ts | 8 ++- src/proxy/ws-transport.ts | 1 + 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/proxy/__tests__/codex-api-headers.test.ts b/src/proxy/__tests__/codex-api-headers.test.ts index 263f88a..8b73930 100644 --- a/src/proxy/__tests__/codex-api-headers.test.ts +++ b/src/proxy/__tests__/codex-api-headers.test.ts @@ -112,13 +112,37 @@ describe("codex-api headers", () => { expect(transport.lastHeaders!["x-codex-turn-state"]).toBeUndefined(); }); - it("excludes turnState and service_tier from JSON body", async () => { + it("excludes turnState from JSON body", async () => { const api = await createApi(); await api.createResponse( - makeRequest({ turnState: "abc", service_tier: "fast" }), + makeRequest({ turnState: "abc" }), ); const body = JSON.parse(transport.lastBody!) as Record; expect(body.turnState).toBeUndefined(); + }); + + it("maps service_tier 'fast' to 'priority' in JSON body", async () => { + const api = await createApi(); + await api.createResponse( + makeRequest({ service_tier: "fast" }), + ); + const body = JSON.parse(transport.lastBody!) as Record; + expect(body.service_tier).toBe("priority"); + }); + + it("passes non-fast service_tier through unchanged", async () => { + const api = await createApi(); + await api.createResponse( + makeRequest({ service_tier: "flex" }), + ); + const body = JSON.parse(transport.lastBody!) as Record; + expect(body.service_tier).toBe("flex"); + }); + + it("omits service_tier from body when null", async () => { + const api = await createApi(); + await api.createResponse(makeRequest({ service_tier: null })); + const body = JSON.parse(transport.lastBody!) as Record; expect(body.service_tier).toBeUndefined(); }); }); @@ -148,5 +172,45 @@ describe("codex-api headers", () => { ); expect(headers["x-codex-turn-state"]).toBe("ws_turn_abc"); }); + + it("maps service_tier 'fast' to 'priority' in WS request", async () => { + mockCreateWebSocketResponse.mockResolvedValue( + new Response("data: {}\n\n", { + headers: { "content-type": "text/event-stream" }, + }), + ); + + const api = await createApi(); + await api.createResponse( + makeRequest({ + previous_response_id: "resp_prev", + useWebSocket: true, + service_tier: "fast", + }), + ); + + const wsRequest = mockCreateWebSocketResponse.mock.calls[0][2] as Record; + expect(wsRequest.service_tier).toBe("priority"); + }); + + it("passes non-fast service_tier through in WS request", async () => { + mockCreateWebSocketResponse.mockResolvedValue( + new Response("data: {}\n\n", { + headers: { "content-type": "text/event-stream" }, + }), + ); + + const api = await createApi(); + await api.createResponse( + makeRequest({ + previous_response_id: "resp_prev", + useWebSocket: true, + service_tier: "flex", + }), + ); + + const wsRequest = mockCreateWebSocketResponse.mock.calls[0][2] as Record; + expect(wsRequest.service_tier).toBe("flex"); + }); }); }); diff --git a/src/proxy/codex-api.ts b/src/proxy/codex-api.ts index 8846a8b..c706cf4 100644 --- a/src/proxy/codex-api.ts +++ b/src/proxy/codex-api.ts @@ -215,7 +215,7 @@ export class CodexApi { if (request.tools?.length) wsRequest.tools = request.tools; if (request.tool_choice) wsRequest.tool_choice = request.tool_choice; if (request.text) wsRequest.text = request.text; - // service_tier is stripped — Codex backend rejects it ("Unsupported service_tier") + if (request.service_tier) wsRequest.service_tier = request.service_tier === "fast" ? "priority" : request.service_tier; if (request.prompt_cache_key) wsRequest.prompt_cache_key = request.prompt_cache_key; if (request.include?.length) wsRequest.include = request.include; @@ -243,8 +243,10 @@ export class CodexApi { headers["x-client-request-id"] = crypto.randomUUID(); if (request.turnState) headers["x-codex-turn-state"] = request.turnState; - const { previous_response_id: _pid, useWebSocket: _ws, turnState: _ts, service_tier: _st, ...bodyFields } = request; - const body = JSON.stringify(bodyFields); + const { previous_response_id: _pid, useWebSocket: _ws, turnState: _ts, service_tier, ...bodyFields } = request; + const body = JSON.stringify( + service_tier ? { ...bodyFields, service_tier: service_tier === "fast" ? "priority" : service_tier } : bodyFields, + ); let transportRes; try { diff --git a/src/proxy/ws-transport.ts b/src/proxy/ws-transport.ts index 1618648..3d93e08 100644 --- a/src/proxy/ws-transport.ts +++ b/src/proxy/ws-transport.ts @@ -51,6 +51,7 @@ export interface WsCreateRequest { strict?: boolean; }; }; + service_tier?: string; prompt_cache_key?: string; include?: string[]; // NOTE: `store` and `stream` are intentionally omitted.