llm-relay 0.77.0 → 0.78.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 (90) hide show
  1. package/dist/accounting-store-schema.js +5 -6
  2. package/dist/accounting-store-schema.js.map +1 -1
  3. package/dist/accounting-store.d.ts +10 -0
  4. package/dist/accounting-store.js +60 -6
  5. package/dist/accounting-store.js.map +1 -1
  6. package/dist/backend/envelope-validator.d.ts +1 -1
  7. package/dist/backend/envelope-validator.js +89 -52
  8. package/dist/backend/envelope-validator.js.map +1 -1
  9. package/dist/backend/health-prober.js +3 -1
  10. package/dist/backend/health-prober.js.map +1 -1
  11. package/dist/backend.d.ts +48 -0
  12. package/dist/backend.js +831 -7
  13. package/dist/backend.js.map +1 -1
  14. package/dist/candidate-runner.d.ts +135 -50
  15. package/dist/candidate-runner.js +160 -55
  16. package/dist/candidate-runner.js.map +1 -1
  17. package/dist/cli.d.ts +24 -2
  18. package/dist/cli.js +122 -20
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config/routing-parser.d.ts +13 -8
  21. package/dist/config/routing-parser.js +369 -183
  22. package/dist/config/routing-parser.js.map +1 -1
  23. package/dist/config-types.d.ts +202 -0
  24. package/dist/config-types.js +2 -0
  25. package/dist/config-types.js.map +1 -1
  26. package/dist/config.d.ts +45 -1
  27. package/dist/config.js +87 -22
  28. package/dist/config.js.map +1 -1
  29. package/dist/dashboard-contract.d.ts +38 -0
  30. package/dist/dashboard-contract.js +19 -3
  31. package/dist/dashboard-contract.js.map +1 -1
  32. package/dist/dispatch-lane-stats.d.ts +33 -4
  33. package/dist/dispatch-lane-stats.js +65 -16
  34. package/dist/dispatch-lane-stats.js.map +1 -1
  35. package/dist/dispatch.d.ts +9 -0
  36. package/dist/dispatch.js +36 -4
  37. package/dist/dispatch.js.map +1 -1
  38. package/dist/key-checker.d.ts +21 -1
  39. package/dist/key-checker.js +38 -12
  40. package/dist/key-checker.js.map +1 -1
  41. package/dist/lane-affinity.d.ts +94 -1
  42. package/dist/lane-affinity.js +52 -4
  43. package/dist/lane-affinity.js.map +1 -1
  44. package/dist/mcp/lane-runner.d.ts +69 -16
  45. package/dist/mcp/lane-runner.js +30 -1
  46. package/dist/mcp/lane-runner.js.map +1 -1
  47. package/dist/mcp/server.d.ts +39 -4
  48. package/dist/mcp/server.js +70 -13
  49. package/dist/mcp/server.js.map +1 -1
  50. package/dist/openai-request.d.ts +21 -0
  51. package/dist/openai-request.js +1 -1
  52. package/dist/openai-request.js.map +1 -1
  53. package/dist/ping/cadence.js +9 -1
  54. package/dist/ping/cadence.js.map +1 -1
  55. package/dist/ping/probe-cache.d.ts +13 -0
  56. package/dist/ping/probe-cache.js +12 -0
  57. package/dist/ping/probe-cache.js.map +1 -1
  58. package/dist/responses-request.d.ts +20 -0
  59. package/dist/responses-request.js +7 -7
  60. package/dist/responses-request.js.map +1 -1
  61. package/dist/routes/admin.d.ts +8 -1
  62. package/dist/routes/admin.js +42 -6
  63. package/dist/routes/admin.js.map +1 -1
  64. package/dist/routes/messages.d.ts +1 -0
  65. package/dist/routes/messages.js +13 -8
  66. package/dist/routes/messages.js.map +1 -1
  67. package/dist/routes/openai-front.js +20 -7
  68. package/dist/routes/openai-front.js.map +1 -1
  69. package/dist/server.d.ts +9 -1
  70. package/dist/server.js +21 -3
  71. package/dist/server.js.map +1 -1
  72. package/dist/setup-claude.d.ts +2 -2
  73. package/dist/setup-claude.js +9 -7
  74. package/dist/setup-claude.js.map +1 -1
  75. package/dist/stream-pipeline.d.ts +116 -0
  76. package/dist/stream-pipeline.js +151 -0
  77. package/dist/stream-pipeline.js.map +1 -1
  78. package/dist/target-facts.d.ts +5 -4
  79. package/dist/target-facts.js +5 -0
  80. package/dist/target-facts.js.map +1 -1
  81. package/dist/telemetry.d.ts +15 -2
  82. package/dist/telemetry.js +4 -1
  83. package/dist/telemetry.js.map +1 -1
  84. package/dist/tier-data.d.ts +22 -0
  85. package/dist/tier-data.js +12 -2
  86. package/dist/tier-data.js.map +1 -1
  87. package/package.json +2 -1
  88. package/skills/llm-relay/SKILL.md +8 -3
  89. package/skills/llm-relay/references/dispatch-lanes.md +2 -1
  90. package/skills/llm-relay/references/operations.md +1 -1
package/dist/backend.js CHANGED
@@ -2,8 +2,9 @@ import { translateBetweenProviders, handleUniversalStreamRequest } from "llm-bri
2
2
  import { buildAuthHeaders } from "./authEnv.js";
3
3
  import { isRecord } from "./json-shape.js";
4
4
  import { DocumentError, transcodeDocuments } from "./documents.js";
5
- import { anthropicRequestToOpenAi, RequestMappingError } from "./openai-request.js";
6
- import { openaiResponsesRequestToAnthropic } from "./responses-request.js";
5
+ import { anthropicRequestToOpenAi, RequestMappingError, ToolCallIds, ThoughtSignatures } from "./openai-request.js";
6
+ import { openaiResponsesRequestToAnthropic, DEFAULT_RESPONSES_MAX_TOKENS } from "./responses-request.js";
7
+ import { observedMaxOutput } from "./context-limits.js";
7
8
  import { DIALECT_REFUSED_DESTRUCTIVE_CODE, describeRefused, dialectRefusalSignal, recoverToolCalls } from "./tool-dialects.js";
8
9
  import { recoverDialectInStream } from "./dialect-stream.js";
9
10
  import { toolSchemaMap } from "./anthropic.js";
@@ -13,6 +14,8 @@ import { invalidEnvelopeReason } from "./backend/envelope-validator.js";
13
14
  import { captureReportedModel, preflightResponseStream } from "./backend/health-prober.js";
14
15
  import { inspectDialectInOpenAiChat, recoverDialectInOpenAiChatStream, } from "./openai-dialect.js";
15
16
  import { observeUsage } from "./usage-observer.js";
17
+ import { createSseTransformStream, parseSseEvent, BufferedSseFrames } from "./sse-frames.js";
18
+ import { syntheticMessageId } from "./emitSse.js";
16
19
  export class DialectUnparseableError extends Error {
17
20
  dialect;
18
21
  constructor(dialect) {
@@ -57,6 +60,7 @@ function oneShotFetch(fetchFn, signal, onEgress) {
57
60
  export const DEGRADED_HEADER = "x-llm-relay-degraded";
58
61
  export const QUOTA_DEMOTED_HEADER = "x-llm-relay-quota-demoted";
59
62
  export const LATENCY_DEMOTED_HEADER = "x-llm-relay-latency-demoted";
63
+ export const PROBATION_HEADER = "x-llm-relay-probation";
60
64
  export const HEDGED_HEADER = "x-llm-relay-hedged";
61
65
  export const HARD_CAP_HEADER = "x-llm-relay-capped";
62
66
  export const PAID_HEADER = "x-llm-relay-paid";
@@ -165,6 +169,9 @@ async function fetchAnthropicBackend(attempt, args, invokeFetch) {
165
169
  return attachUpstreamMetadata(res, metadata);
166
170
  }
167
171
  async function fetchOpenAiBackend(attempt, args, invokeFetch) {
172
+ if (attempt.target.wire === "responses") {
173
+ return fetchOpenAiResponsesBackend(attempt, args, invokeFetch);
174
+ }
168
175
  const target = attempt.target;
169
176
  let reqJson = args.reqJson;
170
177
  try {
@@ -324,6 +331,747 @@ async function fetchOpenAiBackend(attempt, args, invokeFetch) {
324
331
  metadata.thoughtSignatureSentinels = sentinelsStamped;
325
332
  return attachUpstreamMetadata(response, metadata);
326
333
  }
334
+ function describeResponsesType(t) {
335
+ return typeof t === "string" && t.length > 0 ? `"${t.slice(0, 40)}"` : "(missing type)";
336
+ }
337
+ function responsesSystemText(system) {
338
+ if (typeof system === "string")
339
+ return system;
340
+ if (!Array.isArray(system))
341
+ return "";
342
+ const parts = [];
343
+ for (const block of system) {
344
+ if (isRecord(block) && block.type === "text" && typeof block.text === "string")
345
+ parts.push(block.text);
346
+ }
347
+ return parts.join("\n\n");
348
+ }
349
+ function responsesInputImagePart(block) {
350
+ const source = isRecord(block.source) ? block.source : {};
351
+ if (source.type === "url" && typeof source.url === "string")
352
+ return { type: "input_image", image_url: source.url };
353
+ if (source.type === "base64" && typeof source.data === "string" && typeof source.media_type === "string") {
354
+ return { type: "input_image", image_url: `data:${source.media_type};base64,${source.data}` };
355
+ }
356
+ throw new RequestMappingError("image block needs a base64 or url source");
357
+ }
358
+ function responsesToolResultParts(content) {
359
+ if (content === undefined || content === null)
360
+ return { text: "", images: [] };
361
+ if (typeof content === "string")
362
+ return { text: content, images: [] };
363
+ if (!Array.isArray(content)) {
364
+ throw new RequestMappingError("tool_result content must be text or a list of blocks");
365
+ }
366
+ const texts = [];
367
+ const images = [];
368
+ for (const raw of content) {
369
+ if (!isRecord(raw))
370
+ throw new RequestMappingError("tool_result content block is not an object");
371
+ switch (raw.type) {
372
+ case "text":
373
+ texts.push(typeof raw.text === "string" ? raw.text : "");
374
+ break;
375
+ case "image":
376
+ images.push(responsesInputImagePart(raw));
377
+ break;
378
+ default:
379
+ throw new RequestMappingError(`tool_result carries an unsupported ${describeResponsesType(raw.type)} block; a Responses function_call_output carries text and images only`);
380
+ }
381
+ }
382
+ return { text: texts.join("\n"), images };
383
+ }
384
+ function responsesToolResultItem(block, ids) {
385
+ if (typeof block.tool_use_id !== "string" || block.tool_use_id.length === 0) {
386
+ throw new RequestMappingError("tool_result block without a tool_use_id");
387
+ }
388
+ const callId = ids === null ? block.tool_use_id : ids.map(block.tool_use_id);
389
+ const { text, images } = responsesToolResultParts(block.content);
390
+ return { item: { type: "function_call_output", call_id: callId, output: text }, images };
391
+ }
392
+ function responsesFunctionCallItem(block, ids, sigs) {
393
+ if (typeof block.id !== "string" || block.id.length === 0) {
394
+ throw new RequestMappingError("tool_use block without an id");
395
+ }
396
+ if (typeof block.name !== "string" || block.name.length === 0) {
397
+ throw new RequestMappingError("tool_use block without a name");
398
+ }
399
+ const input = block.input ?? {};
400
+ const item = {
401
+ type: "function_call",
402
+ call_id: ids === null ? block.id : ids.map(block.id),
403
+ name: block.name,
404
+ arguments: typeof input === "string" ? input : JSON.stringify(input),
405
+ };
406
+ return sigs === null ? item : sigs.stamp(item);
407
+ }
408
+ function responsesAssistantItems(turn, ids, sigs) {
409
+ const content = turn.content;
410
+ if (typeof content === "string") {
411
+ return content.length > 0 ? [{ role: "assistant", content: [{ type: "output_text", text: content }] }] : [];
412
+ }
413
+ const out = [];
414
+ for (const raw of Array.isArray(content) ? content : []) {
415
+ if (!isRecord(raw))
416
+ throw new RequestMappingError("assistant content block is not an object");
417
+ switch (raw.type) {
418
+ case "text": {
419
+ const text = typeof raw.text === "string" ? raw.text : "";
420
+ if (text.length > 0)
421
+ out.push({ role: "assistant", content: [{ type: "output_text", text }] });
422
+ break;
423
+ }
424
+ case "tool_use":
425
+ out.push(responsesFunctionCallItem(raw, ids, sigs));
426
+ break;
427
+ case "thinking":
428
+ case "redacted_thinking":
429
+ break;
430
+ default:
431
+ throw new RequestMappingError(`unsupported assistant content block ${describeResponsesType(raw.type)}`);
432
+ }
433
+ }
434
+ return out;
435
+ }
436
+ function responsesUserItems(turn, ids) {
437
+ const content = turn.content;
438
+ if (typeof content === "string") {
439
+ return content.length > 0 ? [{ role: "user", content: [{ type: "input_text", text: content }] }] : [];
440
+ }
441
+ const toolResultItems = [];
442
+ const trailingImages = [];
443
+ const contentParts = [];
444
+ for (const raw of Array.isArray(content) ? content : []) {
445
+ if (!isRecord(raw))
446
+ throw new RequestMappingError("user content block is not an object");
447
+ switch (raw.type) {
448
+ case "text": {
449
+ const text = typeof raw.text === "string" ? raw.text : "";
450
+ if (text.length > 0)
451
+ contentParts.push({ type: "input_text", text });
452
+ break;
453
+ }
454
+ case "image":
455
+ contentParts.push(responsesInputImagePart(raw));
456
+ break;
457
+ case "tool_result": {
458
+ const { item, images } = responsesToolResultItem(raw, ids);
459
+ toolResultItems.push(item);
460
+ trailingImages.push(...images);
461
+ break;
462
+ }
463
+ case "thinking":
464
+ case "redacted_thinking":
465
+ break;
466
+ default:
467
+ throw new RequestMappingError(`unsupported user content block ${describeResponsesType(raw.type)}`);
468
+ }
469
+ }
470
+ const out = [...toolResultItems];
471
+ if (trailingImages.length > 0)
472
+ out.push({ role: "user", content: trailingImages });
473
+ if (contentParts.length > 0)
474
+ out.push({ role: "user", content: contentParts });
475
+ return out;
476
+ }
477
+ function mapAnthropicToolsToResponses(tools) {
478
+ if (!Array.isArray(tools) || tools.length === 0)
479
+ return null;
480
+ const out = [];
481
+ for (const raw of tools) {
482
+ if (!isRecord(raw))
483
+ throw new RequestMappingError("tool declaration is not an object");
484
+ if (typeof raw.name !== "string" || raw.name.length === 0) {
485
+ throw new RequestMappingError("tool declaration without a name");
486
+ }
487
+ const fn = { type: "function", name: raw.name };
488
+ if (typeof raw.description === "string" && raw.description.length > 0)
489
+ fn.description = raw.description;
490
+ fn.parameters = isRecord(raw.input_schema) ? raw.input_schema : { type: "object", properties: {} };
491
+ out.push(fn);
492
+ }
493
+ return out;
494
+ }
495
+ function mapAnthropicToolChoiceToResponses(choice) {
496
+ if (!isRecord(choice))
497
+ return {};
498
+ let toolChoice;
499
+ switch (choice.type) {
500
+ case "auto":
501
+ toolChoice = "auto";
502
+ break;
503
+ case "any":
504
+ toolChoice = "required";
505
+ break;
506
+ case "none":
507
+ toolChoice = "none";
508
+ break;
509
+ case "tool":
510
+ toolChoice = typeof choice.name === "string" && choice.name.length > 0
511
+ ? { type: "function", name: choice.name }
512
+ : undefined;
513
+ break;
514
+ default:
515
+ toolChoice = undefined;
516
+ }
517
+ if (toolChoice === undefined)
518
+ return {};
519
+ const parallelToolCalls = choice.disable_parallel_tool_use === true && choice.type !== "none" ? false : undefined;
520
+ return parallelToolCalls === undefined ? { toolChoice } : { toolChoice, parallelToolCalls };
521
+ }
522
+ function responsesRequestOptions(body, opts, out) {
523
+ if (opts.model !== undefined)
524
+ out.model = opts.model;
525
+ const stream = opts.stream ?? (typeof body.stream === "boolean" ? body.stream : undefined);
526
+ if (stream !== undefined)
527
+ out.stream = stream;
528
+ const instructions = responsesSystemText(body.system);
529
+ if (instructions.length > 0)
530
+ out.instructions = instructions;
531
+ if (typeof body.max_tokens === "number")
532
+ out.max_output_tokens = body.max_tokens;
533
+ if (typeof body.temperature === "number")
534
+ out.temperature = body.temperature;
535
+ if (typeof body.top_p === "number")
536
+ out.top_p = body.top_p;
537
+ const tools = mapAnthropicToolsToResponses(body.tools);
538
+ if (!tools)
539
+ return;
540
+ out.tools = tools;
541
+ const { toolChoice, parallelToolCalls } = mapAnthropicToolChoiceToResponses(body.tool_choice);
542
+ if (toolChoice !== undefined)
543
+ out.tool_choice = toolChoice;
544
+ if (parallelToolCalls !== undefined)
545
+ out.parallel_tool_calls = parallelToolCalls;
546
+ }
547
+ export function anthropicRequestToOpenAiResponses(reqJson, opts = {}) {
548
+ const body = isRecord(reqJson) ? reqJson : {};
549
+ const input = [];
550
+ const ids = opts.toolCallIds === "strict9" ? new ToolCallIds() : null;
551
+ const sigs = opts.thoughtSignature === "sentinel" ? new ThoughtSignatures() : null;
552
+ for (const raw of Array.isArray(body.messages) ? body.messages : []) {
553
+ if (!isRecord(raw))
554
+ throw new RequestMappingError("message is not an object");
555
+ if (raw.role === "assistant")
556
+ input.push(...responsesAssistantItems(raw, ids, sigs));
557
+ else
558
+ input.push(...responsesUserItems(raw, ids));
559
+ }
560
+ if (ids !== null)
561
+ opts.onToolCallIdsRewritten?.(ids.count());
562
+ if (sigs !== null)
563
+ opts.onThoughtSignatureSentinels?.(sigs.count());
564
+ const out = { input };
565
+ responsesRequestOptions(body, opts, out);
566
+ return out;
567
+ }
568
+ function parseResponsesFunctionCallOutputItem(raw, ordinal) {
569
+ const id = typeof raw.call_id === "string" ? raw.call_id
570
+ : typeof raw.id === "string" ? raw.id : `tu_${ordinal}`;
571
+ const name = typeof raw.name === "string" ? raw.name : "";
572
+ const rawArgs = typeof raw.arguments === "string" ? raw.arguments : "";
573
+ let input;
574
+ try {
575
+ input = rawArgs.length > 0 ? JSON.parse(rawArgs) : {};
576
+ }
577
+ catch {
578
+ input = rawArgs;
579
+ }
580
+ return { id, name, input };
581
+ }
582
+ function parseResponsesMessageOutputItem(raw) {
583
+ const textParts = [];
584
+ if (!Array.isArray(raw.content))
585
+ return textParts;
586
+ for (const part of raw.content) {
587
+ if (!isRecord(part))
588
+ continue;
589
+ if (part.type === "output_text" && typeof part.text === "string" && part.text.length > 0) {
590
+ textParts.push(part.text);
591
+ }
592
+ else if (part.type === "refusal" && typeof part.refusal === "string" && part.refusal.length > 0) {
593
+ textParts.push(part.refusal);
594
+ }
595
+ }
596
+ return textParts;
597
+ }
598
+ function parseOpenAiResponsesOutput(j) {
599
+ const outputs = Array.isArray(j.output) ? j.output : [];
600
+ const textParts = [];
601
+ const functionCalls = [];
602
+ for (const raw of outputs) {
603
+ if (!isRecord(raw))
604
+ continue;
605
+ const type = typeof raw.type === "string" ? raw.type : "";
606
+ if (type === "function_call") {
607
+ functionCalls.push(parseResponsesFunctionCallOutputItem(raw, functionCalls.length));
608
+ }
609
+ else if (type === "message") {
610
+ textParts.push(...parseResponsesMessageOutputItem(raw));
611
+ }
612
+ }
613
+ return { text: textParts.join(""), functionCalls };
614
+ }
615
+ function mapResponsesStopReason(j, hasToolCalls) {
616
+ if (hasToolCalls)
617
+ return "tool_use";
618
+ const status = typeof j.status === "string" ? j.status : undefined;
619
+ const incompleteReason = isRecord(j.incomplete_details) && typeof j.incomplete_details.reason === "string"
620
+ ? j.incomplete_details.reason
621
+ : undefined;
622
+ if (status === "incomplete" && incompleteReason === "max_output_tokens")
623
+ return "max_tokens";
624
+ return "end_turn";
625
+ }
626
+ function measuredResponsesCacheTokens(v) {
627
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : undefined;
628
+ }
629
+ function extractAnthropicUsageFromResponses(rawUsage) {
630
+ if (!isRecord(rawUsage))
631
+ return null;
632
+ if (typeof rawUsage.input_tokens !== "number" && typeof rawUsage.output_tokens !== "number")
633
+ return null;
634
+ const out = {};
635
+ if (typeof rawUsage.input_tokens === "number") {
636
+ const details = isRecord(rawUsage.input_tokens_details) ? rawUsage.input_tokens_details : undefined;
637
+ const cached = details ? measuredResponsesCacheTokens(details.cached_tokens) : undefined;
638
+ if (cached !== undefined && cached <= rawUsage.input_tokens) {
639
+ out.input_tokens = rawUsage.input_tokens - cached;
640
+ out.cache_read_input_tokens = cached;
641
+ }
642
+ else {
643
+ out.input_tokens = rawUsage.input_tokens;
644
+ }
645
+ }
646
+ if (typeof rawUsage.output_tokens === "number")
647
+ out.output_tokens = rawUsage.output_tokens;
648
+ return Object.keys(out).length > 0 ? out : null;
649
+ }
650
+ function applyResponsesUsage(accumulator, usage) {
651
+ if (!usage)
652
+ return;
653
+ if (typeof usage.input_tokens === "number")
654
+ accumulator.inputTokens = usage.input_tokens;
655
+ if (typeof usage.output_tokens === "number") {
656
+ accumulator.outputTokens = usage.output_tokens;
657
+ accumulator.completionTokens = usage.output_tokens;
658
+ }
659
+ if (typeof usage.cache_read_input_tokens === "number")
660
+ accumulator.cacheReadInputTokens = usage.cache_read_input_tokens;
661
+ }
662
+ function openAiResponsesToAnthropicMessage(j, model, schemas, isDestructive) {
663
+ const parsed = parseOpenAiResponsesOutput(j);
664
+ const content = [];
665
+ let functionCalls = parsed.functionCalls;
666
+ if (functionCalls.length === 0 && parsed.text.length > 0) {
667
+ const { recoveredCalls, textParts } = recoverOpenAiTextDialect(parsed.text, schemas, isDestructive);
668
+ content.push(...textParts);
669
+ if (recoveredCalls.length > 0) {
670
+ functionCalls = recoveredCalls.map((c, i) => ({ id: `tu_recovered_${i}`, name: c.name, input: c.input }));
671
+ }
672
+ }
673
+ else if (parsed.text.length > 0) {
674
+ content.push({ type: "text", text: parsed.text });
675
+ }
676
+ for (const fc of functionCalls) {
677
+ content.push({ type: "tool_use", id: fc.id, name: fc.name, input: fc.input });
678
+ }
679
+ return {
680
+ id: typeof j.id === "string" ? j.id : "msg_translated",
681
+ type: "message",
682
+ role: "assistant",
683
+ model: model || (typeof j.model === "string" ? j.model : ""),
684
+ content,
685
+ stop_reason: mapResponsesStopReason(j, functionCalls.length > 0),
686
+ stop_sequence: null,
687
+ ...(extractAnthropicUsageFromResponses(j.usage) ? { usage: extractAnthropicUsageFromResponses(j.usage) } : {}),
688
+ };
689
+ }
690
+ function anthropicSseEvent(type, data) {
691
+ return `event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
692
+ }
693
+ function anthropicSseError(message) {
694
+ return `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `llm-relay: ${message}` } })}\n\n`;
695
+ }
696
+ function newResponsesStreamState() {
697
+ return {
698
+ messageStarted: false,
699
+ messageStopped: false,
700
+ nextBlockIndex: 0,
701
+ textBlocks: new Map(),
702
+ toolBlocks: new Map(),
703
+ openBlocksByItem: new Map(),
704
+ };
705
+ }
706
+ function ensureResponsesMessageStarted(state, push, response) {
707
+ if (state.messageStarted)
708
+ return;
709
+ state.messageStarted = true;
710
+ push(anthropicSseEvent("message_start", {
711
+ message: {
712
+ id: typeof response?.id === "string" ? response.id : syntheticMessageId(),
713
+ type: "message",
714
+ role: "assistant",
715
+ model: typeof response?.model === "string" ? response.model : "",
716
+ content: [],
717
+ stop_reason: null,
718
+ stop_sequence: null,
719
+ usage: {},
720
+ },
721
+ }));
722
+ }
723
+ function openResponsesTextBlock(state, push, outputIndex, contentIndex) {
724
+ const key = `${outputIndex}:${contentIndex}`;
725
+ const existing = state.textBlocks.get(key);
726
+ if (existing !== undefined)
727
+ return existing;
728
+ const index = state.nextBlockIndex++;
729
+ state.textBlocks.set(key, index);
730
+ const opened = state.openBlocksByItem.get(outputIndex) ?? [];
731
+ opened.push(index);
732
+ state.openBlocksByItem.set(outputIndex, opened);
733
+ push(anthropicSseEvent("content_block_start", { index, content_block: { type: "text", text: "" } }));
734
+ return index;
735
+ }
736
+ function openResponsesToolBlock(state, push, outputIndex, callId, name) {
737
+ const existing = state.toolBlocks.get(outputIndex);
738
+ if (existing !== undefined)
739
+ return existing;
740
+ const index = state.nextBlockIndex++;
741
+ state.toolBlocks.set(outputIndex, index);
742
+ state.openBlocksByItem.set(outputIndex, [index]);
743
+ push(anthropicSseEvent("content_block_start", {
744
+ index, content_block: { type: "tool_use", id: callId, name, input: {} },
745
+ }));
746
+ return index;
747
+ }
748
+ function closeResponsesItemBlocks(state, push, outputIndex) {
749
+ const opened = state.openBlocksByItem.get(outputIndex);
750
+ if (!opened)
751
+ return;
752
+ for (const index of opened)
753
+ push(anthropicSseEvent("content_block_stop", { index }));
754
+ state.openBlocksByItem.delete(outputIndex);
755
+ }
756
+ function synthesizeResponsesFunctionCallBlock(state, push, outputIndex, item) {
757
+ if (state.toolBlocks.has(outputIndex))
758
+ return;
759
+ const callId = typeof item.call_id === "string" ? item.call_id
760
+ : typeof item.id === "string" ? item.id : `tu_${outputIndex}`;
761
+ const index = openResponsesToolBlock(state, push, outputIndex, callId, typeof item.name === "string" ? item.name : "");
762
+ const args = typeof item.arguments === "string" ? item.arguments : "";
763
+ if (args.length > 0) {
764
+ push(anthropicSseEvent("content_block_delta", { index, delta: { type: "input_json_delta", partial_json: args } }));
765
+ }
766
+ }
767
+ function synthesizeResponsesMessageBlocks(state, push, outputIndex, item) {
768
+ if (!Array.isArray(item.content))
769
+ return;
770
+ item.content.forEach((part, contentIndex) => {
771
+ if (!isRecord(part) || (part.type !== "output_text" && part.type !== "refusal"))
772
+ return;
773
+ if (state.textBlocks.has(`${outputIndex}:${contentIndex}`))
774
+ return;
775
+ const text = typeof part.text === "string" ? part.text : typeof part.refusal === "string" ? part.refusal : "";
776
+ const index = openResponsesTextBlock(state, push, outputIndex, contentIndex);
777
+ if (text.length > 0) {
778
+ push(anthropicSseEvent("content_block_delta", { index, delta: { type: "text_delta", text } }));
779
+ }
780
+ });
781
+ }
782
+ function synthesizeMissingResponsesBlocks(state, push, response) {
783
+ if (!Array.isArray(response.output))
784
+ return;
785
+ response.output.forEach((raw, outputIndex) => {
786
+ if (!isRecord(raw))
787
+ return;
788
+ if (raw.type === "function_call")
789
+ synthesizeResponsesFunctionCallBlock(state, push, outputIndex, raw);
790
+ else if (raw.type === "message")
791
+ synthesizeResponsesMessageBlocks(state, push, outputIndex, raw);
792
+ });
793
+ }
794
+ function finishResponsesStream(state, push, response) {
795
+ if (state.messageStopped)
796
+ return;
797
+ state.messageStopped = true;
798
+ ensureResponsesMessageStarted(state, push, response);
799
+ if (response)
800
+ synthesizeMissingResponsesBlocks(state, push, response);
801
+ for (const outputIndex of [...state.openBlocksByItem.keys()])
802
+ closeResponsesItemBlocks(state, push, outputIndex);
803
+ const hasToolCalls = state.toolBlocks.size > 0;
804
+ const stopReason = response ? mapResponsesStopReason(response, hasToolCalls) : (hasToolCalls ? "tool_use" : "end_turn");
805
+ const usage = response ? extractAnthropicUsageFromResponses(response.usage) : null;
806
+ push(anthropicSseEvent("message_delta", {
807
+ delta: { stop_reason: stopReason, stop_sequence: null },
808
+ ...(usage ? { usage } : {}),
809
+ }));
810
+ push(anthropicSseEvent("message_stop", {}));
811
+ }
812
+ function handleResponsesOutputItemAdded(state, push, data) {
813
+ ensureResponsesMessageStarted(state, push, undefined);
814
+ const item = isRecord(data.item) ? data.item : undefined;
815
+ const outputIndex = typeof data.output_index === "number" ? data.output_index : -1;
816
+ if (item?.type === "function_call" && outputIndex >= 0) {
817
+ const callId = typeof item.call_id === "string" ? item.call_id
818
+ : typeof item.id === "string" ? item.id : `tu_${outputIndex}`;
819
+ openResponsesToolBlock(state, push, outputIndex, callId, typeof item.name === "string" ? item.name : "");
820
+ }
821
+ }
822
+ function handleResponsesContentPartAdded(state, push, data) {
823
+ const outputIndex = typeof data.output_index === "number" ? data.output_index : -1;
824
+ const contentIndex = typeof data.content_index === "number" ? data.content_index : 0;
825
+ const part = isRecord(data.part) ? data.part : undefined;
826
+ if (outputIndex >= 0 && (part?.type === "output_text" || part?.type === "refusal")) {
827
+ ensureResponsesMessageStarted(state, push, undefined);
828
+ openResponsesTextBlock(state, push, outputIndex, contentIndex);
829
+ }
830
+ }
831
+ function handleResponsesTextDelta(state, push, data) {
832
+ const outputIndex = typeof data.output_index === "number" ? data.output_index : -1;
833
+ const contentIndex = typeof data.content_index === "number" ? data.content_index : 0;
834
+ const delta = typeof data.delta === "string" ? data.delta : "";
835
+ const index = state.textBlocks.get(`${outputIndex}:${contentIndex}`);
836
+ if (index !== undefined && delta.length > 0) {
837
+ push(anthropicSseEvent("content_block_delta", { index, delta: { type: "text_delta", text: delta } }));
838
+ }
839
+ }
840
+ function handleResponsesToolArgumentsDelta(state, push, data) {
841
+ const outputIndex = typeof data.output_index === "number" ? data.output_index : -1;
842
+ const delta = typeof data.delta === "string" ? data.delta : "";
843
+ const index = state.toolBlocks.get(outputIndex);
844
+ if (index !== undefined && delta.length > 0) {
845
+ push(anthropicSseEvent("content_block_delta", { index, delta: { type: "input_json_delta", partial_json: delta } }));
846
+ }
847
+ }
848
+ function handleResponsesOutputItemDone(state, push, data) {
849
+ const outputIndex = typeof data.output_index === "number" ? data.output_index : -1;
850
+ if (outputIndex >= 0)
851
+ closeResponsesItemBlocks(state, push, outputIndex);
852
+ }
853
+ function handleResponsesFailure(state, push, type, data) {
854
+ const response = isRecord(data.response) ? data.response : undefined;
855
+ ensureResponsesMessageStarted(state, push, response);
856
+ const message = isRecord(response?.error) && typeof response.error.message === "string"
857
+ ? response.error.message
858
+ : typeof data.message === "string" ? data.message
859
+ : isRecord(data.error) && typeof data.error.message === "string" ? data.error.message
860
+ : `stream ended: ${type}`;
861
+ push(anthropicSseError(message));
862
+ state.messageStopped = true;
863
+ }
864
+ const RESPONSES_STREAM_HANDLERS = {
865
+ "response.created": (state, push, data) => ensureResponsesMessageStarted(state, push, isRecord(data.response) ? data.response : undefined),
866
+ "response.in_progress": (state, push, data) => ensureResponsesMessageStarted(state, push, isRecord(data.response) ? data.response : undefined),
867
+ "response.output_item.added": handleResponsesOutputItemAdded,
868
+ "response.content_part.added": handleResponsesContentPartAdded,
869
+ "response.output_text.delta": handleResponsesTextDelta,
870
+ "response.refusal.delta": handleResponsesTextDelta,
871
+ "response.function_call_arguments.delta": handleResponsesToolArgumentsDelta,
872
+ "response.output_item.done": handleResponsesOutputItemDone,
873
+ "response.completed": (state, push, data) => finishResponsesStream(state, push, isRecord(data.response) ? data.response : undefined),
874
+ "response.incomplete": (state, push, data) => finishResponsesStream(state, push, isRecord(data.response) ? data.response : undefined),
875
+ "response.failed": (state, push, data) => handleResponsesFailure(state, push, "response.failed", data),
876
+ "response.cancelled": (state, push, data) => handleResponsesFailure(state, push, "response.cancelled", data),
877
+ error: (state, push, data) => handleResponsesFailure(state, push, "error", data),
878
+ ping: (_state, push) => push(`event: ping\ndata: {"type":"ping"}\n\n`),
879
+ };
880
+ function processResponsesStreamEvent(state, push, type, data) {
881
+ RESPONSES_STREAM_HANDLERS[type]?.(state, push, data);
882
+ }
883
+ function translateResponsesStreamToAnthropic(upstream) {
884
+ return createSseTransformStream(upstream, ({ push, frames }) => {
885
+ const state = newResponsesStreamState();
886
+ return {
887
+ flushHeld: () => finishResponsesStream(state, push, undefined),
888
+ processFrames: () => {
889
+ for (const { frame } of frames) {
890
+ const ev = parseSseEvent(frame);
891
+ if (!ev || !ev.data)
892
+ continue;
893
+ const type = typeof ev.data.type === "string" ? ev.data.type : ev.type;
894
+ processResponsesStreamEvent(state, push, type, ev.data);
895
+ }
896
+ },
897
+ };
898
+ });
899
+ }
900
+ async function postResponsesRequest(attempt, args, reqJson, invokeFetch) {
901
+ const target = attempt.target;
902
+ let responsesBody;
903
+ let toolCallIdsRewritten = 0;
904
+ let sentinelsStamped = 0;
905
+ try {
906
+ responsesBody = anthropicRequestToOpenAiResponses(reqJson, {
907
+ model: target.model,
908
+ stream: args.wantsStream,
909
+ ...(target.toolCallIds !== undefined ? { toolCallIds: target.toolCallIds } : {}),
910
+ onToolCallIdsRewritten: (n) => { toolCallIdsRewritten = n; },
911
+ ...(target.thoughtSignature !== undefined ? { thoughtSignature: target.thoughtSignature } : {}),
912
+ onThoughtSignatureSentinels: (n) => { sentinelsStamped = n; },
913
+ });
914
+ }
915
+ catch (e) {
916
+ if (e instanceof RequestMappingError) {
917
+ return { ok: false, response: anthropicError(400, `llm-relay: ${e.message}`, "local") };
918
+ }
919
+ return { ok: false, response: anthropicError(502, `request translation failed: ${e.message}`, "local") };
920
+ }
921
+ const res = await invokeFetch(target.base + "/responses", {
922
+ method: "POST",
923
+ headers: buildTargetHeaders(attempt),
924
+ body: JSON.stringify(responsesBody),
925
+ signal: args.signal,
926
+ });
927
+ if (!res.ok) {
928
+ let body;
929
+ try {
930
+ body = await res.text();
931
+ }
932
+ catch (cause) {
933
+ return { ok: false, response: attachPostHeaderBodyFailure(res, cause) };
934
+ }
935
+ const hint = res.status === 404
936
+ ? ` — model "${target.model}" is not served by provider "${target.provider}" (a model can be listed in /models and still 404 here)`
937
+ : "";
938
+ return {
939
+ ok: false,
940
+ response: anthropicError(res.status, `openai backend HTTP ${res.status}${hint}: ${body.slice(0, 300)}`, "upstream", {
941
+ ...retryAfterHeader(res.headers),
942
+ }),
943
+ };
944
+ }
945
+ return { ok: true, res, toolCallIdsRewritten, sentinelsStamped };
946
+ }
947
+ async function fetchResponsesStreamed(args, res, toolCallIdsRewritten, sentinelsStamped) {
948
+ if (!res.body) {
949
+ return anthropicError(502, "llm-relay: invalid OpenAI upstream envelope: empty stream", "upstream", {
950
+ ...retryAfterHeader(res.headers),
951
+ }, "invalid_upstream_envelope");
952
+ }
953
+ const preflight = await preflightResponseStream(res.body, "openai-responses");
954
+ if (!preflight.ok) {
955
+ return anthropicError(502, `llm-relay: invalid OpenAI upstream envelope: ${preflight.reason}`, "upstream", {
956
+ ...retryAfterHeader(res.headers),
957
+ }, "invalid_upstream_envelope");
958
+ }
959
+ try {
960
+ const anthStream = translateResponsesStreamToAnthropic(preflight.body);
961
+ const observedStream = args.usage
962
+ ? observeUsage(new Response(anthStream), "anthropic-messages", args.usage, { streamed: true }).body ?? anthStream
963
+ : anthStream;
964
+ const strippedStream = stripThinkTagsInStream(observedStream);
965
+ const schemas = toolSchemaMap(args.reqJson);
966
+ const refusalSignal = dialectRefusalSignal();
967
+ const recovered = schemas.size > 0
968
+ ? recoverDialectInStream(strippedStream, schemas, args.isDestructive, refusalSignal)
969
+ : strippedStream;
970
+ const metadata = preflight.metadata;
971
+ if (toolCallIdsRewritten > 0)
972
+ metadata.toolCallIdRewrites = toolCallIdsRewritten;
973
+ if (sentinelsStamped > 0)
974
+ metadata.thoughtSignatureSentinels = sentinelsStamped;
975
+ const body = rewriteToolUseIdsInStream(recovered, () => knownToolUseIds(args.reqJson), (count) => { metadata.toolUseIdRewrites = count; });
976
+ const streamResponse = attachUpstreamMetadata(new Response(body, {
977
+ status: res.status,
978
+ headers: {
979
+ "content-type": "text/event-stream",
980
+ ...(toolCallIdsRewritten > 0 ? { [TOOL_CALL_IDS_HEADER]: `${toolCallIdsRewritten} rewritten` } : {}),
981
+ },
982
+ }), metadata);
983
+ dialectRefusalSignals.set(streamResponse, refusalSignal);
984
+ return streamResponse;
985
+ }
986
+ catch (e) {
987
+ return anthropicError(502, `llm-relay: response translation failed: ${e.message}`, "local", {}, "relay_mapper_defect");
988
+ }
989
+ }
990
+ function mapResponsesJsonToAnthropicOrError(upstreamJson, target, args) {
991
+ try {
992
+ const schemas = toolSchemaMap(args.reqJson);
993
+ const message = openAiResponsesToAnthropicMessage(upstreamJson, target.model ?? "", schemas, args.isDestructive);
994
+ return { ok: true, message };
995
+ }
996
+ catch (e) {
997
+ if (e instanceof DialectUnparseableError) {
998
+ return { ok: false, response: anthropicError(502, `llm-relay: ${e.message}`, "upstream", {}, "tool_dialect_unparseable") };
999
+ }
1000
+ if (e instanceof DialectDestructiveError) {
1001
+ return {
1002
+ ok: false,
1003
+ response: anthropicError(502, `llm-relay: ${e.message}`, "local", { [TOOL_DIALECT_HEADER]: "refused-destructive" }, DIALECT_REFUSED_DESTRUCTIVE_CODE),
1004
+ };
1005
+ }
1006
+ return { ok: false, response: anthropicError(502, `llm-relay: response translation failed: ${e.message}`, "local", {}, "relay_mapper_defect") };
1007
+ }
1008
+ }
1009
+ async function fetchResponsesBuffered(args, target, res, toolCallIdsRewritten, sentinelsStamped) {
1010
+ let upstreamJson;
1011
+ try {
1012
+ upstreamJson = await res.json();
1013
+ }
1014
+ catch (cause) {
1015
+ if (!(cause instanceof SyntaxError))
1016
+ return attachPostHeaderBodyFailure(res, cause);
1017
+ return anthropicError(502, "llm-relay: invalid OpenAI upstream envelope: body is not valid JSON", "upstream", {
1018
+ ...retryAfterHeader(res.headers),
1019
+ }, "invalid_upstream_envelope");
1020
+ }
1021
+ const invalidReason = invalidEnvelopeReason(upstreamJson, "openai-responses", false);
1022
+ if (invalidReason) {
1023
+ return anthropicError(502, `llm-relay: invalid OpenAI upstream envelope: ${invalidReason}`, "upstream", {
1024
+ ...retryAfterHeader(res.headers),
1025
+ }, "invalid_upstream_envelope");
1026
+ }
1027
+ const mapped = mapResponsesJsonToAnthropicOrError(upstreamJson, target, args);
1028
+ if (!mapped.ok)
1029
+ return mapped.response;
1030
+ let anthropicJson = mapped.message;
1031
+ const mintedIds = mintUniqueToolUseIds(anthropicJson, args.reqJson);
1032
+ if (mintedIds.message)
1033
+ anthropicJson = mintedIds.message;
1034
+ const recoveredDialect = recoveredDialectOf(anthropicJson);
1035
+ if (args.usage) {
1036
+ applyResponsesUsage(args.usage, extractAnthropicUsageFromResponses(upstreamJson.usage));
1037
+ }
1038
+ const response = new Response(JSON.stringify(anthropicJson), {
1039
+ status: 200,
1040
+ headers: {
1041
+ "content-type": "application/json",
1042
+ ...(recoveredDialect ? { [TOOL_DIALECT_HEADER]: recoveredDialect } : {}),
1043
+ ...(mintedIds.rewritten > 0 ? { [TOOL_USE_IDS_HEADER]: `${mintedIds.rewritten} rewritten` } : {}),
1044
+ ...(toolCallIdsRewritten > 0 ? { [TOOL_CALL_IDS_HEADER]: `${toolCallIdsRewritten} rewritten` } : {}),
1045
+ },
1046
+ });
1047
+ const metadata = {};
1048
+ captureReportedModel(metadata, upstreamJson, "openai-responses", false);
1049
+ if (mintedIds.rewritten > 0)
1050
+ metadata.toolUseIdRewrites = mintedIds.rewritten;
1051
+ if (toolCallIdsRewritten > 0)
1052
+ metadata.toolCallIdRewrites = toolCallIdsRewritten;
1053
+ if (sentinelsStamped > 0)
1054
+ metadata.thoughtSignatureSentinels = sentinelsStamped;
1055
+ return attachUpstreamMetadata(response, metadata);
1056
+ }
1057
+ async function fetchOpenAiResponsesBackend(attempt, args, invokeFetch) {
1058
+ let reqJson = args.reqJson;
1059
+ try {
1060
+ reqJson = await transcodeDocuments(reqJson);
1061
+ }
1062
+ catch (e) {
1063
+ if (e instanceof DocumentError)
1064
+ return anthropicError(400, `llm-relay: ${e.message}`, "local");
1065
+ return anthropicError(502, `document conversion failed: ${e.message}`, "local");
1066
+ }
1067
+ const posted = await postResponsesRequest(attempt, args, reqJson, invokeFetch);
1068
+ if (!posted.ok)
1069
+ return posted.response;
1070
+ if (args.wantsStream) {
1071
+ return fetchResponsesStreamed(args, posted.res, posted.toolCallIdsRewritten, posted.sentinelsStamped);
1072
+ }
1073
+ return fetchResponsesBuffered(args, attempt.target, posted.res, posted.toolCallIdsRewritten, posted.sentinelsStamped);
1074
+ }
327
1075
  export async function fetchBackend(attempt, args, fetchFn = fetch) {
328
1076
  const invokeFetch = oneShotFetch(fetchFn, args.signal, args.onEgress);
329
1077
  if (attempt.target.kind === "anthropic") {
@@ -572,12 +1320,14 @@ function formatOpenAiChatCompletion(body, text, toolCalls, model, usage) {
572
1320
  return out;
573
1321
  }
574
1322
  function formatOpenAiResponses(body, text, toolCalls, model, usage) {
1323
+ const capped = body.stop_reason === "max_tokens";
1324
+ const itemStatus = capped ? "incomplete" : "completed";
575
1325
  const output = [];
576
1326
  if (text) {
577
1327
  output.push({
578
1328
  type: "message",
579
1329
  id: `msg_${typeof body.id === "string" ? body.id : "relay"}`,
580
- status: "completed",
1330
+ status: itemStatus,
581
1331
  role: "assistant",
582
1332
  content: [{ type: "output_text", text, annotations: [] }],
583
1333
  });
@@ -590,18 +1340,20 @@ function formatOpenAiResponses(body, text, toolCalls, model, usage) {
590
1340
  call_id: call.id,
591
1341
  name: fn.name,
592
1342
  arguments: fn.arguments,
593
- status: "completed",
1343
+ status: itemStatus,
594
1344
  });
595
1345
  }
596
1346
  const out = {
597
1347
  id: typeof body.id === "string" ? body.id : "resp_relay",
598
1348
  object: "response",
599
1349
  created_at: Math.floor(Date.now() / 1000),
600
- status: "completed",
1350
+ status: capped ? "incomplete" : "completed",
601
1351
  model,
602
1352
  output,
603
1353
  output_text: text,
604
1354
  };
1355
+ if (capped)
1356
+ out.incomplete_details = { reason: "max_output_tokens" };
605
1357
  if (usage)
606
1358
  out.usage = withOpenAiTotal(usage);
607
1359
  return out;
@@ -720,6 +1472,17 @@ function buildTargetHeaders(attempt) {
720
1472
  ...buildAuthHeaders(credential.value, target.authHeader),
721
1473
  };
722
1474
  }
1475
+ function resolveAnthropicResponsesMaxTokens(target, catalogLimits) {
1476
+ if (target.model) {
1477
+ const learned = observedMaxOutput(target.provider, target.model);
1478
+ if (typeof learned === "number" && learned > 0)
1479
+ return learned;
1480
+ const published = catalogLimits?.(target.provider, target.model)?.maxOutputTokens;
1481
+ if (typeof published === "number" && published > 0)
1482
+ return published;
1483
+ }
1484
+ return DEFAULT_RESPONSES_MAX_TOKENS;
1485
+ }
723
1486
  async function fetchDirectOpenAiChat(attempt, args, invokeFetch) {
724
1487
  const target = attempt.target;
725
1488
  const base = (args.reqJson ?? {});
@@ -814,6 +1577,58 @@ async function fetchDirectOpenAiChat(attempt, args, invokeFetch) {
814
1577
  }
815
1578
  return attachUpstreamMetadata(res, metadata);
816
1579
  }
1580
+ function tapAnthropicStopReason(source, onStopReason) {
1581
+ const decoder = new TextDecoder();
1582
+ const frames = new BufferedSseFrames();
1583
+ let seen = false;
1584
+ return source.pipeThrough(new TransformStream({
1585
+ transform(chunk, controller) {
1586
+ controller.enqueue(chunk);
1587
+ if (seen)
1588
+ return;
1589
+ frames.append(decoder.decode(chunk, { stream: true }));
1590
+ for (const { frame } of frames) {
1591
+ if (seen)
1592
+ break;
1593
+ const ev = parseSseEvent(frame);
1594
+ if (ev?.type !== "message_delta" || !isRecord(ev.data))
1595
+ continue;
1596
+ const delta = ev.data.delta;
1597
+ if (isRecord(delta) && typeof delta.stop_reason === "string") {
1598
+ seen = true;
1599
+ onStopReason(delta.stop_reason);
1600
+ }
1601
+ }
1602
+ },
1603
+ }));
1604
+ }
1605
+ function responsesSseEvent(type, data) {
1606
+ return `event: ${type}\ndata: ${JSON.stringify({ ...data, type })}\n\n`;
1607
+ }
1608
+ function markResponsesIncompleteOnMaxTokens(source, stopReason) {
1609
+ return createSseTransformStream(source, ({ push, frames }) => ({
1610
+ processFrames: () => {
1611
+ for (const { frame, raw } of frames) {
1612
+ const ev = parseSseEvent(frame);
1613
+ if (stopReason.value === "max_tokens" &&
1614
+ ev?.type === "response.completed" &&
1615
+ isRecord(ev.data) &&
1616
+ isRecord(ev.data.response)) {
1617
+ push(responsesSseEvent("response.incomplete", {
1618
+ ...ev.data,
1619
+ response: {
1620
+ ...ev.data.response,
1621
+ status: "incomplete",
1622
+ incomplete_details: { reason: "max_output_tokens" },
1623
+ },
1624
+ }));
1625
+ continue;
1626
+ }
1627
+ push(raw);
1628
+ }
1629
+ },
1630
+ }));
1631
+ }
817
1632
  async function fetchTranslatedOpenAiFront(attempt, args, fetchFn) {
818
1633
  const target = attempt.target;
819
1634
  const protocol = args.protocol ?? "chat";
@@ -826,6 +1641,9 @@ async function fetchTranslatedOpenAiFront(attempt, args, fetchFn) {
826
1641
  if (target.model !== undefined)
827
1642
  anthropicBody.model = target.model;
828
1643
  anthropicBody.stream = args.wantsStream;
1644
+ if (protocol === "responses" && target.kind === "anthropic" && typeof anthropicBody.max_tokens !== "number") {
1645
+ anthropicBody.max_tokens = resolveAnthropicResponsesMaxTokens(target, args.catalogLimits);
1646
+ }
829
1647
  }
830
1648
  catch (e) {
831
1649
  if (e instanceof RequestMappingError)
@@ -881,7 +1699,13 @@ async function fetchTranslatedOpenAiFront(attempt, args, fetchFn) {
881
1699
  }
882
1700
  const targetProtocol = protocol === "responses" ? "openai-responses" : "openai";
883
1701
  try {
884
- const output = handleUniversalStreamRequest(preflight.body, "anthropic", targetProtocol);
1702
+ const stopReason = { value: null };
1703
+ const anthropicSource = protocol === "responses"
1704
+ ? tapAnthropicStopReason(preflight.body, (r) => { stopReason.value = r; })
1705
+ : preflight.body;
1706
+ let output = handleUniversalStreamRequest(anthropicSource, "anthropic", targetProtocol);
1707
+ if (protocol === "responses")
1708
+ output = markResponsesIncompleteOnMaxTokens(output, stopReason);
885
1709
  const rewritten = backendRes.headers.get(TOOL_CALL_IDS_HEADER);
886
1710
  const response = new Response(output, {
887
1711
  status: backendRes.status,
@@ -935,7 +1759,7 @@ async function fetchTranslatedOpenAiFront(attempt, args, fetchFn) {
935
1759
  }
936
1760
  export async function fetchOpenAiFront(attempt, args, fetchFn = fetch) {
937
1761
  const protocol = args.protocol ?? "chat";
938
- if (attempt.target.kind === "openai" && protocol === "chat") {
1762
+ if (attempt.target.kind === "openai" && protocol === "chat" && attempt.target.wire !== "responses") {
939
1763
  const invokeFetch = oneShotFetch(fetchFn, args.signal, args.onEgress);
940
1764
  return fetchDirectOpenAiChat(attempt, args, invokeFetch);
941
1765
  }