@bitkyc08/opencodex 2.58.0 → 2.59.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 (188) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +1 -1
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/cursor/catalog.ts +11 -0
  10. package/src/adapters/cursor/effort-map.ts +16 -2
  11. package/src/adapters/cursor/envelope-echo.ts +55 -2
  12. package/src/adapters/cursor/message-mapper.ts +3 -2
  13. package/src/adapters/cursor/protobuf-request.ts +8 -5
  14. package/src/adapters/cursor/request-builder.ts +14 -3
  15. package/src/adapters/cursor/thread-continuity.ts +105 -31
  16. package/src/adapters/cursor/tool-guidance.ts +5 -4
  17. package/src/adapters/cursor.ts +42 -1
  18. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  19. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  20. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  21. package/src/adapters/devin.ts +75 -13
  22. package/src/adapters/google-antigravity-wire.ts +29 -2
  23. package/src/adapters/google-http.ts +8 -1
  24. package/src/adapters/google.ts +23 -4
  25. package/src/adapters/openai-chat/response-events.ts +61 -0
  26. package/src/adapters/openai-chat.ts +5 -10
  27. package/src/adapters/openai-responses/passthrough.ts +10 -1
  28. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  29. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  30. package/src/adapters/responses-tool-schema.ts +76 -46
  31. package/src/adapters/run-turn-queue.ts +17 -4
  32. package/src/bridge/response-json.ts +1 -1
  33. package/src/bridge/sse.ts +165 -24
  34. package/src/claude/context-windows.ts +22 -0
  35. package/src/claude/outbound.ts +35 -4
  36. package/src/cli/account-api.ts +4 -3
  37. package/src/cli/account-extended.ts +22 -2
  38. package/src/cli/account-orca-import.ts +63 -0
  39. package/src/cli/account.ts +32 -4
  40. package/src/cli/capabilities.ts +40 -0
  41. package/src/cli/claude.ts +29 -1
  42. package/src/cli/codex-cli-update.ts +97 -2
  43. package/src/cli/dispatch.ts +54 -0
  44. package/src/cli/doctor.ts +197 -2
  45. package/src/cli/help.ts +4 -1
  46. package/src/cli/index.ts +88 -20
  47. package/src/cli/models-runtime.ts +33 -4
  48. package/src/cli/registry.ts +11 -1
  49. package/src/cli/runtime-api.ts +44 -0
  50. package/src/cli/start-args.ts +94 -0
  51. package/src/cli/system-command.ts +2 -0
  52. package/src/client/machine-api.ts +4 -3
  53. package/src/client/machine-listener.ts +14 -1
  54. package/src/clients/config-export/constants.ts +2 -3
  55. package/src/clients/config-export.ts +5 -5
  56. package/src/codex/account-store.ts +81 -5
  57. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  58. package/src/codex/auth-api/routes.ts +17 -2
  59. package/src/codex/auth-context.ts +16 -12
  60. package/src/codex/catalog/build-entries.ts +25 -4
  61. package/src/codex/catalog/derive-entry.ts +8 -1
  62. package/src/codex/catalog/effort.ts +10 -6
  63. package/src/codex/catalog/gather-capture.ts +1 -0
  64. package/src/codex/catalog/model-hints.ts +37 -5
  65. package/src/codex/catalog/parsing.ts +83 -5
  66. package/src/codex/catalog/reserve-warn.ts +96 -0
  67. package/src/codex/catalog/retained-sync.ts +19 -0
  68. package/src/codex/catalog/routed-gather.ts +42 -3
  69. package/src/codex/cli-installation-identity.ts +210 -0
  70. package/src/codex/cli-installation-targets.ts +158 -0
  71. package/src/codex/convergence.ts +5 -0
  72. package/src/codex/history-provider.ts +4 -1
  73. package/src/codex/history-state-open.ts +105 -0
  74. package/src/codex/inject/config-toml.ts +44 -2
  75. package/src/codex/inject.ts +3 -2
  76. package/src/codex/lineage.ts +83 -32
  77. package/src/codex/loopback-target.ts +31 -0
  78. package/src/codex/main-account-hard-lock.ts +2 -1
  79. package/src/codex/main-account.ts +10 -3
  80. package/src/codex/main-device-reauth.ts +17 -9
  81. package/src/codex/model-entitlements.ts +60 -1
  82. package/src/codex/observed-model-denials.ts +137 -0
  83. package/src/codex/orca-auth-source.ts +94 -0
  84. package/src/codex/orca-import.ts +219 -0
  85. package/src/codex/prompt-text-probe.ts +282 -12
  86. package/src/codex/quota-401-recovery.ts +12 -0
  87. package/src/codex/quota-types.ts +65 -0
  88. package/src/codex/quota.ts +24 -19
  89. package/src/codex/routing/cooldown-math.ts +8 -47
  90. package/src/codex/routing/pin-drain.ts +57 -0
  91. package/src/codex/routing.ts +13 -15
  92. package/src/codex/subagent-model-fallback.ts +94 -0
  93. package/src/codex/windows-installation-files.ts +224 -0
  94. package/src/combos/failover.ts +122 -5
  95. package/src/config/diagnostics.ts +21 -0
  96. package/src/config/load-degrade.ts +15 -0
  97. package/src/config/pending-teardown.ts +8 -0
  98. package/src/config/process-state.ts +36 -3
  99. package/src/config/provider-relative-send-path.ts +16 -0
  100. package/src/config/proxy-env.ts +23 -5
  101. package/src/config/schema/config-schema.ts +21 -0
  102. package/src/config/schema/leaf-validators.ts +64 -17
  103. package/src/generated/compatibility-version.json +235 -163
  104. package/src/generated/model-metadata.ts +1 -1
  105. package/src/lib/bounded-body.ts +4 -2
  106. package/src/lib/destination-policy.ts +48 -6
  107. package/src/lib/errors.ts +3 -15
  108. package/src/lib/local-destinations.ts +32 -5
  109. package/src/lib/provider-outbound.ts +3 -3
  110. package/src/lib/proxy-env.ts +70 -3
  111. package/src/lib/request-execution-budget.ts +11 -3
  112. package/src/lib/response-body-inactivity.ts +193 -0
  113. package/src/lib/retry-delay.ts +69 -0
  114. package/src/lib/socks5-fetch.ts +631 -0
  115. package/src/lib/spend-reservation-ledger.ts +115 -9
  116. package/src/lib/workflow-budget.ts +145 -8
  117. package/src/oauth/account-quota-rank.ts +72 -15
  118. package/src/oauth/generic-account-failover.ts +40 -27
  119. package/src/oauth/orcarouter.ts +15 -2
  120. package/src/oauth/store.ts +8 -0
  121. package/src/providers/codex-capacity.ts +9 -0
  122. package/src/providers/devin-provider-merge-migration.ts +33 -12
  123. package/src/providers/free-directory.ts +20 -2
  124. package/src/providers/key-failover.ts +261 -7
  125. package/src/providers/model-rename-migration.ts +1 -0
  126. package/src/providers/openai-sidecar.ts +4 -0
  127. package/src/providers/opencode-go-transport.ts +14 -5
  128. package/src/providers/quota/report-cache.ts +3 -0
  129. package/src/providers/registry/entries-extended.ts +96 -0
  130. package/src/providers/registry/model-seeds.ts +78 -21
  131. package/src/responses/apply-patch-envelope.ts +44 -11
  132. package/src/responses/bridge-search-replay-cache.ts +152 -0
  133. package/src/responses/code-mode-helper-compat.ts +26 -16
  134. package/src/responses/custom-tool-compat.ts +1 -1
  135. package/src/responses/hosted-tool-policy.ts +85 -2
  136. package/src/responses/schema.ts +9 -2
  137. package/src/server/auth-cors.ts +26 -0
  138. package/src/server/chat-completions.ts +9 -4
  139. package/src/server/chat-native-sse.ts +26 -9
  140. package/src/server/chat-native.ts +10 -4
  141. package/src/server/claude-messages.ts +24 -2
  142. package/src/server/gui-static.ts +36 -2
  143. package/src/server/inbound-body-admission.ts +187 -0
  144. package/src/server/index.ts +15 -19
  145. package/src/server/management/api-access.ts +3 -4
  146. package/src/server/management/config-routes.ts +31 -6
  147. package/src/server/management/provider-capability-config.ts +35 -7
  148. package/src/server/management/provider-routes.ts +70 -18
  149. package/src/server/proxy-liveness.ts +97 -2
  150. package/src/server/relay.ts +17 -24
  151. package/src/server/request-log.ts +25 -1
  152. package/src/server/responses/adapter-continuation.ts +71 -27
  153. package/src/server/responses/adapter-delivery.ts +39 -8
  154. package/src/server/responses/adapter-dispatch.ts +52 -24
  155. package/src/server/responses/compact.ts +60 -11
  156. package/src/server/responses/core-codex-account.ts +83 -22
  157. package/src/server/responses/core-normalize.ts +12 -5
  158. package/src/server/responses/fetch-helpers.ts +68 -2
  159. package/src/server/responses/passthrough-delivery.ts +10 -1
  160. package/src/server/responses/passthrough-dispatch.ts +113 -48
  161. package/src/server/responses/passthrough-execution.ts +11 -1
  162. package/src/server/responses/request-prepare.ts +29 -0
  163. package/src/server/responses/request-send-budget.ts +84 -7
  164. package/src/server/responses/request-sidecar-auth.ts +16 -8
  165. package/src/server/responses/request-spend.ts +38 -9
  166. package/src/server/responses/request-transport.ts +13 -10
  167. package/src/server/responses/run-turn-execution.ts +20 -5
  168. package/src/server/responses/sidecar-execution.ts +2 -0
  169. package/src/server/responses/ws-upstream.ts +2 -1
  170. package/src/server/responses-custom-tool-repair.ts +2 -2
  171. package/src/server/sse-frame-buffer.ts +12 -10
  172. package/src/server/sse-payload-rewrite.ts +36 -9
  173. package/src/server/system-env-shell.ts +5 -1
  174. package/src/server/system-env.ts +7 -1
  175. package/src/server/workflow-refusal.ts +56 -2
  176. package/src/service/cli.ts +16 -6
  177. package/src/service/guards.ts +10 -0
  178. package/src/service/health.ts +43 -0
  179. package/src/service/state.ts +7 -2
  180. package/src/types/accounts.ts +4 -0
  181. package/src/types/config.ts +100 -3
  182. package/src/types/provider.ts +19 -0
  183. package/src/types/request.ts +7 -1
  184. package/src/types/wire.ts +9 -1
  185. package/src/usage/expected-prices.ts +28 -0
  186. package/src/usage/log.ts +87 -4
  187. package/src/web-search/passthrough-bridge.ts +39 -5
  188. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
@@ -315,6 +315,13 @@ export const DEEPSEEK_VISION_PREVIEW_MODEL = "deepseek-v4-flash-vision-exp";
315
315
  */
316
316
  export const COMMAND_CODE_IMAGE_MODELS = [
317
317
  `deepseek/${DEEPSEEK_VISION_PREVIEW_MODEL}`,
318
+ // Probed 2026-09-18 through a running 2.58.0 proxy: a 3x3 random-color grid
319
+ // (180x180 PNG, six candidate colors) came back 9/9 correct both as a user
320
+ // message and as a tool_result, and the request logs show the route served
321
+ // the image natively — no vision-sidecar call in either window. #4505 asked
322
+ // for exactly this upstream probe before promoting the id. The sibling
323
+ // deepseek/deepseek-v4-flash route remains verified-negative above.
324
+ "deepseek/deepseek-v4.1-flash",
318
325
  "gpt-5.6-luna",
319
326
  "gpt-5.6-sol",
320
327
  "MiniMaxAI/MiniMax-M3",
@@ -334,20 +341,19 @@ export const COMMAND_CODE_IMAGE_MODELS = [
334
341
  /**
335
342
  * Native image stays sourced from COMMAND_CODE_IMAGE_MODELS. Text-only routes
336
343
  * sit beside that list so the catalog can still advertise sidecar coverage
337
- * without claiming the gateway itself accepts a picture.
338
- *
339
- * The gateway-prefixed DeepSeek V4.1 Flash route has no verified native image
340
- * support, so declaring it image-capable would hand it a picture it drops. A
341
- * positive text-only declaration makes it a vision-sidecar consumer
344
+ * without claiming the gateway itself accepts a picture. A positive text-only
345
+ * declaration makes the route a vision-sidecar consumer
342
346
  * (src/vision/eligibility.ts), so the catalog advertises image input on its
343
- * behalf and the four-target combo in #4505 intersects to ["text","image"]
344
- * instead of ["text"] — without claiming native vision. modelInputModalities
345
- * is per-key filled, so this reaches an existing install even when
346
- * noVisionModels was persisted before the id joined that list.
347
+ * behalf — without claiming native vision — and modelInputModalities is
348
+ * per-key filled, so that reaches an existing install even when noVisionModels
349
+ * was persisted before the id joined a list.
350
+ *
351
+ * Empty as of 2026-09-18. Its only entry, deepseek/deepseek-v4.1-flash, moved
352
+ * to COMMAND_CODE_IMAGE_MODELS once the #4505-requested probe passed on both
353
+ * the user-message and tool-result paths (see the note at that entry). The
354
+ * mechanism stays for the next route that measures text-only.
347
355
  */
348
- export const COMMAND_CODE_TEXT_ONLY_MODELS = [
349
- "deepseek/deepseek-v4.1-flash",
350
- ] as const;
356
+ export const COMMAND_CODE_TEXT_ONLY_MODELS = [] as const;
351
357
  export const COMMAND_CODE_MODEL_INPUT_MODALITIES: Record<string, ["text"] | ["text", "image"]> = {
352
358
  ...Object.fromEntries(COMMAND_CODE_IMAGE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
353
359
  ...Object.fromEntries(COMMAND_CODE_TEXT_ONLY_MODELS.map(id => [id, ["text"] as ["text"]])),
@@ -439,9 +445,15 @@ export const deepseekReasoningMapFor = (modelId: string): Record<string, string>
439
445
  // https://help.aliyun.com/en/model-studio/token-plan-quickstart
440
446
  // 260909 refresh, re-probed against the live gateway (both regions, both tiers):
441
447
  // https://github.com/oliver-mee/alibaba-token-plan-wiki (machine-readable catalog).
442
- // glm-5.3 / glm-5.3-flash removed from both Token Plan catalogs: they exist on Z.AI
443
- // endpoints but the Token Plan gateway has never served either id (the 260826 seed
444
- // propagated them across every GLM-carrying catalog; a selected row 404s).
448
+ // 260918: glm-5.3 returns. The 260909 removal was correct at the time (the id
449
+ // 404'd on every plan key), but the gateway started serving glm-5.3 on 260917:
450
+ // it now appears on /models for global Team, global Personal, and CN Team, and
451
+ // answers a completion on a Personal key (probed 260918). Contract on the plan
452
+ // gateway: effort low/high/max (default max), thinking always-on (the gateway
453
+ // rejects enable_thinking:false with 400), 1M context, 131,072 max output,
454
+ // text-only input, strict json_schema accepted. glm-5.3-flash REMAINS OUT:
455
+ // still never served by the Token Plan gateway (docs.z.ai VLM id, not plan
456
+ // entitlement).
445
457
  // The Beijing preset keeps the Personal Edition subset; non-chat ids (audio/image/
446
458
  // video families) stay out: they answer only on async endpoints openai-chat cannot
447
459
  // reach. deepseek-v4-pro-0813 is callable but NOT listed by /models, which is the
@@ -457,7 +469,7 @@ export const deepseekReasoningMapFor = (modelId: string): Record<string, string>
457
469
  // drifting ones.
458
470
  export const ALIBABA_TOKEN_PLAN_MODELS = [
459
471
  "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
460
- "deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2",
472
+ "deepseek-v4-pro", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "glm-5.2", "glm-5.3",
461
473
  ];
462
474
  export const ALIBABA_TOKEN_PLAN_QWEN_MODELS = [
463
475
  "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
@@ -474,6 +486,7 @@ export const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
474
486
  // Vision probed on the plan gateway 260915 (user message and tool result, both 200).
475
487
  "deepseek-v4.1-flash": ["text", "image"],
476
488
  "glm-5.2": ["text"],
489
+ "glm-5.3": ["text"],
477
490
  };
478
491
 
479
492
  // 260721 Alibaba Token Plan International (ap-southeast-1 / Singapore, hardened 260721).
@@ -487,7 +500,7 @@ export const ALIBABA_INTL_TOKEN_PLAN_MODELS = [
487
500
  "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
488
501
  "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4.1-flash", "deepseek-v3.2",
489
502
  "kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5",
490
- "glm-5.2", "glm-5.1", "glm-5",
503
+ "glm-5.2", "glm-5.3", "glm-5.1", "glm-5",
491
504
  "MiniMax-M2.5",
492
505
  ];
493
506
  export const ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS = [
@@ -589,7 +602,7 @@ export const ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS: Record<string, number> = {
589
602
  "deepseek-v4-pro": 1_000_000, "deepseek-v4-pro-0813": 1_000_000, "deepseek-v4-flash": 1_000_000,
590
603
  "deepseek-v4-flash-0731": 1_000_000, "deepseek-v4.1-flash": 1_000_000, "deepseek-v3.2": 131_072,
591
604
  "kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 262_144,
592
- "glm-5.2": 1_000_000, "glm-5.1": 202_752, "glm-5": 202_752,
605
+ "glm-5.2": 1_000_000, "glm-5.3": 1_000_000, "glm-5.1": 202_752, "glm-5": 202_752,
593
606
  "MiniMax-M2.5": 196_608,
594
607
  };
595
608
  export const ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS: Record<string, number> = {
@@ -598,17 +611,17 @@ export const ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS: Record<string, number> = {
598
611
  "deepseek-v4-pro": 393_216, "deepseek-v4-pro-0813": 393_216, "deepseek-v4-flash": 393_216,
599
612
  "deepseek-v4-flash-0731": 393_216, "deepseek-v4.1-flash": 393_216, "deepseek-v3.2": 65_536,
600
613
  "kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 98_304,
601
- "glm-5.2": 131_072, "glm-5.1": 128_000, "glm-5": 16_384,
614
+ "glm-5.2": 131_072, "glm-5.3": 131_072, "glm-5.1": 128_000, "glm-5": 16_384,
602
615
  "MiniMax-M2.5": 32_768,
603
616
  };
604
617
  export const ALIBABA_TOKEN_PLAN_NO_VISION = [
605
618
  "qwen3.7-max", "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash",
606
- "deepseek-v4-flash-0731", "deepseek-v3.2", "glm-5.2", "glm-5.1", "glm-5", "MiniMax-M2.5",
619
+ "deepseek-v4-flash-0731", "deepseek-v3.2", "glm-5.2", "glm-5.3", "glm-5.1", "glm-5", "MiniMax-M2.5",
607
620
  ];
608
621
  export const ALIBABA_TOKEN_PLAN_PRESERVE_REASONING = [
609
622
  "qwen3.8-max", "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
610
623
  "deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek-v4-flash", "deepseek-v4-flash-0731",
611
- "deepseek-v4.1-flash", "glm-5.2",
624
+ "deepseek-v4.1-flash", "glm-5.2", "glm-5.3",
612
625
  ];
613
626
 
614
627
  // 260717 Kimi K3: the subscription endpoint uses one upstream id (`k3`) for both
@@ -960,3 +973,47 @@ export const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODALITY_KNOWN_MODELS.filt
960
973
  export const CLINE_PASS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
961
974
  CLINE_PASS_MODALITY_KNOWN_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]),
962
975
  );
976
+
977
+ // Opper seed: bare *pool* names. A pool is every provider Opper serves that model through; Opper
978
+ // picks the route per request. Each name is the `.model` of a `pooled: true` entry in the public
979
+ // catalogue snapshot supplied by the original provider author
980
+ // (https://api.opper.ai/v3/models?limit=2000, captured 2026-09-14); `vendor/model` ids
981
+ // (anthropic/claude-sonnet-4-6) pin one route and stay valid, they are just not seeded.
982
+ export const OPPER_MODELS = [
983
+ "claude-sonnet-4-6",
984
+ "claude-opus-5",
985
+ "gpt-5.5",
986
+ "gpt-5.4-mini",
987
+ "gemini-3.8-flash",
988
+ "deepseek-v4-pro",
989
+ "kimi-k3",
990
+ "mistral-large-2512",
991
+ ];
992
+ // Smallest value across each pool's members in that snapshot, capped at the lab model's own limit
993
+ // (kimi-k3 output); live discovery owns which models exist.
994
+ export const OPPER_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
995
+ "claude-sonnet-4-6": 1_000_000,
996
+ "claude-opus-5": 1_000_000,
997
+ "gpt-5.5": 1_050_000,
998
+ "gpt-5.4-mini": 400_000,
999
+ "gemini-3.8-flash": 1_048_576,
1000
+ "deepseek-v4-pro": 1_000_000,
1001
+ "kimi-k3": 1_048_576,
1002
+ "mistral-large-2512": 256_000,
1003
+ };
1004
+ export const OPPER_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
1005
+ "claude-sonnet-4-6": 64_000,
1006
+ "claude-opus-5": 128_000,
1007
+ "gpt-5.5": 128_000,
1008
+ "gpt-5.4-mini": 128_000,
1009
+ "gemini-3.8-flash": 65_536,
1010
+ "deepseek-v4-pro": 65_536,
1011
+ "kimi-k3": 131_072,
1012
+ "mistral-large-2512": 8_192,
1013
+ };
1014
+ // Pools whose members do not all accept image input (deepseek-v4-pro: no member does; kimi-k3: the
1015
+ // sference route is text-only), so the shared modality set is text.
1016
+ export const OPPER_TEXT_ONLY_MODELS = ["deepseek-v4-pro", "kimi-k3"];
1017
+ export const OPPER_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
1018
+ OPPER_MODELS.map(id => [id, OPPER_TEXT_ONLY_MODELS.includes(id) ? ["text"] : ["text", "image"]]),
1019
+ );
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // Some routed models decorate the first and last lines as
4
4
  // `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
5
- // valid custom-tool payloads. Repair is deliberately limited to a complete,
6
- // structurally recognizable top-level patch: arbitrary `exec` JavaScript is
7
- // caller-authored executable input and must remain byte-identical.
5
+ // valid custom-tool payloads. Repair of executable bodies is deliberately limited to
6
+ // unambiguous wrapper mistakes: one recognized alternate field or one complete outer
7
+ // Markdown fence. Ordinary `exec` JavaScript remains byte-identical.
8
8
  //
9
9
  // This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
10
10
  // repair the one faithful reading, leave genuine patch content alone.
@@ -22,20 +22,52 @@ const PATCH_BEGIN = "*** Begin Patch";
22
22
  const PATCH_END = "*** End Patch";
23
23
  const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
24
24
  const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
25
+ const OUTER_MARKDOWN_CODE_FENCE = /^```[^\r\n]*\r?\n([\s\S]*?)\r?\n```$/;
26
+ const FREEFORM_FALLBACK_KEYS: Readonly<Record<string, readonly string[]>> = {
27
+ exec: ["code", "script", "js", "javascript", "command", "cmd", "content"],
28
+ apply_patch: ["patch", "content"],
29
+ };
30
+
31
+ function stripMarkdownCodeFence(text: string, toolName: string): string {
32
+ if (toolName !== "exec" && toolName !== "apply_patch") return text;
33
+ const match = OUTER_MARKDOWN_CODE_FENCE.exec(text.trim());
34
+ return match ? match[1] : text;
35
+ }
36
+
37
+ /**
38
+ * The single-field wrappers `unwrapFreeformToolInput` accepts for one tool name, besides the
39
+ * canonical `input`.
40
+ *
41
+ * Exported so the streaming side can hold a buffer that is still turning into one of these.
42
+ * A second list of key names beside this one is how the streamed bytes and the completed item
43
+ * come to disagree, which is the defect it exists to prevent (#5047).
44
+ */
45
+ export function freeformFallbackKeys(toolName: string): readonly string[] {
46
+ return FREEFORM_FALLBACK_KEYS[toolName] ?? [];
47
+ }
25
48
 
26
49
  /** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
27
- export function unwrapFreeformToolInput(argumentsText: unknown): string {
50
+ export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): string {
28
51
  if (typeof argumentsText !== "string") return "";
29
52
  try {
30
53
  const parsed: unknown = JSON.parse(argumentsText);
31
54
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
32
- const input = (parsed as { input?: unknown }).input;
33
- if (typeof input === "string") return input;
55
+ const record = parsed as Record<string, unknown>;
56
+ if (Object.prototype.hasOwnProperty.call(record, "input")) {
57
+ return typeof record.input === "string"
58
+ ? stripMarkdownCodeFence(record.input, toolName)
59
+ : argumentsText;
60
+ }
61
+ const fallbackKeys = FREEFORM_FALLBACK_KEYS[toolName] ?? [];
62
+ const candidates = fallbackKeys.filter(key => typeof record[key] === "string");
63
+ if (candidates.length === 1) {
64
+ return stripMarkdownCodeFence(record[candidates[0]] as string, toolName);
65
+ }
34
66
  }
35
67
  } catch {
36
68
  // The string is the freeform body, not nested JSON.
37
69
  }
38
- return argumentsText;
70
+ return stripMarkdownCodeFence(argumentsText, toolName);
39
71
  }
40
72
 
41
73
  /**
@@ -92,17 +124,18 @@ export function mayBecomePatchEnvelope(text: string): boolean {
92
124
  /**
93
125
  * Repair freeform input before Codex sees it.
94
126
  *
95
- * Only a bare or reserved-`functions` `apply_patch` payload may receive delimiter
96
- * repair. Remote namespaces own their grammar; those bodies and every other
97
- * freeform input are unwrapped and left byte-exact.
127
+ * Only a bare or reserved-`functions` tool may receive fallback-field or outer-fence
128
+ * repair, and only `apply_patch` may receive delimiter repair. Remote namespaces own
129
+ * their grammar; those bodies and every other freeform input are unwrapped and left
130
+ * byte-exact.
98
131
  */
99
132
  export function repairFreeformToolInput(
100
133
  argumentsText: unknown,
101
134
  toolName = "",
102
135
  namespace?: string,
103
136
  ): string {
104
- const unwrapped = unwrapFreeformToolInput(argumentsText);
105
137
  const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
138
+ const unwrapped = unwrapFreeformToolInput(argumentsText, ownsApplyPatchGrammar ? toolName : "");
106
139
  return ownsApplyPatchGrammar && toolName === "apply_patch"
107
140
  ? normalizeApplyPatchDelimiters(unwrapped)
108
141
  : unwrapped;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Process-local memo pairing a bridged hosted `web_search` cell with the destination's own
3
+ * call and the result the proxy executed for it (issue #4587).
4
+ *
5
+ * The web-search passthrough bridge (`src/web-search/passthrough-bridge.ts`) intercepts the
6
+ * destination's `function_call` named `web_search`, runs the search itself, and shows the
7
+ * caller a hosted `web_search_call` cell under a proxy-minted `ws_<uuid>` id. The caller
8
+ * stores that cell and replays it on every later turn. The destination, which never produced a
9
+ * `web_search_call` in its life, then sees an unknown item type carrying a query and sources
10
+ * but no result — so it usually just searches again.
11
+ *
12
+ * This memo is what lets the pre-dispatch rewrite in the Responses adapter put the destination's
13
+ * own `function_call` and `function_call_output` back in that item's place. It records exactly
14
+ * what `appendBridgeSearchTurn` would have written onto a continuation leg, so a replayed turn
15
+ * and a continued turn show the destination the same conversation.
16
+ *
17
+ * Scope. Entries are keyed by the upstream destination in addition to the cell id. The cell id is
18
+ * a v4 UUID minted here, so it cannot collide across conversations, but an unscoped key would let
19
+ * a history replayed against a DIFFERENT provider resurrect a call that provider never made.
20
+ *
21
+ * Bounds and privacy. Result text is web content the caller already received, but it is still
22
+ * request-derived data: it lives in memory only, is never logged, serialized, or exported, and is
23
+ * bounded by entry count, total bytes, and TTL so a long-lived proxy cannot grow without limit.
24
+ *
25
+ * A miss is deliberately indistinguishable from "no entry": the caller leaves the replayed item
26
+ * alone. Neither re-running the search nor inventing a result is an acceptable recovery.
27
+ */
28
+
29
+ import { reasoningReplayDestinationIdentity } from "./reasoning-replay-cache";
30
+
31
+ const MAX_ENTRIES = 64;
32
+ const MAX_TOTAL_BYTES = 512 * 1024;
33
+ const TTL_MS = 60 * 60 * 1000;
34
+
35
+ export interface BridgeSearchReplayEntry {
36
+ /** The destination's own call id, as it appeared on the intercepted item. */
37
+ callId: string;
38
+ /** The destination's own item id, replayed when the upstream supplied one. */
39
+ sourceItemId?: string;
40
+ /** The tool name the destination called, recorded rather than assumed. */
41
+ name: string;
42
+ /** The intercepted call's complete arguments text. */
43
+ argumentsText: string;
44
+ /** The tool result the bridge produced for that call. */
45
+ output: string;
46
+ }
47
+
48
+ interface StoredEntry {
49
+ entry: BridgeSearchReplayEntry;
50
+ bytes: number;
51
+ at: number;
52
+ }
53
+
54
+ const entries = new Map<string, StoredEntry>();
55
+ let totalBytes = 0;
56
+ let clockForTests: (() => number) | null = null;
57
+
58
+ const now = (): number => clockForTests?.() ?? Date.now();
59
+
60
+ /**
61
+ * Identify the upstream destination a bridged search belongs to.
62
+ *
63
+ * Reuses the salted process-local destination digest the reasoning replay cache already defines,
64
+ * so both stores agree on what "the same upstream" means and neither invents a second notion of
65
+ * destination identity.
66
+ */
67
+ export function bridgeSearchReplayScope(baseUrl: string | undefined): string | undefined {
68
+ return reasoningReplayDestinationIdentity(baseUrl);
69
+ }
70
+
71
+ function keyFor(scope: string, cellItemId: string): string {
72
+ return scope + "\u0000" + cellItemId;
73
+ }
74
+
75
+ function drop(key: string, stored: StoredEntry): void {
76
+ entries.delete(key);
77
+ totalBytes -= stored.bytes;
78
+ }
79
+
80
+ function sweep(at: number): void {
81
+ for (const [key, stored] of entries) {
82
+ if (at - stored.at >= TTL_MS) drop(key, stored);
83
+ }
84
+ // Map iteration is insertion-ordered, so the oldest surviving entry is always the first one.
85
+ while (entries.size > MAX_ENTRIES || totalBytes > MAX_TOTAL_BYTES) {
86
+ const oldest = entries.entries().next();
87
+ if (oldest.done) {
88
+ totalBytes = 0;
89
+ return;
90
+ }
91
+ drop(oldest.value[0], oldest.value[1]);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Record one executed bridged search.
97
+ *
98
+ * An entry with no call id is not recorded: the restore would have to emit a `function_call`
99
+ * without one, which is not a valid item and could not be paired with its output anyway.
100
+ */
101
+ export function rememberBridgeSearchReplay(
102
+ scope: string | undefined,
103
+ cellItemId: string,
104
+ entry: BridgeSearchReplayEntry,
105
+ ): void {
106
+ if (!scope || cellItemId.length === 0 || entry.callId.length === 0) return;
107
+ const key = keyFor(scope, cellItemId);
108
+ const existing = entries.get(key);
109
+ if (existing) drop(key, existing);
110
+ const bytes = 2 * (
111
+ key.length
112
+ + entry.callId.length
113
+ + (entry.sourceItemId?.length ?? 0)
114
+ + entry.name.length
115
+ + entry.argumentsText.length
116
+ + entry.output.length
117
+ );
118
+ // A single oversized result is refused outright rather than evicting the whole store for it.
119
+ if (bytes > MAX_TOTAL_BYTES) return;
120
+ entries.set(key, { entry: { ...entry }, bytes, at: now() });
121
+ totalBytes += bytes;
122
+ sweep(now());
123
+ }
124
+
125
+ /**
126
+ * Look up one recorded search without consuming it.
127
+ *
128
+ * The same cell is replayed on every subsequent turn of the conversation, so a consuming read
129
+ * would restore the pair once and then silently stop. Expiry stays absolute: a conversation that
130
+ * outlives the TTL degrades to today's behaviour (the hosted cell replays unchanged) rather than
131
+ * pinning entries open for as long as anyone keeps talking.
132
+ */
133
+ export function peekBridgeSearchReplay(
134
+ scope: string | undefined,
135
+ cellItemId: string,
136
+ ): BridgeSearchReplayEntry | undefined {
137
+ if (!scope || cellItemId.length === 0) return undefined;
138
+ const key = keyFor(scope, cellItemId);
139
+ const stored = entries.get(key);
140
+ if (!stored) return undefined;
141
+ if (now() - stored.at >= TTL_MS) {
142
+ drop(key, stored);
143
+ return undefined;
144
+ }
145
+ return stored.entry;
146
+ }
147
+
148
+ export function clearBridgeSearchReplayCacheForTests(clock?: (() => number) | null): void {
149
+ entries.clear();
150
+ totalBytes = 0;
151
+ clockForTests = clock ?? null;
152
+ }
@@ -9,33 +9,43 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
9
9
  return !!value && typeof value === "object" && !Array.isArray(value);
10
10
  }
11
11
 
12
- function unwrapPatchInput(value: string): string {
13
- try {
14
- const parsed: unknown = JSON.parse(value);
15
- if (isPlainObject(parsed)) {
16
- if (typeof parsed.input === "string") return parsed.input;
17
- if (typeof parsed.patch === "string") return parsed.patch;
18
- }
19
- } catch {
20
- // Native custom calls carry the patch body directly.
21
- }
22
- return value;
23
- }
24
-
25
12
  /**
26
13
  * Convert a nested Code Mode helper call into unified-exec JavaScript.
27
14
  *
28
15
  * Parsed values are serialized as data, never interpolated as source, so command and patch text
29
16
  * cannot escape the generated call. Invalid structured helper payloads are also passed as data so
30
17
  * nested-tool validation can reject them without evaluating provider text as JavaScript.
18
+ *
19
+ * `wireToolName` is the name the body actually arrived under, which is not always the helper:
20
+ * a provider that got the NAME right and the BODY wrong sends `exec`, and the apply-patch
21
+ * helper is inferred from the payload. Recognition and compilation must read ONE canonical
22
+ * body, so both unwrap under that same name — see the apply-patch branch below. It defaults to
23
+ * the helper name, which is correct for the name-based path where the wire name IS the helper.
31
24
  */
32
- export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string {
25
+ export function compileCodeModeHelperInput(
26
+ argumentsText: unknown,
27
+ toolName: string,
28
+ wireToolName?: string,
29
+ ): string {
33
30
  if (typeof argumentsText !== "string") return "";
34
31
  const helperName = toolName.startsWith("default.")
35
32
  ? toolName.slice("default.".length)
36
33
  : toolName;
37
34
  if (helperName === "apply_patch") {
38
- const patch = normalizeApplyPatchDelimiters(unwrapPatchInput(argumentsText));
35
+ // `resolveCodeModeHelperName` decides this IS an apply-patch call by reading
36
+ // `unwrapFreeformToolInput(argumentsText, wireToolName)`, which strips an outer Markdown
37
+ // fence and accepts that name's fallback fields (#4983). Compiling from a narrower unwrap
38
+ // meant a body accepted through a fence or a fallback field reached `tools.apply_patch`
39
+ // still wrapped, so the host rejected the JSON or the fence instead of applying the patch
40
+ // (#5046). One unwrap, one body, one decision.
41
+ //
42
+ // The vocabulary is the WIRE name rather than the helper name on purpose. `{"patch": ...}`
43
+ // is an apply_patch wrapper and is not an `exec` fallback field, and the recognizer already
44
+ // declines it under `exec`; reading it here would compile a body that recognition rejected,
45
+ // which is exactly the drift a second, looser unwrap introduces.
46
+ const patch = normalizeApplyPatchDelimiters(
47
+ unwrapFreeformToolInput(argumentsText, wireToolName ?? helperName),
48
+ );
39
49
  return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
40
50
  }
41
51
  let parsed: unknown = argumentsText;
@@ -106,5 +116,5 @@ export function resolveCodeModeHelperName(
106
116
  // `tools.apply_patch(...)` JavaScript would be the mis-route this repair exists to avoid.
107
117
  if (!declaresCodeModeExec(declaredNames)) return undefined;
108
118
  if (typeof argumentsText !== "string" || argumentsText === "") return undefined;
109
- return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText)) ? "apply_patch" : undefined;
119
+ return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText, "exec")) ? "apply_patch" : undefined;
110
120
  }
@@ -326,7 +326,7 @@ export function restoreRoutedCustomCalls(
326
326
  id: customToolItemId(item.id),
327
327
  name: aliased ? targetName : item.name,
328
328
  input: helper
329
- ? compileCodeModeHelperInput(sourceInput, helper)
329
+ ? compileCodeModeHelperInput(sourceInput, helper, aliased ? String(item.name) : targetName)
330
330
  : repairFreeformToolInput(
331
331
  sourceInput,
332
332
  targetName,
@@ -10,7 +10,90 @@ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
10
10
  },
11
11
  ];
12
12
 
13
- /** True when forwarding this hosted tool to the model would be rejected upstream. */
14
- export function isHostedToolUnsupportedForModel(modelId: string, tool: string, baseUrl?: string): boolean {
13
+ /**
14
+ * Hosted-tool declaration names an operator may list in `unsupportedHostedTools`.
15
+ *
16
+ * A gateway must be able to deny anything a client can send it, so this is the set of
17
+ * nameless hosted/private declaration types the proxy recognizes on a Responses request.
18
+ * It is deliberately a closed vocabulary: the provider config schema ends in
19
+ * `.passthrough()`, so an unvalidated misspelling would be accepted, persisted, and then
20
+ * silently strip nothing -- which is the exact 400 the operator set the field to avoid
21
+ * (the `codexToolMode` lesson in #2106).
22
+ */
23
+ export const DECLARABLE_HOSTED_TOOL_TYPES: ReadonlySet<string> = new Set([
24
+ "web_search",
25
+ "web_search_preview",
26
+ "file_search",
27
+ "computer_use_preview",
28
+ "computer_use",
29
+ "code_interpreter",
30
+ "image_generation",
31
+ "image_gen",
32
+ "mcp",
33
+ "tool_search",
34
+ "local_shell",
35
+ "x_search",
36
+ ]);
37
+
38
+ /**
39
+ * Spellings that name one capability. Declaring either member denies both, because the
40
+ * rest of the proxy already treats these as a single tool: the parser folds
41
+ * `web_search_preview` onto one name (`src/responses/parser-tools.ts`), Chat ingress
42
+ * accepts the pair together (`src/chat/inbound.ts`), and the canonical-field strip lists
43
+ * the pair in a single `toolTypes` set
44
+ * (`src/adapters/openai-responses/request-strips.ts`).
45
+ *
46
+ * Without the alias a capability declaration would be honoured for the spelling the
47
+ * operator happened to write and ignored for the one the client happened to send, which
48
+ * reproduces the original rejection while the config claims to have prevented it.
49
+ */
50
+ const HOSTED_TOOL_ALIAS_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
51
+ new Set(["web_search", "web_search_preview"]),
52
+ new Set(["image_generation", "image_gen"]),
53
+ new Set(["computer_use_preview", "computer_use"]),
54
+ ];
55
+
56
+ const NO_DECLARED_HOSTED_TOOLS: ReadonlySet<string> = new Set();
57
+
58
+ /**
59
+ * Expand a provider's declared denials through the alias groups once per request, so the
60
+ * per-tool predicate stays a set lookup. Returns a shared empty set when the provider
61
+ * declares nothing, keeping the common path allocation-free.
62
+ */
63
+ export function declaredUnsupportedHostedTools(
64
+ provider?: { unsupportedHostedTools?: readonly string[] },
65
+ ): ReadonlySet<string> {
66
+ const declared = provider?.unsupportedHostedTools;
67
+ if (!Array.isArray(declared) || declared.length === 0) return NO_DECLARED_HOSTED_TOOLS;
68
+ const out = new Set<string>();
69
+ for (const raw of declared) {
70
+ if (typeof raw !== "string") continue;
71
+ const tool = raw.trim();
72
+ if (!tool) continue;
73
+ out.add(tool);
74
+ for (const group of HOSTED_TOOL_ALIAS_GROUPS) {
75
+ if (!group.has(tool)) continue;
76
+ for (const alias of group) out.add(alias);
77
+ }
78
+ }
79
+ return out.size > 0 ? out : NO_DECLARED_HOSTED_TOOLS;
80
+ }
81
+
82
+ /**
83
+ * True when forwarding this hosted tool to the model would be rejected upstream.
84
+ *
85
+ * `declaredUnsupported` is the provider's own capability declaration, expanded by
86
+ * `declaredUnsupportedHostedTools`. It is additive to the built-in table rather than a
87
+ * replacement for it: the table covers destinations that reject a tool regardless of how
88
+ * the operator configured them, so an operator who never heard of the field stays
89
+ * protected.
90
+ */
91
+ export function isHostedToolUnsupportedForModel(
92
+ modelId: string,
93
+ tool: string,
94
+ baseUrl?: string,
95
+ declaredUnsupported?: ReadonlySet<string>,
96
+ ): boolean {
97
+ if (declaredUnsupported?.has(tool)) return true;
15
98
  return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId, baseUrl) && entry.tools.has(tool));
16
99
  }
@@ -126,10 +126,17 @@ export const toolSchema = z.object({
126
126
 
127
127
  const builtinToolSchema = z.object({ type: z.string() }).loose();
128
128
 
129
- const hostedToolType = z.enum([
129
+ /**
130
+ * Hosted tool types a client may declare on an inbound Responses request. Exported so the
131
+ * provider-side capability vocabulary in `src/responses/hosted-tool-policy.ts` can be
132
+ * asserted to cover all of them: a gateway must be able to deny anything it can be sent.
133
+ */
134
+ export const HOSTED_TOOL_TYPES = [
130
135
  "web_search", "web_search_preview", "file_search", "computer_use_preview",
131
136
  "code_interpreter", "image_generation", "mcp",
132
- ]);
137
+ ] as const;
138
+
139
+ const hostedToolType = z.enum(HOSTED_TOOL_TYPES);
133
140
 
134
141
  const allowedToolEntrySchema = z.object({ type: z.string(), name: z.string().optional() });
135
142