@sentropic/h2a-runtime 0.91.1 → 0.92.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 (58) hide show
  1. package/dist/config.d.ts +2 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +16 -3
  4. package/dist/config.js.map +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +76 -58
  8. package/dist/index.js.map +1 -1
  9. package/dist/llm-gateway-runtime/index.d.ts +6 -0
  10. package/dist/llm-gateway-runtime/index.d.ts.map +1 -1
  11. package/dist/llm-gateway-runtime/index.js +134 -60
  12. package/dist/llm-gateway-runtime/index.js.map +1 -1
  13. package/dist/llm-gateway-runtime/session-ledger.d.ts +38 -55
  14. package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -1
  15. package/dist/llm-gateway-runtime/session-ledger.js +102 -250
  16. package/dist/llm-gateway-runtime/session-ledger.js.map +1 -1
  17. package/dist/llm-gateway-runtime/sticky.d.ts +25 -52
  18. package/dist/llm-gateway-runtime/sticky.d.ts.map +1 -1
  19. package/dist/llm-gateway-runtime/sticky.js +36 -605
  20. package/dist/llm-gateway-runtime/sticky.js.map +1 -1
  21. package/dist/llm-mesh.d.ts +19 -72
  22. package/dist/llm-mesh.d.ts.map +1 -1
  23. package/dist/llm-mesh.js +42 -399
  24. package/dist/llm-mesh.js.map +1 -1
  25. package/dist/llm-routing-config.d.ts +28 -0
  26. package/dist/llm-routing-config.d.ts.map +1 -0
  27. package/dist/llm-routing-config.js +100 -0
  28. package/dist/llm-routing-config.js.map +1 -0
  29. package/dist/registry.d.ts +7 -0
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js +37 -1
  32. package/dist/registry.js.map +1 -1
  33. package/dist/status-projection.d.ts.map +1 -1
  34. package/dist/tmux.d.ts +7 -5
  35. package/dist/tmux.d.ts.map +1 -1
  36. package/dist/tmux.js +33 -7
  37. package/dist/tmux.js.map +1 -1
  38. package/package.json +3 -3
  39. package/dist/llm-gateway-runtime/accounts.d.ts +0 -62
  40. package/dist/llm-gateway-runtime/accounts.d.ts.map +0 -1
  41. package/dist/llm-gateway-runtime/accounts.js +0 -323
  42. package/dist/llm-gateway-runtime/accounts.js.map +0 -1
  43. package/dist/llm-gateway-runtime/model-catalog.d.ts +0 -44
  44. package/dist/llm-gateway-runtime/model-catalog.d.ts.map +0 -1
  45. package/dist/llm-gateway-runtime/model-catalog.js +0 -191
  46. package/dist/llm-gateway-runtime/model-catalog.js.map +0 -1
  47. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts +0 -3
  48. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +0 -1
  49. package/dist/llm-gateway-runtime/proxy-anthropic.js +0 -318
  50. package/dist/llm-gateway-runtime/proxy-anthropic.js.map +0 -1
  51. package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts +0 -11
  52. package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts.map +0 -1
  53. package/dist/llm-gateway-runtime/proxy-cloud-code.js +0 -215
  54. package/dist/llm-gateway-runtime/proxy-cloud-code.js.map +0 -1
  55. package/dist/llm-gateway-runtime/proxy-openai.d.ts +0 -118
  56. package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +0 -1
  57. package/dist/llm-gateway-runtime/proxy-openai.js +0 -1305
  58. package/dist/llm-gateway-runtime/proxy-openai.js.map +0 -1
@@ -1,1305 +0,0 @@
1
- /**
2
- * Anthropic Messages API → OpenAI Chat Completions proxy.
3
- *
4
- * Activated when the account's provider is "openai" or "codex". Translates
5
- * requests and responses between the two formats so Claude Code (which speaks
6
- * Anthropic) can transparently use OpenAI/Codex models.
7
- *
8
- * Model routing comes from @sentropic/llm-gateway's canonical descriptions,
9
- * with OPENAI_MODEL_MAP retained only as an explicit operator override.
10
- *
11
- * Thinking budget_tokens → reasoning effort:
12
- * ≥ 50 000 (xhigh) → "xhigh"
13
- * ≥ 25 000 (high) → "high"
14
- * ≥ 8 000 (med) → "medium"
15
- * < 8 000 / none → "low"
16
- */
17
- import { CODEX_RESPONSES_URL } from "@sentropic/llm-gateway";
18
- import { isCodexOAuthToken, refreshOAuthToken, } from "./accounts.js";
19
- import { routeModelOrThrow } from "./model-catalog.js";
20
- const OPENAI_BASE = process.env.OPENAI_UPSTREAM_URL ?? "https://api.openai.com";
21
- const DEFAULT_CODEX_MAX_INPUT_CHARS = 200_000;
22
- const CODEX_CONTEXT_TRUNCATION_NOTICE = "[llm-gateway: older Claude Code transcript omitted to fit the Codex upstream context window.]";
23
- const TRANSIENT_UPSTREAM_ERROR_CODES = new Set([
24
- "EAI_AGAIN",
25
- "ECONNRESET",
26
- "ETIMEDOUT",
27
- "UND_ERR_CONNECT_TIMEOUT",
28
- "UND_ERR_HEADERS_TIMEOUT",
29
- "UND_ERR_SOCKET",
30
- ]);
31
- const UPSTREAM_RETRY_DELAYS_MS = process.env.NODE_ENV === "test" ? [0, 0] : [250, 1000];
32
- function abortError(err) {
33
- return err.name === "AbortError";
34
- }
35
- function upstreamErrorCause(err) {
36
- const candidate = err;
37
- if (candidate.code || candidate.hostname)
38
- return candidate;
39
- return candidate.cause;
40
- }
41
- function upstreamFetchErrorMessage(err) {
42
- const cause = upstreamErrorCause(err);
43
- if (cause?.code && cause.hostname)
44
- return `${cause.code} ${cause.hostname}`;
45
- if (cause?.code)
46
- return cause.code;
47
- if (err instanceof Error && err.message)
48
- return err.message;
49
- return "unknown upstream fetch error";
50
- }
51
- function isTransientUpstreamError(err) {
52
- const code = upstreamErrorCause(err)?.code;
53
- return !!code && TRANSIENT_UPSTREAM_ERROR_CODES.has(code);
54
- }
55
- function anthropicGatewayError(message) {
56
- return { type: "error", error: { type: "api_error", message } };
57
- }
58
- function asRecord(value) {
59
- return value && typeof value === "object" && !Array.isArray(value)
60
- ? value
61
- : undefined;
62
- }
63
- function prepareCodexResponsesRequest(request) {
64
- const { max_output_tokens: _maxOutputTokens, ...rest } = request;
65
- return {
66
- url: CODEX_RESPONSES_URL,
67
- body: {
68
- ...rest,
69
- store: false,
70
- },
71
- };
72
- }
73
- function codexRequestEffort(body) {
74
- const reasoning = asRecord(body.reasoning);
75
- const effort = reasoning?.effort;
76
- return typeof effort === "string" ? effort : undefined;
77
- }
78
- async function refreshOAuthTokenForRetry(accountId) {
79
- try {
80
- return await refreshOAuthToken(accountId);
81
- }
82
- catch (err) {
83
- console.error(`[llm-gateway] OAuth refresh failed for ${accountId}:`, err);
84
- return null;
85
- }
86
- }
87
- async function sleep(ms) {
88
- if (ms <= 0)
89
- return;
90
- await new Promise((resolve) => setTimeout(resolve, ms));
91
- }
92
- async function fetchUpstreamWithRetry(fetcher) {
93
- for (let attempt = 0;; attempt += 1) {
94
- try {
95
- return await fetcher();
96
- }
97
- catch (err) {
98
- if (abortError(err) ||
99
- !isTransientUpstreamError(err) ||
100
- attempt >= UPSTREAM_RETRY_DELAYS_MS.length) {
101
- throw err;
102
- }
103
- await sleep(UPSTREAM_RETRY_DELAYS_MS[attempt] ?? 0);
104
- }
105
- }
106
- }
107
- // ---------------------------------------------------------------------------
108
- // Model mapping
109
- // ---------------------------------------------------------------------------
110
- export function mapModel(anthropicModel) {
111
- return routeModelOrThrow(anthropicModel).upstreamModel ?? anthropicModel;
112
- }
113
- /**
114
- * Anthropic thinking budget_tokens → OpenAI reasoning_effort.
115
- * Covers all 4 Claude effort tiers:
116
- * xhigh (≥ 50 k) → "xhigh" (pass-through; upstream rejection stays explicit)
117
- * high (≥ 25 k) → "high"
118
- * med (≥ 8 k) → "medium"
119
- * low (< 8 k) → "low"
120
- */
121
- export function budgetToEffort(budgetTokens) {
122
- if (budgetTokens >= 50_000)
123
- return "xhigh";
124
- if (budgetTokens >= 25_000)
125
- return "high";
126
- if (budgetTokens >= 8_000)
127
- return "medium";
128
- return "low";
129
- }
130
- const EMPTY_TOOL_RESULT_PLACEHOLDER = "[llm-gateway: empty tool result omitted.]";
131
- function unsupportedContentPlaceholder(block) {
132
- const record = block;
133
- const source = asRecord(record.source);
134
- const mediaType = typeof source?.media_type === "string"
135
- ? source.media_type
136
- : typeof record.media_type === "string"
137
- ? record.media_type
138
- : undefined;
139
- const label = mediaType ? `${block.type} ${mediaType}` : block.type;
140
- return `[llm-gateway: unsupported ${label} content omitted.]`;
141
- }
142
- function normalizeUnsupportedContentBlocks(content) {
143
- if (typeof content === "string")
144
- return content;
145
- return content.map((block) => {
146
- if (block.type === "text" ||
147
- block.type === "tool_use" ||
148
- block.type === "thinking") {
149
- return block;
150
- }
151
- if (block.type === "tool_result") {
152
- const tr = block;
153
- return {
154
- ...tr,
155
- content: typeof tr.content === "string"
156
- ? tr.content
157
- : normalizeUnsupportedContentBlocks(tr.content),
158
- };
159
- }
160
- return {
161
- type: "text",
162
- text: unsupportedContentPlaceholder(block),
163
- };
164
- });
165
- }
166
- function normalizeUnsupportedRequestContent(body) {
167
- const normalized = {
168
- ...body,
169
- messages: body.messages.map((message) => ({
170
- ...message,
171
- content: normalizeUnsupportedContentBlocks(message.content),
172
- })),
173
- };
174
- if (Array.isArray(body.system)) {
175
- normalized.system = normalizeUnsupportedContentBlocks(body.system);
176
- }
177
- else if (body.system !== undefined) {
178
- normalized.system = body.system;
179
- }
180
- return normalized;
181
- }
182
- function toolResultContentToText(content) {
183
- const text = typeof content === "string"
184
- ? content
185
- : normalizeUnsupportedContentBlocks(content)
186
- .map((block) => block.type === "text"
187
- ? block.text
188
- : unsupportedContentPlaceholder(block))
189
- .join("");
190
- return text.length > 0 ? text : EMPTY_TOOL_RESULT_PLACEHOLDER;
191
- }
192
- function extractAssistantContent(content) {
193
- if (typeof content === "string")
194
- return { text: content || null, toolCalls: [] };
195
- const texts = [];
196
- const toolCalls = [];
197
- for (const item of content) {
198
- if (item.type === "text") {
199
- texts.push(item.text);
200
- }
201
- else if (item.type === "tool_use") {
202
- const tc = item;
203
- toolCalls.push({
204
- id: tc.id,
205
- type: "function",
206
- function: { name: tc.name, arguments: JSON.stringify(tc.input) },
207
- });
208
- }
209
- // thinking blocks: dropped (OpenAI reasons internally)
210
- }
211
- return { text: texts.join("") || null, toolCalls };
212
- }
213
- function toOAIMessages(messages) {
214
- const result = [];
215
- for (const msg of messages) {
216
- if (msg.role === "assistant") {
217
- const { text, toolCalls } = extractAssistantContent(msg.content);
218
- const entry = { role: "assistant", content: text };
219
- if (toolCalls.length > 0)
220
- entry.tool_calls = toolCalls;
221
- result.push(entry);
222
- }
223
- else {
224
- // user: may contain tool_result blocks
225
- if (Array.isArray(msg.content)) {
226
- for (const item of msg.content) {
227
- if (item.type === "tool_result") {
228
- const tr = item;
229
- result.push({
230
- role: "tool",
231
- tool_call_id: tr.tool_use_id,
232
- content: toolResultContentToText(tr.content),
233
- });
234
- }
235
- }
236
- const textItems = msg.content.filter((c) => c.type === "text");
237
- if (textItems.length > 0) {
238
- result.push({
239
- role: "user",
240
- content: textItems.map((t) => ({
241
- type: "text",
242
- text: t.text,
243
- })),
244
- });
245
- }
246
- }
247
- else {
248
- result.push({ role: "user", content: msg.content });
249
- }
250
- }
251
- }
252
- return result;
253
- }
254
- function systemToText(system) {
255
- if (typeof system === "string")
256
- return system || undefined;
257
- if (!Array.isArray(system))
258
- return undefined;
259
- const parts = [];
260
- for (const block of system) {
261
- if (block.type === "text") {
262
- parts.push(block.text);
263
- }
264
- }
265
- return parts.join("\n\n") || undefined;
266
- }
267
- // ---------------------------------------------------------------------------
268
- // Request translation: Anthropic → OpenAI Responses API (Codex OAuth path)
269
- // ---------------------------------------------------------------------------
270
- function toCodexInput(messages) {
271
- const items = [];
272
- for (const msg of messages) {
273
- if (msg.role === "assistant") {
274
- const texts = [];
275
- const toolCalls = [];
276
- if (typeof msg.content === "string") {
277
- if (msg.content)
278
- texts.push(msg.content);
279
- }
280
- else {
281
- for (const block of msg.content) {
282
- if (block.type === "text")
283
- texts.push(block.text);
284
- else if (block.type === "tool_use")
285
- toolCalls.push(block);
286
- // thinking blocks: skip
287
- }
288
- }
289
- const text = texts.join("");
290
- if (text || toolCalls.length === 0) {
291
- items.push({ type: "message", role: "assistant", content: text });
292
- }
293
- for (const tc of toolCalls) {
294
- items.push({
295
- type: "function_call",
296
- call_id: tc.id,
297
- name: tc.name,
298
- arguments: JSON.stringify(tc.input),
299
- });
300
- }
301
- }
302
- else {
303
- const texts = [];
304
- const toolResults = [];
305
- if (typeof msg.content === "string") {
306
- if (msg.content)
307
- texts.push(msg.content);
308
- }
309
- else {
310
- for (const block of msg.content) {
311
- if (block.type === "text")
312
- texts.push(block.text);
313
- else if (block.type === "tool_result")
314
- toolResults.push(block);
315
- }
316
- }
317
- for (const tr of toolResults) {
318
- items.push({
319
- type: "function_call_output",
320
- call_id: tr.tool_use_id,
321
- output: toolResultContentToText(tr.content),
322
- });
323
- }
324
- const text = texts.join("");
325
- if (text)
326
- items.push({ type: "message", role: "user", content: text });
327
- }
328
- }
329
- return items;
330
- }
331
- function codexEffort(budgetTokens) {
332
- return budgetToEffort(budgetTokens);
333
- }
334
- export function toCodexRequest(body, canonicalEffort) {
335
- body = normalizeUnsupportedRequestContent(body);
336
- const req = {
337
- model: mapModel(body.model),
338
- input: toCodexInput(body.messages),
339
- store: false,
340
- stream: true,
341
- };
342
- const instructions = systemToText(body.system);
343
- if (instructions)
344
- req.instructions = instructions;
345
- const effort = canonicalEffort ??
346
- (body.thinking?.type === "enabled"
347
- ? codexEffort(body.thinking.budget_tokens)
348
- : undefined);
349
- if (effort) {
350
- req.reasoning = { effort };
351
- }
352
- if (body.tools && body.tools.length > 0) {
353
- req.tools = body.tools.map((t) => ({
354
- type: "function",
355
- name: t.name,
356
- ...(t.description !== undefined ? { description: t.description } : {}),
357
- parameters: t.input_schema,
358
- strict: false,
359
- }));
360
- }
361
- return req;
362
- }
363
- function codexMaxInputChars() {
364
- const configured = Number.parseInt(process.env.CODEX_MAX_INPUT_CHARS ?? "", 10);
365
- return Number.isFinite(configured) && configured > 0
366
- ? configured
367
- : DEFAULT_CODEX_MAX_INPUT_CHARS;
368
- }
369
- function anthropicInputChars(body) {
370
- return JSON.stringify({ system: body.system, messages: body.messages })
371
- .length;
372
- }
373
- function trimTextTail(text, budget) {
374
- if (text.length <= budget)
375
- return text;
376
- if (budget <= CODEX_CONTEXT_TRUNCATION_NOTICE.length + 8) {
377
- return CODEX_CONTEXT_TRUNCATION_NOTICE.slice(0, Math.max(0, budget));
378
- }
379
- const prefix = `${CODEX_CONTEXT_TRUNCATION_NOTICE}\n\n`;
380
- return prefix + text.slice(-(budget - prefix.length));
381
- }
382
- function trimContentTail(content, budget) {
383
- if (typeof content === "string")
384
- return trimTextTail(content, budget);
385
- const kept = [];
386
- let remaining = budget;
387
- for (let i = content.length - 1; i >= 0; i -= 1) {
388
- const block = content[i];
389
- const size = JSON.stringify(block).length;
390
- if (size <= remaining) {
391
- kept.unshift(block);
392
- remaining -= size;
393
- continue;
394
- }
395
- if (block.type === "text" &&
396
- remaining > CODEX_CONTEXT_TRUNCATION_NOTICE.length + 32) {
397
- kept.unshift({
398
- ...block,
399
- text: trimTextTail(block.text, remaining),
400
- });
401
- }
402
- break;
403
- }
404
- return kept.length > 0
405
- ? kept
406
- : [{ type: "text", text: CODEX_CONTEXT_TRUNCATION_NOTICE }];
407
- }
408
- function stripLeadingOrphanToolResults(messages) {
409
- if (messages.length === 0)
410
- return messages;
411
- const first = messages[0];
412
- if (first.role !== "user" || !Array.isArray(first.content))
413
- return messages;
414
- const filtered = first.content.filter((block) => block.type !== "tool_result");
415
- if (filtered.length === first.content.length)
416
- return messages;
417
- if (filtered.length === 0)
418
- return messages.slice(1);
419
- return [{ ...first, content: filtered }, ...messages.slice(1)];
420
- }
421
- export function trimCodexBodyForContext(body, maxChars = codexMaxInputChars()) {
422
- body = normalizeUnsupportedRequestContent(body);
423
- const beforeChars = anthropicInputChars(body);
424
- if (beforeChars <= maxChars) {
425
- return { body, trimmed: false, beforeChars, afterChars: beforeChars };
426
- }
427
- const notice = {
428
- role: "user",
429
- content: CODEX_CONTEXT_TRUNCATION_NOTICE,
430
- };
431
- const systemChars = JSON.stringify(body.system ?? "").length;
432
- const noticeChars = JSON.stringify(notice).length;
433
- let remaining = Math.max(1024, maxChars - systemChars - noticeChars);
434
- const kept = [];
435
- for (let i = body.messages.length - 1; i >= 0; i -= 1) {
436
- const message = body.messages[i];
437
- const messageChars = JSON.stringify(message).length;
438
- if (messageChars <= remaining) {
439
- kept.unshift(message);
440
- remaining -= messageChars;
441
- continue;
442
- }
443
- if (kept.length === 0 && remaining > 1024) {
444
- kept.unshift({
445
- ...message,
446
- content: trimContentTail(message.content, remaining),
447
- });
448
- }
449
- break;
450
- }
451
- const trimmedMessages = stripLeadingOrphanToolResults(kept);
452
- const trimmedBody = {
453
- ...body,
454
- messages: [notice, ...trimmedMessages],
455
- };
456
- const afterChars = anthropicInputChars(trimmedBody);
457
- return { body: trimmedBody, trimmed: true, beforeChars, afterChars };
458
- }
459
- // ---------------------------------------------------------------------------
460
- // Request translation: Anthropic → OpenAI Chat Completions (standard sk- key)
461
- // ---------------------------------------------------------------------------
462
- export function toOpenAIRequest(body, canonicalEffort) {
463
- body = normalizeUnsupportedRequestContent(body);
464
- const messages = [];
465
- const system = systemToText(body.system);
466
- if (system)
467
- messages.push({ role: "system", content: system });
468
- messages.push(...toOAIMessages(body.messages));
469
- const req = {
470
- model: mapModel(body.model),
471
- messages,
472
- max_completion_tokens: body.max_tokens,
473
- };
474
- if (body.stream) {
475
- req.stream = true;
476
- req.stream_options = { include_usage: true };
477
- }
478
- const effort = canonicalEffort ??
479
- (body.thinking?.type === "enabled"
480
- ? budgetToEffort(body.thinking.budget_tokens)
481
- : undefined);
482
- if (effort) {
483
- req.reasoning_effort = effort;
484
- }
485
- if (body.tools && body.tools.length > 0) {
486
- req.tools = body.tools.map((t) => ({
487
- type: "function",
488
- function: {
489
- name: t.name,
490
- ...(t.description !== undefined ? { description: t.description } : {}),
491
- parameters: t.input_schema,
492
- },
493
- }));
494
- }
495
- return req;
496
- }
497
- export function toAnthropicResponse(openai, originalModel) {
498
- const choice = openai.choices[0];
499
- const message = choice?.message;
500
- const content = [];
501
- if (message?.content) {
502
- content.push({ type: "text", text: message.content });
503
- }
504
- for (const tc of message?.tool_calls ?? []) {
505
- let input = {};
506
- try {
507
- input = JSON.parse(tc.function.arguments);
508
- }
509
- catch {
510
- /* leave empty */
511
- }
512
- content.push({
513
- type: "tool_use",
514
- id: tc.id,
515
- name: tc.function.name,
516
- input,
517
- });
518
- }
519
- const stopReason = choice?.finish_reason === "tool_calls"
520
- ? "tool_use"
521
- : choice?.finish_reason === "length"
522
- ? "max_tokens"
523
- : "end_turn";
524
- return {
525
- id: openai.id ?? `msg_${Date.now().toString(36)}`,
526
- type: "message",
527
- role: "assistant",
528
- content,
529
- model: originalModel,
530
- stop_reason: stopReason,
531
- stop_sequence: null,
532
- usage: {
533
- input_tokens: openai.usage?.prompt_tokens ?? 0,
534
- output_tokens: openai.usage?.completion_tokens ?? 0,
535
- },
536
- };
537
- }
538
- // ---------------------------------------------------------------------------
539
- // Streaming translation: OpenAI SSE → Anthropic SSE
540
- // ---------------------------------------------------------------------------
541
- function sseEvent(event, data) {
542
- return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
543
- }
544
- function closeStreamWithGatewayError(controller, enc, message) {
545
- try {
546
- controller.enqueue(enc.encode(sseEvent("error", anthropicGatewayError(message))));
547
- }
548
- catch {
549
- // The downstream client may already be gone.
550
- }
551
- try {
552
- controller.close();
553
- }
554
- catch {
555
- // The stream may already be closed or errored.
556
- }
557
- }
558
- export function translateOpenAIStreamToAnthropic(openaiStream, originalModel, messageId, estimatedInputTokens) {
559
- const enc = new TextEncoder();
560
- return new ReadableStream({
561
- async start(controller) {
562
- const emit = (s) => controller.enqueue(enc.encode(s));
563
- // Anthropic content block state
564
- let nextBlockIdx = 0;
565
- let textBlockIdx = -1;
566
- let textBlockOpen = false;
567
- // OpenAI tool_call index → Anthropic block index
568
- const toolBlockMap = new Map();
569
- let outputTokens = 0;
570
- let stopReason = "end_turn";
571
- emit(sseEvent("message_start", {
572
- type: "message_start",
573
- message: {
574
- id: messageId,
575
- type: "message",
576
- role: "assistant",
577
- content: [],
578
- model: originalModel,
579
- stop_reason: null,
580
- stop_sequence: null,
581
- usage: { input_tokens: estimatedInputTokens, output_tokens: 0 },
582
- },
583
- }));
584
- emit(sseEvent("ping", { type: "ping" }));
585
- const reader = openaiStream.getReader();
586
- let buf = "";
587
- try {
588
- while (true) {
589
- const { done, value } = await reader.read();
590
- if (done)
591
- break;
592
- buf += new TextDecoder().decode(value);
593
- const lines = buf.split("\n");
594
- buf = lines.pop() ?? "";
595
- for (const line of lines) {
596
- if (!line.startsWith("data: "))
597
- continue;
598
- const raw = line.slice(6).trim();
599
- if (raw === "[DONE]") {
600
- // Close any open blocks
601
- if (textBlockOpen) {
602
- emit(sseEvent("content_block_stop", {
603
- type: "content_block_stop",
604
- index: textBlockIdx,
605
- }));
606
- }
607
- for (const [, blockIdx] of toolBlockMap) {
608
- emit(sseEvent("content_block_stop", {
609
- type: "content_block_stop",
610
- index: blockIdx,
611
- }));
612
- }
613
- emit(sseEvent("message_delta", {
614
- type: "message_delta",
615
- delta: { stop_reason: stopReason, stop_sequence: null },
616
- usage: { output_tokens: outputTokens },
617
- }));
618
- emit(sseEvent("message_stop", { type: "message_stop" }));
619
- controller.close();
620
- return;
621
- }
622
- let chunk;
623
- try {
624
- chunk = JSON.parse(raw);
625
- }
626
- catch {
627
- continue;
628
- }
629
- // Trailing usage (stream_options: {include_usage: true})
630
- if (chunk.usage?.completion_tokens)
631
- outputTokens = chunk.usage.completion_tokens;
632
- const choice = chunk.choices?.[0];
633
- if (!choice)
634
- continue;
635
- if (choice.finish_reason) {
636
- stopReason =
637
- choice.finish_reason === "tool_calls"
638
- ? "tool_use"
639
- : choice.finish_reason === "length"
640
- ? "max_tokens"
641
- : "end_turn";
642
- }
643
- const delta = choice.delta;
644
- if (!delta)
645
- continue;
646
- // Text content
647
- if (typeof delta.content === "string" && delta.content.length > 0) {
648
- if (!textBlockOpen) {
649
- textBlockIdx = nextBlockIdx++;
650
- textBlockOpen = true;
651
- emit(sseEvent("content_block_start", {
652
- type: "content_block_start",
653
- index: textBlockIdx,
654
- content_block: { type: "text", text: "" },
655
- }));
656
- }
657
- emit(sseEvent("content_block_delta", {
658
- type: "content_block_delta",
659
- index: textBlockIdx,
660
- delta: { type: "text_delta", text: delta.content },
661
- }));
662
- }
663
- // Tool calls
664
- for (const tc of delta.tool_calls ?? []) {
665
- // Close text block when tool calls start
666
- if (textBlockOpen) {
667
- emit(sseEvent("content_block_stop", {
668
- type: "content_block_stop",
669
- index: textBlockIdx,
670
- }));
671
- textBlockOpen = false;
672
- }
673
- if (!toolBlockMap.has(tc.index)) {
674
- const blockIdx = nextBlockIdx++;
675
- toolBlockMap.set(tc.index, blockIdx);
676
- emit(sseEvent("content_block_start", {
677
- type: "content_block_start",
678
- index: blockIdx,
679
- content_block: {
680
- type: "tool_use",
681
- id: tc.id ?? `toolu_${tc.index}`,
682
- name: tc.function?.name ?? "",
683
- input: {},
684
- },
685
- }));
686
- }
687
- const blockIdx = toolBlockMap.get(tc.index);
688
- if (tc.function?.arguments) {
689
- emit(sseEvent("content_block_delta", {
690
- type: "content_block_delta",
691
- index: blockIdx,
692
- delta: {
693
- type: "input_json_delta",
694
- partial_json: tc.function.arguments,
695
- },
696
- }));
697
- }
698
- }
699
- }
700
- }
701
- // Stream ended without [DONE] — close gracefully
702
- if (textBlockOpen) {
703
- emit(sseEvent("content_block_stop", {
704
- type: "content_block_stop",
705
- index: textBlockIdx,
706
- }));
707
- }
708
- for (const [, blockIdx] of toolBlockMap) {
709
- emit(sseEvent("content_block_stop", {
710
- type: "content_block_stop",
711
- index: blockIdx,
712
- }));
713
- }
714
- emit(sseEvent("message_delta", {
715
- type: "message_delta",
716
- delta: { stop_reason: stopReason, stop_sequence: null },
717
- usage: { output_tokens: outputTokens },
718
- }));
719
- emit(sseEvent("message_stop", { type: "message_stop" }));
720
- controller.close();
721
- }
722
- catch (err) {
723
- closeStreamWithGatewayError(controller, enc, `Upstream stream failed: ${upstreamFetchErrorMessage(err)}`);
724
- }
725
- finally {
726
- reader.releaseLock();
727
- }
728
- },
729
- });
730
- }
731
- // ---------------------------------------------------------------------------
732
- // Streaming translation: Codex Responses API SSE → Anthropic SSE
733
- // ---------------------------------------------------------------------------
734
- export function translateCodexStreamToAnthropic(codexStream, originalModel, messageId, estimatedInputTokens) {
735
- const enc = new TextEncoder();
736
- return new ReadableStream({
737
- async start(controller) {
738
- const emit = (s) => controller.enqueue(enc.encode(s));
739
- let nextBlockIdx = 0;
740
- // output_index → {type, idx}. `argsSent` accumulates the tool-argument
741
- // JSON already relayed to the client, so the terminal arguments can be
742
- // reconciled against it rather than blindly appended or blindly skipped.
743
- const blockMap = new Map();
744
- let textBlockOpen = false;
745
- let outputTokens = 0;
746
- let stopReason = "end_turn";
747
- emit(sseEvent("message_start", {
748
- type: "message_start",
749
- message: {
750
- id: messageId,
751
- type: "message",
752
- role: "assistant",
753
- content: [],
754
- model: originalModel,
755
- stop_reason: null,
756
- stop_sequence: null,
757
- usage: { input_tokens: estimatedInputTokens, output_tokens: 0 },
758
- },
759
- }));
760
- emit(sseEvent("ping", { type: "ping" }));
761
- const reader = codexStream.getReader();
762
- let buf = "";
763
- let currentEvent = "";
764
- try {
765
- while (true) {
766
- const { done, value } = await reader.read();
767
- if (done)
768
- break;
769
- buf += new TextDecoder().decode(value);
770
- const lines = buf.split("\n");
771
- buf = lines.pop() ?? "";
772
- for (const line of lines) {
773
- if (line.startsWith("event: ")) {
774
- currentEvent = line.slice(7).trim();
775
- continue;
776
- }
777
- if (!line.startsWith("data: ")) {
778
- if (line === "")
779
- currentEvent = "";
780
- continue;
781
- }
782
- const raw = line.slice(6).trim();
783
- if (raw === "[DONE]")
784
- continue;
785
- let data;
786
- try {
787
- data = JSON.parse(raw);
788
- }
789
- catch {
790
- continue;
791
- }
792
- const evType = data.type ?? currentEvent;
793
- const outputIndex = data.output_index ?? 0;
794
- switch (evType) {
795
- case "response.output_item.added": {
796
- const item = data.item;
797
- if (!item)
798
- break;
799
- if (item.type === "function_call") {
800
- const blockIdx = nextBlockIdx++;
801
- blockMap.set(outputIndex, { type: "tool", idx: blockIdx });
802
- emit(sseEvent("content_block_start", {
803
- type: "content_block_start",
804
- index: blockIdx,
805
- content_block: {
806
- type: "tool_use",
807
- id: item.call_id ??
808
- `toolu_${outputIndex}`,
809
- name: item.name ?? "",
810
- input: {},
811
- },
812
- }));
813
- }
814
- break;
815
- }
816
- case "response.output_text.delta": {
817
- const delta = data.delta;
818
- if (!delta)
819
- break;
820
- if (!blockMap.has(outputIndex)) {
821
- const blockIdx = nextBlockIdx++;
822
- blockMap.set(outputIndex, { type: "text", idx: blockIdx });
823
- textBlockOpen = true;
824
- emit(sseEvent("content_block_start", {
825
- type: "content_block_start",
826
- index: blockIdx,
827
- content_block: { type: "text", text: "" },
828
- }));
829
- }
830
- const tBlock = blockMap.get(outputIndex);
831
- emit(sseEvent("content_block_delta", {
832
- type: "content_block_delta",
833
- index: tBlock.idx,
834
- delta: { type: "text_delta", text: delta },
835
- }));
836
- break;
837
- }
838
- case "response.function_call_arguments.delta": {
839
- const delta = data.delta;
840
- if (!delta)
841
- break;
842
- const fBlock = blockMap.get(outputIndex);
843
- if (fBlock?.type === "tool") {
844
- fBlock.argsSent = (fBlock.argsSent ?? "") + delta;
845
- emit(sseEvent("content_block_delta", {
846
- type: "content_block_delta",
847
- index: fBlock.idx,
848
- delta: { type: "input_json_delta", partial_json: delta },
849
- }));
850
- }
851
- break;
852
- }
853
- case "response.output_item.done": {
854
- const block = blockMap.get(outputIndex);
855
- const item = data.item;
856
- if (block) {
857
- // The Codex Responses upstream delivers function-call
858
- // arguments here, as one complete string; today it never emits
859
- // response.function_call_arguments.delta. Relaying them as an
860
- // input_json_delta before closing the block is what lets a
861
- // client reconstruct the tool input; without it every tool
862
- // call reaches the client with an empty input.
863
- //
864
- // The terminal value is authoritative, so it is reconciled
865
- // against what was already relayed instead of being skipped
866
- // whenever any delta arrived: an upstream that streamed only
867
- // PART of the arguments would otherwise leave the client with
868
- // a silently truncated input. Only the missing suffix is sent.
869
- // If what we relayed is not a prefix of the terminal value the
870
- // two cannot be reconciled inside an append-only stream, so
871
- // nothing is emitted rather than corrupting the JSON.
872
- if (block.type === "tool" &&
873
- typeof item?.arguments === "string" &&
874
- item.arguments !== "") {
875
- const sent = block.argsSent ?? "";
876
- if (item.arguments.startsWith(sent)) {
877
- const remainder = item.arguments.slice(sent.length);
878
- if (remainder !== "") {
879
- emit(sseEvent("content_block_delta", {
880
- type: "content_block_delta",
881
- index: block.idx,
882
- delta: {
883
- type: "input_json_delta",
884
- partial_json: remainder,
885
- },
886
- }));
887
- }
888
- }
889
- else {
890
- // Irreconcilable: what was relayed is not a prefix of the
891
- // authoritative terminal value, and an append-only stream
892
- // cannot retract it. Emitting anything would corrupt the
893
- // JSON, so nothing is emitted — but the client is then
894
- // holding a tool input that CONTRADICTS the upstream, and
895
- // that must never be a silent outcome. Announce it, with
896
- // the call identity, so it is attributable in a log.
897
- console.warn(`[llm-gateway] tool-call arguments could not be reconciled for ` +
898
- `${item.name ?? "?"} ` +
899
- `(${item.call_id ?? "?"}): ` +
900
- `${sent.length} relayed chars are not a prefix of the ` +
901
- `${item.arguments.length}-char terminal value; the client ` +
902
- `input differs from upstream`);
903
- }
904
- }
905
- emit(sseEvent("content_block_stop", {
906
- type: "content_block_stop",
907
- index: block.idx,
908
- }));
909
- if (block.type === "text")
910
- textBlockOpen = false;
911
- }
912
- if (item?.type === "function_call")
913
- stopReason = "tool_use";
914
- break;
915
- }
916
- case "response.completed": {
917
- const response = data.response;
918
- const usage = response?.usage;
919
- if (typeof usage?.output_tokens === "number")
920
- outputTokens = usage.output_tokens;
921
- // Close any unclosed blocks (shouldn't happen but be safe)
922
- for (const [, block] of blockMap) {
923
- if (block.type === "text" && textBlockOpen) {
924
- emit(sseEvent("content_block_stop", {
925
- type: "content_block_stop",
926
- index: block.idx,
927
- }));
928
- textBlockOpen = false;
929
- }
930
- }
931
- emit(sseEvent("message_delta", {
932
- type: "message_delta",
933
- delta: { stop_reason: stopReason, stop_sequence: null },
934
- usage: { output_tokens: outputTokens },
935
- }));
936
- emit(sseEvent("message_stop", { type: "message_stop" }));
937
- controller.close();
938
- return;
939
- }
940
- case "response.failed": {
941
- emit(sseEvent("error", {
942
- type: "error",
943
- error: {
944
- type: "api_error",
945
- message: codexFailureMessage(data),
946
- },
947
- }));
948
- controller.close();
949
- return;
950
- }
951
- }
952
- }
953
- }
954
- // Stream ended without response.completed — close gracefully
955
- if (textBlockOpen) {
956
- for (const [, block] of blockMap) {
957
- if (block.type === "text") {
958
- emit(sseEvent("content_block_stop", {
959
- type: "content_block_stop",
960
- index: block.idx,
961
- }));
962
- }
963
- }
964
- }
965
- for (const [, block] of blockMap) {
966
- if (block.type === "tool") {
967
- emit(sseEvent("content_block_stop", {
968
- type: "content_block_stop",
969
- index: block.idx,
970
- }));
971
- }
972
- }
973
- emit(sseEvent("message_delta", {
974
- type: "message_delta",
975
- delta: { stop_reason: stopReason, stop_sequence: null },
976
- usage: { output_tokens: outputTokens },
977
- }));
978
- emit(sseEvent("message_stop", { type: "message_stop" }));
979
- controller.close();
980
- }
981
- catch (err) {
982
- closeStreamWithGatewayError(controller, enc, `Upstream stream failed: ${upstreamFetchErrorMessage(err)}`);
983
- }
984
- finally {
985
- reader.releaseLock();
986
- }
987
- },
988
- });
989
- }
990
- function codexFailureMessage(data) {
991
- const response = data.response;
992
- const error = data.error ??
993
- response?.error;
994
- const message = error?.message ?? error?.code ?? response?.status;
995
- return typeof message === "string" && message
996
- ? message
997
- : "Codex upstream response failed";
998
- }
999
- export async function codexStreamToAnthropicResponse(codexStream, originalModel, messageId, estimatedInputTokens) {
1000
- const reader = codexStream.getReader();
1001
- const blockMap = new Map();
1002
- let nextBlockIdx = 0;
1003
- let outputTokens = 0;
1004
- let buf = "";
1005
- let currentEvent = "";
1006
- const ensureTextBlock = (outputIndex) => {
1007
- const existing = blockMap.get(outputIndex);
1008
- if (existing?.type === "text")
1009
- return existing;
1010
- const block = {
1011
- type: "text",
1012
- idx: nextBlockIdx++,
1013
- text: "",
1014
- };
1015
- blockMap.set(outputIndex, block);
1016
- return block;
1017
- };
1018
- try {
1019
- while (true) {
1020
- const { done, value } = await reader.read();
1021
- if (done)
1022
- break;
1023
- buf += new TextDecoder().decode(value);
1024
- const lines = buf.split("\n");
1025
- buf = lines.pop() ?? "";
1026
- for (const line of lines) {
1027
- if (line.startsWith("event: ")) {
1028
- currentEvent = line.slice(7).trim();
1029
- continue;
1030
- }
1031
- if (!line.startsWith("data: ")) {
1032
- if (line === "")
1033
- currentEvent = "";
1034
- continue;
1035
- }
1036
- const raw = line.slice(6).trim();
1037
- if (raw === "[DONE]")
1038
- continue;
1039
- let data;
1040
- try {
1041
- data = JSON.parse(raw);
1042
- }
1043
- catch {
1044
- continue;
1045
- }
1046
- const evType = data.type ?? currentEvent;
1047
- const outputIndex = data.output_index ?? 0;
1048
- switch (evType) {
1049
- case "response.output_item.added": {
1050
- const item = data.item;
1051
- if (item?.type === "function_call") {
1052
- blockMap.set(outputIndex, {
1053
- type: "tool",
1054
- idx: nextBlockIdx++,
1055
- id: item.call_id ??
1056
- `toolu_${outputIndex}`,
1057
- name: item.name ?? "",
1058
- args: "",
1059
- });
1060
- }
1061
- break;
1062
- }
1063
- case "response.output_text.delta": {
1064
- const delta = data.delta;
1065
- if (delta)
1066
- ensureTextBlock(outputIndex).text += delta;
1067
- break;
1068
- }
1069
- case "response.output_text.done": {
1070
- const text = data.text;
1071
- if (text !== undefined)
1072
- ensureTextBlock(outputIndex).text = text;
1073
- break;
1074
- }
1075
- case "response.function_call_arguments.delta": {
1076
- const delta = data.delta;
1077
- const block = blockMap.get(outputIndex);
1078
- if (delta && block?.type === "tool")
1079
- block.args += delta;
1080
- break;
1081
- }
1082
- case "response.output_item.done": {
1083
- const item = data.item;
1084
- const block = blockMap.get(outputIndex);
1085
- if (item?.type === "function_call" && block?.type === "tool") {
1086
- if (typeof item.call_id === "string")
1087
- block.id = item.call_id;
1088
- if (typeof item.name === "string")
1089
- block.name = item.name;
1090
- if (typeof item.arguments === "string")
1091
- block.args = item.arguments;
1092
- }
1093
- break;
1094
- }
1095
- case "response.completed": {
1096
- const response = data.response;
1097
- const usage = response?.usage;
1098
- if (typeof usage?.output_tokens === "number")
1099
- outputTokens = usage.output_tokens;
1100
- break;
1101
- }
1102
- case "response.failed":
1103
- throw new Error(codexFailureMessage(data));
1104
- }
1105
- }
1106
- }
1107
- }
1108
- finally {
1109
- reader.releaseLock();
1110
- }
1111
- const content = [...blockMap.values()]
1112
- .sort((a, b) => a.idx - b.idx)
1113
- .map((block) => {
1114
- if (block.type === "text")
1115
- return { type: "text", text: block.text };
1116
- let input = {};
1117
- try {
1118
- input = block.args ? JSON.parse(block.args) : {};
1119
- }
1120
- catch {
1121
- input = {};
1122
- }
1123
- return { type: "tool_use", id: block.id, name: block.name, input };
1124
- });
1125
- return {
1126
- id: messageId,
1127
- type: "message",
1128
- role: "assistant",
1129
- content,
1130
- model: originalModel,
1131
- stop_reason: content.some((block) => block.type === "tool_use")
1132
- ? "tool_use"
1133
- : "end_turn",
1134
- stop_sequence: null,
1135
- usage: { input_tokens: estimatedInputTokens, output_tokens: outputTokens },
1136
- };
1137
- }
1138
- // ---------------------------------------------------------------------------
1139
- // Route handler
1140
- // ---------------------------------------------------------------------------
1141
- export async function handleMessagesViaOpenAI(c, session, requestBody) {
1142
- const rawBody = requestBody ?? (await c.req.arrayBuffer());
1143
- let body;
1144
- try {
1145
- body = JSON.parse(new TextDecoder().decode(rawBody));
1146
- }
1147
- catch {
1148
- return c.json({ error: "invalid JSON" }, 400);
1149
- }
1150
- const originalModel = body.model;
1151
- let route;
1152
- try {
1153
- route = routeModelOrThrow(originalModel);
1154
- }
1155
- catch (err) {
1156
- return c.json(anthropicGatewayError(err instanceof Error ? err.message : String(err)), 400);
1157
- }
1158
- const tokenTransport = isCodexOAuthToken(session.token)
1159
- ? "codex-responses"
1160
- : "openai-chat-completions";
1161
- if (session.requiredTransport &&
1162
- tokenTransport !== session.requiredTransport) {
1163
- return c.json({ error: "gateway credential no longer satisfies session transport" }, 503);
1164
- }
1165
- const isCodex = tokenTransport === "codex-responses";
1166
- const codexContext = isCodex ? trimCodexBodyForContext(body) : null;
1167
- const upstreamBody = codexContext?.body ?? body;
1168
- if (codexContext?.trimmed) {
1169
- console.warn(`[llm-gateway] Codex context trimmed for ${originalModel}: ` +
1170
- `${codexContext.beforeChars} -> ${codexContext.afterChars} chars`);
1171
- }
1172
- const upstreamReq = isCodex
1173
- ? toCodexRequest(upstreamBody, route.effort)
1174
- : toOpenAIRequest(upstreamBody, route.effort);
1175
- // Build the Codex request ONCE. Response attestation is derived from this
1176
- // exact post-mapping payload, never copied from the Anthropic input.
1177
- const codexRequest = isCodex
1178
- ? prepareCodexResponsesRequest(upstreamReq)
1179
- : undefined;
1180
- const attestedModel = codexRequest?.body.model;
1181
- const attestedEffort = codexRequest
1182
- ? codexRequestEffort(codexRequest.body)
1183
- : undefined;
1184
- const withCodexAttestation = (response) => {
1185
- if (!codexRequest)
1186
- return response;
1187
- const headers = new Headers(response.headers);
1188
- if (typeof attestedModel === "string")
1189
- headers.set("x-h2a-resolved-model", attestedModel);
1190
- if (attestedEffort)
1191
- headers.set("x-h2a-reasoning-effort", attestedEffort);
1192
- return new Response(response.body, {
1193
- status: response.status,
1194
- statusText: response.statusText,
1195
- headers,
1196
- });
1197
- };
1198
- const messageId = `msg_${Date.now().toString(36)}`;
1199
- const estimatedInputTokens = Math.ceil(JSON.stringify(upstreamBody.messages).length / 4);
1200
- const doFetch = (token) => {
1201
- if (codexRequest) {
1202
- if (attestedEffort) {
1203
- console.info(`[llm-gateway] Codex request: model=${String(codexRequest.body.model ?? "")} reasoning.effort=${attestedEffort}`);
1204
- }
1205
- return fetch(codexRequest.url, {
1206
- method: "POST",
1207
- headers: {
1208
- Authorization: `Bearer ${token}`,
1209
- "content-type": "application/json",
1210
- originator: "opencode",
1211
- "User-Agent": "opencode/0.1.0",
1212
- session_id: `codex_${Date.now().toString(36)}`,
1213
- },
1214
- body: JSON.stringify(codexRequest.body),
1215
- // @ts-expect-error Node 18+ supports duplex for streaming
1216
- duplex: "half",
1217
- signal: c.req.raw.signal,
1218
- });
1219
- }
1220
- return fetch(`${OPENAI_BASE}/v1/chat/completions`, {
1221
- method: "POST",
1222
- headers: {
1223
- Authorization: `Bearer ${token}`,
1224
- "content-type": "application/json",
1225
- },
1226
- body: JSON.stringify(upstreamReq),
1227
- // @ts-expect-error Node 18+ supports duplex for streaming
1228
- duplex: "half",
1229
- signal: c.req.raw.signal,
1230
- });
1231
- };
1232
- let upstream;
1233
- try {
1234
- upstream = await fetchUpstreamWithRetry(() => doFetch(session.token));
1235
- }
1236
- catch (err) {
1237
- if (abortError(err))
1238
- return new Response(null, { status: 499 });
1239
- return c.json(anthropicGatewayError(`Upstream fetch failed: ${upstreamFetchErrorMessage(err)}`), 502);
1240
- }
1241
- // 401 → attempt OAuth token refresh + retry once
1242
- if (upstream.status === 401 && session.accountId) {
1243
- const newToken = await refreshOAuthTokenForRetry(session.accountId);
1244
- if (newToken) {
1245
- await upstream.body?.cancel().catch(() => { });
1246
- try {
1247
- upstream = await fetchUpstreamWithRetry(() => doFetch(newToken));
1248
- }
1249
- catch (err) {
1250
- if (abortError(err))
1251
- return new Response(null, { status: 499 });
1252
- return c.json(anthropicGatewayError(`Upstream fetch failed: ${upstreamFetchErrorMessage(err)}`), 502);
1253
- }
1254
- }
1255
- else {
1256
- await upstream.body?.cancel().catch(() => { });
1257
- return c.json(anthropicGatewayError("Upstream Codex OAuth returned 401 and token refresh failed; re-enroll the Codex account with `h2a llm-mesh enroll codex`."), 502);
1258
- }
1259
- }
1260
- if (!upstream.ok) {
1261
- const errBody = await upstream.text();
1262
- return new Response(errBody, {
1263
- status: upstream.status,
1264
- headers: {
1265
- "content-type": upstream.headers.get("content-type") ?? "application/json",
1266
- },
1267
- });
1268
- }
1269
- if (body.stream && isCodex && upstream.body) {
1270
- const stream = translateCodexStreamToAnthropic(upstream.body, originalModel, messageId, estimatedInputTokens);
1271
- return withCodexAttestation(new Response(stream, {
1272
- status: 200,
1273
- headers: {
1274
- "content-type": "text/event-stream; charset=utf-8",
1275
- "cache-control": "no-cache",
1276
- "x-accel-buffering": "no",
1277
- },
1278
- }));
1279
- }
1280
- // Codex OAuth Responses API streams even for requests where the Anthropic
1281
- // client expects a non-streaming JSON message. In that case collect the SSE
1282
- // upstream and return a normal Anthropic Messages JSON response.
1283
- if (isCodex && upstream.body) {
1284
- try {
1285
- return withCodexAttestation(c.json(await codexStreamToAnthropicResponse(upstream.body, originalModel, messageId, estimatedInputTokens)));
1286
- }
1287
- catch (err) {
1288
- return c.json(anthropicGatewayError(err instanceof Error ? err.message : String(err)), 502);
1289
- }
1290
- }
1291
- if (body.stream && upstream.body) {
1292
- const stream = translateOpenAIStreamToAnthropic(upstream.body, originalModel, messageId, estimatedInputTokens);
1293
- return new Response(stream, {
1294
- status: 200,
1295
- headers: {
1296
- "content-type": "text/event-stream; charset=utf-8",
1297
- "cache-control": "no-cache",
1298
- "x-accel-buffering": "no",
1299
- },
1300
- });
1301
- }
1302
- const openaiResp = (await upstream.json());
1303
- return c.json(toAnthropicResponse(openaiResp, originalModel));
1304
- }
1305
- //# sourceMappingURL=proxy-openai.js.map