@jeffusion/bungee-core 5.20.0 → 5.21.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 (41) hide show
  1. package/dist/config-worker/private-transport.d.ts +2 -0
  2. package/dist/data-admission/errors.d.ts +6 -1
  3. package/dist/gateway/contracts.d.ts +15 -4
  4. package/dist/gateway/forward-plugin.d.ts +2 -0
  5. package/dist/gateway/plugin.js +12 -2
  6. package/dist/gateway/request-plugin.d.ts +6 -0
  7. package/dist/gateway/responses-websocket-session.d.ts +18 -0
  8. package/dist/gateway/runtime.d.ts +1 -0
  9. package/dist/gateway/websocket-contracts.d.ts +1 -0
  10. package/dist/hooks/plugin-hooks.d.ts +2 -3
  11. package/dist/logger/access-log-writer.d.ts +2 -2
  12. package/dist/main.js +1199 -176
  13. package/dist/plugins/ai-transformer/index.js +1 -1
  14. package/dist/plugins/ai-transformer/index.js.map +4 -4
  15. package/dist/plugins/codex-router/control.js +6 -2
  16. package/dist/plugins/codex-router/control.js.map +4 -4
  17. package/dist/plugins/codex-router/index.js +498 -165
  18. package/dist/plugins/codex-router/index.js.map +13 -11
  19. package/dist/plugins/codex-router/manifest.json +124 -8
  20. package/dist/plugins/key-access/index.js +12 -2
  21. package/dist/plugins/key-access/index.js.map +3 -3
  22. package/dist/plugins/key-access/ingress.js +12 -2
  23. package/dist/plugins/key-access/ingress.js.map +3 -3
  24. package/dist/plugins/openai-messages-to-chat/index.js +6 -4
  25. package/dist/plugins/openai-messages-to-chat/index.js.map +4 -4
  26. package/dist/plugins/token-metering/index.js +1 -1
  27. package/dist/plugins/token-metering/index.js.map +4 -4
  28. package/dist/plugins/token-stats/manifest.json +16 -6
  29. package/dist/plugins/token-stats/ui/ClientModelPicker.svelte +13 -61
  30. package/dist/plugins/token-stats/ui/PriceModelPicker.svelte +18 -74
  31. package/dist/plugins/token-stats/ui/TokenStatsSettings.svelte +3 -2
  32. package/dist/plugins/token-stats/ui/model-search.ts +2 -45
  33. package/dist/plugins/token-stats/ui/pricing-catalog.ts +2 -1
  34. package/dist/scoped-plugin-registry.d.ts +7 -0
  35. package/dist/websocket/index.d.ts +1 -1
  36. package/dist/websocket/transport.d.ts +40 -18
  37. package/package.json +2 -2
  38. package/dist/plugins/chatgpt-oauth/ui/account-model.test.js +0 -69
  39. package/dist/plugins/local-accounts/ui/login-errors.test.ts +0 -22
  40. package/dist/plugins/token-stats/ui/client-model-search.test.ts +0 -58
  41. package/dist/plugins/token-stats/ui/pricing-catalog.test.ts +0 -114
@@ -2256,14 +2256,25 @@ var DEFAULT_RESPONSES_CODEC_LIMITS = Object.freeze({
2256
2256
 
2257
2257
  class ResponsesCodecError extends Error {
2258
2258
  code;
2259
- constructor(code, message) {
2259
+ param;
2260
+ constructor(code, message, param) {
2260
2261
  super(message);
2261
2262
  this.code = code;
2263
+ this.param = param;
2262
2264
  this.name = "ResponsesCodecError";
2263
2265
  }
2264
2266
  }
2265
- function fail(code, message) {
2266
- throw new ResponsesCodecError(code, message);
2267
+ function fail(code, message, param) {
2268
+ throw new ResponsesCodecError(code, message, param);
2269
+ }
2270
+ function atParam(param, run) {
2271
+ try {
2272
+ return run();
2273
+ } catch (error) {
2274
+ if (error instanceof ResponsesCodecError && error.param === undefined)
2275
+ throw new ResponsesCodecError(error.code, error.message, param);
2276
+ throw error;
2277
+ }
2267
2278
  }
2268
2279
  function record(value, where) {
2269
2280
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -2426,8 +2437,94 @@ function responsePayload(id, model, output, end, usage, error, createdAt = Math.
2426
2437
  return result;
2427
2438
  }
2428
2439
 
2440
+ // packages/llms/src/responses-codec/request-preferences.ts
2441
+ function fields(value, allowed, path) {
2442
+ for (const key of Object.keys(value))
2443
+ if (!allowed.includes(key)) {
2444
+ fail("unsupported_request", "Unrecognized request option", `${path}.${key}`);
2445
+ }
2446
+ }
2447
+ function applyResponsesPreferences(body, result, protocol, capabilities, maxTokens, diagnostics) {
2448
+ const note = (param, action, reason) => diagnostics.push({ param, action, reason });
2449
+ if (body.stream_options !== undefined)
2450
+ atParam("stream_options", () => {
2451
+ const options = record(body.stream_options, "stream_options");
2452
+ fields(options, ["include_usage", "reasoning_summary_delivery"], "stream_options");
2453
+ if (options.include_usage !== undefined) {
2454
+ if (typeof options.include_usage !== "boolean")
2455
+ fail("invalid_payload", "Usage preference must be boolean", "stream_options.include_usage");
2456
+ note("stream_options.include_usage", protocol === "chat_completions" && body.stream === true ? "mapped" : "omitted", protocol === "chat_completions" && body.stream === true ? "stream_usage_required" : "target_usage_events");
2457
+ }
2458
+ if (options.reasoning_summary_delivery !== undefined) {
2459
+ if (options.reasoning_summary_delivery !== "sequential_cutoff")
2460
+ fail("unsupported_request", "Unsupported summary delivery preference", "stream_options.reasoning_summary_delivery");
2461
+ note("stream_options.reasoning_summary_delivery", "omitted", "source_summary_delivery_not_applicable");
2462
+ }
2463
+ });
2464
+ if (body.access_programs !== undefined)
2465
+ atParam("access_programs", () => {
2466
+ const programs = record(body.access_programs, "access_programs");
2467
+ fields(programs, ["cyber"], "access_programs");
2468
+ if (programs.cyber !== undefined && programs.cyber !== "standard") {
2469
+ if (programs.cyber === "daybreak_blue" || programs.cyber === "daybreak_red") {
2470
+ fail("unsupported_access_program", "Selected access program cannot be represented by the target protocol", "access_programs.cyber");
2471
+ }
2472
+ fail("invalid_payload", "Invalid access program", "access_programs.cyber");
2473
+ }
2474
+ note("access_programs", "omitted", "target_uses_own_access_policy");
2475
+ });
2476
+ if (body.reasoning !== undefined)
2477
+ atParam("reasoning", () => {
2478
+ const reasoning = record(body.reasoning, "reasoning");
2479
+ fields(reasoning, ["effort", "summary", "context"], "reasoning");
2480
+ if (reasoning.summary !== undefined) {
2481
+ if (typeof reasoning.summary !== "string" || !["none", "auto", "concise", "detailed"].includes(reasoning.summary))
2482
+ fail("invalid_payload", "Invalid reasoning summary preference", "reasoning.summary");
2483
+ note("reasoning.summary", "omitted", "target_summary_not_guaranteed");
2484
+ }
2485
+ if (reasoning.context !== undefined) {
2486
+ if (reasoning.context !== "all_turns")
2487
+ fail("unsupported_reasoning", "Unsupported reasoning context preference", "reasoning.context");
2488
+ note("reasoning.context", "omitted", "source_reasoning_context_not_applicable");
2489
+ }
2490
+ if (reasoning.effort !== undefined)
2491
+ atParam("reasoning.effort", () => {
2492
+ const effort = string(reasoning.effort, "reasoning effort", true);
2493
+ if (protocol === "chat_completions" && capabilities.reasoningEffort) {
2494
+ result.reasoning_effort = effort;
2495
+ note("reasoning.effort", "mapped", "target_reasoning_effort");
2496
+ } else if (protocol === "anthropic_messages" && capabilities.anthropicThinkingBudget !== undefined && effort !== "none") {
2497
+ const budget = capabilities.anthropicThinkingBudget;
2498
+ if (!Number.isSafeInteger(budget) || budget < 1024 || typeof maxTokens !== "number" || budget >= maxTokens) {
2499
+ fail("unsupported_reasoning", "Anthropic thinking requires an explicit budget below max_output_tokens");
2500
+ }
2501
+ result.thinking = { type: "enabled", budget_tokens: budget };
2502
+ note("reasoning.effort", "mapped", "explicit_target_thinking_budget");
2503
+ } else
2504
+ note("reasoning.effort", "omitted", "target_reasoning_effort_unavailable");
2505
+ });
2506
+ });
2507
+ if (body.text !== undefined)
2508
+ atParam("text", () => {
2509
+ const text = record(body.text, "text");
2510
+ fields(text, ["verbosity", "format"], "text");
2511
+ if (text.verbosity !== undefined) {
2512
+ if (typeof text.verbosity !== "string" || !["low", "medium", "high"].includes(text.verbosity))
2513
+ fail("invalid_payload", "Invalid text verbosity preference", "text.verbosity");
2514
+ note("text.verbosity", "omitted", "target_verbosity_not_available");
2515
+ }
2516
+ });
2517
+ }
2518
+
2429
2519
  // packages/llms/src/responses-codec/request.ts
2430
2520
  var contentNormalizer = new OpenAIProtocolConversion({ trimWhitespace: false });
2521
+ function definitionKey(value) {
2522
+ if (Array.isArray(value))
2523
+ return `[${value.map(definitionKey).join(",")}]`;
2524
+ if (value && typeof value === "object")
2525
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${definitionKey(value[key])}`).join(",")}}`;
2526
+ return JSON.stringify(value);
2527
+ }
2431
2528
  function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2432
2529
  const body = record(raw, "Responses request");
2433
2530
  const budget = limits(capabilities.limits);
@@ -2453,11 +2550,15 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2453
2550
  "n",
2454
2551
  "truncation",
2455
2552
  "include",
2456
- "store"
2553
+ "store",
2554
+ "prompt_cache_key",
2555
+ "client_metadata",
2556
+ "stream_options",
2557
+ "access_programs"
2457
2558
  ]);
2458
2559
  for (const key of Object.keys(body))
2459
2560
  if (!supported.has(key))
2460
- fail("unsupported_request", `Unsupported Responses request field ${key}`);
2561
+ fail("unsupported_request", "Unrecognized Responses request field", key);
2461
2562
  if (body.store === true)
2462
2563
  fail("unsupported_request", "Stored Responses require caller-owned persistence");
2463
2564
  for (const key of ["previous_response_id", "conversation", "response_id"]) {
@@ -2470,17 +2571,28 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2470
2571
  fail("unsupported_request", "Only one response candidate is supported");
2471
2572
  if (body.truncation !== undefined && body.truncation !== "disabled")
2472
2573
  fail("unsupported_request", "Automatic input truncation cannot be represented");
2473
- if (body.include !== undefined && list(body.include, "include").length)
2574
+ if (body.include !== undefined && list(body.include, "include").some((field) => field !== "reasoning.encrypted_content"))
2474
2575
  fail("unsupported_request", "Requested extra Responses output fields cannot be represented");
2576
+ if (body.prompt_cache_key !== undefined)
2577
+ string(body.prompt_cache_key, "prompt_cache_key");
2578
+ if (body.client_metadata !== undefined)
2579
+ record(body.client_metadata, "client_metadata");
2475
2580
  const model = string(body.model, "model", true);
2581
+ const diagnostics = [];
2582
+ const input = atParam("input", () => typeof body.input === "string" ? [{ role: "user", content: body.input }] : list(body.input, "input"));
2583
+ if (input.length > budget.maxItems)
2584
+ fail("resource_limit", "Too many input items", "input");
2585
+ const canonicalInput = [];
2476
2586
  const toolNames = new Map;
2477
2587
  const byOriginal = new Map;
2588
+ const definitions = new Map;
2589
+ let examinedTools = 0;
2478
2590
  const tools = [];
2479
2591
  const identity = (name, namespace) => JSON.stringify([namespace ?? null, name]);
2480
2592
  const register = (name, namespace, custom) => {
2481
2593
  const key = identity(name, namespace);
2482
2594
  if (byOriginal.has(key))
2483
- fail("tool_name_collision", `Duplicate tool ${namespace ? namespace + "." : ""}${name}`);
2595
+ fail("tool_name_collision", "Duplicate tool identity");
2484
2596
  if (toolNames.size >= budget.maxItems)
2485
2597
  fail("resource_limit", "Too many tools");
2486
2598
  const wire = `bungee_tool_${toolNames.size}`;
@@ -2488,18 +2600,29 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2488
2600
  byOriginal.set(key, wire);
2489
2601
  return wire;
2490
2602
  };
2491
- const expandTool = (rawTool, namespace) => {
2603
+ const expandTool = (rawTool, namespace, path = "tools") => {
2604
+ if (++examinedTools > budget.maxItems)
2605
+ fail("resource_limit", "Too many tool declarations");
2492
2606
  const tool = record(rawTool, "tool");
2607
+ if (namespace === undefined && capabilities.omitOptionalWebSearch && ["web_search", "web_search_preview"].includes(String(tool.type)) && (body.tool_choice === undefined || body.tool_choice === "auto" || body.tool_choice === "none"))
2608
+ return;
2609
+ const allowed = tool.type === "namespace" ? ["type", "name", "description", "tools"] : tool.type === "custom" ? ["type", "name", "description", "format"] : ["type", "name", "description", "parameters", "strict"];
2610
+ for (const key of Object.keys(tool))
2611
+ if (!allowed.includes(key))
2612
+ fail("unsupported_tool", "Unsupported tool definition field", `${path}.${key}`);
2493
2613
  if (tool.type === "namespace") {
2494
2614
  if (namespace !== undefined)
2495
2615
  fail("unsupported_tool", "Nested tool namespaces are not supported");
2496
2616
  const name = string(tool.name, "namespace name", true);
2497
- for (const child of list(tool.tools, "namespace tools"))
2498
- expandTool(child, name);
2617
+ for (const [index, child] of list(tool.tools, "namespace tools").entries()) {
2618
+ const childPath = `${path}.tools[${index}]`;
2619
+ atParam(childPath, () => expandTool(child, name, childPath));
2620
+ }
2499
2621
  return;
2500
2622
  }
2501
2623
  if (tool.type !== "function" && tool.type !== "custom")
2502
- fail("unsupported_tool", `Hosted tool ${String(tool.type)} cannot be executed by Codex through this protocol`);
2624
+ fail("unsupported_tool", "This tool type cannot be executed by Codex through the target protocol");
2625
+ serialized(tool, budget.maxArgumentBytes, "tool definition");
2503
2626
  const name = string(tool.name, "tool name", true);
2504
2627
  const custom = tool.type === "custom";
2505
2628
  if (tool.strict !== undefined && typeof tool.strict !== "boolean")
@@ -2511,7 +2634,15 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2511
2634
  if (format.type !== "text" && format.type !== "grammar")
2512
2635
  fail("unsupported_tool", "Unknown custom tool format");
2513
2636
  }
2637
+ const key = identity(name, namespace);
2638
+ const signature = definitionKey(tool);
2639
+ if (definitions.has(key)) {
2640
+ if (definitions.get(key) !== signature)
2641
+ fail("tool_name_collision", "Conflicting definitions for the same tool identity");
2642
+ return;
2643
+ }
2514
2644
  const wire = register(name, namespace, custom);
2645
+ definitions.set(key, signature);
2515
2646
  const parameters = custom ? { type: "object", properties: { input: { type: "string" } }, required: ["input"], additionalProperties: false } : record(tool.parameters ?? { type: "object", properties: {} }, "tool parameters");
2516
2647
  const description = tool.description === undefined ? undefined : string(tool.description, "tool description");
2517
2648
  const customHint = custom ? `Pass the complete original custom tool input verbatim in the input string.${tool.format ? ` Original input format: ${JSON.stringify(tool.format)}` : ""}` : undefined;
@@ -2524,8 +2655,24 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2524
2655
  }
2525
2656
  };
2526
2657
  if (body.tools !== undefined)
2527
- for (const tool of list(body.tools, "tools"))
2528
- expandTool(tool);
2658
+ atParam("tools", () => {
2659
+ for (const [index, tool] of list(body.tools, "tools").entries())
2660
+ atParam(`tools[${index}]`, () => expandTool(tool, undefined, `tools[${index}]`));
2661
+ });
2662
+ for (const [index, rawItem] of input.entries())
2663
+ atParam(`input[${index}]`, () => {
2664
+ const item = record(rawItem, "input item");
2665
+ if (item.type !== "additional_tools") {
2666
+ canonicalInput.push(item);
2667
+ return;
2668
+ }
2669
+ if (item.content !== undefined)
2670
+ fail("invalid_payload", "A tool declaration carrier cannot also contain message content", `input[${index}].content`);
2671
+ for (const [toolIndex, tool] of list(item.tools, "additional tools").entries()) {
2672
+ const toolPath = `input[${index}].tools[${toolIndex}]`;
2673
+ atParam(toolPath, () => expandTool(tool, undefined, toolPath));
2674
+ }
2675
+ });
2529
2676
  const resolve = (item, custom) => {
2530
2677
  const name = string(item.name, "call name", true);
2531
2678
  const namespace = item.namespace === undefined ? undefined : string(item.namespace, "call namespace", true);
@@ -2533,7 +2680,7 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2533
2680
  if (wire === undefined)
2534
2681
  wire = register(name, namespace, custom);
2535
2682
  if (toolNames.get(wire).custom !== custom)
2536
- fail("tool_name_collision", `Tool kind changed for ${name}`);
2683
+ fail("tool_name_collision", "Historical tool kind conflicts with its current declaration");
2537
2684
  return wire;
2538
2685
  };
2539
2686
  const normalizeContent = (rawContent, role) => {
@@ -2544,6 +2691,8 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2544
2691
  const part = record(rawPart, "content part");
2545
2692
  if (part.type === "input_text" || part.type === "output_text" || part.type === "text")
2546
2693
  string(part.text, "content text");
2694
+ else if (part.type === "refusal" && role === "assistant")
2695
+ string(part.refusal, "refusal");
2547
2696
  else if (part.type === "input_image" && role === "user") {
2548
2697
  if (protocol !== "chat_completions")
2549
2698
  fail("unsupported_content", "Anthropic image conversion requires provider-specific URL/data support");
@@ -2551,11 +2700,14 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2551
2700
  if (part.detail !== undefined && !["auto", "low", "high"].includes(String(part.detail)))
2552
2701
  fail("unsupported_content", "Unknown image detail");
2553
2702
  } else
2554
- fail("unsupported_content", `Unsupported message content: ${String(part.type)}`);
2703
+ fail("unsupported_content", "Unsupported message content type");
2555
2704
  if (part.annotations !== undefined && list(part.annotations, "annotations").length)
2556
2705
  fail("unsupported_content", "Output annotations cannot be represented in input history");
2557
2706
  }
2558
- const normalized = contentNormalizer.normalizeResponsesMessageContent(parts);
2707
+ const normalized = contentNormalizer.normalizeResponsesMessageContent(parts.map((raw) => {
2708
+ const part = raw;
2709
+ return part.type === "refusal" ? { type: "output_text", text: part.refusal } : part;
2710
+ }));
2559
2711
  for (let i = 0;i < parts.length; i++) {
2560
2712
  const part = parts[i];
2561
2713
  if (part.type === "input_image" && part.detail !== undefined)
@@ -2564,6 +2716,8 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2564
2716
  return normalized;
2565
2717
  };
2566
2718
  const chat = [];
2719
+ const messageParams = new WeakMap;
2720
+ let messageParam = "instructions";
2567
2721
  const callIds = new Set;
2568
2722
  const answered = new Set;
2569
2723
  let pendingReasoning = "";
@@ -2571,73 +2725,77 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2571
2725
  if (chat.length >= budget.maxItems)
2572
2726
  fail("resource_limit", "Too many input messages");
2573
2727
  chat.push(message);
2728
+ messageParams.set(message, messageParam);
2574
2729
  };
2575
2730
  if (body.instructions !== undefined)
2576
2731
  addMessage({ role: "system", content: string(body.instructions, "instructions") });
2577
- const input = typeof body.input === "string" ? [{ role: "user", content: body.input }] : list(body.input, "input");
2578
- if (input.length > budget.maxItems)
2579
- fail("resource_limit", "Too many input items");
2580
- for (const rawItem of input) {
2581
- const item = record(rawItem, "input item");
2582
- if (item.encrypted_content !== undefined || item.type === "compaction")
2583
- fail("unsupported_content", "Encrypted reasoning and compaction cannot be restored");
2584
- if (item.type === "item_reference")
2585
- fail("unresolved_reference", "Input item references must be resolved before conversion");
2586
- if (item.type === "reasoning") {
2587
- if (protocol !== "chat_completions" || !capabilities.reasoningEffort)
2588
- fail("unsupported_reasoning", "Reasoning history requires an explicit compatible reasoning capability");
2589
- const summary = list(item.summary, "reasoning summary");
2590
- pendingReasoning += summary.map((part) => {
2591
- const p = record(part, "reasoning summary");
2592
- if (p.type !== "summary_text")
2593
- fail("unsupported_reasoning", "Unknown reasoning summary");
2594
- return string(p.text, "summary text");
2595
- }).join("");
2596
- continue;
2597
- }
2598
- if (item.type === "function_call" || item.type === "custom_tool_call") {
2599
- const custom = item.type === "custom_tool_call";
2600
- const id = string(item.call_id, "call_id", true);
2601
- if (callIds.has(id))
2602
- fail("invalid_tool_history", `Duplicate call_id ${id}`);
2603
- callIds.add(id);
2604
- const args = custom ? JSON.stringify({ input: string(item.input, "custom input") }) : string(item.arguments, "arguments");
2605
- bounded(args, budget.maxArgumentBytes, "arguments");
2606
- argumentObject(args);
2607
- const call = { id, type: "function", function: { name: resolve(item, custom), arguments: args } };
2608
- const previous = chat.at(-1);
2609
- if (previous?.role === "assistant") {
2610
- previous.tool_calls = [...previous.tool_calls ?? [], call];
2611
- if (pendingReasoning)
2612
- previous.reasoning_content = pendingReasoning;
2613
- } else
2614
- addMessage({ role: "assistant", content: null, tool_calls: [call], ...pendingReasoning ? { reasoning_content: pendingReasoning } : {} });
2615
- pendingReasoning = "";
2616
- continue;
2617
- }
2618
- if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
2619
- const id = string(item.call_id, "call_id", true);
2620
- if (!callIds.has(id) || answered.has(id))
2621
- fail("invalid_tool_history", `Unmatched or duplicate output ${id}`);
2622
- answered.add(id);
2623
- const output = typeof item.output === "string" ? item.output : normalizeContent(item.output, "tool");
2624
- addMessage({ role: "tool", tool_call_id: id, content: output, ...item.is_error === true ? { is_error: true } : {} });
2625
- continue;
2626
- }
2627
- if (item.type !== undefined && item.type !== "message")
2628
- fail("unsupported_content", `Unsupported input item ${String(item.type)}`);
2629
- const role = string(item.role, "message role", true);
2630
- if (!["user", "assistant", "system", "developer"].includes(role))
2631
- fail("unsupported_content", `Unknown message role ${role}`);
2632
- const message = { role, content: normalizeContent(item.content, role) };
2633
- if (pendingReasoning) {
2634
- if (role !== "assistant")
2635
- fail("unsupported_reasoning", "Reasoning must precede an assistant item");
2636
- message.reasoning_content = pendingReasoning;
2637
- pendingReasoning = "";
2638
- }
2639
- addMessage(message);
2640
- }
2732
+ for (const [inputIndex, rawItem] of input.entries())
2733
+ atParam(`input[${inputIndex}]`, () => {
2734
+ messageParam = `input[${inputIndex}]`;
2735
+ const item = record(rawItem, "input item");
2736
+ if (item.type === "additional_tools")
2737
+ return;
2738
+ if (item.encrypted_content != null || item.type === "compaction")
2739
+ fail("unsupported_content", "Encrypted reasoning and compaction cannot be restored");
2740
+ if (item.type === "item_reference")
2741
+ fail("unresolved_reference", "Input item references must be resolved before conversion");
2742
+ if (item.type === "reasoning") {
2743
+ if (protocol !== "chat_completions" || !(capabilities.reasoningHistory || capabilities.reasoningEffort))
2744
+ fail("unsupported_reasoning", "Reasoning history requires an explicit compatible reasoning capability");
2745
+ if (item.content != null)
2746
+ fail("unsupported_reasoning", "Reasoning content requires an explicit history mapping", `${messageParam}.content`);
2747
+ const summary = list(item.summary, "reasoning summary");
2748
+ pendingReasoning += summary.map((part) => {
2749
+ const p = record(part, "reasoning summary");
2750
+ if (p.type !== "summary_text")
2751
+ fail("unsupported_reasoning", "Unknown reasoning summary");
2752
+ return string(p.text, "summary text");
2753
+ }).join("");
2754
+ return;
2755
+ }
2756
+ if (item.type === "function_call" || item.type === "custom_tool_call") {
2757
+ const custom = item.type === "custom_tool_call";
2758
+ const id = string(item.call_id, "call_id", true);
2759
+ if (callIds.has(id))
2760
+ fail("invalid_tool_history", "Duplicate tool call identifier");
2761
+ callIds.add(id);
2762
+ const args = custom ? JSON.stringify({ input: string(item.input, "custom input") }) : string(item.arguments, "arguments");
2763
+ bounded(args, budget.maxArgumentBytes, "arguments");
2764
+ argumentObject(args);
2765
+ const call = { id, type: "function", function: { name: resolve(item, custom), arguments: args } };
2766
+ const previous = chat.at(-1);
2767
+ if (previous?.role === "assistant") {
2768
+ previous.tool_calls = [...previous.tool_calls ?? [], call];
2769
+ if (pendingReasoning)
2770
+ previous.reasoning_content = pendingReasoning;
2771
+ } else
2772
+ addMessage({ role: "assistant", content: null, tool_calls: [call], ...pendingReasoning ? { reasoning_content: pendingReasoning } : {} });
2773
+ pendingReasoning = "";
2774
+ return;
2775
+ }
2776
+ if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
2777
+ const id = string(item.call_id, "call_id", true);
2778
+ if (!callIds.has(id) || answered.has(id))
2779
+ fail("invalid_tool_history", "Unmatched or duplicate tool result");
2780
+ answered.add(id);
2781
+ const output = typeof item.output === "string" ? item.output : normalizeContent(item.output, "tool");
2782
+ addMessage({ role: "tool", tool_call_id: id, content: output, ...item.is_error === true ? { is_error: true } : {} });
2783
+ return;
2784
+ }
2785
+ if (item.type !== undefined && item.type !== "message")
2786
+ fail("unsupported_content", "Unsupported input item type");
2787
+ const role = string(item.role, "message role", true);
2788
+ if (!["user", "assistant", "system", "developer"].includes(role))
2789
+ fail("unsupported_content", "Unknown message role");
2790
+ const message = { role, content: normalizeContent(item.content, role) };
2791
+ if (pendingReasoning) {
2792
+ if (role !== "assistant")
2793
+ fail("unsupported_reasoning", "Reasoning must precede an assistant item");
2794
+ message.reasoning_content = pendingReasoning;
2795
+ pendingReasoning = "";
2796
+ }
2797
+ addMessage(message);
2798
+ });
2641
2799
  if (pendingReasoning)
2642
2800
  fail("unsupported_reasoning", "Orphan reasoning history cannot be represented");
2643
2801
  const result = { model, messages: chat };
@@ -2670,41 +2828,35 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2670
2828
  result.tool_choice = protocol === "chat_completions" ? { type: "function", function: { name: wire } } : { type: "tool", name: wire };
2671
2829
  }
2672
2830
  }
2673
- if (body.reasoning !== undefined) {
2674
- const reasoning = record(body.reasoning, "reasoning");
2675
- if (reasoning.summary !== undefined && reasoning.summary !== "none")
2676
- fail("unsupported_reasoning", "Requested reasoning summaries are not guaranteed by this upstream protocol");
2677
- if (reasoning.effort !== undefined && reasoning.effort !== "none") {
2678
- const effort = string(reasoning.effort, "reasoning effort", true);
2679
- if (protocol === "chat_completions") {
2680
- if (!capabilities.reasoningEffort)
2681
- fail("unsupported_reasoning", "Upstream does not advertise reasoning effort");
2682
- result.reasoning_effort = effort;
2683
- } else {
2684
- const thinking = capabilities.anthropicThinkingBudget;
2685
- if (!Number.isSafeInteger(thinking) || thinking < 1024 || typeof maxTokens !== "number" || thinking >= maxTokens)
2686
- fail("unsupported_reasoning", "Anthropic thinking requires an explicit budget below max_output_tokens");
2687
- result.thinking = { type: "enabled", budget_tokens: thinking };
2831
+ applyResponsesPreferences(body, result, protocol, capabilities, maxTokens, diagnostics);
2832
+ if (body.text !== undefined)
2833
+ atParam("text.format", () => {
2834
+ const text = record(body.text, "text");
2835
+ if (text.format !== undefined) {
2836
+ const format = record(text.format, "text format");
2837
+ const allowed = format.type === "json_schema" ? ["type", "name", "schema", "strict", "description"] : ["type"];
2838
+ for (const key of Object.keys(format))
2839
+ if (!allowed.includes(key))
2840
+ fail("unsupported_request", "Unsupported output format field", `text.format.${key}`);
2841
+ if (format.type === "json_schema") {
2842
+ atParam("text.format.name", () => string(format.name, "output schema name", true));
2843
+ atParam("text.format.schema", () => record(format.schema, "output schema"));
2844
+ if (format.strict !== undefined && typeof format.strict !== "boolean")
2845
+ fail("invalid_payload", "Output schema strict must be boolean", "text.format.strict");
2846
+ if (format.description !== undefined)
2847
+ atParam("text.format.description", () => string(format.description, "output schema description"));
2848
+ }
2849
+ if (protocol !== "chat_completions" && format.type !== "text")
2850
+ fail("unsupported_request", "Anthropic structured output requires a separate provider capability");
2851
+ if (format.type === "json_schema")
2852
+ result.response_format = { type: "json_schema", json_schema: { name: format.name, schema: format.schema, ...format.strict !== undefined ? { strict: format.strict } : {}, ...format.description !== undefined ? { description: format.description } : {} } };
2853
+ else if (format.type === "json_object" || format.type === "text") {
2854
+ if (protocol === "chat_completions")
2855
+ result.response_format = format;
2856
+ } else
2857
+ fail("unsupported_request", "Unknown text format");
2688
2858
  }
2689
- }
2690
- }
2691
- if (body.text !== undefined) {
2692
- const text = record(body.text, "text");
2693
- if (text.verbosity !== undefined)
2694
- fail("unsupported_request", "Text verbosity cannot be represented");
2695
- if (text.format !== undefined) {
2696
- const format = record(text.format, "text format");
2697
- if (protocol !== "chat_completions" && format.type !== "text")
2698
- fail("unsupported_request", "Anthropic structured output requires a separate provider capability");
2699
- if (format.type === "json_schema")
2700
- result.response_format = { type: "json_schema", json_schema: { name: format.name, schema: format.schema, ...format.strict !== undefined ? { strict: format.strict } : {}, ...format.description !== undefined ? { description: format.description } : {} } };
2701
- else if (format.type === "json_object" || format.type === "text") {
2702
- if (protocol === "chat_completions")
2703
- result.response_format = format;
2704
- } else
2705
- fail("unsupported_request", "Unknown text format");
2706
- }
2707
- }
2859
+ });
2708
2860
  if (protocol === "anthropic_messages") {
2709
2861
  const system = [];
2710
2862
  const messages = [];
@@ -2718,7 +2870,7 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2718
2870
  for (const msg of chat) {
2719
2871
  if (msg.role === "system" || msg.role === "developer") {
2720
2872
  if (messages.length)
2721
- fail("unsupported_content", "Mid-conversation system instructions cannot be relocated safely");
2873
+ fail("unsupported_content", "Mid-conversation system instructions cannot be relocated safely", `${messageParams.get(msg)}.role`);
2722
2874
  system.push(typeof msg.content === "string" ? msg.content : msg.content.map((p) => string(p.text, "system text")).join(""));
2723
2875
  continue;
2724
2876
  }
@@ -2744,7 +2896,7 @@ function decodeResponsesRequest(raw, protocol, capabilities = {}) {
2744
2896
  delete result.parallel_tool_calls;
2745
2897
  }
2746
2898
  }
2747
- return { body: result, toolNames };
2899
+ return { body: result, toolNames, canonicalInput, diagnostics };
2748
2900
  }
2749
2901
  // packages/llms/src/responses-codec/events.ts
2750
2902
  class ResponsesEventEncoder {
@@ -3157,6 +3309,11 @@ function encodeResponsesResult(raw, protocol, model, toolNames = new Map, config
3157
3309
  fail("missing_terminal", "JSON response has no terminal result");
3158
3310
  return record(end.response, "Responses result");
3159
3311
  }
3312
+ // plugins/codex-router/server/history.ts
3313
+ function serializeHistory(value) {
3314
+ return JSON.stringify(value).replace(/[\uD800-\uDFFF]/g, (unit) => "\\u" + unit.charCodeAt(0).toString(16).padStart(4, "0"));
3315
+ }
3316
+
3160
3317
  // packages/core/src/plugin-logger.ts
3161
3318
  var sinkKey = Symbol.for("@jeffusion/bungee/plugin-log-sink");
3162
3319
  function createLogger(bindings = {}) {
@@ -3210,16 +3367,26 @@ function protocolSSEOutput(payloads, protocol, source) {
3210
3367
  // packages/core/src/plugin-extensions.ts
3211
3368
  var ANONYMOUS_PRINCIPAL = Object.freeze({ domain: "anonymous", keyId: "", credentialVersion: 0 });
3212
3369
  // packages/core/src/data-admission/errors.ts
3370
+ function safeDetails(details) {
3371
+ if (!details)
3372
+ return;
3373
+ const message = typeof details.message === "string" && details.message.length <= 512 && !/[\x00-\x1f\x7f]/.test(details.message) ? details.message : undefined;
3374
+ const param = typeof details.param === "string" && details.param.length <= 256 && /^[a-zA-Z_$][\w.$\[\]-]*$/.test(details.param) ? details.param : undefined;
3375
+ return message === undefined && param === undefined ? undefined : Object.freeze({ message, param });
3376
+ }
3377
+
3213
3378
  class DataAdmissionError extends Error {
3214
3379
  status;
3215
3380
  code;
3216
3381
  retryAfter;
3217
3382
  name = "DataAdmissionError";
3218
- constructor(status, code, retryAfter) {
3383
+ details;
3384
+ constructor(status, code, retryAfter, details) {
3219
3385
  super(code);
3220
3386
  this.status = status;
3221
3387
  this.code = code;
3222
3388
  this.retryAfter = retryAfter;
3389
+ this.details = safeDetails(details);
3223
3390
  }
3224
3391
  }
3225
3392
  // packages/core/src/plugin-durable-state.ts
@@ -3751,7 +3918,7 @@ function historyClient(rpc) {
3751
3918
  throw new Error("codex_router_history_limit");
3752
3919
  },
3753
3920
  async put({ scope, id, value }, options) {
3754
- const text = JSON.stringify(value), total = Math.ceil(text.length / 8192);
3921
+ const text = serializeHistory(value), total = Math.ceil(text.length / 8192);
3755
3922
  if (total > 1024 || Buffer.byteLength(text) > 8 * 1024 * 1024)
3756
3923
  throw new Error("codex_router_history_limit");
3757
3924
  for (let index = 0;index < total; index++) {
@@ -3766,12 +3933,13 @@ function historyClient(rpc) {
3766
3933
  function protocolAdapter(input) {
3767
3934
  const encoder = input.protocol ? new ResponsesEventEncoder(input.protocol, input.model, input.toolNames) : undefined;
3768
3935
  let nativeTerminal;
3936
+ let sourceContext;
3769
3937
  let flushed = false;
3770
3938
  const terminal = async (events) => {
3771
3939
  for (const event of events)
3772
3940
  if (event.response && ["response.completed", "response.incomplete"].includes(event.type)) {
3773
3941
  input.signal.throwIfAborted();
3774
- await input.save(event.response);
3942
+ await input.save(event.response, sourceContext);
3775
3943
  }
3776
3944
  return events;
3777
3945
  };
@@ -3787,7 +3955,7 @@ function protocolAdapter(input) {
3787
3955
  const body = input.protocol ? encodeResponsesResult(raw, input.protocol, input.model, input.toolNames) : { ...raw, model: input.model };
3788
3956
  if (["completed", "incomplete"].includes(body.status)) {
3789
3957
  input.signal.throwIfAborted();
3790
- await input.save(body);
3958
+ await input.save(body, context);
3791
3959
  }
3792
3960
  const headers = new Headers(response.headers);
3793
3961
  headers.delete("content-length");
@@ -3795,7 +3963,8 @@ function protocolAdapter(input) {
3795
3963
  headers.set("content-type", "application/json");
3796
3964
  return new Response(JSON.stringify(body), { status: response.status, headers });
3797
3965
  });
3798
- hooks.onStreamChunk.tapPromise("codex-router.protocol", async (envelope) => {
3966
+ hooks.onStreamChunk.tapPromise("codex-router.protocol", async (envelope, context) => {
3967
+ sourceContext = context;
3799
3968
  if (!encoder) {
3800
3969
  const event = envelope.json;
3801
3970
  if (event?.response && ["response.completed", "response.incomplete"].includes(event.type))
@@ -3817,7 +3986,7 @@ function protocolAdapter(input) {
3817
3986
  return [...chunks, ...protocolSSEOutput(await terminal(encoder.finish()), "responses")];
3818
3987
  if (nativeTerminal) {
3819
3988
  input.signal.throwIfAborted();
3820
- await input.save(nativeTerminal);
3989
+ await input.save(nativeTerminal, sourceContext);
3821
3990
  }
3822
3991
  return chunks;
3823
3992
  });
@@ -3862,7 +4031,6 @@ var manifest_default = {
3862
4031
  "dynamicRuntimeLoad",
3863
4032
  "controlPlane"
3864
4033
  ],
3865
- uiExtensionMode: "none",
3866
4034
  engines: {
3867
4035
  bungee: "^4.2.0 || ^5.0.0"
3868
4036
  },
@@ -3935,10 +4103,41 @@ var manifest_default = {
3935
4103
  type: "object",
3936
4104
  label: "Model",
3937
4105
  properties: [
4106
+ {
4107
+ name: "source",
4108
+ type: "string",
4109
+ label: "editor.source",
4110
+ required: false,
4111
+ validation: {
4112
+ trimmed: true,
4113
+ max: 512
4114
+ }
4115
+ },
4116
+ {
4117
+ name: "sourceProvider",
4118
+ type: "string",
4119
+ label: "editor.sourceProvider",
4120
+ required: false,
4121
+ validation: {
4122
+ trimmed: true,
4123
+ max: 512
4124
+ }
4125
+ },
4126
+ {
4127
+ name: "sourceProtocol",
4128
+ type: "select",
4129
+ label: "editor.sourceProtocol",
4130
+ options: [
4131
+ {
4132
+ value: "responses",
4133
+ label: "Responses"
4134
+ }
4135
+ ]
4136
+ },
3938
4137
  {
3939
4138
  name: "provider",
3940
4139
  type: "string",
3941
- label: "provider",
4140
+ label: "editor.provider",
3942
4141
  required: true,
3943
4142
  validation: {
3944
4143
  trimmed: true,
@@ -3948,7 +4147,7 @@ var manifest_default = {
3948
4147
  {
3949
4148
  name: "model",
3950
4149
  type: "string",
3951
- label: "model",
4150
+ label: "editor.model",
3952
4151
  required: true,
3953
4152
  validation: {
3954
4153
  trimmed: true,
@@ -3968,20 +4167,19 @@ var manifest_default = {
3968
4167
  {
3969
4168
  name: "target",
3970
4169
  type: "gateway_target",
3971
- label: "Target",
4170
+ label: "editor.target",
3972
4171
  required: true
3973
4172
  },
3974
4173
  {
3975
4174
  name: "capabilityOverrides",
3976
4175
  type: "json",
3977
- label: "Capabilities"
4176
+ label: "editor.advanced"
3978
4177
  }
3979
4178
  ]
3980
4179
  }
3981
4180
  }
3982
4181
  ],
3983
4182
  contributes: {
3984
- settings: "/catalog",
3985
4183
  api: [
3986
4184
  {
3987
4185
  path: "/catalog",
@@ -3996,19 +4194,109 @@ var manifest_default = {
3996
4194
  },
3997
4195
  translations: {
3998
4196
  "zh-CN": {
4197
+ "editor.loaded": "\u5DF2\u52A0\u8F7D",
4198
+ "editor.complete": "\u5DF2\u5168\u90E8\u52A0\u8F7D",
4199
+ "editor.loadMore": "\u52A0\u8F7D\u66F4\u591A",
3999
4200
  "metadata.name": "Codex \u6A21\u578B\u8DEF\u7531",
4000
4201
  "plugin.description": "\u4ECE\u5171\u4EAB\u76EE\u5F55\u9009\u62E9\u6A21\u578B\u5E76\u7ED1\u5B9A\u5DF2\u6709\u8DEF\u7531\u6216\u670D\u52A1",
4001
- "models.label": "\u6A21\u578B\u7ED1\u5B9A"
4202
+ "models.label": "\u6A21\u578B\u7ED1\u5B9A",
4203
+ "editor.description": "\u6309\u539F\u59CB\u6A21\u578B\u5339\u914D\u8BF7\u6C42\uFF0C\u5C06\u76EE\u6807\u6A21\u578B\u8F6C\u53D1\u5230\u6307\u5B9A\u8DEF\u7531\u6216\u670D\u52A1\u3002",
4204
+ "editor.binding": "\u7ED1\u5B9A",
4205
+ "editor.match": "\u5339\u914D\u8BF7\u6C42",
4206
+ "editor.sourceProvider": "\u539F\u59CB\u63D0\u4F9B\u5546",
4207
+ "editor.manual": "\u624B\u52A8\u8F93\u5165",
4208
+ "editor.fromCatalog": "\u4ECE\u76EE\u5F55\u9009\u62E9",
4209
+ "editor.source": "\u539F\u59CB\u6A21\u578B",
4210
+ "editor.sourceHint": "\u7CBE\u786E\u5339\u914D\u5BA2\u6237\u7AEF\u8BF7\u6C42\u4E2D\u7684\u6A21\u578B\u6807\u8BC6\uFF1B\u4F9B\u5E94\u5546\u4EC5\u7528\u4E8E\u76EE\u5F55\u7B5B\u9009\u3002",
4211
+ "editor.sourcePlaceholder": "\u4F8B\u5982 gpt-5.4",
4212
+ "editor.provider": "\u76EE\u6807\u63D0\u4F9B\u5546",
4213
+ "editor.model": "\u76EE\u6807\u6A21\u578B",
4214
+ "editor.target": "\u8F6C\u53D1\u8DEF\u7531 / \u670D\u52A1",
4215
+ "editor.targetPlaceholder": "\u9009\u62E9\u5DF2\u6709\u8DEF\u7531\u6216\u670D\u52A1",
4216
+ "editor.search": "\u641C\u7D22\u6A21\u578B",
4217
+ "editor.empty": "\u6CA1\u6709\u5339\u914D\u7684\u6A21\u578B",
4218
+ "editor.loading": "\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u76EE\u5F55",
4219
+ "editor.error": "\u76EE\u5F55\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5 models.dev \u6216\u91CD\u8BD5\u3002",
4220
+ "editor.targetsError": "\u65E0\u6CD5\u8BFB\u53D6\u8F6C\u53D1\u76EE\u6807\uFF0C\u8BF7\u91CD\u8BD5\u3002",
4221
+ "editor.retry": "\u91CD\u8BD5",
4222
+ "editor.add": "\u6DFB\u52A0\u6A21\u578B\u7ED1\u5B9A",
4223
+ "editor.remove": "\u79FB\u9664",
4224
+ "editor.noBindings": "\u5C1A\u672A\u914D\u7F6E\u6A21\u578B\u7ED1\u5B9A\u3002",
4225
+ "editor.noCatalog": "\u76EE\u5F55\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u542F\u7528 models.dev \u5E76\u5237\u65B0\u76EE\u5F55\u3002",
4226
+ "editor.advanced": "\u80FD\u529B\u9650\u5236\uFF08\u53EF\u9009\uFF09",
4227
+ "editor.context": "\u4E0A\u4E0B\u6587\u957F\u5EA6\u4E0A\u9650",
4228
+ "editor.contextHint": "\u7559\u7A7A\u4F7F\u7528\u76EE\u6807\u6A21\u578B\u7684\u4E0A\u4E0B\u6587\u957F\u5EA6\u3002",
4229
+ "editor.toolsOff": "\u7981\u7528\u5DE5\u5177\u8C03\u7528",
4230
+ "editor.imagesOff": "\u7981\u7528\u56FE\u7247\u8F93\u5165",
4231
+ "editor.reasoningOff": "\u7981\u7528\u63A8\u7406\u80FD\u529B",
4232
+ "editor.reasoningEffort": "\u4F20\u9012\u63A8\u7406\u5F3A\u5EA6\uFF08\u76EE\u6807\u9700\u652F\u6301\uFF09",
4233
+ "editor.increase": "\u589E\u52A0\u4E0A\u4E0B\u6587\u4E0A\u9650",
4234
+ "editor.decrease": "\u51CF\u5C11\u4E0A\u4E0B\u6587\u4E0A\u9650",
4235
+ "editor.contextInvalid": "\u8BF7\u8F93\u5165\u6B63\u6574\u6570\u3002",
4236
+ "editor.sourceProtocol": "\u6E90\u6A21\u578B\u534F\u8BAE",
4237
+ "editor.targetProtocol": "\u76EE\u6807\u6A21\u578B\u534F\u8BAE",
4238
+ "editor.targetProtocolPlaceholder": "\u9009\u62E9\u76EE\u6807\u63A5\u6536\u534F\u8BAE",
4239
+ "editor.route": "\u8DEF\u7531",
4240
+ "editor.service": "\u670D\u52A1",
4241
+ "editor.targetUnavailable": "\u76EE\u6807\u4E0D\u53EF\u7528",
4242
+ "editor.noOptions": "\u6CA1\u6709\u5339\u914D\u7684\u9009\u9879"
4002
4243
  },
4003
4244
  en: {
4245
+ "editor.loaded": "Loaded",
4246
+ "editor.complete": "All loaded",
4247
+ "editor.loadMore": "Load more",
4004
4248
  "metadata.name": "Codex Router",
4005
4249
  "plugin.description": "Bind catalog models to existing routes or services",
4006
- "models.label": "Model bindings"
4250
+ "models.label": "Model bindings",
4251
+ "editor.description": "Match the original model and forward the target model through an existing route or service.",
4252
+ "editor.binding": "Binding",
4253
+ "editor.match": "Match request",
4254
+ "editor.sourceProvider": "Original provider",
4255
+ "editor.manual": "Enter manually",
4256
+ "editor.fromCatalog": "Choose from catalog",
4257
+ "editor.source": "Original model",
4258
+ "editor.sourceHint": "Match the exact model ID in client requests; the provider only filters the catalog.",
4259
+ "editor.sourcePlaceholder": "For example, gpt-5.4",
4260
+ "editor.provider": "Target provider",
4261
+ "editor.model": "Target model",
4262
+ "editor.target": "Forwarding route / service",
4263
+ "editor.targetPlaceholder": "Select an existing route or service",
4264
+ "editor.search": "Search models",
4265
+ "editor.empty": "No matching models",
4266
+ "editor.loading": "Loading model catalog",
4267
+ "editor.error": "Catalog unavailable. Check models.dev or retry.",
4268
+ "editor.targetsError": "Unable to load forwarding targets. Please retry.",
4269
+ "editor.retry": "Retry",
4270
+ "editor.add": "Add model binding",
4271
+ "editor.remove": "Remove",
4272
+ "editor.noBindings": "No model bindings configured.",
4273
+ "editor.noCatalog": "The catalog is empty. Enable models.dev and refresh it first.",
4274
+ "editor.advanced": "Capability limits (optional)",
4275
+ "editor.context": "Context length limit",
4276
+ "editor.contextHint": "Leave empty to use the target model context length.",
4277
+ "editor.toolsOff": "Disable tool calls",
4278
+ "editor.imagesOff": "Disable image input",
4279
+ "editor.reasoningOff": "Disable reasoning",
4280
+ "editor.reasoningEffort": "Forward reasoning effort (requires target support)",
4281
+ "editor.increase": "Increase context limit",
4282
+ "editor.decrease": "Decrease context limit",
4283
+ "editor.contextInvalid": "Enter a positive whole number.",
4284
+ "editor.sourceProtocol": "Source protocol",
4285
+ "editor.targetProtocol": "Target protocol",
4286
+ "editor.targetProtocolPlaceholder": "Select the target receiving protocol",
4287
+ "editor.route": "Route",
4288
+ "editor.service": "Service",
4289
+ "editor.targetUnavailable": "Target unavailable",
4290
+ "editor.noOptions": "No matching options"
4007
4291
  }
4008
- }
4292
+ },
4293
+ uiExtensionMode: "none"
4009
4294
  };
4010
4295
 
4011
4296
  // plugins/codex-router/server/config.ts
4297
+ function bindingSource(binding) {
4298
+ return binding.source ?? binding.alias ?? binding.model;
4299
+ }
4012
4300
  function parseBindings(value) {
4013
4301
  if (value === undefined)
4014
4302
  return [];
@@ -4016,16 +4304,22 @@ function parseBindings(value) {
4016
4304
  throw new Error("codex_router_invalid_bindings");
4017
4305
  const ids = new Set;
4018
4306
  return Object.freeze(value.map((entry) => {
4019
- if (!entry || typeof entry !== "object" || Array.isArray(entry) || Object.keys(entry).some((key) => !["provider", "model", "alias", "target", "capabilityOverrides"].includes(key)))
4307
+ if (!entry || typeof entry !== "object" || Array.isArray(entry) || Object.keys(entry).some((key) => !["provider", "model", "source", "sourceProvider", "sourceProtocol", "alias", "target", "capabilityOverrides"].includes(key)) || entry.source !== undefined && entry.alias !== undefined || entry.sourceProvider !== undefined && entry.source === undefined)
4020
4308
  throw new Error("codex_router_invalid_bindings");
4021
- for (const text of [entry.provider, entry.model, ...entry.alias === undefined ? [] : [entry.alias]]) {
4309
+ for (const text of [entry.provider, entry.model, ...entry.source === undefined ? [] : [entry.source], ...entry.sourceProvider === undefined ? [] : [entry.sourceProvider], ...entry.alias === undefined ? [] : [entry.alias]]) {
4022
4310
  if (typeof text !== "string" || !text || text !== text.trim() || text.length > 512 || /[\x00-\x1f\x7f]/.test(text))
4023
4311
  throw new Error("codex_router_invalid_bindings");
4024
4312
  }
4313
+ if (entry.sourceProtocol !== undefined && entry.sourceProtocol !== "responses")
4314
+ throw new Error("codex_router_source_protocol_unsupported");
4025
4315
  const target = entry.target;
4026
- if (!target || !["route", "service"].includes(target.type) || typeof target.id !== "string" || !target.id || Object.keys(target).some((key) => !["type", "id"].includes(key)))
4316
+ if (!target || !["route", "service"].includes(target.type) || typeof target.id !== "string" || !target.id || Object.keys(target).some((key) => !["type", "id", "protocol"].includes(key)))
4027
4317
  throw new Error("codex_router_invalid_target");
4028
- const publicId = entry.alias ?? entry.model;
4318
+ if (target.protocol === undefined)
4319
+ throw new Error("codex_router_target_protocol_required");
4320
+ if (!["responses", "chat_completions", "anthropic_messages"].includes(target.protocol))
4321
+ throw new Error("codex_router_target_protocol_invalid");
4322
+ const publicId = bindingSource(entry);
4029
4323
  if (ids.has(publicId))
4030
4324
  throw new Error("codex_router_model_conflict");
4031
4325
  ids.add(publicId);
@@ -4068,6 +4362,14 @@ var model_template_0_160_1_default = {
4068
4362
  priority: 100,
4069
4363
  default_reasoning_level: "none",
4070
4364
  supported_reasoning_levels: [],
4365
+ experimental_supported_tools: [],
4366
+ upgrade: null,
4367
+ availability_nux: null,
4368
+ model_messages: null,
4369
+ include_skills_usage_instructions: false,
4370
+ include_plugin_usage_instructions: false,
4371
+ include_apps_usage_instructions: false,
4372
+ supports_reasoning_summary_parameter: false,
4071
4373
  base_instructions: "",
4072
4374
  supports_search_tool: false,
4073
4375
  supports_parallel_tool_calls: false,
@@ -4090,22 +4392,34 @@ function mergeCatalog(native, bindings, catalog) {
4090
4392
  const existing = rich ? body.models : body.data;
4091
4393
  const ids = new Set(existing.map((entry) => rich ? entry.slug : entry.id));
4092
4394
  const added = [];
4395
+ const replacements = new Map;
4396
+ const unavailable = new Set;
4397
+ const bound = new Set;
4093
4398
  for (const binding of bindings) {
4094
- const id = binding.alias ?? binding.model;
4095
- if (ids.has(id))
4399
+ const id = bindingSource(binding);
4400
+ if (bound.has(id) || ids.has(id) && binding.source === undefined)
4096
4401
  throw new Error("codex_router_model_conflict");
4402
+ bound.add(id);
4403
+ const replace = ids.has(id);
4097
4404
  ids.add(id);
4098
4405
  const info = catalog.model(binding);
4099
- if (!info || info.contextWindow === null || !info.inputModalities.includes("text"))
4406
+ if (!info || info.contextWindow === null || !info.inputModalities.includes("text")) {
4407
+ if (replace)
4408
+ unavailable.add(id);
4100
4409
  continue;
4410
+ }
4101
4411
  if (!rich) {
4102
- added.push({ id, object: "model", owned_by: binding.provider });
4412
+ const model = { id, object: "model", owned_by: binding.provider };
4413
+ if (replace)
4414
+ replacements.set(id, model);
4415
+ else
4416
+ added.push(model);
4103
4417
  continue;
4104
4418
  }
4105
4419
  const overrides = binding.capabilityOverrides;
4106
4420
  const context = Math.min(info.contextWindow, overrides?.contextWindow ?? info.contextWindow);
4107
4421
  const reasoning = info.reasoning && overrides?.reasoning !== false;
4108
- added.push({
4422
+ const model = {
4109
4423
  ...model_template_0_160_1_default,
4110
4424
  slug: id,
4111
4425
  display_name: info.name,
@@ -4117,9 +4431,13 @@ function mergeCatalog(native, bindings, catalog) {
4117
4431
  supported_reasoning_levels: reasoning ? ["low", "medium", "high"].map((effort) => ({ effort, description: effort })) : [],
4118
4432
  default_reasoning_level: reasoning ? "medium" : "none",
4119
4433
  support_verbosity: false
4120
- });
4434
+ };
4435
+ if (replace)
4436
+ replacements.set(id, model);
4437
+ else
4438
+ added.push(model);
4121
4439
  }
4122
- return { ...body, [rich ? "models" : "data"]: [...existing, ...added] };
4440
+ return { ...body, [rich ? "models" : "data"]: [...existing.filter((entry) => !unavailable.has(rich ? entry.slug : entry.id)).map((entry) => replacements.get(rich ? entry.slug : entry.id) ?? entry), ...added] };
4123
4441
  }
4124
4442
  function catalogEtag(body, version, clientVersion) {
4125
4443
  return `"${createHash("sha256").update(JSON.stringify([body, version, clientVersion])).digest("hex")}"`;
@@ -4132,7 +4450,6 @@ var CodexRouterPlugin = definePlugin(class {
4132
4450
  static configSchema = manifest_default.configSchema;
4133
4451
  bindings;
4134
4452
  history;
4135
- targets = [];
4136
4453
  catalog;
4137
4454
  constructor(options) {
4138
4455
  this.bindings = parseBindings(options?.models);
@@ -4140,7 +4457,6 @@ var CodexRouterPlugin = definePlugin(class {
4140
4457
  async init(context) {
4141
4458
  if (context.scope?.type !== "route")
4142
4459
  throw new Error("codex_router_requires_route_scope");
4143
- this.targets = context.dispatchTargets ?? [];
4144
4460
  this.history = historyClient(context.services.rpc.consume("codex-router", historyRpc));
4145
4461
  this.catalog = context.services.consume("models-dev", MODELS_DEV_CAPABILITIES_SERVICE_ID, MODELS_DEV_CAPABILITIES_CONTRACT_VERSION);
4146
4462
  }
@@ -4148,20 +4464,23 @@ var CodexRouterPlugin = definePlugin(class {
4148
4464
  return { request: context.method === "POST" && /\/responses$/.test(context.url.pathname) ? "json-write" : "none", response: context.method === "GET" && /\/models$/.test(context.url.pathname) ? ["json"] : [] };
4149
4465
  }
4150
4466
  register(hooks) {
4467
+ hooks.onWebSocketSessionMode.tap("codex-router.session", (url) => /\/responses$/.test(url.pathname) ? "responses" : undefined);
4151
4468
  hooks.onDispatchRequest.tapPromise("codex-router.binding", async ({ context, targets, principal, servingRevision, signal }) => {
4152
4469
  if (context.method !== "POST" || !/\/responses$/.test(context.originalUrl.pathname))
4153
4470
  return;
4154
- const binding = this.bindings.find((binding) => (binding.alias ?? binding.model) === context.body?.model);
4471
+ const binding = this.bindings.find((binding) => bindingSource(binding) === context.body?.model);
4155
4472
  if (!binding)
4156
4473
  return;
4157
- const target = targets.find((target) => target.type === binding.target.type && target.id === binding.target.id);
4158
- if (!target?.protocol)
4159
- throw new DataAdmissionError(422, "codex_router_target_protocol_required");
4474
+ if (!targets.some((target) => target.type === binding.target.type && target.id === binding.target.id))
4475
+ throw new DataAdmissionError(422, "codex_router_target_unavailable");
4476
+ const protocol = binding.target.protocol;
4160
4477
  const capabilities = this.catalog.model(binding);
4161
- if (!capabilities)
4478
+ if (!capabilities || capabilities.contextWindow === null || !capabilities.inputModalities.includes("text"))
4162
4479
  throw new DataAdmissionError(503, "codex_router_model_unavailable");
4163
4480
  const scope = principal?.domain !== "anonymous" && principal?.keyId ? createHash2("sha256").update(JSON.stringify([principal, context.routeId, servingRevision ?? null, this.bindings])).digest("hex") : null;
4164
4481
  const input = structuredClone(context.body);
4482
+ let requiredUpstreamId;
4483
+ let opaqueRestored = false;
4165
4484
  if (input.previous_response_id) {
4166
4485
  if (!scope)
4167
4486
  throw new DataAdmissionError(422, "codex_router_history_identity_required");
@@ -4171,46 +4490,60 @@ var CodexRouterPlugin = definePlugin(class {
4171
4490
  const delta = typeof input.input === "string" ? [{ role: "user", content: input.input }] : input.input;
4172
4491
  if (!Array.isArray(delta))
4173
4492
  throw new DataAdmissionError(422, "codex_router_invalid_history");
4174
- if (JSON.stringify(prior.target) !== JSON.stringify(binding.target) && prior.items.some((item) => item.encrypted_content || item.type === "compaction"))
4175
- throw new DataAdmissionError(422, "codex_router_unrestorable_history_start_new_conversation");
4493
+ if (prior.items.some((item) => item.encrypted_content || item.type === "compaction")) {
4494
+ if (protocol !== "responses" || JSON.stringify(prior.target) !== JSON.stringify(binding.target) || prior.origin?.provider !== binding.provider || prior.origin?.model !== binding.model || !prior.origin?.upstreamId)
4495
+ throw new DataAdmissionError(422, "codex_router_unrestorable_history_start_new_conversation");
4496
+ requiredUpstreamId = prior.origin.upstreamId;
4497
+ opaqueRestored = true;
4498
+ }
4176
4499
  input.input = [...prior.items, ...delta];
4177
4500
  delete input.previous_response_id;
4178
4501
  }
4179
4502
  if (input.conversation || input.response_id)
4180
4503
  throw new DataAdmissionError(422, "codex_router_unsupported_history_reference");
4181
4504
  input.model = binding.model;
4182
- const logicalInput = typeof input.input === "string" ? [{ role: "user", content: input.input }] : input.input;
4505
+ let logicalInput = typeof input.input === "string" ? [{ role: "user", content: input.input }] : input.input;
4183
4506
  if (!Array.isArray(logicalInput))
4184
4507
  throw new DataAdmissionError(422, "codex_router_invalid_history");
4508
+ if (!opaqueRestored && logicalInput.some((item) => item?.encrypted_content || item?.type === "compaction"))
4509
+ throw new DataAdmissionError(422, "codex_router_unrestorable_history_start_new_conversation");
4510
+ const hasTools = input.tools?.length || logicalInput.some((item) => item?.type === "additional_tools" && item.tools?.length);
4511
+ if (hasTools && (!capabilities.toolCall || binding.capabilityOverrides?.tools === false))
4512
+ throw new DataAdmissionError(422, "codex_router_tools_unsupported");
4185
4513
  let toolNames;
4186
- if (target.protocol !== "responses") {
4514
+ let diagnostics;
4515
+ if (protocol !== "responses") {
4187
4516
  try {
4188
- const decoded = decodeResponsesRequest(input, target.protocol, {
4517
+ const decoded = decodeResponsesRequest(input, protocol, {
4518
+ omitOptionalWebSearch: true,
4189
4519
  reasoningEffort: capabilities.reasoning && binding.capabilityOverrides?.reasoningEffort === true,
4190
- anthropicThinkingBudget: binding.capabilityOverrides?.anthropicThinkingBudget,
4520
+ reasoningHistory: capabilities.reasoning && binding.capabilityOverrides?.reasoning !== false,
4521
+ anthropicThinkingBudget: undefined,
4191
4522
  maxOutputTokens: capabilities.outputLimit ?? undefined
4192
4523
  });
4193
4524
  context.body = decoded.body;
4194
4525
  toolNames = decoded.toolNames;
4526
+ logicalInput = decoded.canonicalInput;
4527
+ diagnostics = decoded.diagnostics;
4195
4528
  } catch (error) {
4196
4529
  if (error instanceof ResponsesCodecError)
4197
- throw new DataAdmissionError(422, `codex_router_${error.code}`);
4530
+ throw new DataAdmissionError(422, `codex_router_${error.code}`, undefined, { message: error.message, param: error.param });
4198
4531
  throw error;
4199
4532
  }
4200
- context.url.pathname = context.url.pathname.replace(/\/responses$/, target.protocol === "chat_completions" ? "/chat/completions" : "/messages");
4533
+ context.url.pathname = context.url.pathname.replace(/\/responses$/, protocol === "chat_completions" ? "/chat/completions" : "/messages");
4201
4534
  } else
4202
4535
  context.body = input;
4203
4536
  const adapter = protocolAdapter({
4204
- protocol: target.protocol === "responses" ? undefined : target.protocol,
4205
- model: binding.alias ?? binding.model,
4537
+ protocol: protocol === "responses" ? undefined : protocol,
4538
+ model: bindingSource(binding),
4206
4539
  toolNames,
4207
4540
  signal,
4208
- save: async (response) => {
4541
+ save: async (response, responseContext) => {
4209
4542
  if (scope && typeof response.id === "string")
4210
- await this.history.put({ scope, id: response.id, value: { target: binding.target, items: [...logicalInput, ...response.output] } }, { signal, operationId: crypto.randomUUID() });
4543
+ await this.history.put({ scope, id: response.id, value: { target: binding.target, origin: { provider: binding.provider, model: binding.model, upstreamId: responseContext?.upstreamId }, items: [...logicalInput, ...response.output] } }, { signal, operationId: crypto.randomUUID() });
4211
4544
  }
4212
4545
  });
4213
- return { target: binding.target, adapter };
4546
+ return { target: binding.target, protocol, requiredUpstreamId, adapter, ...protocol !== "responses" ? { canonicalInput: logicalInput, diagnostics } : {} };
4214
4547
  });
4215
4548
  hooks.onBeforeRequest.tap("codex-router.catalog-validator", (context) => {
4216
4549
  if (context.method === "GET" && /\/models$/.test(context.originalUrl.pathname)) {
@@ -4225,11 +4558,11 @@ var CodexRouterPlugin = definePlugin(class {
4225
4558
  return response;
4226
4559
  const native = await context.bodyHandle.json({ id: "codex-router.catalog", mandatory: true });
4227
4560
  const bindings = this.bindings.map((binding) => {
4228
- const protocol = this.targets?.find((target) => target.type === binding.target.type && target.id === binding.target.id)?.protocol;
4561
+ const protocol = binding.target.protocol;
4229
4562
  return { ...binding, capabilityOverrides: {
4230
4563
  ...binding.capabilityOverrides,
4231
4564
  ...protocol === "anthropic_messages" ? { images: false } : {},
4232
- ...protocol && protocol !== "responses" ? { reasoning: protocol === "chat_completions" ? binding.capabilityOverrides?.reasoningEffort === true : !!binding.capabilityOverrides?.anthropicThinkingBudget } : {}
4565
+ ...protocol !== "responses" ? { reasoning: protocol === "chat_completions" ? binding.capabilityOverrides?.reasoningEffort === true : false } : {}
4233
4566
  } };
4234
4567
  });
4235
4568
  const body = mergeCatalog(native, bindings, this.catalog);
@@ -4249,4 +4582,4 @@ export {
4249
4582
  server_default as default
4250
4583
  };
4251
4584
 
4252
- //# debugId=E29014A25F222FA164756E2164756E21
4585
+ //# debugId=3BEC03CB648CDF5064756E2164756E21