@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  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 +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
package/src/router.ts CHANGED
@@ -15,10 +15,12 @@ import { assertProviderDestinationAllowed } from "./lib/destination-policy";
15
15
  import { redactSecretString, redactUrlForLog } from "./lib/redact";
16
16
  import {
17
17
  PROVIDER_REGISTRY,
18
- mergeRegistryStaticHeaders,
19
18
  providerCodexAccountMode,
20
- registryModelServiceTierCapabilityApplies,
19
+ registryEntryForProviderDestination,
21
20
  } from "./providers/registry";
21
+ // Imported from the module directly rather than through the registry facade, which is at its
22
+ // file-size cap.
23
+ import { registryModelIdKeys } from "./providers/registry/model-ids";
22
24
  import { applyDirectReasoningEffortContracts, hasLegacyClinePassReasoningEfforts } from "./providers/derive";
23
25
  import { cloneFastWire } from "./providers/fastwire";
24
26
  import {
@@ -29,11 +31,10 @@ import {
29
31
  isCanonicalOpenAiForwardProvider,
30
32
  LEGACY_CHATGPT_PROVIDER_ID,
31
33
  LEGACY_OPENAI_MULTI_PROVIDER_ID,
32
- OPENAI_API_PROVIDER_ID,
33
34
  OPENAI_CODEX_PROVIDER_ID,
34
35
  } from "./providers/openai-tiers";
35
36
  import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
36
- import { resolveModelAlias } from "./providers/default-aliases";
37
+ import { effectiveProviderAliasDecision, resolveModelAlias } from "./providers/default-aliases";
37
38
  import { resolveBlockedModelRedirect } from "./lib/shadow-call";
38
39
  import { getStaleCached } from "./codex/model-cache";
39
40
  import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
@@ -46,6 +47,7 @@ import {
46
47
  import { getRoutingProfile, resolvePolicyProfileId, POLICY_NAMESPACE } from "./routing/profile";
47
48
  import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/evaluator";
48
49
  import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
50
+ import { resolveModelPolicy, type ResolvedModelPolicy } from "./providers/resolved-model-policy";
49
51
 
50
52
  export class UnknownRoutingPolicyError extends Error {
51
53
  constructor(readonly profileId: string) {
@@ -69,6 +71,8 @@ export interface RouteResult {
69
71
  providerName: string;
70
72
  provider: OcxProviderConfig;
71
73
  modelId: string;
74
+ /** Immutable static policy for the current final wire model. */
75
+ staticPolicy: ResolvedModelPolicy;
72
76
  /** Which deterministic routing path produced this route (RI-01). */
73
77
  routeKind: RouteDecisionKind;
74
78
  /** Stable wire reason code for the selected route (RI-01). */
@@ -83,6 +87,29 @@ export interface RouteResult {
83
87
  routeDecision?: RouteDecisionTraceV1;
84
88
  }
85
89
 
90
+ export function captureRouteStaticPolicy(
91
+ providerName: string,
92
+ modelId: string,
93
+ provider: OcxProviderConfig,
94
+ effectiveAlias?: string | null,
95
+ inboundWire: "responses" | "chat" | "anthropic" = "responses",
96
+ ): ResolvedModelPolicy {
97
+ const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
98
+ const transportMatchedRegistry = !!registryEntry
99
+ && providerMatchesRegistryTransportWithStaticGuards(providerName, provider);
100
+ return resolveModelPolicy({
101
+ providerName,
102
+ modelId,
103
+ provider,
104
+ registryEntry,
105
+ transportMatchedRegistry,
106
+ inboundWire,
107
+ modelCapabilities: provider.modelCapabilities?.[modelId],
108
+ ...(provider.authMode ? { effectiveAuth: { authMode: provider.authMode } } : {}),
109
+ ...(effectiveAlias !== undefined ? { effectiveAlias } : {}),
110
+ });
111
+ }
112
+
86
113
  const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string[] }> = [
87
114
  {
88
115
  providerNames: ["anthropic"],
@@ -117,19 +144,11 @@ export function knownModelIdsForProvider(
117
144
  : undefined;
118
145
  for (const id of registry?.models ?? []) ids.add(id);
119
146
  // Registry model-keyed hint maps double as known native ids (e.g. NVIDIA carries no
120
- // static models list but names `moonshotai/kimi-k2.6` in its effort/window maps).
121
- for (const map of [
122
- registry?.modelContextWindows,
123
- registry?.modelInputModalities,
124
- registry?.modelReasoningEfforts,
125
- registry?.modelDefaultReasoningEfforts,
126
- registry?.modelReasoningEffortMap,
127
- registry?.modelMaxOutputTokens,
128
- registry?.modelSupportsServiceTier,
129
- registry?.modelSupportsVerbosity,
130
- ]) {
131
- for (const id of Object.keys(map ?? {})) ids.add(id);
132
- }
147
+ // static models list but names `moonshotai/kimi-k2.6` in its effort/window maps). Which
148
+ // maps count is classified by the registry itself rather than listed here, so an id declared
149
+ // only in a map this function forgot is no longer undecodable, and a new model-keyed field
150
+ // fails typecheck until its keys are given a meaning.
151
+ for (const id of registry ? registryModelIdKeys(registry) : []) ids.add(id);
133
152
  for (const cached of getStaleCached(provName) ?? []) ids.add(cached.id);
134
153
  for (const model of config?.customModels ?? []) {
135
154
  if (model.provider === provName && model.modelId) ids.add(model.modelId);
@@ -137,66 +156,6 @@ export function knownModelIdsForProvider(
137
156
  return [...ids];
138
157
  }
139
158
 
140
- // Merge registry-default effort maps under user values so built-in provider configs can
141
- // carry real upstream aliases without a disk migration. User overrides win per-key.
142
- function mergeRecord(
143
- seed: Record<string, string> | undefined,
144
- user: Record<string, string> | undefined,
145
- ): Record<string, string> | undefined {
146
- if (!seed && !user) return undefined;
147
- return { ...(seed ?? {}), ...(user ?? {}) };
148
- }
149
-
150
- function mergeNestedRecord(
151
- seed: Record<string, Record<string, string>> | undefined,
152
- user: Record<string, Record<string, string>> | undefined,
153
- ): Record<string, Record<string, string>> | undefined {
154
- if (!seed && !user) return undefined;
155
- const out: Record<string, Record<string, string>> = {};
156
- for (const [key, value] of Object.entries(seed ?? {})) out[key] = { ...value };
157
- for (const [key, value] of Object.entries(user ?? {})) out[key] = { ...(out[key] ?? {}), ...value };
158
- return out;
159
- }
160
-
161
- function mergeStringArray(
162
- seed: string[] | undefined,
163
- user: string[] | undefined,
164
- ): string[] | undefined {
165
- if (!seed && !user) return undefined;
166
- return [...new Set([...(seed ?? []), ...(user ?? [])])];
167
- }
168
-
169
- function mergeRecordFill<T>(
170
- seed: Record<string, T> | undefined,
171
- user: Record<string, T> | undefined,
172
- ): Record<string, T> | undefined {
173
- if (!seed && !user) return undefined;
174
- return { ...(seed ?? {}), ...(user ?? {}) };
175
- }
176
-
177
- function mergePositiveNumberCaps(
178
- seed: Record<string, number> | undefined,
179
- user: Record<string, number> | undefined,
180
- ): Record<string, number> | undefined {
181
- if (!seed && !user) return undefined;
182
- const out = { ...(seed ?? {}) };
183
- for (const [key, value] of Object.entries(user ?? {})) {
184
- out[key] = typeof out[key] === "number" ? Math.min(out[key]!, value) : value;
185
- }
186
- return out;
187
- }
188
-
189
- function mergeStringArrayRecord(
190
- seed: Record<string, string[]> | undefined,
191
- user: Record<string, string[]> | undefined,
192
- ): Record<string, string[]> | undefined {
193
- if (!seed && !user) return undefined;
194
- const out: Record<string, string[]> = {};
195
- for (const [key, value] of Object.entries(seed ?? {})) out[key] = [...value];
196
- for (const [key, value] of Object.entries(user ?? {})) out[key] = [...value];
197
- return out;
198
- }
199
-
200
159
  /** Same endpoint modulo surrounding space and trailing slashes — matches `matchBaseUrlChoice`. */
201
160
  function isSameEndpoint(a: string, b: string): boolean {
202
161
  return a.trim().replace(/\/+$/, "") === b.trim().replace(/\/+$/, "");
@@ -302,7 +261,20 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
302
261
  const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
303
262
  if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
304
263
  assertProviderDestinationAllowed(providerName, provider);
305
- return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey) };
264
+ // A row whose adapter no longer matches its registry entry still reaches the Responses
265
+ // adapter when one model opts in through `modelAdapters` — a Volcengine Coding Plan config
266
+ // saved on Chat, for instance. The replay-drop flag belongs to the DESTINATION rather than
267
+ // to the provider-wide wire, so it is filled here too; without it that continuation forwards
268
+ // the reasoning item the upstream answers 400 to. Destination matching refuses templated and
269
+ // overridable base URLs, so this cannot follow a retargeted row, and an explicit value wins.
270
+ const destination = registryEntryForProviderDestination(provider);
271
+ return {
272
+ ...provider,
273
+ apiKey: usableResolvedApiKey(provider.apiKey),
274
+ ...(provider.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined
275
+ ? { dropResponsesReasoningItems: destination.dropResponsesReasoningItems }
276
+ : {}),
277
+ };
306
278
  }
307
279
  const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
308
280
  const staticModelCatalog = !providerSupportsLiveModelDiscovery(providerName, provider);
@@ -317,46 +289,42 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
317
289
  : registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
318
290
  ? registryEntry.authKind
319
291
  : provider.authMode === "forward" ? undefined : provider.authMode;
320
- const reasoningEffortMap = mergeRecord(registryEntry.reasoningEffortMap, provider.reasoningEffortMap);
321
- const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
322
- const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
323
- const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
324
- const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID
325
- ? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows)
326
- : mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows);
327
- const modelInputModalities = mergeRecordFill(registryEntry.modelInputModalities, provider.modelInputModalities);
292
+ const staticPolicy = resolveModelPolicy({
293
+ providerName,
294
+ modelId: "__provider_static__",
295
+ provider,
296
+ registryEntry,
297
+ transportMatchedRegistry: true,
298
+ ...(canonicalAuthMode ? { effectiveAuth: { authMode: canonicalAuthMode } } : {}),
299
+ }).provider;
300
+ const reasoningEffortMap = staticPolicy.reasoningEffortMap;
301
+ const modelReasoningEffortMap = staticPolicy.modelReasoningEffortMap;
302
+ const modelReasoningEfforts = staticPolicy.modelReasoningEfforts;
303
+ const modelDefaultReasoningEfforts = staticPolicy.modelDefaultReasoningEfforts;
304
+ const modelContextWindows = staticPolicy.modelContextWindows;
305
+ const modelInputModalities = staticPolicy.modelInputModalities;
328
306
  // Registry static headers are documented as applying to every upstream request, so they are
329
307
  // filled at resolve time rather than only at seed time: a config written before a header
330
308
  // existed, or one carrying any header of its own, would otherwise never receive it. User
331
309
  // headers win, matched case-insensitively so an override replaces rather than duplicates.
332
- const headers = mergeRegistryStaticHeaders(registryEntry.staticHeaders, provider.headers);
333
- const modelMaxInputTokens = providerName === OPENAI_API_PROVIDER_ID
334
- ? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
335
- : mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
336
- const modelMaxOutputTokens = mergeRecordFill(registryEntry.modelMaxOutputTokens, provider.modelMaxOutputTokens);
337
- const modelSupportsServiceTier = mergeRecordFill(
338
- registryModelServiceTierCapabilityApplies(registryEntry, provider)
339
- ? registryEntry.modelSupportsServiceTier
340
- : undefined,
341
- provider.modelSupportsServiceTier,
342
- );
343
- const modelSupportsVerbosity = mergeRecordFill(
344
- registryEntry.modelSupportsVerbosity,
345
- provider.modelSupportsVerbosity,
346
- );
347
- const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
348
- const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
349
- const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
350
- const noTopPModels = mergeStringArray(registryEntry.noTopPModels, provider.noTopPModels);
351
- const noPenaltyModels = mergeStringArray(registryEntry.noPenaltyModels, provider.noPenaltyModels);
352
- const noJsonSchemaModels = mergeStringArray(registryEntry.noJsonSchemaModels, provider.noJsonSchemaModels);
353
- const autoToolChoiceOnlyModels = mergeStringArray(registryEntry.autoToolChoiceOnlyModels, provider.autoToolChoiceOnlyModels);
354
- const preserveReasoningContentModels = mergeStringArray(registryEntry.preserveReasoningContentModels, provider.preserveReasoningContentModels);
355
- const requiresReasoningPlaceholderModels = mergeStringArray(registryEntry.requiresReasoningPlaceholderModels, provider.requiresReasoningPlaceholderModels);
356
- const reasoningSplitModels = mergeStringArray(registryEntry.reasoningSplitModels, provider.reasoningSplitModels);
357
- const reasoningDetailsModels = mergeStringArray(registryEntry.reasoningDetailsModels, provider.reasoningDetailsModels);
358
- const thinkingToggleModels = mergeStringArray(registryEntry.thinkingToggleModels, provider.thinkingToggleModels);
359
- const thinkingBudgetModels = mergeStringArray(registryEntry.thinkingBudgetModels, provider.thinkingBudgetModels);
310
+ const headers = staticPolicy.headers;
311
+ const modelMaxInputTokens = staticPolicy.modelMaxInputTokens;
312
+ const modelMaxOutputTokens = staticPolicy.modelMaxOutputTokens;
313
+ const modelSupportsServiceTier = staticPolicy.modelSupportsServiceTier;
314
+ const modelSupportsVerbosity = staticPolicy.modelSupportsVerbosity;
315
+ const noVisionModels = staticPolicy.noVisionModels;
316
+ const noReasoningModels = staticPolicy.noReasoningModels;
317
+ const noTemperatureModels = staticPolicy.noTemperatureModels;
318
+ const noTopPModels = staticPolicy.noTopPModels;
319
+ const noPenaltyModels = staticPolicy.noPenaltyModels;
320
+ const noJsonSchemaModels = staticPolicy.noJsonSchemaModels;
321
+ const autoToolChoiceOnlyModels = staticPolicy.autoToolChoiceOnlyModels;
322
+ const preserveReasoningContentModels = staticPolicy.preserveReasoningContentModels;
323
+ const requiresReasoningPlaceholderModels = staticPolicy.requiresReasoningPlaceholderModels;
324
+ const reasoningSplitModels = staticPolicy.reasoningSplitModels;
325
+ const reasoningDetailsModels = staticPolicy.reasoningDetailsModels;
326
+ const thinkingToggleModels = staticPolicy.thinkingToggleModels;
327
+ const thinkingBudgetModels = staticPolicy.thinkingBudgetModels;
360
328
  const registryBaseUrlIsTemplate = /\{[^}]*\}/.test(registryEntry.baseUrl);
361
329
  const userBaseUrl = typeof provider.baseUrl === "string" ? provider.baseUrl.trim() : "";
362
330
  const userBaseUrlIsResolved = userBaseUrl.length > 0 && !/\{[^}]*\}/.test(userBaseUrl);
@@ -417,6 +385,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
417
385
  ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined
418
386
  ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent }
419
387
  : {}),
388
+ ...(provider.dropResponsesReasoningItems === undefined && registryEntry.dropResponsesReasoningItems !== undefined
389
+ ? { dropResponsesReasoningItems: registryEntry.dropResponsesReasoningItems }
390
+ : {}),
420
391
  // The request path resolves through routedProviderConfig() and never calls
421
392
  // enrichProviderFromRegistry(), so a saved provider row written before the
422
393
  // registry learned this flag must be backfilled here or route.provider never
@@ -565,10 +536,15 @@ function routeResult(
565
536
  const effectiveModelId = redirected ?? modelId;
566
537
  const effectiveRouteReason = redirected ? "blocked-model-redirect" : routeReason;
567
538
  const codexAccountMode = providerCodexAccountMode(providerName, provider);
539
+ const routedProvider = routedProviderConfig(providerName, provider);
540
+ const effectiveAlias = config
541
+ ? effectiveProviderAliasDecision(providerName, provider, config)
542
+ : undefined;
568
543
  return {
569
544
  providerName,
570
- provider: routedProviderConfig(providerName, provider),
545
+ provider: routedProvider,
571
546
  modelId: effectiveModelId,
547
+ staticPolicy: captureRouteStaticPolicy(providerName, effectiveModelId, routedProvider, effectiveAlias),
572
548
  routeKind,
573
549
  routeReason: effectiveRouteReason,
574
550
  ...(codexAccountMode ? { codexAccountMode } : {}),
@@ -200,6 +200,12 @@ export function resolveProductionBehaviorValues(
200
200
  : reasoningEfforts.length > 0 || Boolean(registryEntry?.reasoningEfforts),
201
201
  ),
202
202
  "reasoning.efforts": behaviorRow("provider_config", reasoningEfforts),
203
+ // Whether the configured ladder above is what the WIRE uses, or only what the catalog
204
+ // advertises. An adapter shipping its own per-model table (command-code) otherwise wins for
205
+ // models it knows, and it also repairs a refused rung by replaying without the effort. Two
206
+ // routes that disagree about this send different bytes and recover differently, so they are
207
+ // not the same compatibility subject and must not share evidence.
208
+ "reasoning.effortsAuthoritative": behaviorRow("provider_config", effective.modelReasoningEffortsAuthoritative === true),
203
209
  "reasoning.defaultEffort": behaviorRow("provider_config", defaultReasoningEffort),
204
210
  "reasoning.effortMap": behaviorRow("provider_config", reasoningEffortMap),
205
211
  "reasoning.wireFormat": behaviorRow("provider_config", effective.reasoningWireFormat ?? null),
@@ -209,6 +215,9 @@ export function resolveProductionBehaviorValues(
209
215
  }),
210
216
  "reasoning.replayMode": behaviorRow("provider_config", {
211
217
  preserveResponses: effective.preserveResponsesReasoningContent === true,
218
+ // Dropping replayed reasoning items changes the continuation body, so two routes that
219
+ // disagree about it are not the same compatibility subject and must not share evidence.
220
+ dropResponses: effective.dropResponsesReasoningItems === true,
212
221
  preserveContent: includesModel(effective.preserveReasoningContentModels, modelId),
213
222
  placeholder: includesModel(effective.requiresReasoningPlaceholderModels, modelId),
214
223
  }),
@@ -1,5 +1,8 @@
1
1
  import type { OcxConfig, OcxProviderConfig } from "../../types";
2
2
  import type { InboundWire } from "../../providers/registry";
3
+ import { getProviderRegistryEntry } from "../../providers/registry";
4
+ import { providerMatchesRegistryTransportWithStaticGuards } from "../../providers/static-model-discovery";
5
+ import { resolveModelPolicy } from "../../providers/resolved-model-policy";
3
6
  import { subjectIdForSubject } from "../../lab/digest";
4
7
  import { buildRouteSubjectV1 } from "../../lab/subject/route-subject";
5
8
  import { buildProtocolSubjectV1 } from "../../lab/subject/protocol-subject";
@@ -65,7 +68,19 @@ export function resolveCompatibilitySubjectsForInboundWire(
65
68
  inboundWire: InboundWire,
66
69
  configDir?: string,
67
70
  ): ResolvedPolicyCompatibilitySubjects {
68
- const effective = resolveWireProtocolOverride(providerName, modelId, routed, inboundWire);
71
+ const registryEntry = getProviderRegistryEntry(providerName);
72
+ const staticPolicy = resolveModelPolicy({
73
+ providerName,
74
+ modelId,
75
+ provider: routed,
76
+ registryEntry,
77
+ transportMatchedRegistry: !!registryEntry
78
+ && providerMatchesRegistryTransportWithStaticGuards(providerName, routed),
79
+ inboundWire,
80
+ modelCapabilities: routed.modelCapabilities?.[modelId],
81
+ ...(routed.authMode ? { effectiveAuth: { authMode: routed.authMode } } : {}),
82
+ });
83
+ const effective = resolveWireProtocolOverride(providerName, modelId, routed, inboundWire, staticPolicy);
69
84
  const baseUrl = typeof effective.baseUrl === "string" ? effective.baseUrl.trim() : "";
70
85
  const adapter = effective.adapter ?? "openai-responses";
71
86
  const inboundProtocol = inboundProtocolForWire(inboundWire);
@@ -3,6 +3,7 @@ import type { OcxProviderConfig } from "../types";
3
3
  import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
4
4
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
5
5
  import { type InboundWire, providerModelWireDefault } from "../providers/registry";
6
+ import type { ResolvedModelPolicy } from "../providers/resolved-model-policy";
6
7
 
7
8
  /**
8
9
  * Resolve the wire a single model should use: a hard pin first, then a configured
@@ -22,7 +23,15 @@ export function resolveWireProtocolOverride(
22
23
  modelId: string,
23
24
  providerConfig: OcxProviderConfig,
24
25
  inbound: InboundWire = "responses",
26
+ staticPolicy?: ResolvedModelPolicy,
25
27
  ): OcxProviderConfig {
28
+ // RouteResult policy is recaptured for the original inbound protocol whenever routing selects
29
+ // or replaces a destination, so every downstream rebuild consumes the same adapter authority.
30
+ if (staticPolicy) {
31
+ return staticPolicy.model.adapter !== providerConfig.adapter
32
+ ? { ...providerConfig, adapter: staticPolicy.model.adapter }
33
+ : providerConfig;
34
+ }
26
35
  const pinned = pinnedWireAdapter(providerName, modelId);
27
36
  if (pinned && providerConfig.adapter !== pinned) {
28
37
  return { ...providerConfig, adapter: pinned };
@@ -1,3 +1,4 @@
1
+ import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path";
1
2
  import { modelCapabilitiesConfigError } from "../config/provider-validation";
2
3
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3
4
  import { initialModelSelection } from "../providers/initial-model-selection";
@@ -29,6 +30,7 @@ import {
29
30
  } from "../config/provider-validation";
30
31
  import { providerDestinationConfigError } from "../lib/destination-policy";
31
32
  import { redactSecretString } from "../lib/redact";
33
+ import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
32
34
  import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
33
35
  import { providerConfigSeed } from "../providers/derive";
34
36
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -754,6 +756,10 @@ export function providerManagementConfigError(
754
756
  }
755
757
  const destinationError = providerDestinationConfigError(name, typed);
756
758
  if (destinationError) return `provider ${name} ${destinationError}`;
759
+ for (const field of ["responsesPath", "chatCompletionsPath"] as const) {
760
+ const sendPathError = providerRelativeSendPathConfigError(field, raw[field]);
761
+ if (sendPathError) return `provider ${JSON.stringify(redactSecretString(name))} ${sendPathError}`;
762
+ }
757
763
  const headersError = providerHeadersConfigError(typed.headers);
758
764
  if (headersError) return `provider ${name} ${headersError}`;
759
765
  const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429);
@@ -793,6 +799,8 @@ export function providerManagementConfigError(
793
799
  }
794
800
  const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
795
801
  if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
802
+ const suppressSyntheticMaxError = booleanRecordConfigError(raw.modelSuppressSyntheticMax, "modelSuppressSyntheticMax");
803
+ if (suppressSyntheticMaxError) return `provider ${name} ${suppressSyntheticMaxError}`;
796
804
  const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
797
805
  raw.modelReasoningSummaryDelivery,
798
806
  raw.modelSupportsReasoningSummaries,
@@ -834,6 +842,22 @@ export function providerManagementConfigError(
834
842
  "omitReasoningEffortWithToolsModels",
835
843
  );
836
844
  if (toolReasoningOptOutError) return `provider ${name} ${toolReasoningOptOutError}`;
845
+ const unsupportedHostedToolsError = nonBlankStringArrayConfigError(
846
+ raw.unsupportedHostedTools,
847
+ "unsupportedHostedTools",
848
+ );
849
+ if (unsupportedHostedToolsError) return `provider ${name} ${unsupportedHostedToolsError}`;
850
+ if (Array.isArray(raw.unsupportedHostedTools)) {
851
+ // Closed vocabulary, same reason as the config schema: an unrecognized name would be
852
+ // stored and then strip nothing, so the operator would keep getting the upstream 400
853
+ // this field exists to prevent.
854
+ const unknownTool = (raw.unsupportedHostedTools as unknown[])
855
+ .find(tool => typeof tool === "string" && !DECLARABLE_HOSTED_TOOL_TYPES.has(tool.trim()));
856
+ if (unknownTool !== undefined) {
857
+ return `provider ${name} unsupportedHostedTools must name only hosted tool types: `
858
+ + `${[...DECLARABLE_HOSTED_TOOL_TYPES].join(", ")}`;
859
+ }
860
+ }
837
861
  const openRouterError = openRouterRoutingConfigError(typed);
838
862
  if (openRouterError) return `provider ${name} ${openRouterError}`;
839
863
  const vercelError = vercelGatewayRoutingConfigError(typed);
@@ -918,12 +942,15 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
918
942
  supportsServiceTier: "editor",
919
943
  modelSupportsServiceTier: "editor",
920
944
  preserveResponsesReasoningContent: "editor",
945
+ dropResponsesReasoningItems: "editor",
946
+ modelReasoningEffortsAuthoritative: "editor",
921
947
  decodesNativeCompactionBlobs: "editor",
922
948
  allowEncryptedV2AgentTasks: "editor",
923
949
  allowPrivateNetwork: "editor",
924
950
  upstreamHttpVersion: "editor",
925
951
  upstreamWebsocket: "editor",
926
952
  directGeminiWireRenames: "editor",
953
+ googleToolSchemaPolicy: "editor",
927
954
  disabled: "editor",
928
955
  codexAccountMode: "editor",
929
956
  apiKey: "redacted",
@@ -964,6 +991,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
964
991
  refreshPolicy: "editor",
965
992
  reasoningEfforts: "editor",
966
993
  modelReasoningEfforts: "editor",
994
+ modelSuppressSyntheticMax: "editor",
967
995
  modelDefaultReasoningEfforts: "editor",
968
996
  pinnedReasoningEffort: "editor",
969
997
  modelPinnedReasoningEfforts: "editor",
@@ -979,6 +1007,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
979
1007
  xaiResponsesDefaultVersion: "runtime",
980
1008
  zaiResponsesDefaultVersion: "runtime",
981
1009
  supportsResponsesCustomTools: "editor",
1010
+ unsupportedHostedTools: "editor",
982
1011
  responsesSnapshotRepair: "editor",
983
1012
  webSearchBridge: "editor",
984
1013
  reasoningEffortMap: "editor",
@@ -23,7 +23,7 @@ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPo
23
23
  import { redactSecretString } from "../lib/redact";
24
24
  import { resolveClientRetryAfter } from "../lib/retry-after";
25
25
  import { estimateTokens } from "../lib/token-estimate";
26
- import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
26
+ import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
27
27
  import { evidenceFromBody } from "../routing/request-evidence";
28
28
  import { resolveWireProtocolOverride } from "./adapter-resolve";
29
29
  import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
@@ -150,10 +150,18 @@ async function handleChatCompletionsWithBudget(
150
150
  let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
151
151
  try {
152
152
  const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
153
- route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req));
154
- // Settle the wire once so every branch below reads the adapter this model will
155
- // actually use, not the provider-wide default (#404).
156
- route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
153
+ // Preserve the routed destination for Go recognition, then settle the wire before
154
+ // deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
155
+ const routedProvider = route.provider;
156
+ route.staticPolicy = captureRouteStaticPolicy(
157
+ route.providerName, route.modelId, routedProvider, route.staticPolicy.effectiveAlias, "chat",
158
+ );
159
+ const wireProvider = resolveWireProtocolOverride(route.providerName, route.modelId, routedProvider, "chat", route.staticPolicy);
160
+ route.provider = resolveOpenCodeGoTransport(
161
+ wireProvider,
162
+ getOrAllocateRequestSessionLane(req),
163
+ routedProvider,
164
+ );
157
165
  logCtx.model = route.modelId;
158
166
  logCtx.providerAdapter = route.provider.adapter;
159
167
  logCtx.requestedModel = requestedModel;
@@ -152,6 +152,8 @@ export function nativeChatSse(
152
152
  let sawFinish = false;
153
153
  let sawDone = false;
154
154
  let firstOutput = false;
155
+ let sawTextOutput = false;
156
+ let sawToolCalls = false;
155
157
  let settled = false;
156
158
  let cancelled = false;
157
159
  let cancelledBySignal = false;
@@ -264,7 +266,7 @@ export function nativeChatSse(
264
266
  const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
265
267
  const finished = choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0);
266
268
  if (finished) sawFinish = true;
267
- const hasProgress = choices.some(choice => {
269
+ const hasText = choices.some(choice => {
268
270
  if (!isRec(choice) || !isRec(choice.delta)) return false;
269
271
  const delta = choice.delta;
270
272
  return (typeof delta.content === "string" && delta.content.length > 0)
@@ -272,15 +274,22 @@ export function nativeChatSse(
272
274
  || (typeof delta.reasoning === "string" && delta.reasoning.length > 0)
273
275
  || (Array.isArray(delta.reasoning_details) && delta.reasoning_details.some(detail =>
274
276
  isRec(detail) && typeof detail.text === "string" && detail.text.length > 0))
275
- || (typeof delta.refusal === "string" && delta.refusal.length > 0)
276
- || (Array.isArray(delta.tool_calls) && delta.tool_calls.some(tool => {
277
- if (!isRec(tool)) return false;
278
- const fn = isRec(tool.function) ? tool.function : {};
279
- return (typeof tool.id === "string" && tool.id.length > 0)
280
- || (typeof fn.name === "string" && fn.name.length > 0)
281
- || (typeof fn.arguments === "string" && fn.arguments.length > 0);
282
- }));
277
+ || (typeof delta.refusal === "string" && delta.refusal.length > 0);
283
278
  });
279
+ const hasTool = choices.some(choice => {
280
+ if (!isRec(choice) || !isRec(choice.delta)) return false;
281
+ const delta = choice.delta;
282
+ return Array.isArray(delta.tool_calls) && delta.tool_calls.some(tool => {
283
+ if (!isRec(tool)) return false;
284
+ const fn = isRec(tool.function) ? tool.function : {};
285
+ return (typeof tool.id === "string" && tool.id.length > 0)
286
+ || (typeof fn.name === "string" && fn.name.length > 0)
287
+ || (typeof fn.arguments === "string" && fn.arguments.length > 0);
288
+ });
289
+ });
290
+ if (hasText) sawTextOutput = true;
291
+ if (hasTool) sawToolCalls = true;
292
+ const hasProgress = hasText || hasTool;
284
293
  if (hasProgress || finished) pullDeadline = performance.now() + stallMs;
285
294
  else if (performance.now() >= pullDeadline) throw stalled;
286
295
  if (!firstOutput && hasProgress) {
@@ -358,6 +367,14 @@ export function nativeChatSse(
358
367
  if (!sawDone) enqueue(controller, "data: [DONE]\n\n");
359
368
  settle(200);
360
369
  controller.close();
370
+ } else if (sawToolCalls) {
371
+ // Mid tool-call truncation must fail closed to prevent downstream executing partial arguments.
372
+ fail(controller, "upstream SSE ended mid tool call without a terminal signal — possible truncation", "upstream_sse_truncated");
373
+ } else if (sawTextOutput) {
374
+ // Upstream produced text or reasoning output but missed the terminal frame; recover cleanly with [DONE].
375
+ enqueue(controller, "data: [DONE]\n\n");
376
+ settle(200);
377
+ controller.close();
361
378
  } else {
362
379
  fail(controller, "upstream SSE ended before a terminal event", "upstream_sse_truncated");
363
380
  }
@@ -50,7 +50,7 @@ import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate
50
50
  import type { OcxProviderTransport } from "../providers/xai-transport";
51
51
  import type { RouteResult } from "../router";
52
52
  import type { OcxConfig, OcxProviderConfig } from "../types";
53
- import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./responses/fetch-helpers";
53
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
54
54
  import { linkAbortSignal } from "./responses";
55
55
  import {
56
56
  addFinalRequestLog,
@@ -351,9 +351,15 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
351
351
  if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
352
352
  if (init.signal?.aborted) throw init.signal.reason;
353
353
  noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
354
- const dispatched = await ((activeProvider as OcxProviderTransport).fetch ?? execute)(request.url, applyUpstreamRecoveryInit({
355
- ...init, method: request.method, headers, body: request.body,
356
- }, transportRecovery));
354
+ // A reselected provider transport is still a physical send: the connection policy
355
+ // and manual-redirect ownership wrap the selected implementation (#4992).
356
+ const dispatched = await sendWithConnectionPolicy(
357
+ (activeProvider as OcxProviderTransport).fetch ?? execute,
358
+ request.url,
359
+ applyUpstreamRecoveryInit({
360
+ ...init, method: request.method, headers, body: request.body,
361
+ }, transportRecovery),
362
+ );
357
363
  if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
358
364
  return dispatched;
359
365
  },