@dreb/ai 2.14.1 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/models.js CHANGED
@@ -122,7 +122,10 @@ export function calculateCost(model, usage) {
122
122
  * - Opus 4.6+ models (xhigh maps to adaptive effort "max" on Anthropic-compatible providers)
123
123
  */
124
124
  export function supportsXhigh(model) {
125
- if (model.id.includes("gpt-5.2") || model.id.includes("gpt-5.3") || model.id.includes("gpt-5.4")) {
125
+ if (model.id.includes("gpt-5.2") ||
126
+ model.id.includes("gpt-5.3") ||
127
+ model.id.includes("gpt-5.4") ||
128
+ model.id.includes("gpt-5.5")) {
126
129
  return true;
127
130
  }
128
131
  if (model.id.includes("opus-4-6") ||
@@ -1 +1 @@
1
- {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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","sourcesContent":["import { MODELS } from \"./models.generated.js\";\nimport type { Api, KnownProvider, Model, Usage } from \"./types.js\";\n\nconst modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();\n\n// Initialize registry from MODELS on module load\nfor (const [provider, models] of Object.entries(MODELS)) {\n\tconst providerModels = new Map<string, Model<Api>>();\n\tfor (const [id, model] of Object.entries(models)) {\n\t\tproviderModels.set(id, model as Model<Api>);\n\t}\n\tmodelRegistry.set(provider, providerModels);\n}\n\ntype ModelApi<\n\tTProvider extends KnownProvider,\n\tTModelId extends keyof (typeof MODELS)[TProvider],\n> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;\n\nexport function getModel<TProvider extends KnownProvider, TModelId extends keyof (typeof MODELS)[TProvider]>(\n\tprovider: TProvider,\n\tmodelId: TModelId,\n): Model<ModelApi<TProvider, TModelId>> {\n\tconst providerModels = modelRegistry.get(provider);\n\treturn providerModels?.get(modelId as string) as Model<ModelApi<TProvider, TModelId>>;\n}\n\n/**\n * Check if a model ID looks like an alias (no date suffix).\n * Aliases are preferred over dated versions when fuzzy matching.\n *\n * IDs ending with `-latest` are treated as aliases.\n * IDs ending with a date pattern (`-YYYYMMDD`) are treated as dated versions.\n */\nexport function isModelAlias(id: string): boolean {\n\tif (id.endsWith(\"-latest\")) return true;\n\treturn !/-\\d{8}$/.test(id);\n}\n\n/**\n * Find a model by fuzzy matching against the provider's registered models.\n *\n * Resolution order:\n * 1. Exact match by provider + model ID (via registry Map.get)\n * 2. Case-insensitive substring match against model ID and display name\n * 3. Among matches, prefer aliases (non-dated IDs) over dated versions\n * 4. Among ties, pick the lexicographically highest (latest) ID\n *\n * This is the same matching logic used by the CLI, subagent model resolution,\n * and interactive mode — centralised here so tests can exercise the real path.\n *\n * @example\n * findModel(\"anthropic\", \"sonnet\") // → latest claude-sonnet alias\n * findModel(\"anthropic\", \"haiku\") // → latest claude-haiku alias\n * findModel(\"openai\", \"gpt-5\") // → latest gpt-5 alias\n */\nexport function findModel(provider: string, pattern: string): Model<Api> | undefined {\n\tconst providerModels = modelRegistry.get(provider);\n\tif (!providerModels) return undefined;\n\n\t// Try exact match first\n\tconst exact = providerModels.get(pattern);\n\tif (exact) return exact;\n\n\t// Substring match (case-insensitive)\n\tconst normalizedPattern = pattern.toLowerCase();\n\tconst matches = Array.from(providerModels.values()).filter(\n\t\t(m) => m.id.toLowerCase().includes(normalizedPattern) || m.name?.toLowerCase().includes(normalizedPattern),\n\t);\n\n\tif (matches.length === 0) return undefined;\n\tif (matches.length === 1) return matches[0];\n\n\t// Multiple matches — separate into aliases and dated versions\n\tconst aliases = matches.filter((m) => isModelAlias(m.id));\n\tconst datedVersions = matches.filter((m) => !isModelAlias(m.id));\n\n\tif (aliases.length > 0) {\n\t\t// Prefer alias — if multiple, pick the lexicographically highest\n\t\taliases.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn aliases[0];\n\t}\n\n\t// All dated — prefer the latest\n\tdatedVersions.sort((a, b) => b.id.localeCompare(a.id));\n\treturn datedVersions[0];\n}\n\n/**\n * Find a model by fuzzy matching against a flat array of models.\n * Same algorithm as findModel() but operates on an arbitrary model list\n * instead of the built-in registry.\n *\n * Used by model-resolver.ts and other code that manages its own model lists.\n */\nexport function findModelInList(pattern: string, models: Model<Api>[]): Model<Api> | undefined {\n\tif (models.length === 0) return undefined;\n\n\tconst normalizedPattern = pattern.toLowerCase();\n\n\t// Exact ID match (case-insensitive)\n\tconst exactById = models.find((m) => m.id.toLowerCase() === normalizedPattern);\n\tif (exactById) return exactById;\n\n\t// Substring match (case-insensitive)\n\tconst matches = models.filter(\n\t\t(m) => m.id.toLowerCase().includes(normalizedPattern) || m.name?.toLowerCase().includes(normalizedPattern),\n\t);\n\n\tif (matches.length === 0) return undefined;\n\tif (matches.length === 1) return matches[0];\n\n\t// Multiple matches — separate into aliases and dated versions\n\tconst aliases = matches.filter((m) => isModelAlias(m.id));\n\tconst datedVersions = matches.filter((m) => !isModelAlias(m.id));\n\n\tif (aliases.length > 0) {\n\t\taliases.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn aliases[0];\n\t}\n\n\tdatedVersions.sort((a, b) => b.id.localeCompare(a.id));\n\treturn datedVersions[0];\n}\n\nexport function getProviders(): KnownProvider[] {\n\treturn Array.from(modelRegistry.keys()) as KnownProvider[];\n}\n\nexport function getModels<TProvider extends KnownProvider>(\n\tprovider: TProvider,\n): Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[] {\n\tconst models = modelRegistry.get(provider);\n\treturn models ? (Array.from(models.values()) as Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[]) : [];\n}\n\nexport function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage[\"cost\"] {\n\tusage.cost.input = (model.cost.input / 1000000) * usage.input;\n\tusage.cost.output = (model.cost.output / 1000000) * usage.output;\n\tusage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead;\n\tusage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n\treturn usage.cost;\n}\n\n/**\n * Check if a model supports xhigh thinking level.\n *\n * Supported today:\n * - GPT-5.2 / GPT-5.3 / GPT-5.4 model families\n * - Opus 4.6+ models (xhigh maps to adaptive effort \"max\" on Anthropic-compatible providers)\n */\nexport function supportsXhigh<TApi extends Api>(model: Model<TApi>): boolean {\n\tif (model.id.includes(\"gpt-5.2\") || model.id.includes(\"gpt-5.3\") || model.id.includes(\"gpt-5.4\")) {\n\t\treturn true;\n\t}\n\n\tif (\n\t\tmodel.id.includes(\"opus-4-6\") ||\n\t\tmodel.id.includes(\"opus-4.6\") ||\n\t\tmodel.id.includes(\"opus-4-7\") ||\n\t\tmodel.id.includes(\"opus-4.7\")\n\t) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n/**\n * Check if two models are equal by comparing both their id and provider.\n * Returns false if either model is null or undefined.\n */\nexport function modelsAreEqual<TApi extends Api>(\n\ta: Model<TApi> | null | undefined,\n\tb: Model<TApi> | null | undefined,\n): boolean {\n\tif (!a || !b) return false;\n\treturn a.id === b.id && a.provider === b.provider;\n}\n"]}
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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","sourcesContent":["import { MODELS } from \"./models.generated.js\";\nimport type { Api, KnownProvider, Model, Usage } from \"./types.js\";\n\nconst modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();\n\n// Initialize registry from MODELS on module load\nfor (const [provider, models] of Object.entries(MODELS)) {\n\tconst providerModels = new Map<string, Model<Api>>();\n\tfor (const [id, model] of Object.entries(models)) {\n\t\tproviderModels.set(id, model as Model<Api>);\n\t}\n\tmodelRegistry.set(provider, providerModels);\n}\n\ntype ModelApi<\n\tTProvider extends KnownProvider,\n\tTModelId extends keyof (typeof MODELS)[TProvider],\n> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;\n\nexport function getModel<TProvider extends KnownProvider, TModelId extends keyof (typeof MODELS)[TProvider]>(\n\tprovider: TProvider,\n\tmodelId: TModelId,\n): Model<ModelApi<TProvider, TModelId>> {\n\tconst providerModels = modelRegistry.get(provider);\n\treturn providerModels?.get(modelId as string) as Model<ModelApi<TProvider, TModelId>>;\n}\n\n/**\n * Check if a model ID looks like an alias (no date suffix).\n * Aliases are preferred over dated versions when fuzzy matching.\n *\n * IDs ending with `-latest` are treated as aliases.\n * IDs ending with a date pattern (`-YYYYMMDD`) are treated as dated versions.\n */\nexport function isModelAlias(id: string): boolean {\n\tif (id.endsWith(\"-latest\")) return true;\n\treturn !/-\\d{8}$/.test(id);\n}\n\n/**\n * Find a model by fuzzy matching against the provider's registered models.\n *\n * Resolution order:\n * 1. Exact match by provider + model ID (via registry Map.get)\n * 2. Case-insensitive substring match against model ID and display name\n * 3. Among matches, prefer aliases (non-dated IDs) over dated versions\n * 4. Among ties, pick the lexicographically highest (latest) ID\n *\n * This is the same matching logic used by the CLI, subagent model resolution,\n * and interactive mode — centralised here so tests can exercise the real path.\n *\n * @example\n * findModel(\"anthropic\", \"sonnet\") // → latest claude-sonnet alias\n * findModel(\"anthropic\", \"haiku\") // → latest claude-haiku alias\n * findModel(\"openai\", \"gpt-5\") // → latest gpt-5 alias\n */\nexport function findModel(provider: string, pattern: string): Model<Api> | undefined {\n\tconst providerModels = modelRegistry.get(provider);\n\tif (!providerModels) return undefined;\n\n\t// Try exact match first\n\tconst exact = providerModels.get(pattern);\n\tif (exact) return exact;\n\n\t// Substring match (case-insensitive)\n\tconst normalizedPattern = pattern.toLowerCase();\n\tconst matches = Array.from(providerModels.values()).filter(\n\t\t(m) => m.id.toLowerCase().includes(normalizedPattern) || m.name?.toLowerCase().includes(normalizedPattern),\n\t);\n\n\tif (matches.length === 0) return undefined;\n\tif (matches.length === 1) return matches[0];\n\n\t// Multiple matches — separate into aliases and dated versions\n\tconst aliases = matches.filter((m) => isModelAlias(m.id));\n\tconst datedVersions = matches.filter((m) => !isModelAlias(m.id));\n\n\tif (aliases.length > 0) {\n\t\t// Prefer alias — if multiple, pick the lexicographically highest\n\t\taliases.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn aliases[0];\n\t}\n\n\t// All dated — prefer the latest\n\tdatedVersions.sort((a, b) => b.id.localeCompare(a.id));\n\treturn datedVersions[0];\n}\n\n/**\n * Find a model by fuzzy matching against a flat array of models.\n * Same algorithm as findModel() but operates on an arbitrary model list\n * instead of the built-in registry.\n *\n * Used by model-resolver.ts and other code that manages its own model lists.\n */\nexport function findModelInList(pattern: string, models: Model<Api>[]): Model<Api> | undefined {\n\tif (models.length === 0) return undefined;\n\n\tconst normalizedPattern = pattern.toLowerCase();\n\n\t// Exact ID match (case-insensitive)\n\tconst exactById = models.find((m) => m.id.toLowerCase() === normalizedPattern);\n\tif (exactById) return exactById;\n\n\t// Substring match (case-insensitive)\n\tconst matches = models.filter(\n\t\t(m) => m.id.toLowerCase().includes(normalizedPattern) || m.name?.toLowerCase().includes(normalizedPattern),\n\t);\n\n\tif (matches.length === 0) return undefined;\n\tif (matches.length === 1) return matches[0];\n\n\t// Multiple matches — separate into aliases and dated versions\n\tconst aliases = matches.filter((m) => isModelAlias(m.id));\n\tconst datedVersions = matches.filter((m) => !isModelAlias(m.id));\n\n\tif (aliases.length > 0) {\n\t\taliases.sort((a, b) => b.id.localeCompare(a.id));\n\t\treturn aliases[0];\n\t}\n\n\tdatedVersions.sort((a, b) => b.id.localeCompare(a.id));\n\treturn datedVersions[0];\n}\n\nexport function getProviders(): KnownProvider[] {\n\treturn Array.from(modelRegistry.keys()) as KnownProvider[];\n}\n\nexport function getModels<TProvider extends KnownProvider>(\n\tprovider: TProvider,\n): Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[] {\n\tconst models = modelRegistry.get(provider);\n\treturn models ? (Array.from(models.values()) as Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[]) : [];\n}\n\nexport function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage[\"cost\"] {\n\tusage.cost.input = (model.cost.input / 1000000) * usage.input;\n\tusage.cost.output = (model.cost.output / 1000000) * usage.output;\n\tusage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead;\n\tusage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n\treturn usage.cost;\n}\n\n/**\n * Check if a model supports xhigh thinking level.\n *\n * Supported today:\n * - GPT-5.2 / GPT-5.3 / GPT-5.4 model families\n * - Opus 4.6+ models (xhigh maps to adaptive effort \"max\" on Anthropic-compatible providers)\n */\nexport function supportsXhigh<TApi extends Api>(model: Model<TApi>): boolean {\n\tif (\n\t\tmodel.id.includes(\"gpt-5.2\") ||\n\t\tmodel.id.includes(\"gpt-5.3\") ||\n\t\tmodel.id.includes(\"gpt-5.4\") ||\n\t\tmodel.id.includes(\"gpt-5.5\")\n\t) {\n\t\treturn true;\n\t}\n\n\tif (\n\t\tmodel.id.includes(\"opus-4-6\") ||\n\t\tmodel.id.includes(\"opus-4.6\") ||\n\t\tmodel.id.includes(\"opus-4-7\") ||\n\t\tmodel.id.includes(\"opus-4.7\")\n\t) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n/**\n * Check if two models are equal by comparing both their id and provider.\n * Returns false if either model is null or undefined.\n */\nexport function modelsAreEqual<TApi extends Api>(\n\ta: Model<TApi> | null | undefined,\n\tb: Model<TApi> | null | undefined,\n): boolean {\n\tif (!a || !b) return false;\n\treturn a.id === b.id && a.provider === b.provider;\n}\n"]}
@@ -1,8 +1,15 @@
1
+ import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
1
2
  import type { SimpleStreamOptions, StreamFunction, StreamOptions } from "../types.js";
2
3
  export interface OpenAICodexResponsesOptions extends StreamOptions {
3
4
  reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
4
5
  reasoningSummary?: "auto" | "concise" | "detailed" | "off" | "on" | null;
5
6
  textVerbosity?: "low" | "medium" | "high";
7
+ serviceTier?: ResponseCreateParamsStreaming["service_tier"];
8
+ /** Optional callback invoked when the HTTP response headers are received (SSE only). */
9
+ onResponse?: (response: {
10
+ status: number;
11
+ headers: Record<string, string>;
12
+ }) => void | Promise<void>;
6
13
  }
7
14
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions>;
8
15
  export declare const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions>;
@@ -1 +1 @@
1
- {"version":3,"file":"openai-codex-responses.d.ts","sourceRoot":"","sources":["../../src/providers/openai-codex-responses.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAKX,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,MAAM,aAAa,CAAC;AA4BrB,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IACjE,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAC3E,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IACzE,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CAC1C;AAkDD,eAAO,MAAM,0BAA0B,EAAE,cAAc,CAAC,wBAAwB,EAAE,2BAA2B,CAiK5G,CAAC;AAEF,eAAO,MAAM,gCAAgC,EAAE,cAAc,CAAC,wBAAwB,EAAE,mBAAmB,CAiB1G,CAAC","sourcesContent":["import type * as NodeOs from \"node:os\";\nimport type { Tool as OpenAITool, ResponseInput, ResponseStreamEvent } from \"openai/resources/responses/responses.js\";\n\n// NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui)\nlet _os: typeof NodeOs | null = null;\n\ntype DynamicImport = (specifier: string) => Promise<unknown>;\n\nconst dynamicImport: DynamicImport = (specifier) => import(specifier);\nconst NODE_OS_SPECIFIER = \"node:\" + \"os\";\n\nif (typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun)) {\n\tdynamicImport(NODE_OS_SPECIFIER).then((m) => {\n\t\t_os = m as typeof NodeOs;\n\t});\n}\n\nimport { getEnvApiKey } from \"../env-api-keys.js\";\nimport { supportsXhigh } from \"../models.js\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tModel,\n\tSimpleStreamOptions,\n\tStreamFunction,\n\tStreamOptions,\n} from \"../types.js\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.js\";\nimport { convertResponsesMessages, convertResponsesTools, processResponsesStream } from \"./openai-responses-shared.js\";\nimport { buildBaseOptions, clampReasoning } from \"./simple-options.js\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst DEFAULT_CODEX_BASE_URL = \"https://chatgpt.com/backend-api\";\nconst JWT_CLAIM_PATH = \"https://api.openai.com/auth\" as const;\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\nconst CODEX_TOOL_CALL_PROVIDERS = new Set([\"openai\", \"openai-codex\", \"opencode\"]);\n\nconst CODEX_RESPONSE_STATUSES = new Set<CodexResponseStatus>([\n\t\"completed\",\n\t\"incomplete\",\n\t\"failed\",\n\t\"cancelled\",\n\t\"queued\",\n\t\"in_progress\",\n]);\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface OpenAICodexResponsesOptions extends StreamOptions {\n\treasoningEffort?: \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\treasoningSummary?: \"auto\" | \"concise\" | \"detailed\" | \"off\" | \"on\" | null;\n\ttextVerbosity?: \"low\" | \"medium\" | \"high\";\n}\n\ntype CodexResponseStatus = \"completed\" | \"incomplete\" | \"failed\" | \"cancelled\" | \"queued\" | \"in_progress\";\n\ninterface RequestBody {\n\tmodel: string;\n\tstore?: boolean;\n\tstream?: boolean;\n\tinstructions?: string;\n\tinput?: ResponseInput;\n\ttools?: OpenAITool[];\n\ttool_choice?: \"auto\";\n\tparallel_tool_calls?: boolean;\n\ttemperature?: number;\n\treasoning?: { effort?: string; summary?: string };\n\ttext?: { verbosity?: string };\n\tinclude?: string[];\n\tprompt_cache_key?: string;\n\t[key: string]: unknown;\n}\n\n// ============================================================================\n// Retry Helpers\n// ============================================================================\n\nfunction isRetryableError(status: number, errorText: string): boolean {\n\tif (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {\n\t\treturn true;\n\t}\n\treturn /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\"abort\", () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t});\n\t});\n}\n\n// ============================================================================\n// Main Stream Function\n// ============================================================================\n\nexport const streamOpenAICodexResponses: StreamFunction<\"openai-codex-responses\", OpenAICodexResponsesOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: \"openai-codex-responses\" as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider) || \"\";\n\t\t\tif (!apiKey) {\n\t\t\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t\t\t}\n\n\t\t\tconst accountId = extractAccountId(apiKey);\n\t\t\tlet body = buildRequestBody(model, context, options);\n\t\t\tconst nextBody = await options?.onPayload?.(body, model);\n\t\t\tif (nextBody !== undefined) {\n\t\t\t\tbody = nextBody as RequestBody;\n\t\t\t}\n\t\t\tconst websocketRequestId = options?.sessionId || createCodexRequestId();\n\t\t\tconst sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);\n\t\t\tconst websocketHeaders = buildWebSocketHeaders(\n\t\t\t\tmodel.headers,\n\t\t\t\toptions?.headers,\n\t\t\t\taccountId,\n\t\t\t\tapiKey,\n\t\t\t\twebsocketRequestId,\n\t\t\t);\n\t\t\tconst bodyJson = JSON.stringify(body);\n\t\t\tconst transport = options?.transport || \"sse\";\n\n\t\t\tif (transport !== \"sse\") {\n\t\t\t\tlet websocketStarted = false;\n\t\t\t\ttry {\n\t\t\t\t\tawait processWebSocketStream(\n\t\t\t\t\t\tresolveCodexWebSocketUrl(model.baseUrl),\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\twebsocketHeaders,\n\t\t\t\t\t\toutput,\n\t\t\t\t\t\tstream,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\twebsocketStarted = true;\n\t\t\t\t\t\t},\n\t\t\t\t\t\toptions,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t}\n\t\t\t\t\tstream.push({\n\t\t\t\t\t\ttype: \"done\",\n\t\t\t\t\t\treason: output.stopReason as \"stop\" | \"length\" | \"toolUse\",\n\t\t\t\t\t\tmessage: output,\n\t\t\t\t\t});\n\t\t\t\t\tstream.end();\n\t\t\t\t\treturn;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (transport === \"websocket\" || websocketStarted) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fetch with retry logic for rate limits and transient errors\n\t\t\tlet response: Response | undefined;\n\t\t\tlet lastError: Error | undefined;\n\n\t\t\tfor (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await fetch(resolveCodexUrl(model.baseUrl), {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: sseHeaders,\n\t\t\t\t\t\tbody: bodyJson,\n\t\t\t\t\t\tsignal: options?.signal,\n\t\t\t\t\t});\n\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst errorText = await response.text();\n\t\t\t\t\tif (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Parse error for friendly message on final attempt or non-retryable error\n\t\t\t\t\tconst fakeResponse = new Response(errorText, {\n\t\t\t\t\t\tstatus: response.status,\n\t\t\t\t\t\tstatusText: response.statusText,\n\t\t\t\t\t});\n\t\t\t\t\tconst info = await parseErrorResponse(fakeResponse);\n\t\t\t\t\tthrow new Error(info.friendlyMessage || info.message);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof Error) {\n\t\t\t\t\t\tif (error.name === \"AbortError\" || error.message === \"Request was aborted\") {\n\t\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t// Network errors are retryable\n\t\t\t\t\tif (attempt < MAX_RETRIES && !lastError.message.includes(\"usage limit\")) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthrow lastError;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!response?.ok) {\n\t\t\t\tthrow lastError ?? new Error(\"Failed after retries\");\n\t\t\t}\n\n\t\t\tif (!response.body) {\n\t\t\t\tthrow new Error(\"No response body\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"start\", partial: output });\n\t\t\tawait processStream(response, output, stream, model, options);\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason as \"stop\" | \"length\" | \"toolUse\", message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\nexport const streamSimpleOpenAICodexResponses: StreamFunction<\"openai-codex-responses\", SimpleStreamOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider);\n\tif (!apiKey) {\n\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t}\n\n\tconst base = buildBaseOptions(model, options, apiKey);\n\tconst reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning);\n\n\treturn streamOpenAICodexResponses(model, context, {\n\t\t...base,\n\t\treasoningEffort,\n\t} satisfies OpenAICodexResponsesOptions);\n};\n\n// ============================================================================\n// Request Building\n// ============================================================================\n\nfunction buildRequestBody(\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): RequestBody {\n\tconst messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {\n\t\tincludeSystemPrompt: false,\n\t});\n\n\tconst body: RequestBody = {\n\t\tmodel: model.id,\n\t\tstore: false,\n\t\tstream: true,\n\t\tinstructions: context.systemPrompt,\n\t\tinput: messages,\n\t\ttext: { verbosity: options?.textVerbosity || \"medium\" },\n\t\tinclude: [\"reasoning.encrypted_content\"],\n\t\tprompt_cache_key: options?.sessionId,\n\t\ttool_choice: \"auto\",\n\t\tparallel_tool_calls: true,\n\t};\n\n\tif (options?.temperature !== undefined) {\n\t\tbody.temperature = options.temperature;\n\t}\n\n\tif (context.tools) {\n\t\tbody.tools = convertResponsesTools(context.tools, { strict: null });\n\t}\n\n\tif (options?.reasoningEffort !== undefined) {\n\t\tbody.reasoning = {\n\t\t\teffort: clampReasoningEffort(model.id, options.reasoningEffort),\n\t\t\tsummary: options.reasoningSummary ?? \"auto\",\n\t\t};\n\t}\n\n\treturn body;\n}\n\nfunction clampReasoningEffort(modelId: string, effort: string): string {\n\tconst id = modelId.includes(\"/\") ? modelId.split(\"/\").pop()! : modelId;\n\tif ((id.startsWith(\"gpt-5.2\") || id.startsWith(\"gpt-5.3\") || id.startsWith(\"gpt-5.4\")) && effort === \"minimal\")\n\t\treturn \"low\";\n\tif (id === \"gpt-5.1\" && effort === \"xhigh\") return \"high\";\n\tif (id === \"gpt-5.1-codex-mini\") return effort === \"high\" || effort === \"xhigh\" ? \"high\" : \"medium\";\n\treturn effort;\n}\n\nfunction resolveCodexUrl(baseUrl?: string): string {\n\tconst raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;\n\tconst normalized = raw.replace(/\\/+$/, \"\");\n\tif (normalized.endsWith(\"/codex/responses\")) return normalized;\n\tif (normalized.endsWith(\"/codex\")) return `${normalized}/responses`;\n\treturn `${normalized}/codex/responses`;\n}\n\nfunction resolveCodexWebSocketUrl(baseUrl?: string): string {\n\tconst url = new URL(resolveCodexUrl(baseUrl));\n\tif (url.protocol === \"https:\") url.protocol = \"wss:\";\n\tif (url.protocol === \"http:\") url.protocol = \"ws:\";\n\treturn url.toString();\n}\n\n// ============================================================================\n// Response Processing\n// ============================================================================\n\nasync function processStream(\n\tresponse: Response,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tawait processResponsesStream(mapCodexEvents(parseSSE(response, options)), output, stream, model, options);\n}\n\nasync function* mapCodexEvents(events: AsyncIterable<Record<string, unknown>>): AsyncGenerator<ResponseStreamEvent> {\n\tfor await (const event of events) {\n\t\tconst type = typeof event.type === \"string\" ? event.type : undefined;\n\t\tif (!type) continue;\n\n\t\tif (type === \"error\") {\n\t\t\tconst code = (event as { code?: string }).code || \"\";\n\t\t\tconst message = (event as { message?: string }).message || \"\";\n\t\t\tthrow new Error(`Codex error: ${message || code || JSON.stringify(event)}`);\n\t\t}\n\n\t\tif (type === \"response.failed\") {\n\t\t\tconst msg = (event as { response?: { error?: { message?: string } } }).response?.error?.message;\n\t\t\tthrow new Error(msg || \"Codex response failed\");\n\t\t}\n\n\t\tif (type === \"response.done\" || type === \"response.completed\" || type === \"response.incomplete\") {\n\t\t\tconst response = (event as { response?: { status?: unknown } }).response;\n\t\t\tconst normalizedResponse = response\n\t\t\t\t? { ...response, status: normalizeCodexStatus(response.status) }\n\t\t\t\t: response;\n\t\t\tyield { ...event, type: \"response.completed\", response: normalizedResponse } as ResponseStreamEvent;\n\t\t\treturn;\n\t\t}\n\n\t\tyield event as unknown as ResponseStreamEvent;\n\t}\n}\n\nfunction normalizeCodexStatus(status: unknown): CodexResponseStatus | undefined {\n\tif (typeof status !== \"string\") return undefined;\n\treturn CODEX_RESPONSE_STATUSES.has(status as CodexResponseStatus) ? (status as CodexResponseStatus) : undefined;\n}\n\n// ============================================================================\n// SSE Parsing\n// ============================================================================\n\nasync function* parseSSE(\n\tresponse: Response,\n\toptions?: OpenAICodexResponsesOptions,\n): AsyncGenerator<Record<string, unknown>> {\n\tif (!response.body) return;\n\n\tconst reader = response.body.getReader();\n\tconst decoder = new TextDecoder();\n\tlet sseParseErrors = 0;\n\tlet buffer = \"\";\n\n\ttry {\n\t\twhile (true) {\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\tlet idx = buffer.indexOf(\"\\n\\n\");\n\t\t\twhile (idx !== -1) {\n\t\t\t\tconst chunk = buffer.slice(0, idx);\n\t\t\t\tbuffer = buffer.slice(idx + 2);\n\n\t\t\t\tconst dataLines = chunk\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.filter((l) => l.startsWith(\"data:\"))\n\t\t\t\t\t.map((l) => l.slice(5).trim());\n\t\t\t\tif (dataLines.length > 0) {\n\t\t\t\t\tconst data = dataLines.join(\"\\n\").trim();\n\t\t\t\t\tif (data && data !== \"[DONE]\") {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tyield JSON.parse(data);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Malformed SSE event — track for end-of-stream summary\n\t\t\t\t\t\t\tsseParseErrors++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tidx = buffer.indexOf(\"\\n\\n\");\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tif (sseParseErrors > 0) {\n\t\t\toptions?.onWarning?.(\n\t\t\t\t\"sse_parse_error\",\n\t\t\t\t`${sseParseErrors} malformed SSE event(s) were dropped during streaming. Response content may be incomplete.`,\n\t\t\t);\n\t\t}\n\t\ttry {\n\t\t\tawait reader.cancel();\n\t\t} catch {\n\t\t\t// Reader may already be closed — cleanup errors are safe to ignore\n\t\t}\n\t\ttry {\n\t\t\treader.releaseLock();\n\t\t} catch {\n\t\t\t// Reader may already be closed — cleanup errors are safe to ignore\n\t\t}\n\t}\n}\n\n// ============================================================================\n// WebSocket Parsing\n// ============================================================================\n\nconst OPENAI_BETA_RESPONSES_WEBSOCKETS = \"responses_websockets=2026-02-06\";\nconst SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;\n\ntype WebSocketEventType = \"open\" | \"message\" | \"error\" | \"close\";\ntype WebSocketListener = (event: unknown) => void;\n\ninterface WebSocketLike {\n\tclose(code?: number, reason?: string): void;\n\tsend(data: string): void;\n\taddEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n\tremoveEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n}\n\ninterface CachedWebSocketConnection {\n\tsocket: WebSocketLike;\n\tbusy: boolean;\n\tidleTimer?: ReturnType<typeof setTimeout>;\n}\n\nconst websocketSessionCache = new Map<string, CachedWebSocketConnection>();\n\ntype WebSocketConstructor = new (\n\turl: string,\n\tprotocols?: string | string[] | { headers?: Record<string, string> },\n) => WebSocketLike;\n\nfunction getWebSocketConstructor(): WebSocketConstructor | null {\n\tconst ctor = (globalThis as { WebSocket?: unknown }).WebSocket;\n\tif (typeof ctor !== \"function\") return null;\n\treturn ctor as unknown as WebSocketConstructor;\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n\tconst out: Record<string, string> = {};\n\tfor (const [key, value] of headers.entries()) {\n\t\tout[key] = value;\n\t}\n\treturn out;\n}\n\nfunction getWebSocketReadyState(socket: WebSocketLike): number | undefined {\n\tconst readyState = (socket as { readyState?: unknown }).readyState;\n\treturn typeof readyState === \"number\" ? readyState : undefined;\n}\n\nfunction isWebSocketReusable(socket: WebSocketLike): boolean {\n\tconst readyState = getWebSocketReadyState(socket);\n\t// If readyState is unavailable, assume the runtime keeps it open/reusable.\n\treturn readyState === undefined || readyState === 1;\n}\n\nfunction closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = \"done\"): void {\n\ttry {\n\t\tsocket.close(code, reason);\n\t} catch {\n\t\t// Intentionally silent — socket may already be closed\n\t}\n}\n\nfunction scheduleSessionWebSocketExpiry(sessionId: string, entry: CachedWebSocketConnection): void {\n\tif (entry.idleTimer) {\n\t\tclearTimeout(entry.idleTimer);\n\t}\n\tentry.idleTimer = setTimeout(() => {\n\t\tif (entry.busy) return;\n\t\tcloseWebSocketSilently(entry.socket, 1000, \"idle_timeout\");\n\t\twebsocketSessionCache.delete(sessionId);\n\t}, SESSION_WEBSOCKET_CACHE_TTL_MS);\n}\n\nasync function connectWebSocket(url: string, headers: Headers, signal?: AbortSignal): Promise<WebSocketLike> {\n\tconst WebSocketCtor = getWebSocketConstructor();\n\tif (!WebSocketCtor) {\n\t\tthrow new Error(\"WebSocket transport is not available in this runtime\");\n\t}\n\n\tconst wsHeaders = headersToRecord(headers);\n\tdelete wsHeaders[\"OpenAI-Beta\"];\n\n\treturn new Promise<WebSocketLike>((resolve, reject) => {\n\t\tlet settled = false;\n\t\tlet socket: WebSocketLike;\n\n\t\ttry {\n\t\t\tsocket = new WebSocketCtor(url, { headers: wsHeaders });\n\t\t} catch (error) {\n\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onOpen: WebSocketListener = () => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tresolve(socket);\n\t\t};\n\t\tconst onError: WebSocketListener = (event) => {\n\t\t\tconst error = extractWebSocketError(event);\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tconst onClose: WebSocketListener = (event) => {\n\t\t\tconst error = extractWebSocketCloseError(event);\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tsocket.close(1000, \"aborted\");\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t};\n\n\t\tconst cleanup = () => {\n\t\t\tsocket.removeEventListener(\"open\", onOpen);\n\t\t\tsocket.removeEventListener(\"error\", onError);\n\t\t\tsocket.removeEventListener(\"close\", onClose);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\n\t\tsocket.addEventListener(\"open\", onOpen);\n\t\tsocket.addEventListener(\"error\", onError);\n\t\tsocket.addEventListener(\"close\", onClose);\n\t\tsignal?.addEventListener(\"abort\", onAbort);\n\t});\n}\n\nasync function acquireWebSocket(\n\turl: string,\n\theaders: Headers,\n\tsessionId: string | undefined,\n\tsignal?: AbortSignal,\n): Promise<{ socket: WebSocketLike; release: (options?: { keep?: boolean }) => void }> {\n\tif (!sessionId) {\n\t\tconst socket = await connectWebSocket(url, headers, signal);\n\t\treturn {\n\t\t\tsocket,\n\t\t\trelease: ({ keep } = {}) => {\n\t\t\t\tif (keep === false) {\n\t\t\t\t\tcloseWebSocketSilently(socket);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcloseWebSocketSilently(socket);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst cached = websocketSessionCache.get(sessionId);\n\tif (cached) {\n\t\tif (cached.idleTimer) {\n\t\t\tclearTimeout(cached.idleTimer);\n\t\t\tcached.idleTimer = undefined;\n\t\t}\n\t\tif (!cached.busy && isWebSocketReusable(cached.socket)) {\n\t\t\tcached.busy = true;\n\t\t\treturn {\n\t\t\t\tsocket: cached.socket,\n\t\t\t\trelease: ({ keep } = {}) => {\n\t\t\t\t\tif (!keep || !isWebSocketReusable(cached.socket)) {\n\t\t\t\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcached.busy = false;\n\t\t\t\t\tscheduleSessionWebSocketExpiry(sessionId, cached);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (cached.busy) {\n\t\t\tconst socket = await connectWebSocket(url, headers, signal);\n\t\t\treturn {\n\t\t\t\tsocket,\n\t\t\t\trelease: () => {\n\t\t\t\t\tcloseWebSocketSilently(socket);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (!isWebSocketReusable(cached.socket)) {\n\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t}\n\t}\n\n\tconst socket = await connectWebSocket(url, headers, signal);\n\tconst entry: CachedWebSocketConnection = { socket, busy: true };\n\twebsocketSessionCache.set(sessionId, entry);\n\treturn {\n\t\tsocket,\n\t\trelease: ({ keep } = {}) => {\n\t\t\tif (!keep || !isWebSocketReusable(entry.socket)) {\n\t\t\t\tcloseWebSocketSilently(entry.socket);\n\t\t\t\tif (entry.idleTimer) clearTimeout(entry.idleTimer);\n\t\t\t\tif (websocketSessionCache.get(sessionId) === entry) {\n\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tentry.busy = false;\n\t\t\tscheduleSessionWebSocketExpiry(sessionId, entry);\n\t\t},\n\t};\n}\n\nfunction extractWebSocketError(event: unknown): Error {\n\tif (event && typeof event === \"object\" && \"message\" in event) {\n\t\tconst message = (event as { message?: unknown }).message;\n\t\tif (typeof message === \"string\" && message.length > 0) {\n\t\t\treturn new Error(message);\n\t\t}\n\t}\n\treturn new Error(\"WebSocket error\");\n}\n\nfunction extractWebSocketCloseError(event: unknown): Error {\n\tif (event && typeof event === \"object\") {\n\t\tconst code = \"code\" in event ? (event as { code?: unknown }).code : undefined;\n\t\tconst reason = \"reason\" in event ? (event as { reason?: unknown }).reason : undefined;\n\t\tconst codeText = typeof code === \"number\" ? ` ${code}` : \"\";\n\t\tconst reasonText = typeof reason === \"string\" && reason.length > 0 ? ` ${reason}` : \"\";\n\t\treturn new Error(`WebSocket closed${codeText}${reasonText}`.trim());\n\t}\n\treturn new Error(\"WebSocket closed\");\n}\n\nasync function decodeWebSocketData(data: unknown): Promise<string | null> {\n\tif (typeof data === \"string\") return data;\n\tif (data instanceof ArrayBuffer) {\n\t\treturn new TextDecoder().decode(new Uint8Array(data));\n\t}\n\tif (ArrayBuffer.isView(data)) {\n\t\tconst view = data as ArrayBufferView;\n\t\treturn new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));\n\t}\n\tif (data && typeof data === \"object\" && \"arrayBuffer\" in data) {\n\t\tconst blobLike = data as { arrayBuffer: () => Promise<ArrayBuffer> };\n\t\tconst arrayBuffer = await blobLike.arrayBuffer();\n\t\treturn new TextDecoder().decode(new Uint8Array(arrayBuffer));\n\t}\n\treturn null;\n}\n\nasync function* parseWebSocket(\n\tsocket: WebSocketLike,\n\tsignal?: AbortSignal,\n\toptions?: OpenAICodexResponsesOptions,\n): AsyncGenerator<Record<string, unknown>> {\n\tconst queue: Record<string, unknown>[] = [];\n\tlet pending: (() => void) | null = null;\n\tlet done = false;\n\tlet failed: Error | null = null;\n\tlet sawCompletion = false;\n\n\tconst wake = () => {\n\t\tif (!pending) return;\n\t\tconst resolve = pending;\n\t\tpending = null;\n\t\tresolve();\n\t};\n\n\tconst onMessage: WebSocketListener = (event) => {\n\t\tvoid (async () => {\n\t\t\tif (!event || typeof event !== \"object\" || !(\"data\" in event)) return;\n\t\t\tconst text = await decodeWebSocketData((event as { data?: unknown }).data);\n\t\t\tif (!text) return;\n\t\t\ttry {\n\t\t\t\tconst parsed = JSON.parse(text) as Record<string, unknown>;\n\t\t\t\tconst type = typeof parsed.type === \"string\" ? parsed.type : \"\";\n\t\t\t\tif (type === \"response.completed\" || type === \"response.done\" || type === \"response.incomplete\") {\n\t\t\t\t\tsawCompletion = true;\n\t\t\t\t\tdone = true;\n\t\t\t\t}\n\t\t\t\tqueue.push(parsed);\n\t\t\t} catch {\n\t\t\t\t// Malformed WebSocket message — warn but don't drop wake()\n\t\t\t\toptions?.onWarning?.(\"ws_parse_error\", `Malformed WebSocket message dropped: ${text.slice(0, 200)}`);\n\t\t\t} finally {\n\t\t\t\t// Always wake the consumer — even on parse failure — to prevent stream hangs\n\t\t\t\twake();\n\t\t\t}\n\t\t})();\n\t};\n\n\tconst onError: WebSocketListener = (event) => {\n\t\tfailed = extractWebSocketError(event);\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onClose: WebSocketListener = (event) => {\n\t\tif (sawCompletion) {\n\t\t\tdone = true;\n\t\t\twake();\n\t\t\treturn;\n\t\t}\n\t\tif (!failed) {\n\t\t\tfailed = extractWebSocketCloseError(event);\n\t\t}\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onAbort = () => {\n\t\tfailed = new Error(\"Request was aborted\");\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tsocket.addEventListener(\"message\", onMessage);\n\tsocket.addEventListener(\"error\", onError);\n\tsocket.addEventListener(\"close\", onClose);\n\tsignal?.addEventListener(\"abort\", onAbort);\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tif (queue.length > 0) {\n\t\t\t\tyield queue.shift()!;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (done) break;\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tpending = resolve;\n\t\t\t});\n\t\t}\n\n\t\tif (failed) {\n\t\t\tthrow failed;\n\t\t}\n\t\tif (!sawCompletion) {\n\t\t\tthrow new Error(\"WebSocket stream closed before response.completed\");\n\t\t}\n\t} finally {\n\t\tsocket.removeEventListener(\"message\", onMessage);\n\t\tsocket.removeEventListener(\"error\", onError);\n\t\tsocket.removeEventListener(\"close\", onClose);\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\nasync function processWebSocketStream(\n\turl: string,\n\tbody: RequestBody,\n\theaders: Headers,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\tonStart: () => void,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tconst { socket, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);\n\tlet keepConnection = true;\n\ttry {\n\t\tsocket.send(JSON.stringify({ type: \"response.create\", ...body }));\n\t\tonStart();\n\t\tstream.push({ type: \"start\", partial: output });\n\t\tawait processResponsesStream(\n\t\t\tmapCodexEvents(parseWebSocket(socket, options?.signal, options)),\n\t\t\toutput,\n\t\t\tstream,\n\t\t\tmodel,\n\t\t\toptions,\n\t\t);\n\t\tif (options?.signal?.aborted) {\n\t\t\tkeepConnection = false;\n\t\t}\n\t} catch (error) {\n\t\tkeepConnection = false;\n\t\tthrow error;\n\t} finally {\n\t\trelease({ keep: keepConnection });\n\t}\n}\n\n// ============================================================================\n// Error Handling\n// ============================================================================\n\nasync function parseErrorResponse(response: Response): Promise<{ message: string; friendlyMessage?: string }> {\n\tconst raw = await response.text();\n\tlet message = raw || response.statusText || \"Request failed\";\n\tlet friendlyMessage: string | undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as {\n\t\t\terror?: { code?: string; type?: string; message?: string; plan_type?: string; resets_at?: number };\n\t\t};\n\t\tconst err = parsed?.error;\n\t\tif (err) {\n\t\t\tconst code = err.code || err.type || \"\";\n\t\t\tif (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {\n\t\t\t\tconst plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : \"\";\n\t\t\t\tconst mins = err.resets_at\n\t\t\t\t\t? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000))\n\t\t\t\t\t: undefined;\n\t\t\t\tconst when = mins !== undefined ? ` Try again in ~${mins} min.` : \"\";\n\t\t\t\tfriendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();\n\t\t\t}\n\t\t\tmessage = err.message || friendlyMessage || message;\n\t\t}\n\t} catch {\n\t\t// Best-effort extraction of friendly error message — raw text is the fallback\n\t}\n\n\treturn { message, friendlyMessage };\n}\n\n// ============================================================================\n// Auth & Headers\n// ============================================================================\n\nfunction extractAccountId(token: string): string {\n\ttry {\n\t\tconst parts = token.split(\".\");\n\t\tif (parts.length !== 3) throw new Error(\"Invalid token\");\n\t\tconst payload = JSON.parse(atob(parts[1]));\n\t\tconst accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;\n\t\tif (!accountId) throw new Error(\"No account ID in token\");\n\t\treturn accountId;\n\t} catch {\n\t\t// Re-throw with unified message for any JWT decode failure\n\t\tthrow new Error(\"Failed to extract accountId from token\");\n\t}\n}\n\nfunction createCodexRequestId(): string {\n\tif (typeof globalThis.crypto?.randomUUID === \"function\") {\n\t\treturn globalThis.crypto.randomUUID();\n\t}\n\treturn `codex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction buildBaseCodexHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: Record<string, string> | undefined,\n\taccountId: string,\n\ttoken: string,\n): Headers {\n\tconst headers = new Headers(initHeaders);\n\tfor (const [key, value] of Object.entries(additionalHeaders || {})) {\n\t\theaders.set(key, value);\n\t}\n\theaders.set(\"Authorization\", `Bearer ${token}`);\n\theaders.set(\"chatgpt-account-id\", accountId);\n\theaders.set(\"originator\", \"dreb\");\n\tconst userAgent = _os ? `dreb (${_os.platform()} ${_os.release()}; ${_os.arch()})` : \"dreb (browser)\";\n\theaders.set(\"User-Agent\", userAgent);\n\treturn headers;\n}\n\nfunction buildSSEHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: Record<string, string> | undefined,\n\taccountId: string,\n\ttoken: string,\n\tsessionId?: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.set(\"OpenAI-Beta\", \"responses=experimental\");\n\theaders.set(\"accept\", \"text/event-stream\");\n\theaders.set(\"content-type\", \"application/json\");\n\n\tif (sessionId) {\n\t\theaders.set(\"session_id\", sessionId);\n\t}\n\n\treturn headers;\n}\n\nfunction buildWebSocketHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: Record<string, string> | undefined,\n\taccountId: string,\n\ttoken: string,\n\trequestId: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.delete(\"accept\");\n\theaders.delete(\"content-type\");\n\theaders.delete(\"OpenAI-Beta\");\n\theaders.delete(\"openai-beta\");\n\theaders.set(\"OpenAI-Beta\", OPENAI_BETA_RESPONSES_WEBSOCKETS);\n\theaders.set(\"x-client-request-id\", requestId);\n\theaders.set(\"session_id\", requestId);\n\treturn headers;\n}\n"]}
1
+ {"version":3,"file":"openai-codex-responses.d.ts","sourceRoot":"","sources":["../../src/providers/openai-codex-responses.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEX,6BAA6B,EAG7B,MAAM,yCAAyC,CAAC;AAkBjD,OAAO,KAAK,EAKX,mBAAmB,EACnB,cAAc,EACd,aAAa,EAEb,MAAM,aAAa,CAAC;AA4BrB,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IACjE,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAC3E,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IACzE,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC1C,WAAW,CAAC,EAAE,6BAA6B,CAAC,cAAc,CAAC,CAAC;IAC5D,wFAAwF;IACxF,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrG;AAoDD,eAAO,MAAM,0BAA0B,EAAE,cAAc,CAAC,wBAAwB,EAAE,2BAA2B,CAyK5G,CAAC;AAEF,eAAO,MAAM,gCAAgC,EAAE,cAAc,CAAC,wBAAwB,EAAE,mBAAmB,CAiB1G,CAAC","sourcesContent":["import type * as NodeOs from \"node:os\";\nimport type {\n\tTool as OpenAITool,\n\tResponseCreateParamsStreaming,\n\tResponseInput,\n\tResponseStreamEvent,\n} from \"openai/resources/responses/responses.js\";\n\n// NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui)\nlet _os: typeof NodeOs | null = null;\n\ntype DynamicImport = (specifier: string) => Promise<unknown>;\n\nconst dynamicImport: DynamicImport = (specifier) => import(specifier);\nconst NODE_OS_SPECIFIER = \"node:\" + \"os\";\n\nif (typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun)) {\n\tdynamicImport(NODE_OS_SPECIFIER).then((m) => {\n\t\t_os = m as typeof NodeOs;\n\t});\n}\n\nimport { getEnvApiKey } from \"../env-api-keys.js\";\nimport { supportsXhigh } from \"../models.js\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tModel,\n\tSimpleStreamOptions,\n\tStreamFunction,\n\tStreamOptions,\n\tUsage,\n} from \"../types.js\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.js\";\nimport { convertResponsesMessages, convertResponsesTools, processResponsesStream } from \"./openai-responses-shared.js\";\nimport { buildBaseOptions, clampReasoning } from \"./simple-options.js\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst DEFAULT_CODEX_BASE_URL = \"https://chatgpt.com/backend-api\";\nconst JWT_CLAIM_PATH = \"https://api.openai.com/auth\" as const;\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\nconst CODEX_TOOL_CALL_PROVIDERS = new Set([\"openai\", \"openai-codex\", \"opencode\"]);\n\nconst CODEX_RESPONSE_STATUSES = new Set<CodexResponseStatus>([\n\t\"completed\",\n\t\"incomplete\",\n\t\"failed\",\n\t\"cancelled\",\n\t\"queued\",\n\t\"in_progress\",\n]);\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface OpenAICodexResponsesOptions extends StreamOptions {\n\treasoningEffort?: \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\treasoningSummary?: \"auto\" | \"concise\" | \"detailed\" | \"off\" | \"on\" | null;\n\ttextVerbosity?: \"low\" | \"medium\" | \"high\";\n\tserviceTier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\t/** Optional callback invoked when the HTTP response headers are received (SSE only). */\n\tonResponse?: (response: { status: number; headers: Record<string, string> }) => void | Promise<void>;\n}\n\ntype CodexResponseStatus = \"completed\" | \"incomplete\" | \"failed\" | \"cancelled\" | \"queued\" | \"in_progress\";\n\ninterface RequestBody {\n\tmodel: string;\n\tstore?: boolean;\n\tstream?: boolean;\n\tinstructions?: string;\n\tinput?: ResponseInput;\n\ttools?: OpenAITool[];\n\ttool_choice?: \"auto\";\n\tparallel_tool_calls?: boolean;\n\ttemperature?: number;\n\treasoning?: { effort?: string; summary?: string };\n\ttext?: { verbosity?: string };\n\tinclude?: string[];\n\tprompt_cache_key?: string;\n\tservice_tier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\tprevious_response_id?: string;\n\t[key: string]: unknown;\n}\n\n// ============================================================================\n// Retry Helpers\n// ============================================================================\n\nfunction isRetryableError(status: number, errorText: string): boolean {\n\tif (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {\n\t\treturn true;\n\t}\n\treturn /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\"abort\", () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t});\n\t});\n}\n\n// ============================================================================\n// Main Stream Function\n// ============================================================================\n\nexport const streamOpenAICodexResponses: StreamFunction<\"openai-codex-responses\", OpenAICodexResponsesOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: \"openai-codex-responses\" as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider) || \"\";\n\t\t\tif (!apiKey) {\n\t\t\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t\t\t}\n\n\t\t\tconst accountId = extractAccountId(apiKey);\n\t\t\tlet body = buildRequestBody(model, context, options);\n\t\t\tconst nextBody = await options?.onPayload?.(body, model);\n\t\t\tif (nextBody !== undefined) {\n\t\t\t\tbody = nextBody as RequestBody;\n\t\t\t}\n\t\t\tconst websocketRequestId = options?.sessionId || createCodexRequestId();\n\t\t\tconst sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);\n\t\t\tconst websocketHeaders = buildWebSocketHeaders(\n\t\t\t\tmodel.headers,\n\t\t\t\toptions?.headers,\n\t\t\t\taccountId,\n\t\t\t\tapiKey,\n\t\t\t\twebsocketRequestId,\n\t\t\t);\n\t\t\tconst bodyJson = JSON.stringify(body);\n\t\t\tconst transport = options?.transport || \"sse\";\n\n\t\t\tif (transport !== \"sse\") {\n\t\t\t\tlet websocketStarted = false;\n\t\t\t\ttry {\n\t\t\t\t\tawait processWebSocketStream(\n\t\t\t\t\t\tresolveCodexWebSocketUrl(model.baseUrl),\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\twebsocketHeaders,\n\t\t\t\t\t\toutput,\n\t\t\t\t\t\tstream,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\twebsocketStarted = true;\n\t\t\t\t\t\t},\n\t\t\t\t\t\toptions,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t}\n\t\t\t\t\tstream.push({\n\t\t\t\t\t\ttype: \"done\",\n\t\t\t\t\t\treason: output.stopReason as \"stop\" | \"length\" | \"toolUse\",\n\t\t\t\t\t\tmessage: output,\n\t\t\t\t\t});\n\t\t\t\t\tstream.end();\n\t\t\t\t\treturn;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (transport === \"websocket\" || websocketStarted) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fetch with retry logic for rate limits and transient errors\n\t\t\tlet response: Response | undefined;\n\t\t\tlet lastError: Error | undefined;\n\n\t\t\tfor (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await fetch(resolveCodexUrl(model.baseUrl), {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: sseHeaders,\n\t\t\t\t\t\tbody: bodyJson,\n\t\t\t\t\t\tsignal: options?.signal,\n\t\t\t\t\t});\n\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst errorText = await response.text();\n\t\t\t\t\tif (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Parse error for friendly message on final attempt or non-retryable error\n\t\t\t\t\tconst fakeResponse = new Response(errorText, {\n\t\t\t\t\t\tstatus: response.status,\n\t\t\t\t\t\tstatusText: response.statusText,\n\t\t\t\t\t});\n\t\t\t\t\tconst info = await parseErrorResponse(fakeResponse);\n\t\t\t\t\tthrow new Error(info.friendlyMessage || info.message);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof Error) {\n\t\t\t\t\t\tif (error.name === \"AbortError\" || error.message === \"Request was aborted\") {\n\t\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t// Network errors are retryable\n\t\t\t\t\tif (attempt < MAX_RETRIES && !lastError.message.includes(\"usage limit\")) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthrow lastError;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!response?.ok) {\n\t\t\t\tthrow lastError ?? new Error(\"Failed after retries\");\n\t\t\t}\n\n\t\t\t// Fire onResponse exactly once, after the retry loop succeeds.\n\t\t\t// Wrap in its own try/catch so callback errors don't affect stream processing.\n\t\t\ttry {\n\t\t\t\tawait options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) });\n\t\t\t} catch {\n\t\t\t\t// Intentionally ignored — callback errors must not disrupt the stream\n\t\t\t}\n\n\t\t\tif (!response.body) {\n\t\t\t\tthrow new Error(\"No response body\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"start\", partial: output });\n\t\t\tawait processStream(response, output, stream, model, options);\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason as \"stop\" | \"length\" | \"toolUse\", message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\nexport const streamSimpleOpenAICodexResponses: StreamFunction<\"openai-codex-responses\", SimpleStreamOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider);\n\tif (!apiKey) {\n\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t}\n\n\tconst base = buildBaseOptions(model, options, apiKey);\n\tconst reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning);\n\n\treturn streamOpenAICodexResponses(model, context, {\n\t\t...base,\n\t\treasoningEffort,\n\t} satisfies OpenAICodexResponsesOptions);\n};\n\n// ============================================================================\n// Request Building\n// ============================================================================\n\nfunction buildRequestBody(\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): RequestBody {\n\tconst messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {\n\t\tincludeSystemPrompt: false,\n\t});\n\n\tconst body: RequestBody = {\n\t\tmodel: model.id,\n\t\tstore: false,\n\t\tstream: true,\n\t\tinstructions: context.systemPrompt,\n\t\tinput: messages,\n\t\ttext: { verbosity: options?.textVerbosity || \"low\" },\n\t\tinclude: [\"reasoning.encrypted_content\"],\n\t\tprompt_cache_key: options?.sessionId,\n\t\ttool_choice: \"auto\",\n\t\tparallel_tool_calls: true,\n\t};\n\n\tif (options?.temperature !== undefined) {\n\t\tbody.temperature = options.temperature;\n\t}\n\n\tif (context.tools) {\n\t\tbody.tools = convertResponsesTools(context.tools, { strict: null });\n\t}\n\n\tif (options?.reasoningEffort !== undefined) {\n\t\tbody.reasoning = {\n\t\t\teffort: clampReasoningEffort(model.id, options.reasoningEffort),\n\t\t\tsummary: options.reasoningSummary ?? \"auto\",\n\t\t};\n\t}\n\n\tif (options?.serviceTier !== undefined) {\n\t\tbody.service_tier = options.serviceTier;\n\t}\n\n\treturn body;\n}\n\nfunction clampReasoningEffort(modelId: string, effort: string): string {\n\tconst id = modelId.includes(\"/\") ? modelId.split(\"/\").pop()! : modelId;\n\tif (\n\t\t(id.startsWith(\"gpt-5.2\") || id.startsWith(\"gpt-5.3\") || id.startsWith(\"gpt-5.4\") || id.startsWith(\"gpt-5.5\")) &&\n\t\teffort === \"minimal\"\n\t)\n\t\treturn \"low\";\n\tif (id === \"gpt-5.1\" && effort === \"xhigh\") return \"high\";\n\tif (id === \"gpt-5.1-codex-mini\") return effort === \"high\" || effort === \"xhigh\" ? \"high\" : \"medium\";\n\treturn effort;\n}\n\nfunction resolveCodexUrl(baseUrl?: string): string {\n\tconst raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;\n\tconst normalized = raw.replace(/\\/+$/, \"\");\n\tif (normalized.endsWith(\"/codex/responses\")) return normalized;\n\tif (normalized.endsWith(\"/codex\")) return `${normalized}/responses`;\n\treturn `${normalized}/codex/responses`;\n}\n\nfunction resolveCodexWebSocketUrl(baseUrl?: string): string {\n\tconst url = new URL(resolveCodexUrl(baseUrl));\n\tif (url.protocol === \"https:\") url.protocol = \"wss:\";\n\tif (url.protocol === \"http:\") url.protocol = \"ws:\";\n\treturn url.toString();\n}\n\n// ============================================================================\n// Response Processing\n// ============================================================================\n\nasync function processStream(\n\tresponse: Response,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tawait processResponsesStream(mapCodexEvents(parseSSE(response, options)), output, stream, model, {\n\t\tserviceTier: options?.serviceTier,\n\t\tresolveServiceTier: resolveCodexServiceTier,\n\t\tapplyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),\n\t\tonWarning: options?.onWarning,\n\t});\n}\n\nasync function* mapCodexEvents(events: AsyncIterable<Record<string, unknown>>): AsyncGenerator<ResponseStreamEvent> {\n\tfor await (const event of events) {\n\t\tconst type = typeof event.type === \"string\" ? event.type : undefined;\n\t\tif (!type) continue;\n\n\t\tif (type === \"error\") {\n\t\t\tconst code = (event as { code?: string }).code || \"\";\n\t\t\tconst message = (event as { message?: string }).message || \"\";\n\t\t\tthrow new Error(`Codex error: ${message || code || JSON.stringify(event)}`);\n\t\t}\n\n\t\tif (type === \"response.failed\") {\n\t\t\tconst msg = (event as { response?: { error?: { message?: string } } }).response?.error?.message;\n\t\t\tthrow new Error(msg || \"Codex response failed\");\n\t\t}\n\n\t\tif (type === \"response.done\" || type === \"response.completed\" || type === \"response.incomplete\") {\n\t\t\tconst response = (event as { response?: { status?: unknown } }).response;\n\t\t\tconst normalizedResponse = response\n\t\t\t\t? { ...response, status: normalizeCodexStatus(response.status) }\n\t\t\t\t: response;\n\t\t\tyield { ...event, type: \"response.completed\", response: normalizedResponse } as ResponseStreamEvent;\n\t\t\treturn;\n\t\t}\n\n\t\tyield event as unknown as ResponseStreamEvent;\n\t}\n}\n\nfunction normalizeCodexStatus(status: unknown): CodexResponseStatus | undefined {\n\tif (typeof status !== \"string\") return undefined;\n\treturn CODEX_RESPONSE_STATUSES.has(status as CodexResponseStatus) ? (status as CodexResponseStatus) : undefined;\n}\n\n// ============================================================================\n// SSE Parsing\n// ============================================================================\n\nasync function* parseSSE(\n\tresponse: Response,\n\toptions?: OpenAICodexResponsesOptions,\n): AsyncGenerator<Record<string, unknown>> {\n\tif (!response.body) return;\n\n\tconst reader = response.body.getReader();\n\tconst decoder = new TextDecoder();\n\tlet sseParseErrors = 0;\n\tlet buffer = \"\";\n\n\ttry {\n\t\twhile (true) {\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\tlet idx = buffer.indexOf(\"\\n\\n\");\n\t\t\twhile (idx !== -1) {\n\t\t\t\tconst chunk = buffer.slice(0, idx);\n\t\t\t\tbuffer = buffer.slice(idx + 2);\n\n\t\t\t\tconst dataLines = chunk\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.filter((l) => l.startsWith(\"data:\"))\n\t\t\t\t\t.map((l) => l.slice(5).trim());\n\t\t\t\tif (dataLines.length > 0) {\n\t\t\t\t\tconst data = dataLines.join(\"\\n\").trim();\n\t\t\t\t\tif (data && data !== \"[DONE]\") {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tyield JSON.parse(data);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Malformed SSE event — track for end-of-stream summary\n\t\t\t\t\t\t\tsseParseErrors++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tidx = buffer.indexOf(\"\\n\\n\");\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tif (sseParseErrors > 0) {\n\t\t\toptions?.onWarning?.(\n\t\t\t\t\"sse_parse_error\",\n\t\t\t\t`${sseParseErrors} malformed SSE event(s) were dropped during streaming. Response content may be incomplete.`,\n\t\t\t);\n\t\t}\n\t\ttry {\n\t\t\tawait reader.cancel();\n\t\t} catch {\n\t\t\t// Reader may already be closed — cleanup errors are safe to ignore\n\t\t}\n\t\ttry {\n\t\t\treader.releaseLock();\n\t\t} catch {\n\t\t\t// Reader may already be closed — cleanup errors are safe to ignore\n\t\t}\n\t}\n}\n\n// ============================================================================\n// WebSocket Parsing\n// ============================================================================\n\nconst OPENAI_BETA_RESPONSES_WEBSOCKETS = \"responses_websockets=2026-02-06\";\nconst SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;\n\ntype WebSocketEventType = \"open\" | \"message\" | \"error\" | \"close\";\ntype WebSocketListener = (event: unknown) => void;\n\ninterface WebSocketLike {\n\tclose(code?: number, reason?: string): void;\n\tsend(data: string): void;\n\taddEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n\tremoveEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n}\n\ninterface CachedWebSocketContinuationState {\n\tlastRequestBody: RequestBody;\n\tlastResponseId: string;\n\tlastResponseItems: ResponseInput;\n}\n\ninterface CachedWebSocketConnection {\n\tsocket: WebSocketLike;\n\tbusy: boolean;\n\tidleTimer?: ReturnType<typeof setTimeout>;\n\tcontinuation?: CachedWebSocketContinuationState;\n}\n\nconst websocketSessionCache = new Map<string, CachedWebSocketConnection>();\n\ntype WebSocketConstructor = new (\n\turl: string,\n\tprotocols?: string | string[] | { headers?: Record<string, string> },\n) => WebSocketLike;\n\nfunction getWebSocketConstructor(): WebSocketConstructor | null {\n\tconst ctor = (globalThis as { WebSocket?: unknown }).WebSocket;\n\tif (typeof ctor !== \"function\") return null;\n\treturn ctor as unknown as WebSocketConstructor;\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n\tconst out: Record<string, string> = {};\n\tfor (const [key, value] of headers.entries()) {\n\t\tout[key] = value;\n\t}\n\treturn out;\n}\n\nfunction getWebSocketReadyState(socket: WebSocketLike): number | undefined {\n\tconst readyState = (socket as { readyState?: unknown }).readyState;\n\treturn typeof readyState === \"number\" ? readyState : undefined;\n}\n\nfunction isWebSocketReusable(socket: WebSocketLike): boolean {\n\tconst readyState = getWebSocketReadyState(socket);\n\t// If readyState is unavailable, assume the runtime keeps it open/reusable.\n\treturn readyState === undefined || readyState === 1;\n}\n\nfunction closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = \"done\"): void {\n\ttry {\n\t\tsocket.close(code, reason);\n\t} catch {\n\t\t// Intentionally silent — socket may already be closed\n\t}\n}\n\nfunction scheduleSessionWebSocketExpiry(sessionId: string, entry: CachedWebSocketConnection): void {\n\tif (entry.idleTimer) {\n\t\tclearTimeout(entry.idleTimer);\n\t}\n\tentry.idleTimer = setTimeout(() => {\n\t\tif (entry.busy) return;\n\t\tcloseWebSocketSilently(entry.socket, 1000, \"idle_timeout\");\n\t\twebsocketSessionCache.delete(sessionId);\n\t}, SESSION_WEBSOCKET_CACHE_TTL_MS);\n}\n\nasync function connectWebSocket(url: string, headers: Headers, signal?: AbortSignal): Promise<WebSocketLike> {\n\tconst WebSocketCtor = getWebSocketConstructor();\n\tif (!WebSocketCtor) {\n\t\tthrow new Error(\"WebSocket transport is not available in this runtime\");\n\t}\n\n\tconst wsHeaders = headersToRecord(headers);\n\tdelete wsHeaders[\"OpenAI-Beta\"];\n\n\treturn new Promise<WebSocketLike>((resolve, reject) => {\n\t\tlet settled = false;\n\t\tlet socket: WebSocketLike;\n\n\t\ttry {\n\t\t\tsocket = new WebSocketCtor(url, { headers: wsHeaders });\n\t\t} catch (error) {\n\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onOpen: WebSocketListener = () => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tresolve(socket);\n\t\t};\n\t\tconst onError: WebSocketListener = (event) => {\n\t\t\tconst error = extractWebSocketError(event);\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tconst onClose: WebSocketListener = (event) => {\n\t\t\tconst error = extractWebSocketCloseError(event);\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tsocket.close(1000, \"aborted\");\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t};\n\n\t\tconst cleanup = () => {\n\t\t\tsocket.removeEventListener(\"open\", onOpen);\n\t\t\tsocket.removeEventListener(\"error\", onError);\n\t\t\tsocket.removeEventListener(\"close\", onClose);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\n\t\tsocket.addEventListener(\"open\", onOpen);\n\t\tsocket.addEventListener(\"error\", onError);\n\t\tsocket.addEventListener(\"close\", onClose);\n\t\tsignal?.addEventListener(\"abort\", onAbort);\n\t});\n}\n\nasync function acquireWebSocket(\n\turl: string,\n\theaders: Headers,\n\tsessionId: string | undefined,\n\tsignal?: AbortSignal,\n): Promise<{\n\tsocket: WebSocketLike;\n\tentry?: CachedWebSocketConnection;\n\trelease: (options?: { keep?: boolean }) => void;\n}> {\n\tif (!sessionId) {\n\t\tconst socket = await connectWebSocket(url, headers, signal);\n\t\treturn {\n\t\t\tsocket,\n\t\t\trelease: () => {\n\t\t\t\tcloseWebSocketSilently(socket);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst cached = websocketSessionCache.get(sessionId);\n\tif (cached) {\n\t\tif (cached.idleTimer) {\n\t\t\tclearTimeout(cached.idleTimer);\n\t\t\tcached.idleTimer = undefined;\n\t\t}\n\t\tif (!cached.busy && isWebSocketReusable(cached.socket)) {\n\t\t\tcached.busy = true;\n\t\t\treturn {\n\t\t\t\tsocket: cached.socket,\n\t\t\t\tentry: cached,\n\t\t\t\trelease: ({ keep } = {}) => {\n\t\t\t\t\tif (!keep || !isWebSocketReusable(cached.socket)) {\n\t\t\t\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcached.busy = false;\n\t\t\t\t\tscheduleSessionWebSocketExpiry(sessionId, cached);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (cached.busy) {\n\t\t\tconst socket = await connectWebSocket(url, headers, signal);\n\t\t\treturn {\n\t\t\t\tsocket,\n\t\t\t\trelease: () => {\n\t\t\t\t\tcloseWebSocketSilently(socket);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (!isWebSocketReusable(cached.socket)) {\n\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t}\n\t}\n\n\tconst socket = await connectWebSocket(url, headers, signal);\n\tconst entry: CachedWebSocketConnection = { socket, busy: true };\n\twebsocketSessionCache.set(sessionId, entry);\n\treturn {\n\t\tsocket,\n\t\tentry,\n\t\trelease: ({ keep } = {}) => {\n\t\t\tif (!keep || !isWebSocketReusable(entry.socket)) {\n\t\t\t\tcloseWebSocketSilently(entry.socket);\n\t\t\t\tif (entry.idleTimer) clearTimeout(entry.idleTimer);\n\t\t\t\tif (websocketSessionCache.get(sessionId) === entry) {\n\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tentry.busy = false;\n\t\t\tscheduleSessionWebSocketExpiry(sessionId, entry);\n\t\t},\n\t};\n}\n\nfunction extractWebSocketError(event: unknown): Error {\n\tif (event && typeof event === \"object\" && \"message\" in event) {\n\t\tconst message = (event as { message?: unknown }).message;\n\t\tif (typeof message === \"string\" && message.length > 0) {\n\t\t\treturn new Error(message);\n\t\t}\n\t}\n\treturn new Error(\"WebSocket error\");\n}\n\nfunction extractWebSocketCloseError(event: unknown): Error {\n\tif (event && typeof event === \"object\") {\n\t\tconst code = \"code\" in event ? (event as { code?: unknown }).code : undefined;\n\t\tconst reason = \"reason\" in event ? (event as { reason?: unknown }).reason : undefined;\n\t\tconst codeText = typeof code === \"number\" ? ` ${code}` : \"\";\n\t\tconst reasonText = typeof reason === \"string\" && reason.length > 0 ? ` ${reason}` : \"\";\n\t\treturn new Error(`WebSocket closed${codeText}${reasonText}`.trim());\n\t}\n\treturn new Error(\"WebSocket closed\");\n}\n\nasync function decodeWebSocketData(data: unknown): Promise<string | null> {\n\tif (typeof data === \"string\") return data;\n\tif (data instanceof ArrayBuffer) {\n\t\treturn new TextDecoder().decode(new Uint8Array(data));\n\t}\n\tif (ArrayBuffer.isView(data)) {\n\t\tconst view = data as ArrayBufferView;\n\t\treturn new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));\n\t}\n\tif (data && typeof data === \"object\" && \"arrayBuffer\" in data) {\n\t\tconst blobLike = data as { arrayBuffer: () => Promise<ArrayBuffer> };\n\t\tconst arrayBuffer = await blobLike.arrayBuffer();\n\t\treturn new TextDecoder().decode(new Uint8Array(arrayBuffer));\n\t}\n\treturn null;\n}\n\nasync function* parseWebSocket(\n\tsocket: WebSocketLike,\n\tsignal?: AbortSignal,\n\toptions?: OpenAICodexResponsesOptions,\n): AsyncGenerator<Record<string, unknown>> {\n\tconst queue: Record<string, unknown>[] = [];\n\tlet pending: (() => void) | null = null;\n\tlet done = false;\n\tlet failed: Error | null = null;\n\tlet sawCompletion = false;\n\n\tconst wake = () => {\n\t\tif (!pending) return;\n\t\tconst resolve = pending;\n\t\tpending = null;\n\t\tresolve();\n\t};\n\n\tconst onMessage: WebSocketListener = (event) => {\n\t\tvoid (async () => {\n\t\t\tif (!event || typeof event !== \"object\" || !(\"data\" in event)) return;\n\t\t\tlet text: string | null | undefined;\n\t\t\ttry {\n\t\t\t\ttext = await decodeWebSocketData((event as { data?: unknown }).data);\n\t\t\t\tif (!text) {\n\t\t\t\t\twake();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst parsed = JSON.parse(text) as Record<string, unknown>;\n\t\t\t\tconst type = typeof parsed.type === \"string\" ? parsed.type : \"\";\n\t\t\t\tif (type === \"response.completed\" || type === \"response.done\" || type === \"response.incomplete\") {\n\t\t\t\t\tsawCompletion = true;\n\t\t\t\t\tdone = true;\n\t\t\t\t}\n\t\t\t\tqueue.push(parsed);\n\t\t\t} catch (e) {\n\t\t\t\t// Malformed WebSocket message — warn but don't drop wake()\n\t\t\t\tif (text === undefined) {\n\t\t\t\t\toptions?.onWarning?.(\n\t\t\t\t\t\t\"ws_decode_error\",\n\t\t\t\t\t\t`Failed to decode WebSocket message: ${e instanceof Error ? e.message : String(e)}`,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\toptions?.onWarning?.(\n\t\t\t\t\t\t\"ws_parse_error\",\n\t\t\t\t\t\t`Malformed WebSocket message dropped: ${text?.slice(0, 200) ?? \"\"}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\t// Always wake the consumer — even on parse failure — to prevent stream hangs\n\t\t\t\twake();\n\t\t\t}\n\t\t})();\n\t};\n\n\tconst onError: WebSocketListener = (event) => {\n\t\tfailed = extractWebSocketError(event);\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onClose: WebSocketListener = (event) => {\n\t\tif (sawCompletion) {\n\t\t\tdone = true;\n\t\t\twake();\n\t\t\treturn;\n\t\t}\n\t\tif (!failed) {\n\t\t\tfailed = extractWebSocketCloseError(event);\n\t\t}\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onAbort = () => {\n\t\tfailed = new Error(\"Request was aborted\");\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tsocket.addEventListener(\"message\", onMessage);\n\tsocket.addEventListener(\"error\", onError);\n\tsocket.addEventListener(\"close\", onClose);\n\tsignal?.addEventListener(\"abort\", onAbort);\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tif (queue.length > 0) {\n\t\t\t\tyield queue.shift()!;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (done) break;\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tpending = resolve;\n\t\t\t});\n\t\t}\n\n\t\tif (failed) {\n\t\t\tthrow failed;\n\t\t}\n\t\tif (!sawCompletion) {\n\t\t\tthrow new Error(\"WebSocket stream closed before response.completed\");\n\t\t}\n\t} finally {\n\t\tsocket.removeEventListener(\"message\", onMessage);\n\t\tsocket.removeEventListener(\"error\", onError);\n\t\tsocket.removeEventListener(\"close\", onClose);\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\nfunction requestBodyWithoutInput(body: RequestBody): RequestBody {\n\tconst { input: _input, previous_response_id: _previousResponseId, ...rest } = body;\n\treturn rest;\n}\n\nfunction responseInputsEqual(a: ResponseInput | undefined, b: ResponseInput | undefined): boolean {\n\treturn JSON.stringify(a ?? []) === JSON.stringify(b ?? []);\n}\n\nfunction requestBodiesMatchExceptInput(a: RequestBody, b: RequestBody): boolean {\n\treturn JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));\n}\n\nfunction getCachedWebSocketInputDelta(\n\tbody: RequestBody,\n\tcontinuation: CachedWebSocketContinuationState,\n): ResponseInput | undefined {\n\tif (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {\n\t\treturn undefined;\n\t}\n\n\tconst currentInput = body.input ?? [];\n\tconst baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];\n\tif (currentInput.length < baseline.length) {\n\t\treturn undefined;\n\t}\n\n\tconst prefix = currentInput.slice(0, baseline.length);\n\tif (!responseInputsEqual(prefix, baseline)) {\n\t\treturn undefined;\n\t}\n\n\treturn currentInput.slice(baseline.length);\n}\n\nfunction buildCachedWebSocketRequestBody(entry: CachedWebSocketConnection, body: RequestBody): RequestBody {\n\tconst continuation = entry.continuation;\n\tif (!continuation) {\n\t\treturn body;\n\t}\n\n\tconst delta = getCachedWebSocketInputDelta(body, continuation);\n\tif (!delta || !continuation.lastResponseId) {\n\t\tentry.continuation = undefined;\n\t\treturn body;\n\t}\n\n\treturn {\n\t\t...body,\n\t\tprevious_response_id: continuation.lastResponseId,\n\t\tinput: delta,\n\t};\n}\n\nasync function processWebSocketStream(\n\turl: string,\n\tbody: RequestBody,\n\theaders: Headers,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\tonStart: () => void,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tconst { socket, entry, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);\n\tlet keepConnection = true;\n\tconst fullBody = body;\n\tconst requestBody = entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;\n\ttry {\n\t\tsocket.send(JSON.stringify({ type: \"response.create\", ...requestBody }));\n\t\tonStart();\n\t\tstream.push({ type: \"start\", partial: output });\n\t\tawait processResponsesStream(\n\t\t\tmapCodexEvents(parseWebSocket(socket, options?.signal, options)),\n\t\t\toutput,\n\t\t\tstream,\n\t\t\tmodel,\n\t\t\t{\n\t\t\t\tserviceTier: options?.serviceTier,\n\t\t\t\tresolveServiceTier: resolveCodexServiceTier,\n\t\t\t\tapplyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),\n\t\t\t\tonWarning: options?.onWarning,\n\t\t\t},\n\t\t);\n\t\tif (entry && output.responseId && !options?.signal?.aborted) {\n\t\t\tconst responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {\n\t\t\t\tincludeSystemPrompt: false,\n\t\t\t}).filter((item) => item.type !== \"function_call_output\");\n\t\t\tentry.continuation = {\n\t\t\t\tlastRequestBody: fullBody,\n\t\t\t\tlastResponseId: output.responseId,\n\t\t\t\tlastResponseItems: responseItems,\n\t\t\t};\n\t\t}\n\t\tif (options?.signal?.aborted) {\n\t\t\tkeepConnection = false;\n\t\t}\n\t} catch (error) {\n\t\tif (entry) {\n\t\t\tentry.continuation = undefined;\n\t\t}\n\t\tkeepConnection = false;\n\t\tthrow error;\n\t} finally {\n\t\trelease({ keep: keepConnection });\n\t}\n}\n\n// ============================================================================\n// Service Tier Pricing\n// ============================================================================\n\nfunction getServiceTierCostMultiplier(\n\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\tmodelId?: string,\n): number {\n\tswitch (serviceTier) {\n\t\tcase \"flex\":\n\t\t\treturn 0.5;\n\t\tcase \"priority\": {\n\t\t\tconst id = modelId?.includes(\"/\") ? modelId.split(\"/\").pop()! : modelId;\n\t\t\tif (id?.startsWith(\"gpt-5.5\")) return 2.5;\n\t\t\treturn 2;\n\t\t}\n\t\tdefault:\n\t\t\treturn 1;\n\t}\n}\n\nfunction applyServiceTierPricing(\n\tusage: Usage,\n\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\tmodel: Pick<Model<\"openai-codex-responses\">, \"id\">,\n) {\n\tconst multiplier = getServiceTierCostMultiplier(serviceTier, model.id);\n\tif (multiplier === 1) return;\n\n\tusage.cost.input *= multiplier;\n\tusage.cost.output *= multiplier;\n\tusage.cost.cacheRead *= multiplier;\n\tusage.cost.cacheWrite *= multiplier;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n}\n\nfunction resolveCodexServiceTier(\n\tresponseTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\trequestedTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n): ResponseCreateParamsStreaming[\"service_tier\"] | undefined {\n\tif (responseTier === \"default\" && (requestedTier === \"flex\" || requestedTier === \"priority\")) {\n\t\treturn requestedTier;\n\t}\n\treturn responseTier ?? requestedTier;\n}\n\n// ============================================================================\n// Error Handling\n// ============================================================================\n\nasync function parseErrorResponse(response: Response): Promise<{ message: string; friendlyMessage?: string }> {\n\tconst raw = await response.text();\n\tlet message = raw || response.statusText || \"Request failed\";\n\tlet friendlyMessage: string | undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as {\n\t\t\terror?: { code?: string; type?: string; message?: string; plan_type?: string; resets_at?: number };\n\t\t};\n\t\tconst err = parsed?.error;\n\t\tif (err) {\n\t\t\tconst code = err.code || err.type || \"\";\n\t\t\tif (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {\n\t\t\t\tconst plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : \"\";\n\t\t\t\tconst mins = err.resets_at\n\t\t\t\t\t? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000))\n\t\t\t\t\t: undefined;\n\t\t\t\tconst when = mins !== undefined ? ` Try again in ~${mins} min.` : \"\";\n\t\t\t\tfriendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();\n\t\t\t}\n\t\t\tmessage = err.message || friendlyMessage || message;\n\t\t}\n\t} catch {\n\t\t// Best-effort extraction of friendly error message — raw text is the fallback\n\t}\n\n\treturn { message, friendlyMessage };\n}\n\n// ============================================================================\n// Auth & Headers\n// ============================================================================\n\nfunction extractAccountId(token: string): string {\n\ttry {\n\t\tconst parts = token.split(\".\");\n\t\tif (parts.length !== 3) throw new Error(\"Invalid token\");\n\t\tconst payload = JSON.parse(atob(parts[1]));\n\t\tconst accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;\n\t\tif (!accountId) throw new Error(\"No account ID in token\");\n\t\treturn accountId;\n\t} catch {\n\t\t// Re-throw with unified message for any JWT decode failure\n\t\tthrow new Error(\"Failed to extract accountId from token\");\n\t}\n}\n\nfunction createCodexRequestId(): string {\n\tif (typeof globalThis.crypto?.randomUUID === \"function\") {\n\t\treturn globalThis.crypto.randomUUID();\n\t}\n\treturn `codex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction buildBaseCodexHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: Record<string, string> | undefined,\n\taccountId: string,\n\ttoken: string,\n): Headers {\n\tconst headers = new Headers(initHeaders);\n\tfor (const [key, value] of Object.entries(additionalHeaders || {})) {\n\t\theaders.set(key, value);\n\t}\n\theaders.set(\"Authorization\", `Bearer ${token}`);\n\theaders.set(\"chatgpt-account-id\", accountId);\n\theaders.set(\"originator\", \"dreb\");\n\tconst userAgent = _os ? `dreb (${_os.platform()} ${_os.release()}; ${_os.arch()})` : \"dreb (browser)\";\n\theaders.set(\"User-Agent\", userAgent);\n\treturn headers;\n}\n\nfunction buildSSEHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: Record<string, string> | undefined,\n\taccountId: string,\n\ttoken: string,\n\tsessionId?: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.set(\"OpenAI-Beta\", \"responses=experimental\");\n\theaders.set(\"accept\", \"text/event-stream\");\n\theaders.set(\"content-type\", \"application/json\");\n\n\tif (sessionId) {\n\t\theaders.set(\"session_id\", sessionId);\n\t}\n\n\treturn headers;\n}\n\nfunction buildWebSocketHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: Record<string, string> | undefined,\n\taccountId: string,\n\ttoken: string,\n\trequestId: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.delete(\"accept\");\n\theaders.delete(\"content-type\");\n\theaders.delete(\"OpenAI-Beta\");\n\theaders.delete(\"openai-beta\");\n\theaders.set(\"OpenAI-Beta\", OPENAI_BETA_RESPONSES_WEBSOCKETS);\n\theaders.set(\"x-client-request-id\", requestId);\n\theaders.set(\"session_id\", requestId);\n\treturn headers;\n}\n"]}
@@ -162,6 +162,14 @@ export const streamOpenAICodexResponses = (model, context, options) => {
162
162
  if (!response?.ok) {
163
163
  throw lastError ?? new Error("Failed after retries");
164
164
  }
165
+ // Fire onResponse exactly once, after the retry loop succeeds.
166
+ // Wrap in its own try/catch so callback errors don't affect stream processing.
167
+ try {
168
+ await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) });
169
+ }
170
+ catch {
171
+ // Intentionally ignored — callback errors must not disrupt the stream
172
+ }
165
173
  if (!response.body) {
166
174
  throw new Error("No response body");
167
175
  }
@@ -207,7 +215,7 @@ function buildRequestBody(model, context, options) {
207
215
  stream: true,
208
216
  instructions: context.systemPrompt,
209
217
  input: messages,
210
- text: { verbosity: options?.textVerbosity || "medium" },
218
+ text: { verbosity: options?.textVerbosity || "low" },
211
219
  include: ["reasoning.encrypted_content"],
212
220
  prompt_cache_key: options?.sessionId,
213
221
  tool_choice: "auto",
@@ -225,11 +233,15 @@ function buildRequestBody(model, context, options) {
225
233
  summary: options.reasoningSummary ?? "auto",
226
234
  };
227
235
  }
236
+ if (options?.serviceTier !== undefined) {
237
+ body.service_tier = options.serviceTier;
238
+ }
228
239
  return body;
229
240
  }
230
241
  function clampReasoningEffort(modelId, effort) {
231
242
  const id = modelId.includes("/") ? modelId.split("/").pop() : modelId;
232
- if ((id.startsWith("gpt-5.2") || id.startsWith("gpt-5.3") || id.startsWith("gpt-5.4")) && effort === "minimal")
243
+ if ((id.startsWith("gpt-5.2") || id.startsWith("gpt-5.3") || id.startsWith("gpt-5.4") || id.startsWith("gpt-5.5")) &&
244
+ effort === "minimal")
233
245
  return "low";
234
246
  if (id === "gpt-5.1" && effort === "xhigh")
235
247
  return "high";
@@ -258,7 +270,12 @@ function resolveCodexWebSocketUrl(baseUrl) {
258
270
  // Response Processing
259
271
  // ============================================================================
260
272
  async function processStream(response, output, stream, model, options) {
261
- await processResponsesStream(mapCodexEvents(parseSSE(response, options)), output, stream, model, options);
273
+ await processResponsesStream(mapCodexEvents(parseSSE(response, options)), output, stream, model, {
274
+ serviceTier: options?.serviceTier,
275
+ resolveServiceTier: resolveCodexServiceTier,
276
+ applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
277
+ onWarning: options?.onWarning,
278
+ });
262
279
  }
263
280
  async function* mapCodexEvents(events) {
264
281
  for await (const event of events) {
@@ -460,11 +477,7 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
460
477
  const socket = await connectWebSocket(url, headers, signal);
461
478
  return {
462
479
  socket,
463
- release: ({ keep } = {}) => {
464
- if (keep === false) {
465
- closeWebSocketSilently(socket);
466
- return;
467
- }
480
+ release: () => {
468
481
  closeWebSocketSilently(socket);
469
482
  },
470
483
  };
@@ -479,6 +492,7 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
479
492
  cached.busy = true;
480
493
  return {
481
494
  socket: cached.socket,
495
+ entry: cached,
482
496
  release: ({ keep } = {}) => {
483
497
  if (!keep || !isWebSocketReusable(cached.socket)) {
484
498
  closeWebSocketSilently(cached.socket);
@@ -509,6 +523,7 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
509
523
  websocketSessionCache.set(sessionId, entry);
510
524
  return {
511
525
  socket,
526
+ entry,
512
527
  release: ({ keep } = {}) => {
513
528
  if (!keep || !isWebSocketReusable(entry.socket)) {
514
529
  closeWebSocketSilently(entry.socket);
@@ -577,10 +592,13 @@ async function* parseWebSocket(socket, signal, options) {
577
592
  void (async () => {
578
593
  if (!event || typeof event !== "object" || !("data" in event))
579
594
  return;
580
- const text = await decodeWebSocketData(event.data);
581
- if (!text)
582
- return;
595
+ let text;
583
596
  try {
597
+ text = await decodeWebSocketData(event.data);
598
+ if (!text) {
599
+ wake();
600
+ return;
601
+ }
584
602
  const parsed = JSON.parse(text);
585
603
  const type = typeof parsed.type === "string" ? parsed.type : "";
586
604
  if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
@@ -589,9 +607,14 @@ async function* parseWebSocket(socket, signal, options) {
589
607
  }
590
608
  queue.push(parsed);
591
609
  }
592
- catch {
610
+ catch (e) {
593
611
  // Malformed WebSocket message — warn but don't drop wake()
594
- options?.onWarning?.("ws_parse_error", `Malformed WebSocket message dropped: ${text.slice(0, 200)}`);
612
+ if (text === undefined) {
613
+ options?.onWarning?.("ws_decode_error", `Failed to decode WebSocket message: ${e instanceof Error ? e.message : String(e)}`);
614
+ }
615
+ else {
616
+ options?.onWarning?.("ws_parse_error", `Malformed WebSocket message dropped: ${text?.slice(0, 200) ?? ""}`);
617
+ }
595
618
  }
596
619
  finally {
597
620
  // Always wake the consumer — even on parse failure — to prevent stream hangs
@@ -654,19 +677,80 @@ async function* parseWebSocket(socket, signal, options) {
654
677
  signal?.removeEventListener("abort", onAbort);
655
678
  }
656
679
  }
680
+ function requestBodyWithoutInput(body) {
681
+ const { input: _input, previous_response_id: _previousResponseId, ...rest } = body;
682
+ return rest;
683
+ }
684
+ function responseInputsEqual(a, b) {
685
+ return JSON.stringify(a ?? []) === JSON.stringify(b ?? []);
686
+ }
687
+ function requestBodiesMatchExceptInput(a, b) {
688
+ return JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));
689
+ }
690
+ function getCachedWebSocketInputDelta(body, continuation) {
691
+ if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {
692
+ return undefined;
693
+ }
694
+ const currentInput = body.input ?? [];
695
+ const baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];
696
+ if (currentInput.length < baseline.length) {
697
+ return undefined;
698
+ }
699
+ const prefix = currentInput.slice(0, baseline.length);
700
+ if (!responseInputsEqual(prefix, baseline)) {
701
+ return undefined;
702
+ }
703
+ return currentInput.slice(baseline.length);
704
+ }
705
+ function buildCachedWebSocketRequestBody(entry, body) {
706
+ const continuation = entry.continuation;
707
+ if (!continuation) {
708
+ return body;
709
+ }
710
+ const delta = getCachedWebSocketInputDelta(body, continuation);
711
+ if (!delta || !continuation.lastResponseId) {
712
+ entry.continuation = undefined;
713
+ return body;
714
+ }
715
+ return {
716
+ ...body,
717
+ previous_response_id: continuation.lastResponseId,
718
+ input: delta,
719
+ };
720
+ }
657
721
  async function processWebSocketStream(url, body, headers, output, stream, model, onStart, options) {
658
- const { socket, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
722
+ const { socket, entry, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
659
723
  let keepConnection = true;
724
+ const fullBody = body;
725
+ const requestBody = entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
660
726
  try {
661
- socket.send(JSON.stringify({ type: "response.create", ...body }));
727
+ socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
662
728
  onStart();
663
729
  stream.push({ type: "start", partial: output });
664
- await processResponsesStream(mapCodexEvents(parseWebSocket(socket, options?.signal, options)), output, stream, model, options);
730
+ await processResponsesStream(mapCodexEvents(parseWebSocket(socket, options?.signal, options)), output, stream, model, {
731
+ serviceTier: options?.serviceTier,
732
+ resolveServiceTier: resolveCodexServiceTier,
733
+ applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
734
+ onWarning: options?.onWarning,
735
+ });
736
+ if (entry && output.responseId && !options?.signal?.aborted) {
737
+ const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
738
+ includeSystemPrompt: false,
739
+ }).filter((item) => item.type !== "function_call_output");
740
+ entry.continuation = {
741
+ lastRequestBody: fullBody,
742
+ lastResponseId: output.responseId,
743
+ lastResponseItems: responseItems,
744
+ };
745
+ }
665
746
  if (options?.signal?.aborted) {
666
747
  keepConnection = false;
667
748
  }
668
749
  }
669
750
  catch (error) {
751
+ if (entry) {
752
+ entry.continuation = undefined;
753
+ }
670
754
  keepConnection = false;
671
755
  throw error;
672
756
  }
@@ -675,6 +759,39 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
675
759
  }
676
760
  }
677
761
  // ============================================================================
762
+ // Service Tier Pricing
763
+ // ============================================================================
764
+ function getServiceTierCostMultiplier(serviceTier, modelId) {
765
+ switch (serviceTier) {
766
+ case "flex":
767
+ return 0.5;
768
+ case "priority": {
769
+ const id = modelId?.includes("/") ? modelId.split("/").pop() : modelId;
770
+ if (id?.startsWith("gpt-5.5"))
771
+ return 2.5;
772
+ return 2;
773
+ }
774
+ default:
775
+ return 1;
776
+ }
777
+ }
778
+ function applyServiceTierPricing(usage, serviceTier, model) {
779
+ const multiplier = getServiceTierCostMultiplier(serviceTier, model.id);
780
+ if (multiplier === 1)
781
+ return;
782
+ usage.cost.input *= multiplier;
783
+ usage.cost.output *= multiplier;
784
+ usage.cost.cacheRead *= multiplier;
785
+ usage.cost.cacheWrite *= multiplier;
786
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
787
+ }
788
+ function resolveCodexServiceTier(responseTier, requestedTier) {
789
+ if (responseTier === "default" && (requestedTier === "flex" || requestedTier === "priority")) {
790
+ return requestedTier;
791
+ }
792
+ return responseTier ?? requestedTier;
793
+ }
794
+ // ============================================================================
678
795
  // Error Handling
679
796
  // ============================================================================
680
797
  async function parseErrorResponse(response) {