@bitkyc08/opencodex 2.86.0 → 2.87.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 (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/router.ts CHANGED
@@ -14,25 +14,16 @@ import {
14
14
  import type { NormalizedComboConfig } from "./combos/types";
15
15
  import { hasOwnProvider } from "./config/provider-name";
16
16
  import { anthropicInstanceRowShapeMatches } from "./providers/anthropic-instance-id";
17
- import { providerUsesKeyAuthOverride, resolveProviderApiKey } from "./providers/key-store";
18
- import { captureProviderApiKeySelection } from "./providers/api-key-selection-capture";
17
+ import { reconcileRouterWarningMemos, routedProviderConfig } from "./providers/routed-config";
19
18
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
20
- import { redactSecretString, redactUrlForLog } from "./lib/redact";
21
19
  import {
22
20
  PROVIDER_REGISTRY,
23
21
  providerCodexAccountMode,
24
- registryEntryForProviderDestination,
25
22
  } from "./providers/registry";
26
23
  // Imported from the module directly rather than through the registry facade, which is at its
27
24
  // file-size cap.
28
25
  import { registryModelIdKeys } from "./providers/registry/model-ids";
29
- import { applyDirectReasoningEffortContracts, hasLegacyClinePassReasoningEfforts } from "./providers/derive";
30
- import { cloneFastWire } from "./providers/fastwire";
31
- import { fastSwitchOff } from "./providers/fast-opt-in";
32
- import {
33
- providerMatchesRegistryTransportWithStaticGuards,
34
- providerSupportsLiveModelDiscovery,
35
- } from "./providers/static-model-discovery";
26
+ import { providerMatchesRegistryTransportWithStaticGuards } from "./providers/static-model-discovery";
36
27
  import {
37
28
  isCanonicalOpenAiForwardProvider,
38
29
  LEGACY_CHATGPT_PROVIDER_ID,
@@ -202,82 +193,6 @@ export function knownModelIdsForProvider(
202
193
  return [...ids];
203
194
  }
204
195
 
205
- /** Same endpoint modulo surrounding space and trailing slashes — matches `matchBaseUrlChoice`. */
206
- function isSameEndpoint(a: string, b: string): boolean {
207
- return a.trim().replace(/\/+$/, "") === b.trim().replace(/\/+$/, "");
208
- }
209
-
210
- /**
211
- * Origin of a user-configured URL, with the path withheld.
212
- *
213
- * A configured `baseUrl` is user-controlled and its path may itself be the credential — an
214
- * account-scoped route token such as `https://proxy.example/v1/8fK2mP7qR4nV6x` is opaque and
215
- * high-entropy, so it matches none of the prefix patterns in `redactSecretString`. Pattern
216
- * redaction cannot be trusted for this value, so no path segment is logged at all. `URL.origin`
217
- * also excludes userinfo, query and fragment.
218
- *
219
- * `…/…` marks that a path was present without revealing it, so a reader can tell an origin-only
220
- * config apart from one whose path was dropped.
221
- */
222
- function configuredOriginForLog(url: string): string {
223
- try {
224
- const parsed = new URL(url.trim());
225
- // "null" is what URL.origin yields for non-special schemes; treat it as unusable.
226
- if (!parsed.origin || parsed.origin === "null") return "(unloggable URL)";
227
- const hasPath = parsed.pathname !== "" && parsed.pathname !== "/";
228
- return hasPath ? `${parsed.origin}/…` : parsed.origin;
229
- } catch {
230
- return "(unparseable URL)";
231
- }
232
- }
233
-
234
- // `routedProviderConfig` runs per request, so warn once per (provider, discarded, effective) triple.
235
- // Keyed by the URLs too: editing config.json to a different wrong value warns again.
236
- const discardedBaseUrlWarnings = new Set<string>();
237
- let lastWarningReconciledGeneration = 0;
238
-
239
- export function reconcileRouterWarningMemos(generation: number): number {
240
- if (generation <= lastWarningReconciledGeneration) return 0;
241
- const removed = discardedBaseUrlWarnings.size;
242
- discardedBaseUrlWarnings.clear();
243
- lastWarningReconciledGeneration = generation;
244
- return removed;
245
- }
246
-
247
- /**
248
- * A pinned registry entry — non-template `baseUrl`, no `allowBaseUrlOverride` — outranks a saved
249
- * `baseUrl`. Dropping it silently is a footgun: requests go to an endpoint the user never
250
- * configured, and a wrong-region or wrong-account URL then surfaces only as a 401 with nothing
251
- * pointing back at the discarded setting.
252
- *
253
- * Warns rather than throws. The effective route is exactly what it was before, so a hard error
254
- * here would break configs that route fine today (a stale `baseUrl` left over from an earlier
255
- * provider is harmless whenever it names the same endpoint the registry pins).
256
- */
257
- function warnIfBaseUrlDiscarded(providerName: string, userBaseUrl: string, effectiveBaseUrl: string): void {
258
- if (isSameEndpoint(userBaseUrl, effectiveBaseUrl)) return;
259
- // Asymmetric on purpose. Past the guard above, `effectiveBaseUrl` is necessarily
260
- // `registryEntry.baseUrl`: the caller passes the resolved URL, and whenever that resolution
261
- // kept the user's value the two are equal and we have already returned. So the effective side
262
- // is a constant from this repo's registry and safe to print in full — it is also the useful
263
- // half, naming the endpoint requests will actually use. The configured side is untrusted.
264
- const discarded = configuredOriginForLog(userBaseUrl);
265
- const effective = redactSecretString(redactUrlForLog(effectiveBaseUrl));
266
- // Key off the logged forms: no raw credential is retained for the process lifetime, and
267
- // rotating a key embedded in the URL no longer re-warns about the same endpoint mismatch.
268
- // Coarser than the raw URLs — two bad paths on one host warn once, which is the right grain.
269
- const key = `${providerName} | ${discarded} | ${effective}`;
270
- if (discardedBaseUrlWarnings.has(key)) return;
271
- discardedBaseUrlWarnings.add(key);
272
- console.warn(
273
- // Routing is what this warning speaks for: an adapter may adjust the endpoint again
274
- // downstream (kiro re-derives the region), so do not promise where the request lands.
275
- `⚠️ config.json provider "${providerName}": configured baseUrl ${discarded} is ignored`
276
- + ` because this provider's endpoint is fixed at ${effective}. A URL saved for a different`
277
- + ` account or region is a common cause of 401s here — drop it, or use the provider whose endpoint matches.`,
278
- );
279
- }
280
-
281
196
  /**
282
197
  * One notice per provider id: the destination is an operator-configured key
283
198
  * (never a caller-supplied string), so the log carries no request content.
@@ -297,239 +212,10 @@ export function resetCompactionFallbackWarningsForTests(): void {
297
212
  compactionFallbackWarnings.clear();
298
213
  }
299
214
 
300
- function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
301
- const resolved = resolveProviderApiKey(apiKey);
302
- return typeof resolved === "string" && resolved.trim().length > 0 ? resolved : undefined;
303
- }
304
-
305
- export function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
306
- provider = { ...provider, _apiKeyAttempt: provider._apiKeyAttempt ?? captureProviderApiKeySelection(provider) };
307
- const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
308
- if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
309
- assertProviderDestinationAllowed(providerName, provider);
310
- // A row whose adapter no longer matches its registry entry still reaches the Responses
311
- // adapter when one model opts in through `modelAdapters` — a Volcengine Coding Plan config
312
- // saved on Chat, for instance. The replay-drop flag belongs to the DESTINATION rather than
313
- // to the provider-wide wire, so it is filled here too; without it that continuation forwards
314
- // the reasoning item the upstream answers 400 to. Destination matching refuses templated and
315
- // overridable base URLs, so this cannot follow a retargeted row, and an explicit value wins.
316
- const destination = registryEntryForProviderDestination(provider);
317
- return {
318
- ...provider,
319
- apiKey: usableResolvedApiKey(provider.apiKey),
320
- ...(provider.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined
321
- ? { dropResponsesReasoningItems: destination.dropResponsesReasoningItems }
322
- : {}),
323
- };
324
- }
325
- const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
326
- const staticModelCatalog = !providerSupportsLiveModelDiscovery(providerName, provider);
327
- const repairLegacyMimoFreeAuth = providerName === "mimo-free"
328
- && staticModelCatalog
329
- && (provider.authMode === undefined || provider.authMode === "local");
330
- const explicitKeyOverride = providerUsesKeyAuthOverride(registryEntry, provider, resolvedApiKey);
331
- const canonicalAuthMode = explicitKeyOverride
332
- ? "key"
333
- : repairLegacyMimoFreeAuth
334
- ? "key"
335
- : registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
336
- ? registryEntry.authKind
337
- : provider.authMode === "forward" ? undefined : provider.authMode;
338
- const staticPolicy = resolveModelPolicy({
339
- providerName,
340
- modelId: "__provider_static__",
341
- provider,
342
- registryEntry,
343
- transportMatchedRegistry: true,
344
- ...(canonicalAuthMode ? { effectiveAuth: { authMode: canonicalAuthMode } } : {}),
345
- }).provider;
346
- const reasoningEffortMap = staticPolicy.reasoningEffortMap;
347
- const modelReasoningEffortMap = staticPolicy.modelReasoningEffortMap;
348
- const modelReasoningEfforts = staticPolicy.modelReasoningEfforts;
349
- const modelDefaultReasoningEfforts = staticPolicy.modelDefaultReasoningEfforts;
350
- const modelContextWindows = staticPolicy.modelContextWindows;
351
- const modelInputModalities = staticPolicy.modelInputModalities;
352
- // Registry static headers are documented as applying to every upstream request, so they are
353
- // filled at resolve time rather than only at seed time: a config written before a header
354
- // existed, or one carrying any header of its own, would otherwise never receive it. User
355
- // headers win, matched case-insensitively so an override replaces rather than duplicates.
356
- const headers = staticPolicy.headers;
357
- const modelMaxInputTokens = staticPolicy.modelMaxInputTokens;
358
- const modelMaxOutputTokens = staticPolicy.modelMaxOutputTokens;
359
- const modelSupportsServiceTier = staticPolicy.modelSupportsServiceTier;
360
- const modelSupportsVerbosity = staticPolicy.modelSupportsVerbosity;
361
- const noVisionModels = staticPolicy.noVisionModels;
362
- const noReasoningModels = staticPolicy.noReasoningModels;
363
- const noTemperatureModels = staticPolicy.noTemperatureModels;
364
- const noTopPModels = staticPolicy.noTopPModels;
365
- const noStopModels = staticPolicy.noStopModels;
366
- const noPenaltyModels = staticPolicy.noPenaltyModels;
367
- const noJsonSchemaModels = staticPolicy.noJsonSchemaModels;
368
- const autoToolChoiceOnlyModels = staticPolicy.autoToolChoiceOnlyModels;
369
- const preserveReasoningContentModels = staticPolicy.preserveReasoningContentModels;
370
- const requiresReasoningPlaceholderModels = staticPolicy.requiresReasoningPlaceholderModels;
371
- const reasoningSplitModels = staticPolicy.reasoningSplitModels;
372
- const inlineThinkTagModels = staticPolicy.inlineThinkTagModels;
373
- const reasoningDetailsModels = staticPolicy.reasoningDetailsModels;
374
- const thinkingToggleModels = staticPolicy.thinkingToggleModels;
375
- const thinkingBudgetModels = staticPolicy.thinkingBudgetModels;
376
- const registryBaseUrlIsTemplate = /\{[^}]*\}/.test(registryEntry.baseUrl);
377
- const userBaseUrl = typeof provider.baseUrl === "string" ? provider.baseUrl.trim() : "";
378
- const userBaseUrlIsResolved = userBaseUrl.length > 0 && !/\{[^}]*\}/.test(userBaseUrl);
379
- if (registryEntry.allowBaseUrlOverride && !userBaseUrlIsResolved) {
380
- throw new Error(`Invalid baseUrl for provider "${providerName}": expected a nonblank URL without unresolved placeholders`);
381
- }
382
- // Registry template URLs are presets; local/self-hosted entries opt in explicitly.
383
- const baseUrl = (registryBaseUrlIsTemplate || registryEntry.allowBaseUrlOverride) && userBaseUrlIsResolved
384
- ? userBaseUrl
385
- : registryEntry.baseUrl;
386
- if (userBaseUrlIsResolved) warnIfBaseUrlDiscarded(providerName, userBaseUrl, baseUrl);
387
- assertProviderDestinationAllowed(providerName, { baseUrl, allowPrivateNetwork: provider.allowPrivateNetwork });
388
-
389
- const resolved: OcxProviderConfig = {
390
- ...provider,
391
- adapter: registryEntry.adapter,
392
- baseUrl,
393
- ...(provider.responsesPath === undefined && registryEntry.responsesPath !== undefined
394
- ? { responsesPath: registryEntry.responsesPath }
395
- : {}),
396
- ...(provider.chatCompletionsPath === undefined && registryEntry.chatCompletionsPath !== undefined
397
- ? { chatCompletionsPath: registryEntry.chatCompletionsPath }
398
- : {}),
399
- ...(provider.requiresAdjacentResponsesToolResults === undefined
400
- && registryEntry.requiresAdjacentResponsesToolResults !== undefined
401
- ? { requiresAdjacentResponsesToolResults: registryEntry.requiresAdjacentResponsesToolResults }
402
- : {}),
403
- ...(provider.requiresPairedResponsesToolResults === undefined
404
- && registryEntry.requiresPairedResponsesToolResults !== undefined
405
- ? { requiresPairedResponsesToolResults: registryEntry.requiresPairedResponsesToolResults }
406
- : {}),
407
- ...(provider.annotateEmptyToolOutputs === undefined
408
- && registryEntry.annotateEmptyToolOutputs !== undefined
409
- ? { annotateEmptyToolOutputs: registryEntry.annotateEmptyToolOutputs }
410
- : {}),
411
- ...(provider.fastWire === undefined && registryEntry.fastWire !== undefined
412
- ? {
413
- fastWire: cloneFastWire(registryEntry.fastWire),
414
- }
415
- : {}),
416
- ...(provider.supportsServiceTier === undefined && registryEntry.supportsServiceTier !== undefined
417
- ? { supportsServiceTier: registryEntry.supportsServiceTier }
418
- : {}),
419
- // An off Fast switch is a provider-wide denial on the runtime provider, so a Fast policy
420
- // resolved without the provider name still refuses (providerFastSwitchOff).
421
- ...(fastSwitchOff(provider, registryEntry) ? { supportsServiceTier: false } : {}),
422
- // Registry-only web-search capability: without this backfill a saved provider row reaches
423
- // the Responses adapter with the flag `undefined`, so the capability gate added in #2262
424
- // reads "unclassified" and forwards Codex's OpenAI-only `web_search` config fields. xAI
425
- // rejects the whole request before inference ("Argument not supported:
426
- // external_web_access"), which killed every routed Grok turn on the Responses lane.
427
- // enrichProviderFromRegistry() already fills this, but the request path resolves through
428
- // routedProviderConfig() and never called it.
429
- ...(provider.supportsOpenAiWebSearchToolFields === undefined
430
- && registryEntry.supportsOpenAiWebSearchToolFields !== undefined
431
- ? { supportsOpenAiWebSearchToolFields: registryEntry.supportsOpenAiWebSearchToolFields }
432
- : {}),
433
- ...(provider.supportsResponsesCustomTools === undefined && registryEntry.supportsResponsesCustomTools !== undefined
434
- ? { supportsResponsesCustomTools: registryEntry.supportsResponsesCustomTools }
435
- : {}),
436
- ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined
437
- ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent }
438
- : {}),
439
- ...(provider.preserveResponsesInputItemIds === undefined && registryEntry.preserveResponsesInputItemIds !== undefined
440
- ? { preserveResponsesInputItemIds: registryEntry.preserveResponsesInputItemIds }
441
- : {}),
442
- ...(provider.preserveResponsesMessageMetadata === undefined && registryEntry.preserveResponsesMessageMetadata !== undefined
443
- ? { preserveResponsesMessageMetadata: registryEntry.preserveResponsesMessageMetadata }
444
- : {}),
445
- ...(provider.dropResponsesReasoningItems === undefined && registryEntry.dropResponsesReasoningItems !== undefined
446
- ? { dropResponsesReasoningItems: registryEntry.dropResponsesReasoningItems }
447
- : {}),
448
- // The request path resolves through routedProviderConfig() and never calls
449
- // enrichProviderFromRegistry(), so a saved provider row written before the
450
- // registry learned this flag must be backfilled here or route.provider never
451
- // carries it and the showThinkingSummary opt-in stays dead.
452
- ...(provider.showThinkingSummary === undefined && registryEntry.showThinkingSummary !== undefined
453
- ? { showThinkingSummary: registryEntry.showThinkingSummary }
454
- : {}),
455
- // Registry-only client-facing repair policy (#938): fill only when the
456
- // saved provider has no explicit policy; clone so runtime never aliases
457
- // the registry constant.
458
- ...(provider.responsesItemIdRepair === undefined && registryEntry.responsesItemIdRepair
459
- ? {
460
- responsesItemIdRepair: {
461
- ...(registryEntry.responsesItemIdRepair.message ? { message: [...registryEntry.responsesItemIdRepair.message] } : {}),
462
- ...(registryEntry.responsesItemIdRepair.reasoning ? { reasoning: [...registryEntry.responsesItemIdRepair.reasoning] } : {}),
463
- ...(registryEntry.responsesItemIdRepair.repairMissingTerminalIds !== undefined
464
- ? { repairMissingTerminalIds: registryEntry.responsesItemIdRepair.repairMissingTerminalIds }
465
- : {}),
466
- ...(registryEntry.responsesItemIdRepair.repairInvalidIds !== undefined
467
- ? { repairInvalidIds: registryEntry.responsesItemIdRepair.repairInvalidIds }
468
- : {}),
469
- },
470
- }
471
- : {}),
472
- authMode: canonicalAuthMode,
473
- apiKey: resolvedApiKey,
474
- ...(staticModelCatalog ? { liveModels: false } : {}),
475
- ...(headers ? { headers } : {}),
476
- // Backfill the Google wire mode + Vertex project/location from the registry when the user
477
- // config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
478
- // through the correct branch (CCA/Vertex) instead of falling back to AI Studio.
479
- ...(provider.googleMode === undefined && registryEntry.googleMode !== undefined ? { googleMode: registryEntry.googleMode } : {}),
480
- ...(provider.project === undefined && registryEntry.project !== undefined ? { project: registryEntry.project } : {}),
481
- ...(provider.location === undefined && registryEntry.location !== undefined ? { location: registryEntry.location } : {}),
482
- ...(provider.contextWindow === undefined && registryEntry.contextWindow !== undefined ? { contextWindow: registryEntry.contextWindow } : {}),
483
- ...((provider.reasoningEfforts === undefined || hasLegacyClinePassReasoningEfforts(providerName, provider))
484
- && registryEntry.reasoningEfforts !== undefined
485
- ? { reasoningEfforts: [...registryEntry.reasoningEfforts] }
486
- : {}),
487
- ...(provider.escapeBuiltinToolNames === undefined && registryEntry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: registryEntry.escapeBuiltinToolNames } : {}),
488
- ...(provider.keyOptional === undefined && registryEntry.keyOptional !== undefined ? { keyOptional: registryEntry.keyOptional } : {}),
489
- ...(provider.modelSuffixBracketStrip === undefined && registryEntry.modelSuffixBracketStrip !== undefined ? { modelSuffixBracketStrip: registryEntry.modelSuffixBracketStrip } : {}),
490
- // Scalar backfill: a persisted config created before the flag shipped inherits the registry
491
- // opt-in, while an explicit user `false` keeps overriding registry `true`.
492
- ...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
493
- ...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}),
494
- ...(provider.chatServiceTier === undefined && registryEntry.chatServiceTier !== undefined ? { chatServiceTier: registryEntry.chatServiceTier } : {}),
495
- ...(provider.openaiChatEofTolerance === undefined && registryEntry.openaiChatEofTolerance !== undefined
496
- ? { openaiChatEofTolerance: registryEntry.openaiChatEofTolerance }
497
- : {}),
498
- ...(provider.reasoningWireFormat === undefined && registryEntry.reasoningWireFormat !== undefined
499
- ? { reasoningWireFormat: registryEntry.reasoningWireFormat }
500
- : {}),
501
- ...(provider.defaultMaxOutputTokens === undefined && registryEntry.defaultMaxOutputTokens !== undefined
502
- ? { defaultMaxOutputTokens: registryEntry.defaultMaxOutputTokens }
503
- : {}),
504
- ...(modelContextWindows ? { modelContextWindows } : {}),
505
- ...(modelInputModalities ? { modelInputModalities } : {}),
506
- ...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
507
- ...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
508
- ...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
509
- ...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
510
- ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
511
- ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
512
- ...(reasoningEffortMap ? { reasoningEffortMap } : {}),
513
- ...(modelReasoningEffortMap ? { modelReasoningEffortMap } : {}),
514
- ...(noVisionModels ? { noVisionModels } : {}),
515
- ...(noReasoningModels ? { noReasoningModels } : {}),
516
- ...(noTemperatureModels ? { noTemperatureModels } : {}),
517
- ...(noTopPModels ? { noTopPModels } : {}),
518
- ...(noStopModels ? { noStopModels } : {}),
519
- ...(noPenaltyModels ? { noPenaltyModels } : {}),
520
- ...(noJsonSchemaModels ? { noJsonSchemaModels } : {}),
521
- ...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}),
522
- ...(preserveReasoningContentModels ? { preserveReasoningContentModels } : {}),
523
- ...(requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels } : {}),
524
- ...(reasoningSplitModels ? { reasoningSplitModels } : {}),
525
- ...(inlineThinkTagModels ? { inlineThinkTagModels } : {}),
526
- ...(reasoningDetailsModels ? { reasoningDetailsModels } : {}),
527
- ...(thinkingToggleModels ? { thinkingToggleModels } : {}),
528
- ...(thinkingBudgetModels ? { thinkingBudgetModels } : {}),
529
- };
530
- applyDirectReasoningEffortContracts(registryEntry, resolved, provider);
531
- return resolved;
532
- }
215
+ // `routedProviderConfig` and its warning memo moved to `providers/routed-config.ts`: the
216
+ // request-time resolver needs no routing state, and `providers/api-key-selection.ts` importing
217
+ // it through this file created an import cycle (router -> quota -> api-keys -> api-key-selection).
218
+ export { reconcileRouterWarningMemos, routedProviderConfig } from "./providers/routed-config";
533
219
 
534
220
  /**
535
221
  * Rows eligible for bare-model inference (configured default model, model list, model alias).
@@ -622,7 +308,8 @@ function crossProviderRedirectTarget(config: OcxConfig, providerName: string, va
622
308
  }
623
309
  }
624
310
 
625
- function isBareOpenAiFamilyModel(modelId: string): boolean {
311
+ /** Exported for the main-account hard-lock fallback: only this family is ever redirected. */
312
+ export function isBareOpenAiFamilyModel(modelId: string): boolean {
626
313
  return !modelId.includes("/")
627
314
  && (/^(?:gpt-|o1-|o3-|o4-)/.test(modelId) || CODEX_INTERNAL_OPENAI_MODELS.has(modelId));
628
315
  }
@@ -94,7 +94,6 @@ interface HealthSample {
94
94
  }
95
95
 
96
96
  interface HealthRow extends HealthSample {
97
- attemptCount?: number;
98
97
  rowJson?: string | null;
99
98
  }
100
99
 
@@ -103,33 +102,53 @@ interface HealthRow extends HealthSample {
103
102
  * Combo/failover requests store each upstream try in `entry.attempts` while
104
103
  * the top-level row records the final outcome; a provider/model that failed
105
104
  * as a non-final attempt must still contribute its own health samples.
105
+ *
106
+ * Only physical sends are evidence: an attempt answered locally or refused
107
+ * before it left the proxy says nothing about the target. The row's terminal
108
+ * facts belong to its last physical send. Returns `undefined` when the row
109
+ * itself is the sample (no attempts, a single attempt, or none named for this
110
+ * candidate) and `[]` when the row sent nothing upstream at all.
106
111
  */
107
112
  function attemptSamplesFor(
108
- row: Pick<HealthRow, "timestamp" | "attemptCount" | "rowJson">,
113
+ row: Pick<HealthRow, "timestamp" | "rowJson">,
109
114
  provider: string,
110
115
  model: string,
111
- ): HealthSample[] {
112
- if (!row.rowJson || (row.attemptCount ?? 1) <= 1) return [];
116
+ ): HealthSample[] | undefined {
117
+ if (!row.rowJson) return undefined;
113
118
  try {
114
- const parsed = JSON.parse(row.rowJson) as { attempts?: unknown };
115
- if (!Array.isArray(parsed.attempts)) return [];
119
+ const parsed = JSON.parse(row.rowJson) as {
120
+ attempts?: unknown;
121
+ closeReason?: unknown;
122
+ terminalStatus?: unknown;
123
+ failureCause?: unknown;
124
+ };
125
+ // A local refusal logged before any attempt began never reached a target.
126
+ if (!Array.isArray(parsed.attempts)) return parsed.failureCause === "local-refusal" ? [] : undefined;
127
+ const attempts = parsed.attempts.filter(
128
+ (attempt): attempt is Record<string, unknown> =>
129
+ !!attempt && typeof attempt === "object" && !Array.isArray(attempt),
130
+ );
131
+ const sent = attempts.filter(attempt => attempt.locallyAnswered !== true && attempt.sendCount !== 0);
132
+ if (sent.length === 0) return [];
133
+ const mine = attempts.filter(attempt => attempt.provider === provider && attempt.model === model);
134
+ if (attempts.length <= 1 || mine.length === 0) return undefined;
135
+ const finalSend = sent.at(-1);
116
136
  const samples: HealthSample[] = [];
117
- for (const attempt of parsed.attempts) {
118
- if (!attempt || typeof attempt !== "object" || Array.isArray(attempt)) continue;
119
- const record = attempt as Record<string, unknown>;
120
- if (record.provider !== provider || record.model !== model) continue;
137
+ for (const record of mine) {
138
+ if (!sent.includes(record)) continue;
121
139
  if (typeof record.status !== "number" || typeof record.durationMs !== "number") continue;
140
+ const isFinal = record === finalSend;
122
141
  samples.push({
123
142
  status: record.status,
124
- closeReason: null,
125
- terminalStatus: null,
143
+ closeReason: isFinal && typeof parsed.closeReason === "string" ? parsed.closeReason : null,
144
+ terminalStatus: isFinal && typeof parsed.terminalStatus === "string" ? parsed.terminalStatus : null,
126
145
  durationMs: record.durationMs,
127
146
  timestamp: row.timestamp,
128
147
  });
129
148
  }
130
149
  return samples;
131
150
  } catch {
132
- return [];
151
+ return undefined;
133
152
  }
134
153
  }
135
154
 
@@ -243,8 +262,7 @@ function computeHistoricalHealthEvidence(
243
262
  }
244
263
  const rows = handle.query(
245
264
  `SELECT status, close_reason AS closeReason, terminal_status AS terminalStatus,
246
- duration_ms AS durationMs, timestamp,
247
- attempt_count AS attemptCount, row_json AS rowJson
265
+ duration_ms AS durationMs, timestamp, row_json AS rowJson
248
266
  FROM requests WHERE ${where.join(" AND ")}
249
267
  ORDER BY timestamp DESC LIMIT ?`,
250
268
  ).all(...values, HEALTH_MAX_SAMPLES) as HealthRow[];
@@ -254,7 +272,7 @@ function computeHistoricalHealthEvidence(
254
272
  // rows that cannot contribute samples for this candidate.
255
273
  const escapeLike = (value: string): string => value.replace(/[\\%_]/g, match => `\\${match}`);
256
274
  const attemptRows = handle.query(
257
- `SELECT timestamp, attempt_count AS attemptCount, row_json AS rowJson
275
+ `SELECT timestamp, row_json AS rowJson
258
276
  FROM requests WHERE timestamp >= ? AND attempt_count > 1
259
277
  AND row_json LIKE ? ESCAPE '\\'
260
278
  AND row_json LIKE ? ESCAPE '\\'
@@ -267,17 +285,14 @@ function computeHistoricalHealthEvidence(
267
285
  input.provider,
268
286
  input.model,
269
287
  HEALTH_MAX_SAMPLES,
270
- ) as Array<
271
- Pick<HealthRow, "timestamp" | "attemptCount" | "rowJson">
272
- >;
288
+ ) as Array<Pick<HealthRow, "timestamp" | "rowJson">>;
273
289
 
274
290
  const samples: HealthSample[] = [];
275
291
  for (const row of rows) {
276
- const attemptSamples = attemptSamplesFor(row, input.provider, input.model);
277
- samples.push(...(attemptSamples.length > 0 ? attemptSamples : [row]));
292
+ samples.push(...(attemptSamplesFor(row, input.provider, input.model) ?? [row]));
278
293
  }
279
294
  for (const row of attemptRows) {
280
- samples.push(...attemptSamplesFor(row, input.provider, input.model));
295
+ samples.push(...(attemptSamplesFor(row, input.provider, input.model) ?? []));
281
296
  }
282
297
  // Newest first for the consecutive-failure walk; attempt samples inherit
283
298
  // their row's timestamp.
@@ -195,17 +195,18 @@ function readStoredMetaField(dbHandle: Database, key: string): string {
195
195
  return metaValue(dbHandle, key) ?? "";
196
196
  }
197
197
 
198
- function sourceIdentityMatches(dbHandle: Database, revision: UsageLogRevision | null): boolean {
198
+ export function sourceIdentityMatches(dbHandle: Database, revision: UsageLogRevision | null): boolean {
199
199
  const stored = readIndexedMeta(dbHandle);
200
200
  if (revision === null) return stored.sourceSize === 0;
201
201
  const storedPath = readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourcePath);
202
202
  if (!storedPath) return false;
203
- const storedDev = Number(readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceDev));
204
- const storedIno = Number(readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceIno));
203
+ // Compared as exact decimal strings; see UsageLogRevision for why ids are not Numbers.
204
+ const storedDev = readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceDev);
205
+ const storedIno = readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceIno);
205
206
  const storedBirthtimeMs = Number(readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceBirthtimeMs));
206
207
  return storedPath === revision.path
207
- && storedDev === Number(revision.dev)
208
- && storedIno === Number(revision.ino)
208
+ && storedDev === revision.dev
209
+ && storedIno === revision.ino
209
210
  && storedBirthtimeMs === Number(revision.birthtimeMs);
210
211
  }
211
212
 
@@ -10,6 +10,7 @@ import type { Database } from "bun:sqlite";
10
10
  import { closeSync, fstatSync, openSync, readSync } from "node:fs";
11
11
  import {
12
12
  encodePersistedRequestedModel,
13
+ exactFileIdentity,
13
14
  type PersistedUsageEntry,
14
15
  type UsageLogRevision,
15
16
  } from "../../usage/log";
@@ -177,8 +178,11 @@ export function ingestSourceTail(
177
178
  fd = openSync(path, "r");
178
179
  const stat = fstatSync(fd);
179
180
  const expected = options.expected;
180
- if (expected && (Number(stat.dev) !== Number(expected.dev) || Number(stat.ino) !== Number(expected.ino))) {
181
- throw new RequestHistoryIndexStaleError("usage log changed before indexing");
181
+ if (expected) {
182
+ const opened = exactFileIdentity(fd);
183
+ if (opened.dev !== expected.dev || opened.ino !== expected.ino) {
184
+ throw new RequestHistoryIndexStaleError("usage log changed before indexing");
185
+ }
182
186
  }
183
187
  const size = options.endOffset === undefined
184
188
  ? Number(stat.size)
@@ -12,7 +12,7 @@ import type {
12
12
  OcxRoutingUnknownCostCapMode,
13
13
  } from "../types";
14
14
  import { codexAccountNamespaceEntries } from "../codex/account-namespaces";
15
- import { listComboIds, resolveComboId } from "../combos";
15
+ import { resolveComboId } from "../combos/identifiers";
16
16
  import { hasOwnProvider } from "../config/provider-name";
17
17
  import { MAX_COMPATIBILITY_REQUIRED_SUITES } from "./compatibility/types";
18
18
  import { POLICY_NAMESPACE } from "./profile-namespace";
@@ -1,4 +1,5 @@
1
1
  import type { DataPlaneAdmission } from "./auth-cors";
2
+ import { isApiAuthRequired } from "./auth-cors";
2
3
  import type { OcxApiKeyEntry, OcxConfig } from "../types";
3
4
  import { markResponseNonReplayable } from "../lib/upstream-retry";
4
5
 
@@ -77,6 +78,22 @@ export function resolveAdmissionModelScope(
77
78
  return entry ? admissionModelScopeOf(entry) : undefined;
78
79
  }
79
80
 
81
+ /** Consent comes from stored operator config, never request headers. Use the full listener config. */
82
+ export function admissionMayUseMeteredComboTargets(
83
+ config: Pick<OcxConfig, "apiKeys" | "hostname">,
84
+ admission: DataPlaneAdmission | undefined,
85
+ combo?: { allowMeteredTargets?: boolean },
86
+ ): boolean {
87
+ if (admission?.kind === "configured") {
88
+ const entry = (config.apiKeys ?? []).find(key => key.id === admission.keyId);
89
+ return entry?.allowMeteredComboTargets === true;
90
+ }
91
+ // Linked ingress always carries keyed admission, even when the primary bind is loopback.
92
+ // A secondary loopback listener on a public proxy does not qualify either.
93
+ return (!admission || admission.kind === "loopback")
94
+ && !isApiAuthRequired(config) && combo?.allowMeteredTargets === true;
95
+ }
96
+
80
97
  /**
81
98
  * Does this scope admit this resolved destination?
82
99
  *
@@ -30,6 +30,7 @@ import {
30
30
  providerForwardClientHeadersConfigError,
31
31
  reasoningSummaryDeliveryRecordConfigError,
32
32
  upstreamHttpVersionConfigError,
33
+ credentialPinnedProviderConfigError,
33
34
  } from "../config/provider-validation";
34
35
  import { providerDestinationConfigError } from "../lib/destination-policy";
35
36
  import { providerEgressConfigError } from "../lib/provider-egress";
@@ -784,6 +785,8 @@ export function providerManagementConfigError(
784
785
  delete canonicalCandidate.pinnedReasoningEffort;
785
786
  delete canonicalCandidate.modelPinnedReasoningEfforts;
786
787
  delete canonicalCandidate.responsesSnapshotRepair;
788
+ // Explicit history-loss opt-in changes request content, never auth or transport identity.
789
+ delete canonicalCandidate.projectResponsesLiteHostedSearchHistory;
787
790
  // modelCosts is a user-owned display overlay, not part of the canonical
788
791
  // forward seed; it is validated separately below (providerModelCostsConfigError).
789
792
  delete canonicalCandidate.modelCosts;
@@ -912,6 +915,8 @@ export function providerManagementConfigError(
912
915
  if (reasoningSummaryDeliveryError) return `provider ${name} ${reasoningSummaryDeliveryError}`;
913
916
  const modelAdaptersError = modelAdapterRecordConfigError(raw.modelAdapters, "modelAdapters", name, typed);
914
917
  if (modelAdaptersError) return `provider ${name} ${modelAdaptersError}`;
918
+ const credentialPinError = credentialPinnedProviderConfigError(name, typed);
919
+ if (credentialPinError) return `provider ${name} ${credentialPinError}`;
915
920
  const preferHostedToolsError = modelPreferHostedToolsConfigError(
916
921
  raw.modelPreferHostedTools,
917
922
  "modelPreferHostedTools",
@@ -919,6 +924,9 @@ export function providerManagementConfigError(
919
924
  typed,
920
925
  );
921
926
  if (preferHostedToolsError) return `provider ${name} ${preferHostedToolsError}`;
927
+ if (raw.projectResponsesLiteHostedSearchHistory !== undefined && typeof raw.projectResponsesLiteHostedSearchHistory !== "boolean") {
928
+ return `provider ${name} projectResponsesLiteHostedSearchHistory must be a boolean`;
929
+ }
922
930
  if (raw.responsesSnapshotRepair !== undefined && typeof raw.responsesSnapshotRepair !== "boolean") {
923
931
  return `provider ${name} responsesSnapshotRepair must be a boolean`;
924
932
  }
@@ -1053,9 +1061,12 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
1053
1061
  preserveResponsesReasoningContent: "editor",
1054
1062
  preserveResponsesInputItemIds: "editor",
1055
1063
  preserveResponsesMessageMetadata: "editor",
1064
+ projectResponsesLiteHostedSearchHistory: "editor",
1056
1065
  dropResponsesReasoningItems: "editor",
1057
1066
  modelReasoningEffortsAuthoritative: "editor",
1058
1067
  decodesNativeCompactionBlobs: "editor",
1068
+ supportsNativeCompactionTrigger: "editor",
1069
+ supportsNativeCompactEndpoint: "editor",
1059
1070
  allowEncryptedV2AgentTasks: "editor",
1060
1071
  allowPrivateNetwork: "editor",
1061
1072
  // A proxy URL routinely embeds `user:password@`, so it never reaches the dashboard DTO and
@@ -54,6 +54,7 @@ import {
54
54
  } from "./request-log";
55
55
  import { createFinalRequestLog } from "./inference/final-log";
56
56
  import { clientWireLogOf, clientWireOf } from "./inference/client-wire";
57
+ import { attachAdvisorReasoning } from "./inference/advisor-diagnostics";
57
58
  import { directEncodersApply } from "./inference/client-encoder-delivery";
58
59
  import { responseWithDeferredRequestLog } from "./relay";
59
60
  import { handleResponses } from "./responses";
@@ -114,8 +115,10 @@ export async function handleChatCompletions(
114
115
  ): Promise<Response> {
115
116
  const translatorBudget = createTranslatorBudget();
116
117
  try {
118
+ let response = await handleChatCompletionsWithBudget(req, config, logCtx, translatorBudget, logIds);
119
+ if (isInternalCallCapability(req.headers.get(ADVISOR_INTERNAL_CAPABILITY_HEADER))) response = attachAdvisorReasoning(response, logCtx);
117
120
  return finalizeTranslatorBudgetResponse(
118
- await handleChatCompletionsWithBudget(req, config, logCtx, translatorBudget, logIds),
121
+ response,
119
122
  translatorBudget,
120
123
  req.signal,
121
124
  );
@@ -601,6 +604,9 @@ async function handleChatCompletionsWithBudget(
601
604
  // below could never restore this one -- the translated client was told the provider
602
605
  // throttled the turn, and handed a two-second wait to send it again.
603
606
  classified.code = UPSTREAM_RESET_REPLAY_REFUSED_CODE;
607
+ } else if (upstreamCode === "claude_subscription_cooldown") {
608
+ classified.code = "claude_subscription_cooldown";
609
+ classified.type = "rate_limit_error";
604
610
  } else if (upstreamCode === "model_not_found") {
605
611
  // Structured model_not_found must win over classifyError's generic remaps.
606
612
  classified.code = "model_not_found";