u1s1-cli 1.18.0 → 1.20.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.
Files changed (43) hide show
  1. package/dist/agents/agent-config.d.ts +1 -1
  2. package/dist/agents/agent-config.js +1 -1
  3. package/dist/agents/claude.d.ts +4 -1
  4. package/dist/agents/claude.js +6 -3
  5. package/dist/agents/codex.d.ts +4 -1
  6. package/dist/agents/codex.js +6 -3
  7. package/dist/agents/custom-models.d.ts +61 -0
  8. package/dist/agents/custom-models.js +175 -0
  9. package/dist/agents/dsh.d.ts +90 -0
  10. package/dist/agents/dsh.js +243 -0
  11. package/dist/agents/launch.d.ts +24 -2
  12. package/dist/agents/launch.js +115 -15
  13. package/dist/agents/models-list.d.ts +65 -0
  14. package/dist/agents/models-list.js +297 -0
  15. package/dist/agents/node-runtime.d.ts +91 -0
  16. package/dist/agents/node-runtime.js +223 -0
  17. package/dist/agents/runtime.d.ts +26 -3
  18. package/dist/agents/runtime.js +50 -14
  19. package/dist/agents/status.d.ts +4 -0
  20. package/dist/agents/status.js +32 -7
  21. package/dist/api.d.ts +2 -0
  22. package/dist/chat-protocol/anthropic.d.ts +29 -0
  23. package/dist/chat-protocol/anthropic.js +484 -0
  24. package/dist/chat-protocol/json.d.ts +20 -0
  25. package/dist/chat-protocol/json.js +22 -0
  26. package/dist/chat-protocol/responses.d.ts +97 -0
  27. package/dist/chat-protocol/responses.js +1076 -0
  28. package/dist/chat-protocol/sse.d.ts +19 -0
  29. package/dist/chat-protocol/sse.js +67 -0
  30. package/dist/chat-protocol/thinking.d.ts +22 -0
  31. package/dist/chat-protocol/thinking.js +58 -0
  32. package/dist/config-command.d.ts +1 -1
  33. package/dist/config-command.js +20 -10
  34. package/dist/config.d.ts +23 -1
  35. package/dist/config.js +52 -1
  36. package/dist/custom-endpoint-proxy.d.ts +92 -0
  37. package/dist/custom-endpoint-proxy.js +461 -0
  38. package/dist/device-auth.d.ts +14 -3
  39. package/dist/device-auth.js +36 -2
  40. package/dist/index.js +12 -3
  41. package/dist/launcher.d.ts +5 -1
  42. package/dist/launcher.js +35 -11
  43. package/package.json +1 -1
@@ -0,0 +1,1076 @@
1
+ // GENERATED from workers/gateway/src/chat-protocol/responses.ts; do not edit here. Edit the canonical file, then run: node scripts/sync-chat-protocol.mjs
2
+ import { asRecord } from "./json.js";
3
+ import { SseParser } from "./sse.js";
4
+ import { responsesThinkingLevel } from "./thinking.js";
5
+ // OpenAI Responses (POST /v1/responses, Codex CLI) <-> OpenAI Chat Completions.
6
+ // Moved verbatim from the gateway's responses-inbound.ts; the per-model
7
+ // reasoning fields are injected (responsesRequestToChat). Shared with the u1s1
8
+ // CLI's local proxy; see json.ts for the rules of this directory.
9
+ //
10
+ // Inbound OpenAI Responses adapter (POST /v1/responses), used by Codex CLI.
11
+ // Same shape as anthropic-inbound.ts: the request becomes a Chat Completions
12
+ // body before auth/billing/routing see it, and the finished Chat response
13
+ // (JSON or SSE) is converted back here.
14
+ //
15
+ // Only the stateless subset is supported: `store: false`, full `input` every
16
+ // turn, no `previous_response_id` (exactly what Codex sends over HTTP).
17
+ // Unknown top-level fields, input items and tool kinds are ignored or skipped
18
+ // and reported, never rejected: Codex adds new item/tool kinds often.
19
+ //
20
+ // Mappings (checked against Codex CLI 0.167.2 talking to a local mock):
21
+ // - `custom` (freeform/grammar) tools become a function with one string
22
+ // parameter `input`; the call comes back as `custom_tool_call`. The grammar
23
+ // is only described to the model, not enforced.
24
+ // - `namespace` tools are flattened to `<namespace>__<name>` functions and the
25
+ // call comes back as `function_call` with `name` + `namespace`.
26
+ // - `tool_search` with `execution: "client"` (Codex runs the BM25 search over
27
+ // its deferred tools itself) becomes a plain function; the call comes back as
28
+ // `tool_search_call` with object `arguments`, and the `tool_search_output`
29
+ // Codex sends next turn adds the discovered tools to the Chat tool list.
30
+ // `additional_tools` input items add their tools the same way.
31
+ // - Reasoning streams as reasoning-summary events. The upstream's reasoning
32
+ // text and any structured `reasoning_details` (signed/encrypted reasoning
33
+ // from aggregator-style upstreams) are wrapped into a gateway-issued
34
+ // `encrypted_content` and replayed on the next assistant turn. Foreign blobs
35
+ // (issued by the real Responses API) cannot be replayed and are dropped.
36
+ //
37
+ // Skipped (reported in notes, logged by userAuth):
38
+ // - Hosted tools the gateway has no backend for: `web_search`,
39
+ // `image_generation`, `file_search`, `code_interpreter`, `computer_use*`,
40
+ // `mcp`, server-side `tool_search`, `local_shell` (legacy, not sent to the
41
+ // models Codex reaches through a custom provider).
42
+ // - Input items with no Chat equivalent: `web_search_call`,
43
+ // `image_generation_call`, `compaction` / `context_compaction` (encrypted
44
+ // remote-compaction summaries; custom providers compact locally),
45
+ // `agent_message` (inter-agent message whose shape is not documented),
46
+ // `local_shell_call`, `configuration_update`, `compaction_trigger` and
47
+ // anything new. A tool output whose call was skipped is replayed as user
48
+ // text so the Chat history stays valid.
49
+ //
50
+ // Privacy: Codex puts local paths and an installation id into the
51
+ // `x-codex-turn-metadata` header and the `client_metadata` body field. Only
52
+ // the converted Chat body is archived (request-archive.ts → R2) and request
53
+ // headers are never archived, so both are dropped here by construction: the
54
+ // converter copies a fixed set of fields and nothing else.
55
+ export const RESPONSES_PATH = "/v1/responses";
56
+ const NAMESPACE_SEPARATOR = "__";
57
+ const REASONING_BLOB_PREFIX = "u1s1r1.";
58
+ /** Chat function name the client-executed `tool_search` tool is exposed as. */
59
+ const TOOL_SEARCH_NAME = "tool_search";
60
+ // ---- reasoning blob ----
61
+ function base64UrlEncode(text) {
62
+ const bytes = new TextEncoder().encode(text);
63
+ let binary = "";
64
+ for (const byte of bytes)
65
+ binary += String.fromCharCode(byte);
66
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
67
+ }
68
+ function base64UrlDecode(text) {
69
+ try {
70
+ const binary = atob(text.replaceAll("-", "+").replaceAll("_", "/"));
71
+ return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ /**
78
+ * Opaque reasoning blob handed to the client as `encrypted_content`. Not
79
+ * encrypted: the text is the user's own conversation and replaying a tampered
80
+ * one is no different from editing a message (signed details are verified by
81
+ * the upstream that issued them). `{r}` is the P0 layout; `d` was added
82
+ * without a version bump because older readers ignore it.
83
+ */
84
+ export function encodeReasoningBlob(reasoning, details) {
85
+ return REASONING_BLOB_PREFIX + base64UrlEncode(JSON.stringify(details?.length ? { r: reasoning, d: details } : { r: reasoning }));
86
+ }
87
+ export function decodeReasoningPayload(blob) {
88
+ if (typeof blob !== "string" || !blob.startsWith(REASONING_BLOB_PREFIX))
89
+ return null;
90
+ const json = base64UrlDecode(blob.slice(REASONING_BLOB_PREFIX.length));
91
+ if (json === null)
92
+ return null;
93
+ try {
94
+ const value = asRecord(JSON.parse(json));
95
+ if (typeof value?.r !== "string")
96
+ return null;
97
+ const details = Array.isArray(value.d) ? value.d.map(asRecord).filter((d) => d !== null) : [];
98
+ return details.length ? { text: value.r, details } : { text: value.r };
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ export function decodeReasoningBlob(blob) {
105
+ return decodeReasoningPayload(blob)?.text ?? null;
106
+ }
107
+ /**
108
+ * Merges streamed `reasoning_details` fragments: entries sharing an `index`
109
+ * are one block whose `text`/`summary`/`data` arrive in pieces and whose
110
+ * `signature`/`id`/`format` arrive once. Fragments without an index append.
111
+ */
112
+ export function mergeReasoningDetails(into, fragments) {
113
+ for (const entry of Array.isArray(fragments) ? fragments : []) {
114
+ const fragment = asRecord(entry);
115
+ if (!fragment)
116
+ continue;
117
+ const existing = typeof fragment.index === "number"
118
+ ? into.find((detail) => detail.index === fragment.index && detail.type === fragment.type)
119
+ : undefined;
120
+ if (!existing) {
121
+ into.push({ ...fragment });
122
+ continue;
123
+ }
124
+ for (const [key, value] of Object.entries(fragment)) {
125
+ if ((key === "text" || key === "summary" || key === "data") && typeof value === "string" && typeof existing[key] === "string") {
126
+ existing[key] = existing[key] + value;
127
+ }
128
+ else if (value !== null && value !== undefined) {
129
+ existing[key] = value;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ // ---- request: Responses → Chat Completions ----
135
+ function contentParts(content) {
136
+ if (typeof content === "string")
137
+ return [{ type: "text", text: content }];
138
+ const parts = [];
139
+ for (const item of Array.isArray(content) ? content : []) {
140
+ const part = asRecord(item);
141
+ if (!part)
142
+ continue;
143
+ if ((part.type === "input_text" || part.type === "output_text" || part.type === "text") && typeof part.text === "string") {
144
+ parts.push({ type: "text", text: part.text });
145
+ }
146
+ else if (part.type === "refusal" && typeof part.refusal === "string") {
147
+ parts.push({ type: "text", text: part.refusal });
148
+ }
149
+ else if (part.type === "input_image") {
150
+ const url = typeof part.image_url === "string" ? part.image_url : null;
151
+ if (url) {
152
+ parts.push({
153
+ type: "image_url",
154
+ image_url: { url, ...(typeof part.detail === "string" && part.detail !== "auto" ? { detail: part.detail === "original" ? "high" : part.detail } : {}) },
155
+ });
156
+ }
157
+ else {
158
+ parts.push({ type: "text", text: "[image attachment omitted: only inline image URLs are supported]" });
159
+ }
160
+ }
161
+ else if (part.type === "input_file") {
162
+ parts.push({ type: "text", text: "[file attachment omitted: unsupported by this endpoint]" });
163
+ }
164
+ }
165
+ return parts;
166
+ }
167
+ function textOnly(parts) {
168
+ return parts.filter((part) => part.type === "text").map((part) => part.text).join("\n\n");
169
+ }
170
+ /** text/image parts of a turn; a text-only turn collapses to a plain string. */
171
+ function collapse(parts) {
172
+ return parts.every((part) => part.type === "text") ? textOnly(parts) : parts;
173
+ }
174
+ /** Chat Completions upstreams reject function names longer than this. */
175
+ const MAX_CHAT_TOOL_NAME = 64;
176
+ function fnv1a(text) {
177
+ let hash = 0x811c9dc5;
178
+ for (let i = 0; i < text.length; i += 1)
179
+ hash = Math.imul(hash ^ text.charCodeAt(i), 0x01000193) >>> 0;
180
+ return hash.toString(16).padStart(8, "0");
181
+ }
182
+ /**
183
+ * Chat function name for a (possibly namespaced) tool. Deterministic, because
184
+ * the request side (tool list, history items) and the response side compute it
185
+ * independently every turn; over-long names are cut and suffixed with a hash.
186
+ */
187
+ function chatToolName(name, namespace) {
188
+ const full = typeof namespace === "string" && namespace ? `${namespace}${NAMESPACE_SEPARATOR}${name}` : name;
189
+ if (full.length <= MAX_CHAT_TOOL_NAME)
190
+ return full;
191
+ return `${full.slice(0, MAX_CHAT_TOOL_NAME - 9)}_${fnv1a(full)}`;
192
+ }
193
+ function toolOutputText(output, images) {
194
+ if (typeof output === "string")
195
+ return output;
196
+ const parts = contentParts(output);
197
+ for (const part of parts)
198
+ if (part.type === "image_url")
199
+ images.push(part);
200
+ return textOnly(parts);
201
+ }
202
+ function functionTool(name, description, parameters, strict) {
203
+ return {
204
+ type: "function",
205
+ function: {
206
+ name,
207
+ ...(typeof description === "string" && description ? { description } : {}),
208
+ parameters: asRecord(parameters) ?? { type: "object", properties: {} },
209
+ ...(strict === true ? { strict: true } : {}),
210
+ },
211
+ };
212
+ }
213
+ function customToolDescription(tool) {
214
+ const description = typeof tool.description === "string" ? tool.description : "";
215
+ const format = asRecord(tool.format);
216
+ const grammar = format?.type === "grammar" && typeof format.definition === "string"
217
+ ? `\n\nPut the raw tool input (not JSON-encoded) in the \`input\` string. It must match this ${String(format.syntax ?? "")} grammar:\n${format.definition}`
218
+ : "\n\nPut the raw tool input in the `input` string.";
219
+ return description + grammar;
220
+ }
221
+ function convertTools(value, notes, out, namespace) {
222
+ for (const item of Array.isArray(value) ? value : []) {
223
+ const tool = asRecord(item);
224
+ if (!tool)
225
+ continue;
226
+ const name = typeof tool.name === "string" ? tool.name : "";
227
+ const chatName = chatToolName(name, namespace);
228
+ if (name && chatName !== (namespace ? `${namespace}${NAMESPACE_SEPARATOR}${name}` : name)) {
229
+ notes.aliases[chatName] = namespace ? { name, namespace } : { name };
230
+ }
231
+ // The same tool can arrive twice (tools + a later tool_search_output);
232
+ // duplicate Chat function names are rejected by most upstreams.
233
+ if (name && out.some((existing) => asRecord(existing.function)?.name === chatName))
234
+ continue;
235
+ if (tool.type === "function" && name) {
236
+ out.push(functionTool(chatName, tool.description, tool.parameters, tool.strict));
237
+ }
238
+ else if (tool.type === "custom" && name) {
239
+ notes.customTools.push(chatName);
240
+ out.push(functionTool(chatName, customToolDescription(tool), {
241
+ type: "object",
242
+ properties: { input: { type: "string", description: "Raw tool input." } },
243
+ required: ["input"],
244
+ additionalProperties: false,
245
+ }));
246
+ }
247
+ else if (tool.type === "namespace" && name && !namespace) {
248
+ if (!notes.namespaces.includes(name))
249
+ notes.namespaces.push(name);
250
+ convertTools(tool.tools, notes, out, name);
251
+ }
252
+ else if (tool.type === "tool_search" && tool.execution === "client" && !namespace) {
253
+ if (notes.toolSearch)
254
+ continue;
255
+ notes.toolSearch = true;
256
+ out.push(functionTool(TOOL_SEARCH_NAME, tool.description, tool.parameters));
257
+ }
258
+ else {
259
+ notes.skippedTools.push(String(tool.type ?? "unknown") + (name ? `:${name}` : ""));
260
+ }
261
+ }
262
+ }
263
+ /** Tool names a `tool_search_output` made available, for the tool message text. */
264
+ function discoveredToolNames(value) {
265
+ const names = [];
266
+ for (const item of Array.isArray(value) ? value : []) {
267
+ const tool = asRecord(item);
268
+ if (typeof tool?.name !== "string")
269
+ continue;
270
+ if (tool.type === "namespace") {
271
+ for (const inner of Array.isArray(tool.tools) ? tool.tools : []) {
272
+ const name = asRecord(inner)?.name;
273
+ if (typeof name === "string")
274
+ names.push(chatToolName(name, tool.name));
275
+ }
276
+ }
277
+ else {
278
+ names.push(tool.name);
279
+ }
280
+ }
281
+ return names;
282
+ }
283
+ function convertToolChoice(value) {
284
+ if (value === "auto" || value === "none" || value === "required")
285
+ return value;
286
+ const choice = asRecord(value);
287
+ if ((choice?.type === "function" || choice?.type === "custom") && typeof choice.name === "string") {
288
+ return { type: "function", function: { name: chatToolName(choice.name, choice.namespace) } };
289
+ }
290
+ return undefined;
291
+ }
292
+ function responseFormat(text) {
293
+ const format = asRecord(asRecord(text)?.format);
294
+ if (format?.type === "json_schema" && asRecord(format.schema)) {
295
+ return {
296
+ type: "json_schema",
297
+ json_schema: {
298
+ name: typeof format.name === "string" ? format.name : "output",
299
+ schema: format.schema,
300
+ ...(typeof format.strict === "boolean" ? { strict: format.strict } : {}),
301
+ },
302
+ };
303
+ }
304
+ if (format?.type === "json_object")
305
+ return { type: "json_object" };
306
+ return undefined;
307
+ }
308
+ /** Builds Chat messages from Responses `input`, merging assistant items of one turn. */
309
+ class MessageBuilder {
310
+ messages = [];
311
+ assistant = null;
312
+ pendingReasoning = [];
313
+ /** Images returned by tool calls, replayed as one user turn after the tool messages. */
314
+ pendingImages = [];
315
+ notes;
316
+ constructor(notes) {
317
+ this.notes = notes;
318
+ }
319
+ flushAssistant() {
320
+ if (!this.assistant)
321
+ return;
322
+ if (this.assistant.content === null && !this.assistant.tool_calls)
323
+ this.assistant.content = "";
324
+ this.messages.push(this.assistant);
325
+ this.assistant = null;
326
+ }
327
+ flushImages() {
328
+ if (!this.pendingImages.length)
329
+ return;
330
+ // Chat Completions tool messages cannot carry images: replay them as a user turn.
331
+ this.messages.push({ role: "user", content: [{ type: "text", text: "Images returned by the tool calls above:" }, ...this.pendingImages] });
332
+ this.pendingImages = [];
333
+ }
334
+ currentAssistant() {
335
+ if (!this.assistant) {
336
+ this.flushImages();
337
+ this.assistant = { role: "assistant", content: null };
338
+ // Thinking-mode models require reasoning_content replay on tool-call turns;
339
+ // signed/encrypted details go back as `reasoning_details`.
340
+ if (this.pendingReasoning.length) {
341
+ this.assistant.reasoning_content = this.pendingReasoning.map((r) => r.text).join("");
342
+ const details = this.pendingReasoning.flatMap((r) => r.details ?? []);
343
+ if (details.length)
344
+ this.assistant.reasoning_details = details;
345
+ }
346
+ this.pendingReasoning = [];
347
+ }
348
+ return this.assistant;
349
+ }
350
+ push(message) {
351
+ this.flushAssistant();
352
+ this.flushImages();
353
+ this.pendingReasoning = [];
354
+ this.messages.push(message);
355
+ }
356
+ system(text) {
357
+ if (text)
358
+ this.push({ role: "system", content: text });
359
+ }
360
+ message(item) {
361
+ const parts = contentParts(item.content);
362
+ if (item.role === "assistant") {
363
+ const text = textOnly(parts);
364
+ const assistant = this.currentAssistant();
365
+ // A text message after tool calls starts a new assistant turn in Chat terms.
366
+ if (assistant.tool_calls) {
367
+ this.flushAssistant();
368
+ this.currentAssistant().content = text;
369
+ }
370
+ else {
371
+ assistant.content = typeof assistant.content === "string" ? assistant.content + text : text;
372
+ }
373
+ return;
374
+ }
375
+ if (!parts.length)
376
+ return;
377
+ // Codex sends its base instructions and permission updates as `developer`
378
+ // messages. Chat upstreams other than OpenAI only know `system`.
379
+ const role = item.role === "developer" || item.role === "system" ? "system" : "user";
380
+ this.push({ role, content: role === "system" ? textOnly(parts) : collapse(parts) });
381
+ }
382
+ reasoning(item) {
383
+ const payload = decodeReasoningPayload(item.encrypted_content);
384
+ // Foreign blobs (issued by the real Responses API) cannot be replayed here.
385
+ if (payload === null)
386
+ return;
387
+ this.flushAssistant();
388
+ this.pendingReasoning.push(payload);
389
+ }
390
+ addToolCall(callId, chatName, args) {
391
+ const assistant = this.currentAssistant();
392
+ const calls = (assistant.tool_calls ??= []);
393
+ calls.push({
394
+ id: typeof callId === "string" && callId ? callId : `call_${crypto.randomUUID()}`,
395
+ type: "function",
396
+ function: { name: chatName, arguments: args },
397
+ });
398
+ }
399
+ toolCall(item, custom) {
400
+ const name = typeof item.name === "string" ? item.name : "";
401
+ if (!name) {
402
+ this.notes.skippedItems.push(String(item.type));
403
+ return;
404
+ }
405
+ const args = custom
406
+ ? JSON.stringify({ input: typeof item.input === "string" ? item.input : "" })
407
+ : typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {});
408
+ this.addToolCall(item.call_id, chatToolName(name, item.namespace), args);
409
+ }
410
+ /** Client-executed tool_search: `arguments` is an object on this item. */
411
+ toolSearchCall(item) {
412
+ if (item.execution !== undefined && item.execution !== "client") {
413
+ this.notes.skippedItems.push("tool_search_call:server");
414
+ return;
415
+ }
416
+ const args = typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {});
417
+ this.addToolCall(item.call_id, TOOL_SEARCH_NAME, args);
418
+ }
419
+ toolOutput(item, text) {
420
+ this.flushAssistant();
421
+ this.messages.push({
422
+ role: "tool",
423
+ tool_call_id: typeof item.call_id === "string" ? item.call_id : "unknown",
424
+ content: text ?? toolOutputText(item.output, this.pendingImages),
425
+ });
426
+ }
427
+ finish() {
428
+ this.flushAssistant();
429
+ this.flushImages();
430
+ return pairToolMessages(this.messages);
431
+ }
432
+ }
433
+ const MISSING_TOOL_OUTPUT = "No output was recorded for this tool call.";
434
+ /**
435
+ * Chat upstreams reject a tool message without a matching assistant tool call
436
+ * and an assistant tool call without its tool message. Either happens when one
437
+ * side of a pair was a skipped item kind, or the client dropped an output: the
438
+ * orphan output becomes user text, the orphan call gets a placeholder output.
439
+ */
440
+ function pairToolMessages(messages) {
441
+ const out = [];
442
+ let pending = [];
443
+ const closePending = () => {
444
+ for (const id of pending)
445
+ out.push({ role: "tool", tool_call_id: id, content: MISSING_TOOL_OUTPUT });
446
+ pending = [];
447
+ };
448
+ for (const message of messages) {
449
+ if (message.role === "tool") {
450
+ const id = String(message.tool_call_id);
451
+ const at = pending.indexOf(id);
452
+ if (at >= 0) {
453
+ pending.splice(at, 1);
454
+ out.push(message);
455
+ }
456
+ else {
457
+ out.push({ role: "user", content: `Output of tool call ${id}:\n${String(message.content ?? "")}` });
458
+ }
459
+ continue;
460
+ }
461
+ closePending();
462
+ out.push(message);
463
+ if (message.role === "assistant" && Array.isArray(message.tool_calls)) {
464
+ pending = message.tool_calls.map((call) => String(asRecord(call)?.id ?? ""));
465
+ }
466
+ }
467
+ closePending();
468
+ return out;
469
+ }
470
+ /**
471
+ * Responses request → Chat Completions body. Only the fields below are read;
472
+ * everything else (`store`, `include`, `client_metadata`, `text.verbosity`,
473
+ * `service_tier`, `truncation`, `metadata`, future fields …) is ignored.
474
+ * `prompt_cache_key` is forwarded (Chat whitelist field, also feeds
475
+ * upstream session affinity).
476
+ */
477
+ export function responsesRequestToChat(body, reasoningFields) {
478
+ const notes = { skippedItems: [], skippedTools: [], customTools: [], namespaces: [], aliases: {}, toolSearch: false };
479
+ const builder = new MessageBuilder(notes);
480
+ if (typeof body.instructions === "string")
481
+ builder.system(body.instructions);
482
+ const extraTools = [];
483
+ const input = typeof body.input === "string" ? [{ type: "message", role: "user", content: body.input }] : body.input;
484
+ for (const entry of Array.isArray(input) ? input : []) {
485
+ const item = asRecord(entry);
486
+ if (!item)
487
+ continue;
488
+ const type = item.type ?? (typeof item.role === "string" ? "message" : undefined);
489
+ if (type === "message")
490
+ builder.message(item);
491
+ else if (type === "reasoning")
492
+ builder.reasoning(item);
493
+ else if (type === "function_call")
494
+ builder.toolCall(item, false);
495
+ else if (type === "custom_tool_call")
496
+ builder.toolCall(item, true);
497
+ else if (type === "function_call_output" || type === "custom_tool_call_output")
498
+ builder.toolOutput(item);
499
+ else if (type === "tool_search_call")
500
+ builder.toolSearchCall(item);
501
+ else if (type === "tool_search_output") {
502
+ if (Array.isArray(item.tools))
503
+ extraTools.push(...item.tools);
504
+ const names = discoveredToolNames(item.tools);
505
+ builder.toolOutput(item, names.length ? `Tools now available: ${names.join(", ")}` : "No matching tools found.");
506
+ }
507
+ else if (type === "additional_tools" && Array.isArray(item.tools))
508
+ extraTools.push(...item.tools);
509
+ else
510
+ notes.skippedItems.push(String(type ?? "unknown"));
511
+ }
512
+ const model = body.model;
513
+ const converted = { model, messages: builder.finish(), stream: body.stream === true };
514
+ if (typeof body.max_output_tokens === "number")
515
+ converted.max_tokens = body.max_output_tokens;
516
+ for (const field of ["temperature", "top_p"]) {
517
+ if (typeof body[field] === "number")
518
+ converted[field] = body[field];
519
+ }
520
+ if (typeof body.prompt_cache_key === "string" && body.prompt_cache_key)
521
+ converted.prompt_cache_key = body.prompt_cache_key;
522
+ Object.assign(converted, reasoningFields(String(model ?? ""), responsesThinkingLevel(body)));
523
+ const format = responseFormat(body.text);
524
+ if (format)
525
+ converted.response_format = format;
526
+ const tools = [];
527
+ convertTools(body.tools, notes, tools);
528
+ convertTools(extraTools, notes, tools);
529
+ if (tools.length) {
530
+ converted.tools = tools;
531
+ const toolChoice = convertToolChoice(body.tool_choice);
532
+ if (toolChoice !== undefined)
533
+ converted.tool_choice = toolChoice;
534
+ if (body.parallel_tool_calls === false)
535
+ converted.parallel_tool_calls = false;
536
+ }
537
+ return { body: converted, notes };
538
+ }
539
+ // ---- response: Chat Completions → Responses ----
540
+ function responsesUsage(value) {
541
+ const usage = asRecord(value) ?? {};
542
+ // Some OpenAI-compatible upstreams put Responses-shaped usage on Chat chunks; accept both.
543
+ const input = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
544
+ const output = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
545
+ const inputDetails = asRecord(usage.prompt_tokens_details) ?? asRecord(usage.input_tokens_details);
546
+ const outputDetails = asRecord(usage.completion_tokens_details) ?? asRecord(usage.output_tokens_details);
547
+ const cached = Number(inputDetails?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0) || 0;
548
+ const reasoning = Number(outputDetails?.reasoning_tokens ?? 0) || 0;
549
+ return {
550
+ input_tokens: input,
551
+ input_tokens_details: { cached_tokens: cached },
552
+ output_tokens: output,
553
+ output_tokens_details: { reasoning_tokens: reasoning },
554
+ total_tokens: input + output,
555
+ };
556
+ }
557
+ function responseId(raw) {
558
+ const suffix = typeof raw === "string" && /^[A-Za-z0-9_-]{6,80}$/.test(raw) ? raw.replace(/^(chatcmpl-|resp_)/, "") : crypto.randomUUID().replaceAll("-", "");
559
+ return `resp_${suffix}`;
560
+ }
561
+ function itemId(prefix) {
562
+ return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
563
+ }
564
+ /**
565
+ * Splits a flattened `<namespace>__<name>` back unless it is a plain tool name.
566
+ * Namespaces may contain the separator themselves (MCP servers are exposed as
567
+ * `mcp__<server>`), so the longest known namespace prefix wins.
568
+ */
569
+ function splitToolName(chatName, namespaces) {
570
+ let best = "";
571
+ for (const namespace of namespaces) {
572
+ if (namespace.length > best.length && chatName.length > namespace.length + NAMESPACE_SEPARATOR.length
573
+ && chatName.startsWith(namespace + NAMESPACE_SEPARATOR))
574
+ best = namespace;
575
+ }
576
+ return best ? { namespace: best, name: chatName.slice(best.length + NAMESPACE_SEPARATOR.length) } : { name: chatName };
577
+ }
578
+ export function responsesContext(body, notes) {
579
+ const namespaces = new Set(notes.namespaces);
580
+ for (const item of Array.isArray(body.tools) ? body.tools : []) {
581
+ const tool = asRecord(item);
582
+ if (tool?.type === "namespace" && typeof tool.name === "string")
583
+ namespaces.add(tool.name);
584
+ }
585
+ const conversation = typeof body.conversation === "string" ? body.conversation : asRecord(body.conversation)?.id;
586
+ return {
587
+ model: typeof body.model === "string" ? body.model : "",
588
+ previousResponseId: typeof body.previous_response_id === "string" && body.previous_response_id ? body.previous_response_id : null,
589
+ conversation: typeof conversation === "string" && conversation ? conversation : null,
590
+ customTools: notes.customTools,
591
+ namespaces: [...namespaces],
592
+ aliases: notes.aliases,
593
+ toolSearch: notes.toolSearch,
594
+ stream: body.stream === true,
595
+ };
596
+ }
597
+ function parseJsonObject(text) {
598
+ try {
599
+ return asRecord(JSON.parse(text));
600
+ }
601
+ catch {
602
+ return null;
603
+ }
604
+ }
605
+ function isToolSearch(chatName, ctx) {
606
+ return ctx.toolSearch === true && chatName === TOOL_SEARCH_NAME;
607
+ }
608
+ /** Calls whose arguments Codex reads from the final item only (no per-chunk deltas). */
609
+ function wholeArgumentsOnly(chatName, ctx) {
610
+ return ctx.customTools.includes(chatName) || isToolSearch(chatName, ctx);
611
+ }
612
+ function toolCallItem(id, callId, chatName, args, ctx, status) {
613
+ if (isToolSearch(chatName, ctx)) {
614
+ // Codex 0.167 parses `arguments` as an object here; a JSON string is
615
+ // silently treated as an empty search.
616
+ return { type: "tool_search_call", id, status, call_id: callId, execution: "client", arguments: parseJsonObject(args) ?? {} };
617
+ }
618
+ const { name, namespace } = ctx.aliases?.[chatName] ?? splitToolName(chatName, ctx.namespaces);
619
+ if (ctx.customTools.includes(chatName)) {
620
+ const parsed = parseJsonObject(args);
621
+ // Unparseable arguments are handed over raw; the tool reports the error.
622
+ const input = typeof parsed?.input === "string" ? parsed.input : args;
623
+ return { type: "custom_tool_call", id, status, call_id: callId, name, input, ...(namespace ? { namespace } : {}) };
624
+ }
625
+ return { type: "function_call", id, status, call_id: callId, name, arguments: args, ...(namespace ? { namespace } : {}) };
626
+ }
627
+ function reasoningItem(id, text, details) {
628
+ return {
629
+ type: "reasoning",
630
+ id,
631
+ summary: text ? [{ type: "summary_text", text }] : [],
632
+ encrypted_content: encodeReasoningBlob(text, details),
633
+ };
634
+ }
635
+ function messageItem(id, text, status) {
636
+ return {
637
+ type: "message",
638
+ id,
639
+ status,
640
+ role: "assistant",
641
+ content: [{ type: "output_text", text, annotations: [] }],
642
+ };
643
+ }
644
+ function responseObject(id, ctx, status, output, usage, extra = {}) {
645
+ return {
646
+ id,
647
+ object: "response",
648
+ created_at: Math.floor(Date.now() / 1000),
649
+ status,
650
+ model: ctx.model,
651
+ output,
652
+ parallel_tool_calls: true,
653
+ store: false,
654
+ usage,
655
+ ...extra,
656
+ };
657
+ }
658
+ function incompleteDetails(finishReason) {
659
+ if (finishReason === "length")
660
+ return { reason: "max_output_tokens" };
661
+ if (finishReason === "content_filter")
662
+ return { reason: "content_filter" };
663
+ return null;
664
+ }
665
+ export function openAICompletionToResponses(value, ctx) {
666
+ const completion = asRecord(value) ?? {};
667
+ const choice = asRecord(Array.isArray(completion.choices) ? completion.choices[0] : null) ?? {};
668
+ const message = asRecord(choice.message) ?? {};
669
+ const output = [];
670
+ const reasoning = message.reasoning_content ?? message.reasoning;
671
+ const reasoningText = typeof reasoning === "string" ? reasoning : "";
672
+ const details = [];
673
+ mergeReasoningDetails(details, message.reasoning_details);
674
+ if (reasoningText || details.length)
675
+ output.push(reasoningItem(itemId("rs"), reasoningText, details));
676
+ if (typeof message.content === "string" && message.content)
677
+ output.push(messageItem(itemId("msg"), message.content, "completed"));
678
+ for (const item of Array.isArray(message.tool_calls) ? message.tool_calls : []) {
679
+ const call = asRecord(item);
680
+ const fn = asRecord(call?.function);
681
+ if (typeof fn?.name !== "string")
682
+ continue;
683
+ output.push(toolCallItem(itemId("fc"), typeof call?.id === "string" ? call.id : `call_${crypto.randomUUID()}`, fn.name, typeof fn.arguments === "string" ? fn.arguments : "", ctx, "completed"));
684
+ }
685
+ const incomplete = incompleteDetails(choice.finish_reason);
686
+ return responseObject(responseId(completion.id), ctx, incomplete ? "incomplete" : "completed", output, responsesUsage(completion.usage), {
687
+ incomplete_details: incomplete,
688
+ });
689
+ }
690
+ /** OpenAI chat.completion.chunk SSE → Responses SSE event sequence. */
691
+ export function openAISseToResponses(ctx) {
692
+ const decoder = new TextDecoder();
693
+ const encoder = new TextEncoder();
694
+ const parser = new SseParser();
695
+ let id = responseId(null);
696
+ let started = false;
697
+ let finished = false;
698
+ let sequence = 0;
699
+ let finishReason = null;
700
+ let usage = null;
701
+ const items = [];
702
+ const tools = new Map();
703
+ let reasoning = null;
704
+ let message = null;
705
+ const emit = (controller, type, data) => {
706
+ controller.enqueue(encoder.encode(`event: ${type}\ndata: ${JSON.stringify({ type, sequence_number: sequence++, ...data })}\n\n`));
707
+ };
708
+ const finalItem = (item) => {
709
+ if (item.kind === "reasoning")
710
+ return reasoningItem(item.id, item.text, item.details);
711
+ if (item.kind === "message")
712
+ return messageItem(item.id, item.text, "completed");
713
+ return toolCallItem(item.id, item.callId, item.name, item.text, ctx, "completed");
714
+ };
715
+ const start = (controller, rawId) => {
716
+ if (started)
717
+ return;
718
+ started = true;
719
+ id = responseId(rawId);
720
+ const response = responseObject(id, ctx, "in_progress", [], null);
721
+ emit(controller, "response.created", { response });
722
+ emit(controller, "response.in_progress", { response });
723
+ };
724
+ const open = (controller, item, added) => {
725
+ items.push(item);
726
+ emit(controller, "response.output_item.added", { output_index: item.outputIndex, item: added });
727
+ };
728
+ const close = (controller, item) => {
729
+ if (!item || item.done)
730
+ return;
731
+ item.done = true;
732
+ const base = { item_id: item.id, output_index: item.outputIndex };
733
+ if (item.kind === "reasoning" && item.summaryOpen) {
734
+ emit(controller, "response.reasoning_summary_text.done", { ...base, summary_index: 0, text: item.text });
735
+ emit(controller, "response.reasoning_summary_part.done", { ...base, summary_index: 0, part: { type: "summary_text", text: item.text } });
736
+ }
737
+ else if (item.kind === "message") {
738
+ emit(controller, "response.output_text.done", { ...base, content_index: 0, text: item.text });
739
+ emit(controller, "response.content_part.done", { ...base, content_index: 0, part: { type: "output_text", text: item.text, annotations: [] } });
740
+ }
741
+ else if (item.kind === "tool") {
742
+ if (isToolSearch(item.name, ctx)) {
743
+ // tool_search_call has no argument events; the final item carries them.
744
+ }
745
+ else if (ctx.customTools.includes(item.name)) {
746
+ const final = finalItem(item);
747
+ emit(controller, "response.custom_tool_call_input.done", { ...base, input: final.input });
748
+ }
749
+ else {
750
+ emit(controller, "response.function_call_arguments.done", { ...base, arguments: item.text });
751
+ }
752
+ }
753
+ emit(controller, "response.output_item.done", { output_index: item.outputIndex, item: finalItem(item) });
754
+ };
755
+ const closeAll = (controller) => {
756
+ for (const item of items)
757
+ close(controller, item);
758
+ };
759
+ const openReasoning = (controller) => {
760
+ if (!reasoning || reasoning.done) {
761
+ reasoning = { kind: "reasoning", outputIndex: items.length, id: itemId("rs"), text: "", details: [], done: false };
762
+ open(controller, reasoning, { type: "reasoning", id: reasoning.id, summary: [] });
763
+ }
764
+ return reasoning;
765
+ };
766
+ // Streamed as one reasoning summary part; Codex 0.167 renders
767
+ // reasoning_summary_text.delta live and ignores the *.done events.
768
+ const onReasoning = (controller, text) => {
769
+ const item = openReasoning(controller);
770
+ const base = { item_id: item.id, output_index: item.outputIndex, summary_index: 0 };
771
+ if (!item.summaryOpen) {
772
+ item.summaryOpen = true;
773
+ emit(controller, "response.reasoning_summary_part.added", { ...base, part: { type: "summary_text", text: "" } });
774
+ }
775
+ item.text += text;
776
+ emit(controller, "response.reasoning_summary_text.delta", { ...base, delta: text });
777
+ };
778
+ // Signed/encrypted reasoning (`reasoning_details`) is not shown,
779
+ // only carried in the item's encrypted_content for replay.
780
+ const onReasoningDetails = (controller, details) => {
781
+ mergeReasoningDetails(openReasoning(controller).details, details);
782
+ };
783
+ const onText = (controller, text) => {
784
+ close(controller, reasoning);
785
+ if (!message || message.done) {
786
+ message = { kind: "message", outputIndex: items.length, id: itemId("msg"), text: "", done: false };
787
+ open(controller, message, { type: "message", id: message.id, status: "in_progress", role: "assistant", content: [] });
788
+ emit(controller, "response.content_part.added", {
789
+ item_id: message.id,
790
+ output_index: message.outputIndex,
791
+ content_index: 0,
792
+ part: { type: "output_text", text: "", annotations: [] },
793
+ });
794
+ }
795
+ message.text += text;
796
+ emit(controller, "response.output_text.delta", {
797
+ item_id: message.id,
798
+ output_index: message.outputIndex,
799
+ content_index: 0,
800
+ delta: text,
801
+ });
802
+ };
803
+ const onToolCalls = (controller, calls) => {
804
+ close(controller, reasoning);
805
+ close(controller, message);
806
+ for (const entry of calls) {
807
+ const call = asRecord(entry);
808
+ if (!call)
809
+ continue;
810
+ const fn = asRecord(call.function) ?? {};
811
+ const index = Number(call.index ?? 0);
812
+ let item = tools.get(index);
813
+ if (!item) {
814
+ item = {
815
+ kind: "tool",
816
+ outputIndex: items.length,
817
+ id: itemId("fc"),
818
+ text: "",
819
+ callId: typeof call.id === "string" ? call.id : `call_${crypto.randomUUID()}`,
820
+ name: typeof fn.name === "string" ? fn.name : "",
821
+ done: false,
822
+ };
823
+ tools.set(index, item);
824
+ open(controller, item, toolCallItem(item.id, item.callId, item.name, "", ctx, "in_progress"));
825
+ }
826
+ if (typeof fn.arguments === "string" && fn.arguments) {
827
+ item.text += fn.arguments;
828
+ // Custom tool input is only known once the JSON wrapper is complete and
829
+ // tool_search arguments are an object; Codex reads the final item, so
830
+ // both skip the per-chunk deltas.
831
+ if (!wholeArgumentsOnly(item.name, ctx)) {
832
+ emit(controller, "response.function_call_arguments.delta", {
833
+ item_id: item.id,
834
+ output_index: item.outputIndex,
835
+ delta: fn.arguments,
836
+ });
837
+ }
838
+ }
839
+ }
840
+ };
841
+ const finish = (controller) => {
842
+ if (finished)
843
+ return;
844
+ start(controller, null);
845
+ closeAll(controller);
846
+ finished = true;
847
+ const incomplete = incompleteDetails(finishReason);
848
+ const response = responseObject(id, ctx, incomplete ? "incomplete" : "completed", items.map(finalItem), usage ?? responsesUsage(null), {
849
+ incomplete_details: incomplete,
850
+ });
851
+ emit(controller, incomplete ? "response.incomplete" : "response.completed", { response });
852
+ };
853
+ const fail = (controller, errorMessage, code) => {
854
+ if (finished)
855
+ return;
856
+ start(controller, null);
857
+ finished = true;
858
+ emit(controller, "response.failed", {
859
+ response: responseObject(id, ctx, "failed", items.filter((item) => item.done).map(finalItem), usage, {
860
+ error: { code, message: errorMessage },
861
+ }),
862
+ });
863
+ };
864
+ const consume = (event, controller) => {
865
+ if (finished)
866
+ return;
867
+ const data = event.data.trim();
868
+ if (!data) {
869
+ // Keepalive comment frames: SSE clients ignore comments, forward as-is.
870
+ controller.enqueue(encoder.encode(": keepalive\n\n"));
871
+ return;
872
+ }
873
+ if (data === "[DONE]") {
874
+ finish(controller);
875
+ return;
876
+ }
877
+ let chunk;
878
+ try {
879
+ chunk = asRecord(JSON.parse(data));
880
+ }
881
+ catch {
882
+ chunk = null;
883
+ }
884
+ if (!chunk)
885
+ return;
886
+ const error = asRecord(chunk.error);
887
+ if (error) {
888
+ // Frames sent after the keepalive headers carry the status they replaced;
889
+ // post-output stream failures carry a numeric code (502/504) instead.
890
+ const status = typeof error.upstream_status === "number" ? error.upstream_status : typeof error.code === "number" ? error.code : 502;
891
+ fail(controller, typeof error.message === "string" ? error.message : "upstream stream error", responsesStreamErrorCode(status, error.code));
892
+ return;
893
+ }
894
+ start(controller, chunk.id);
895
+ if (asRecord(chunk.usage))
896
+ usage = responsesUsage(chunk.usage);
897
+ const choice = asRecord(Array.isArray(chunk.choices) ? chunk.choices[0] : null);
898
+ if (!choice)
899
+ return;
900
+ const d = asRecord(choice.delta) ?? {};
901
+ const reasoningText = d.reasoning_content ?? d.reasoning;
902
+ if (typeof reasoningText === "string" && reasoningText)
903
+ onReasoning(controller, reasoningText);
904
+ if (Array.isArray(d.reasoning_details) && d.reasoning_details.length)
905
+ onReasoningDetails(controller, d.reasoning_details);
906
+ if (typeof d.content === "string" && d.content)
907
+ onText(controller, d.content);
908
+ if (Array.isArray(d.tool_calls))
909
+ onToolCalls(controller, d.tool_calls);
910
+ if (typeof choice.finish_reason === "string")
911
+ finishReason = choice.finish_reason;
912
+ };
913
+ return new TransformStream({
914
+ transform(bytes, controller) {
915
+ try {
916
+ for (const event of parser.push(decoder.decode(bytes, { stream: true })))
917
+ consume(event, controller);
918
+ }
919
+ catch (error) {
920
+ fail(controller, error instanceof Error ? error.message : "stream conversion failed", "server_error");
921
+ }
922
+ },
923
+ flush(controller) {
924
+ try {
925
+ for (const event of parser.finish(decoder.decode()))
926
+ consume(event, controller);
927
+ }
928
+ catch (error) {
929
+ fail(controller, error instanceof Error ? error.message : "stream conversion failed", "server_error");
930
+ }
931
+ // A stream that ended without [DONE] was cut off upstream.
932
+ if (!finished) {
933
+ if (started)
934
+ fail(controller, "stream ended unexpectedly", "server_error");
935
+ else
936
+ finish(controller);
937
+ }
938
+ },
939
+ });
940
+ }
941
+ /** Gateway codes that are final whatever their HTTP status (429 quota, 403 review, 503 model). */
942
+ const FINAL_ERROR_CODES = new Set([
943
+ "quota_exceeded",
944
+ "insufficient_quota",
945
+ "client_integrity_review",
946
+ "u1s1_client_only",
947
+ "model_not_found",
948
+ "model_unavailable",
949
+ "request_body_too_large",
950
+ ]);
951
+ /** HTTP 400 codes whose `message` Codex shows as is (any other code prints the raw body). */
952
+ const CODEX_MESSAGE_400_CODES = new Set(["invalid_prompt", "model_not_found"]);
953
+ export function classifyError(status, code) {
954
+ if (code === "context_length_exceeded")
955
+ return "context";
956
+ if (typeof code === "string" && FINAL_ERROR_CODES.has(code))
957
+ return "final";
958
+ if (status === 408 || status === 409 || status === 429 || status >= 500)
959
+ return "retry";
960
+ return status >= 400 ? "final" : "retry";
961
+ }
962
+ /** `response.failed` error code for a gateway error frame. */
963
+ export function responsesStreamErrorCode(status, code) {
964
+ const kind = classifyError(status, code);
965
+ if (kind === "context")
966
+ return "context_length_exceeded";
967
+ if (kind === "final")
968
+ return "invalid_prompt";
969
+ return status === 429 ? "rate_limit_exceeded" : "server_error";
970
+ }
971
+ /** SSE body with just `response.created` + `response.failed`, for errors Codex only understands in-stream. */
972
+ export function failedStream(ctx, code, message, headers) {
973
+ const encoder = new TextEncoder();
974
+ const id = responseId(null);
975
+ let sequence = 0;
976
+ const frame = (type, data) => `event: ${type}\ndata: ${JSON.stringify({ type, sequence_number: sequence++, ...data })}\n\n`;
977
+ const body = frame("response.created", { response: responseObject(id, ctx, "in_progress", [], null) })
978
+ + frame("response.failed", { response: responseObject(id, ctx, "failed", [], null, { error: { code, message } }) });
979
+ headers.set("content-type", "text/event-stream");
980
+ headers.set("cache-control", "no-cache");
981
+ return new Response(encoder.encode(body), { status: 200, headers });
982
+ }
983
+ /**
984
+ * The one shape of a final error on /v1/responses: HTTP 400, `code: "invalid_prompt"`
985
+ * (or `model_not_found`, the other code Codex displays), `x-should-retry: false`, the
986
+ * gateway's own code and status kept as `u1s1_code` / `u1s1_status`. Codex sends the
987
+ * turn once and shows `message`. Used for chat-pipeline errors (errorToResponses) and
988
+ * auth-stage refusals (auth.ts) alike.
989
+ */
990
+ export function responsesFinalError(error, status, message, headers = new Headers()) {
991
+ const gatewayCode = error?.code;
992
+ const code = typeof gatewayCode === "string" && CODEX_MESSAGE_400_CODES.has(gatewayCode) ? gatewayCode : "invalid_prompt";
993
+ // OpenAI SDKs honour x-should-retry; Codex keys off the 400 status.
994
+ headers.set("x-should-retry", "false");
995
+ headers.set("content-type", "application/json");
996
+ headers.delete("content-length");
997
+ return new Response(JSON.stringify({
998
+ error: {
999
+ message,
1000
+ type: typeof error?.type === "string" && error.type ? error.type : "invalid_request_error",
1001
+ code,
1002
+ param: typeof error?.param === "string" ? error.param : null,
1003
+ ...(gatewayCode !== undefined && gatewayCode !== null && gatewayCode !== code ? { u1s1_code: gatewayCode } : {}),
1004
+ ...(status !== 400 ? { u1s1_status: status } : {}),
1005
+ ...(typeof error?.request_id === "string" ? { request_id: error.request_id } : {}),
1006
+ },
1007
+ }), { status: 400, headers });
1008
+ }
1009
+ async function errorToResponses(response, ctx) {
1010
+ const text = await response.text();
1011
+ let error = null;
1012
+ try {
1013
+ error = asRecord(asRecord(JSON.parse(text))?.error);
1014
+ }
1015
+ catch {
1016
+ // Non-JSON error bodies keep their raw text as the message.
1017
+ }
1018
+ const message = typeof error?.message === "string" ? error.message : text.slice(0, 2_000) || response.statusText || "request failed";
1019
+ const headers = new Headers();
1020
+ for (const name of ["x-request-id", "retry-after"]) {
1021
+ const value = response.headers.get(name);
1022
+ if (value)
1023
+ headers.set(name, value);
1024
+ }
1025
+ const gatewayCode = error?.code;
1026
+ const kind = classifyError(response.status, gatewayCode);
1027
+ // Context overflow: in-stream `context_length_exceeded` is the shape Codex
1028
+ // maps to its context-window handling; over HTTP 400 it would print raw JSON.
1029
+ if (kind === "context" && ctx.stream)
1030
+ return failedStream(ctx, "context_length_exceeded", message, headers);
1031
+ if (kind === "final")
1032
+ return responsesFinalError(error, response.status, message, headers);
1033
+ // Upstream 5xx with every channel already tried (see the table above): in-stream
1034
+ // server_error, so Codex retries 6 times instead of 30.
1035
+ if (ctx.stream && response.status >= 500 && gatewayCode === "upstream_error") {
1036
+ return failedStream(ctx, "server_error", message, headers);
1037
+ }
1038
+ const requestId = typeof error?.request_id === "string" ? { request_id: error.request_id } : {};
1039
+ headers.set("content-type", "application/json");
1040
+ const code = kind === "context" ? "context_length_exceeded" : response.status === 429 ? "rate_limit_exceeded" : "server_error";
1041
+ return new Response(JSON.stringify({
1042
+ error: {
1043
+ message,
1044
+ type: typeof error?.type === "string" && error.type ? error.type : "api_error",
1045
+ code: kind === "context" ? code : typeof gatewayCode === "string" && gatewayCode ? gatewayCode : code,
1046
+ param: typeof error?.param === "string" ? error.param : null,
1047
+ ...requestId,
1048
+ },
1049
+ }), { status: kind === "context" ? 400 : response.status, headers });
1050
+ }
1051
+ /** Convert the Chat Completions handler's response into a Responses API response. */
1052
+ export async function openAIResponseToResponses(response, ctx) {
1053
+ if (!response.ok)
1054
+ return errorToResponses(response, ctx);
1055
+ const headers = new Headers();
1056
+ const requestId = response.headers.get("x-request-id");
1057
+ if (requestId)
1058
+ headers.set("x-request-id", requestId);
1059
+ const isStream = (response.headers.get("content-type") ?? "").includes("text/event-stream");
1060
+ if (isStream) {
1061
+ if (!response.body)
1062
+ return errorToResponses(new Response("missing stream body", { status: 502 }), ctx);
1063
+ headers.set("content-type", "text/event-stream");
1064
+ headers.set("cache-control", "no-cache");
1065
+ return new Response(response.body.pipeThrough(openAISseToResponses(ctx)), { status: response.status, headers });
1066
+ }
1067
+ let payload;
1068
+ try {
1069
+ payload = await response.json();
1070
+ }
1071
+ catch {
1072
+ return errorToResponses(new Response("malformed completion body", { status: 502 }), ctx);
1073
+ }
1074
+ headers.set("content-type", "application/json");
1075
+ return new Response(JSON.stringify(openAICompletionToResponses(payload, ctx)), { status: response.status, headers });
1076
+ }