From f829f8088581a01390914b96491b752e92a70be3 Mon Sep 17 00:00:00 2001 From: Markus Ylisiurunen <8409947+markusylisiurunen@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:04:11 +0300 Subject: [PATCH] fix(ai): support service tier in codex responses (#2996) --- .../src/providers/openai-codex-responses.ts | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index a544e3d4..1574e178 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1,5 +1,10 @@ import type * as NodeOs from "node:os"; -import type { Tool as OpenAITool, ResponseInput, ResponseStreamEvent } from "openai/resources/responses/responses.js"; +import type { + Tool as OpenAITool, + ResponseCreateParamsStreaming, + ResponseInput, + ResponseStreamEvent, +} from "openai/resources/responses/responses.js"; // NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui) let _os: typeof NodeOs | null = null; @@ -25,6 +30,7 @@ import type { SimpleStreamOptions, StreamFunction, StreamOptions, + Usage, } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; @@ -56,6 +62,7 @@ const CODEX_RESPONSE_STATUSES = new Set([ export interface OpenAICodexResponsesOptions extends StreamOptions { reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; reasoningSummary?: "auto" | "concise" | "detailed" | "off" | "on" | null; + serviceTier?: ResponseCreateParamsStreaming["service_tier"]; textVerbosity?: "low" | "medium" | "high"; } @@ -72,6 +79,7 @@ interface RequestBody { parallel_tool_calls?: boolean; temperature?: number; reasoning?: { effort?: string; summary?: string }; + service_tier?: ResponseCreateParamsStreaming["service_tier"]; text?: { verbosity?: string }; include?: string[]; prompt_cache_key?: string; @@ -251,7 +259,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses" } stream.push({ type: "start", partial: output }); - await processStream(response, output, stream, model); + await processStream(response, output, stream, model, options); if (options?.signal?.aborted) { throw new Error("Request was aborted"); @@ -319,6 +327,10 @@ function buildRequestBody( body.temperature = options.temperature; } + if (options?.serviceTier !== undefined) { + body.service_tier = options.serviceTier; + } + if (context.tools) { body.tools = convertResponsesTools(context.tools, { strict: null }); } @@ -342,6 +354,28 @@ function clampReasoningEffort(modelId: string, effort: string): string { return effort; } +function getServiceTierCostMultiplier(serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined): number { + switch (serviceTier) { + case "flex": + return 0.5; + case "priority": + return 2; + default: + return 1; + } +} + +function applyServiceTierPricing(usage: Usage, serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined) { + const multiplier = getServiceTierCostMultiplier(serviceTier); + if (multiplier === 1) return; + + usage.cost.input *= multiplier; + usage.cost.output *= multiplier; + usage.cost.cacheRead *= multiplier; + usage.cost.cacheWrite *= multiplier; + usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite; +} + function resolveCodexUrl(baseUrl?: string): string { const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL; const normalized = raw.replace(/\/+$/, ""); @@ -366,8 +400,12 @@ async function processStream( output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<"openai-codex-responses">, + options?: OpenAICodexResponsesOptions, ): Promise { - await processResponsesStream(mapCodexEvents(parseSSE(response)), output, stream, model); + await processResponsesStream(mapCodexEvents(parseSSE(response)), output, stream, model, { + serviceTier: options?.serviceTier, + applyServiceTierPricing, + }); } async function* mapCodexEvents(events: AsyncIterable>): AsyncGenerator { @@ -806,7 +844,10 @@ async function processWebSocketStream( socket.send(JSON.stringify({ type: "response.create", ...body })); onStart(); stream.push({ type: "start", partial: output }); - await processResponsesStream(mapCodexEvents(parseWebSocket(socket, options?.signal)), output, stream, model); + await processResponsesStream(mapCodexEvents(parseWebSocket(socket, options?.signal)), output, stream, model, { + serviceTier: options?.serviceTier, + applyServiceTierPricing, + }); if (options?.signal?.aborted) { keepConnection = false; }