@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923

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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-EJxiUFMq.js +0 -50
  259. package/gui/dist/assets/index-BmJwNBHL.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -37,7 +37,7 @@ export type StaticProviderPolicyField =
37
37
  | "supportsResponsesCustomTools" | "preserveResponsesReasoningContent" | "dropResponsesReasoningItems"
38
38
  | "modelSupportsReasoningSummaries"
39
39
  | "supportsVerbosity" | "modelSupportsVerbosity" | "responsesItemIdRepair" | "autoToolChoiceOnlyModels"
40
- | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels"
40
+ | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "inlineThinkTagModels"
41
41
  | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "showThinkingSummary"
42
42
  | "escapeBuiltinToolNames" | "googleMode" | "project" | "location" | "modelCapabilities"
43
43
  | "modelAutoCompactTokenLimits" | "modelSuppressSyntheticMax" | "modelReasoningSummaryDelivery"
@@ -230,6 +230,8 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
230
230
  "preserveReasoningContentModels", "requiresReasoningPlaceholderModels",
231
231
  "reasoningSplitModels", "reasoningDetailsModels", "thinkingToggleModels", "thinkingBudgetModels",
232
232
  ] as const) putUnion(key, entry?.[key]);
233
+ // This parser is opt-in: an explicit list, including [], overrides registry defaults.
234
+ putScalar("inlineThinkTagModels", entry?.inlineThinkTagModels);
233
235
  for (const directModel of entry?.directReasoningEffortModels ?? []) {
234
236
  const staleBudget = [directModel, ...(entry?.thinkingBudgetModels ?? [])];
235
237
  const routedStaleBudget = [...(entry?.thinkingBudgetModels ?? []), directModel];
@@ -281,7 +283,7 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
281
283
  return exact !== "unknown" ? exact : operatorDefault !== undefined ? "operator" : registryDefault !== undefined ? "registry" : "unknown";
282
284
  };
283
285
  const configuredAdapter = provider.modelAdapters?.[input.modelId];
284
- const pin = pinnedWireAdapter(input.providerName, input.modelId);
286
+ const pin = pinnedWireAdapter(input.providerName, input.modelId, provider);
285
287
  const normalizedModelId = input.modelId.trim().toLowerCase();
286
288
  const registryWire = wireDefault(entry?.modelWireDefaults?.[normalizedModelId], provider, entry,
287
289
  input.inboundWire ?? "responses", resolvedAuthMode);
@@ -1,5 +1,5 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
- import { captureWireAdapterHardPins } from "../types";
2
+ import { captureWireAdapterHardPinPrefixes, captureWireAdapterHardPins } from "../types";
3
3
  import { isCanonicalOpenAiForwardProvider } from "./openai-tiers";
4
4
  import {
5
5
  getProviderRegistryEntry,
@@ -113,6 +113,7 @@ function buildFastPolicyAuthority(
113
113
  }),
114
114
  modelAdapters: Object.freeze({ ...(provider.modelAdapters ?? {}) }),
115
115
  hardPins: captureWireAdapterHardPins(providerName),
116
+ hardPinPrefixes: captureWireAdapterHardPinPrefixes(providerName, provider),
116
117
  registryWireDefaults: cloneRegistryWireDefaults(registry?.modelWireDefaults),
117
118
  });
118
119
  return authority;
@@ -158,6 +159,7 @@ function authorityForProvider(
158
159
  ...authority,
159
160
  modelAdapters: Object.freeze({}),
160
161
  hardPins: Object.freeze({}),
162
+ hardPinPrefixes: Object.freeze({}),
161
163
  registryWireDefaults: Object.freeze({}),
162
164
  });
163
165
  }
@@ -168,11 +168,12 @@ export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId:
168
168
  }
169
169
  // models.dev publishes the per-model ladder that routed providers never expose on /models.
170
170
  // (OpenCode Zen Go answers ids only). Only consulted when nothing was configured for this
171
- // model, so every hand-written contract stays authoritative. The snapshot refreshes itself in
172
- // the background; no snapshot means the previous behaviour.
173
- // The refresh is asked for only once a snapshot has already answered, which means it only ever
174
- // refreshes a STALE snapshot. Review asked for the opposite — refresh when the snapshot is
175
- // missing or corrupt, since that is the case this lookup cannot serve. That is declined here:
171
+ // model, so every hand-written contract stays authoritative. Catalog sync bootstraps the
172
+ // snapshot with a bounded wait; an existing stale snapshot refreshes in the background.
173
+ // The refresh helper refuses to fetch a missing snapshot, so it only ever refreshes a STALE
174
+ // snapshot, including when a classified model supplied the fallback ladder. Review asked for
175
+ // the opposite — refresh when the snapshot is missing or corrupt, since that is the case this
176
+ // lookup cannot serve. That is declined here:
176
177
  // a missing snapshot is the default state of every fresh install and every test process, so
177
178
  // requesting the fetch here puts a models.dev request on the request path of the first routed
178
179
  // turn to a gated destination. Refreshing a snapshot that does not exist is catalog-sync work,
@@ -4,6 +4,7 @@ import {
4
4
  unwrapFreeformToolInput,
5
5
  } from "./apply-patch-envelope";
6
6
  import { declaresCodeModeExec } from "../types/tools";
7
+ import { parseCodeModeShellInput } from "./code-mode-shell-input";
7
8
 
8
9
  function isPlainObject(value: unknown): value is Record<string, unknown> {
9
10
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -31,6 +32,10 @@ export function compileCodeModeHelperInput(
31
32
  const helperName = toolName.startsWith("default.")
32
33
  ? toolName.slice("default.".length)
33
34
  : toolName;
35
+ if (helperName === "exec_command" && wireToolName === "exec") {
36
+ const args = parseCodeModeShellInput(argumentsText);
37
+ if (args) return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
38
+ }
34
39
  if (helperName === "apply_patch") {
35
40
  // `resolveCodeModeHelperName` decides this IS an apply-patch call by reading
36
41
  // `unwrapFreeformToolInput(argumentsText, wireToolName)`, which strips an outer Markdown
@@ -43,8 +48,12 @@ export function compileCodeModeHelperInput(
43
48
  // is an apply_patch wrapper and is not an `exec` fallback field, and the recognizer already
44
49
  // declines it under `exec`; reading it here would compile a body that recognition rejected,
45
50
  // which is exactly the drift a second, looser unwrap introduces.
51
+ const bodyToolName = wireToolName ?? helperName;
52
+ const normalizedBodyToolName = bodyToolName.startsWith("default.")
53
+ ? bodyToolName.slice("default.".length)
54
+ : bodyToolName;
46
55
  const patch = normalizeApplyPatchDelimiters(
47
- unwrapFreeformToolInput(argumentsText, wireToolName ?? helperName),
56
+ unwrapFreeformToolInput(argumentsText, normalizedBodyToolName),
48
57
  );
49
58
  return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
50
59
  }
@@ -96,8 +105,8 @@ export function compileCodeModeHelperInput(
96
105
  * wrong.
97
106
  *
98
107
  * This adds that second case: the name is already `exec` so nothing was rewritten, but
99
- * the body is a complete patch envelope and therefore cannot be the JavaScript that
100
- * `exec` runs. Same inference the name-based path makes, drawn from the payload.
108
+ * the body is a complete patch envelope or an unambiguous structured shell call.
109
+ * Same inference the name-based path makes, drawn from the payload.
101
110
  *
102
111
  * Returns undefined for everything else, including JavaScript that merely mentions a
103
112
  * patch envelope — that body is a real program and is forwarded byte-identical.
@@ -116,5 +125,6 @@ export function resolveCodeModeHelperName(
116
125
  // `tools.apply_patch(...)` JavaScript would be the mis-route this repair exists to avoid.
117
126
  if (!declaresCodeModeExec(declaredNames)) return undefined;
118
127
  if (typeof argumentsText !== "string" || argumentsText === "") return undefined;
119
- return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText, "exec")) ? "apply_patch" : undefined;
128
+ if (isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText, "exec"))) return "apply_patch";
129
+ return parseCodeModeShellInput(argumentsText) ? "exec_command" : undefined;
120
130
  }
@@ -0,0 +1,54 @@
1
+ import { unwrapFreeformToolInput } from "./apply-patch-envelope";
2
+ import { scanFreeformWrapper } from "./freeform-wrapper-scan";
3
+
4
+ const SHELL_ARGUMENT_KEYS = new Set([
5
+ "cmd", "command", "workdir", "shell", "login", "tty", "yield_time_ms",
6
+ "max_output_tokens", "sandbox_permissions", "justification", "prefix_rule",
7
+ ]);
8
+ let javascriptParser: Bun.Transpiler | undefined;
9
+
10
+ /** Recognize shell arguments, never guess a shell from an ordinary freeform program. */
11
+ export function parseCodeModeShellInput(argumentsText: string): Record<string, unknown> | undefined {
12
+ let parsed: unknown;
13
+ try {
14
+ // Only the canonical input wrapper is removed: the cmd/command object is the payload.
15
+ parsed = JSON.parse(unwrapFreeformToolInput(argumentsText));
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
20
+ const args = parsed as Record<string, unknown>;
21
+ if (Object.keys(args).some(key => !SHELL_ARGUMENT_KEYS.has(key))) return undefined;
22
+ const keys = ["cmd", "command"].filter(key => Object.hasOwn(args, key));
23
+ if (keys.length !== 1) return undefined;
24
+ const command = args[keys[0]!];
25
+ if (typeof command !== "string" || command.trim() === "") return undefined;
26
+ // cmd/command also exist as historical JavaScript fallback fields. Preserve every valid
27
+ // program, including ambiguous identifiers such as `ls`. Parsing never executes the source.
28
+ try {
29
+ javascriptParser ??= new Bun.Transpiler({ loader: "js" });
30
+ javascriptParser.scan(`async function __codeModeInput() {\n${command}\n}`);
31
+ return undefined;
32
+ } catch {
33
+ const { command: _alias, ...rest } = args;
34
+ return { ...rest, cmd: command };
35
+ }
36
+ }
37
+
38
+ /** Hold possible shell objects until completion can choose their executable representation. */
39
+ export function mayBecomeCodeModeShellInput(argumentsText: string, input: string): boolean {
40
+ const head = input.trimStart();
41
+ if (head === "" || head.startsWith("{")) return true;
42
+ // Canonical JavaScript streams progressively; avoid reparsing its growing wrapper on every
43
+ // delta. The shared prefix scanner is bounded independently of the command's size.
44
+ if (input === argumentsText || scanFreeformWrapper(argumentsText).kind === "input") return false;
45
+ try {
46
+ const args = JSON.parse(argumentsText);
47
+ // A fallback cmd value becomes visible only when the outer object closes. Do not emit
48
+ // that command before completion replaces it with tools.exec_command JavaScript.
49
+ return !!args && typeof args === "object" && !Object.hasOwn(args, "input")
50
+ && (Object.hasOwn(args, "cmd") || Object.hasOwn(args, "command"));
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
@@ -241,6 +241,160 @@ function rewriteForUpstream(
241
241
  return changed ? next : value;
242
242
  }
243
243
 
244
+ /** Request-layer compatibility failure. Callers map this to HTTP 400, never an unhandled 500. */
245
+ export class RoutedCustomToolCompatError extends Error {
246
+ readonly code = "custom_tool_compat";
247
+ constructor(
248
+ readonly stage: string,
249
+ readonly itemType: string,
250
+ ) {
251
+ super(`custom_tool_compat: ${stage}: ${itemType}`);
252
+ this.name = "RoutedCustomToolCompatError";
253
+ }
254
+ }
255
+
256
+ function collectDeclaredFunctionWireNames(body: unknown): Set<string> {
257
+ const names = new Set<string>();
258
+ const register = (tool: unknown, namespace?: string): void => {
259
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") return;
260
+ names.add(customToolWireName(namespace, tool.name));
261
+ };
262
+ for (const group of collectResponsesToolGroups(body)) {
263
+ for (const tool of group) {
264
+ if (!isPlainObject(tool)) continue;
265
+ if (tool.type === "namespace" && typeof tool.name === "string" && Array.isArray(tool.tools)) {
266
+ for (const child of tool.tools) register(child, tool.name);
267
+ continue;
268
+ }
269
+ register(tool);
270
+ }
271
+ }
272
+ return names;
273
+ }
274
+
275
+ function historicalCallIdentity(
276
+ item: Record<string, unknown>,
277
+ ): { name: string; namespace?: string } | undefined {
278
+ if (typeof item.name !== "string" || item.name.length === 0) return undefined;
279
+ return {
280
+ name: item.name,
281
+ ...(typeof item.namespace === "string" ? { namespace: item.namespace } : {}),
282
+ };
283
+ }
284
+
285
+ function sameHistoricalIdentity(
286
+ left: { name: string; namespace?: string },
287
+ right: { name: string; namespace?: string },
288
+ ): boolean {
289
+ return left.name === right.name && left.namespace === right.namespace;
290
+ }
291
+
292
+ /**
293
+ * Convert remaining protocol-history custom items when the destination has denied native custom
294
+ * tools. Walks only the top-level `input` array so tool-output JSON cannot be rewritten, and does
295
+ * not merge historical names into the live declaration / restore sets.
296
+ */
297
+ function rewriteHistoricalCustomItems(
298
+ body: unknown,
299
+ declaredFunctionWireNames: ReadonlySet<string>,
300
+ ): unknown {
301
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
302
+
303
+ const calls = new Map<string, { name: string; namespace?: string }>();
304
+ const historicalCustomCallIds = new Set<string>();
305
+ for (const item of body.input) {
306
+ if (!isPlainObject(item)) continue;
307
+ if (item.type !== "custom_tool_call" && item.type !== "function_call") continue;
308
+ if (typeof item.call_id !== "string" || item.call_id.length === 0) {
309
+ if (item.type === "custom_tool_call") {
310
+ throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.call_id");
311
+ }
312
+ continue;
313
+ }
314
+ const identity = historicalCallIdentity(item);
315
+ if (!identity) {
316
+ if (item.type === "custom_tool_call") {
317
+ throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.name");
318
+ }
319
+ continue;
320
+ }
321
+ const existing = calls.get(item.call_id);
322
+ if (existing) {
323
+ throw new RoutedCustomToolCompatError(
324
+ "historical_item",
325
+ sameHistoricalIdentity(existing, identity) ? "duplicate_call_id" : "call_id",
326
+ );
327
+ }
328
+ calls.set(item.call_id, identity);
329
+ if (item.type === "custom_tool_call") historicalCustomCallIds.add(item.call_id);
330
+ }
331
+
332
+ let changed = false;
333
+ const input = body.input.map(item => {
334
+ if (!isPlainObject(item)) return item;
335
+ if (item.type === "custom_tool_call") {
336
+ if (typeof item.call_id !== "string" || item.call_id.length === 0) {
337
+ throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.call_id");
338
+ }
339
+ if (typeof item.name !== "string" || item.name.length === 0) {
340
+ throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.name");
341
+ }
342
+ if (typeof item.input !== "string") {
343
+ throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.input");
344
+ }
345
+ const wireName = customToolWireName(
346
+ typeof item.namespace === "string" ? item.namespace : undefined,
347
+ item.name,
348
+ );
349
+ if (declaredFunctionWireNames.has(wireName)) {
350
+ throw new RoutedCustomToolCompatError("historical_collision", "declared_function_name");
351
+ }
352
+ const { input: rawInput, id: _id, ...rest } = item;
353
+ changed = true;
354
+ return {
355
+ ...rest,
356
+ type: "function_call",
357
+ arguments: JSON.stringify({ input: rawInput }),
358
+ };
359
+ }
360
+ if (
361
+ item.type === "custom_tool_call_output"
362
+ && typeof item.call_id === "string"
363
+ && historicalCustomCallIds.has(item.call_id)
364
+ ) {
365
+ changed = true;
366
+ return { ...item, type: "function_call_output" };
367
+ }
368
+ return item;
369
+ });
370
+ return changed ? { ...body, input } : body;
371
+ }
372
+
373
+ export function validateFinalCustomToolCompatibility(
374
+ body: unknown,
375
+ supportsResponsesCustomTools?: boolean,
376
+ ): void {
377
+ if (supportsResponsesCustomTools !== false || !isPlainObject(body)) return;
378
+
379
+ const rejectCustomDeclaration = (tool: unknown): void => {
380
+ if (!isPlainObject(tool)) return;
381
+ if (tool.type === "custom") throw new RoutedCustomToolCompatError("final_guard", "custom");
382
+ if (tool.type === "namespace" && Array.isArray(tool.tools)) {
383
+ for (const child of tool.tools) rejectCustomDeclaration(child);
384
+ }
385
+ };
386
+ for (const group of collectResponsesToolGroups(body)) {
387
+ for (const tool of group) rejectCustomDeclaration(tool);
388
+ }
389
+ if (!Array.isArray(body.input)) return;
390
+ for (const item of body.input) {
391
+ if (!isPlainObject(item) || typeof item.type !== "string") continue;
392
+ if (item.type === "custom_tool_call" || item.type === "custom_tool_call_output") {
393
+ throw new RoutedCustomToolCompatError("final_guard", item.type);
394
+ }
395
+ }
396
+ }
397
+
244
398
  export function rewriteRoutedCustomToolsForUpstream(
245
399
  body: unknown,
246
400
  supportsResponsesCustomTools?: boolean,
@@ -255,22 +409,36 @@ export function rewriteRoutedCustomToolsForUpstream(
255
409
  for (const name of repairNames) {
256
410
  if (!toolChoiceAllowsRoutedCustomTool(body, name, repairNames)) repairNames.delete(name);
257
411
  }
258
- if (conversionNames.size === 0) return { body, names, repairNames };
259
- const callIds = new Set<string>();
260
- collectConvertedCallIds(body, conversionNames, callIds);
261
- return { body: rewriteForUpstream(body, conversionNames, callIds), names, repairNames };
412
+ if (conversionNames.size === 0 && supportsResponsesCustomTools !== false) {
413
+ return { body, names, repairNames };
414
+ }
415
+ let next = body;
416
+ if (conversionNames.size > 0) {
417
+ const callIds = new Set<string>();
418
+ collectConvertedCallIds(body, conversionNames, callIds);
419
+ next = rewriteForUpstream(body, conversionNames, callIds);
420
+ }
421
+ if (supportsResponsesCustomTools === false) {
422
+ next = rewriteHistoricalCustomItems(next, collectDeclaredFunctionWireNames(body));
423
+ }
424
+ return { body: next, names, repairNames };
262
425
  }
263
426
 
264
427
  /**
265
428
  * A delta result has no tool name. Without its call, lowering cannot tell whether it belongs
266
429
  * to a converted function or a native custom tool. Request full replay instead of guessing.
430
+ * A destination that has denied custom tools also cannot map an orphan result when the current
431
+ * catalog is empty, so that case must request replay rather than forwarding the native type.
267
432
  */
268
433
  export function hasUnmappedRoutedCustomToolOutput(
269
434
  body: unknown,
270
435
  supportsResponsesCustomTools?: boolean,
271
436
  ): boolean {
272
437
  if (!isPlainObject(body) || !Array.isArray(body.input)) return false;
273
- if (collectRoutedCustomToolNames(body, supportsResponsesCustomTools).size === 0) return false;
438
+ if (
439
+ supportsResponsesCustomTools !== false
440
+ && collectRoutedCustomToolNames(body, supportsResponsesCustomTools).size === 0
441
+ ) return false;
274
442
  const callIds = new Set<string>();
275
443
  for (const item of body.input) {
276
444
  if (isPlainObject(item)
@@ -42,6 +42,28 @@ function replayThoughtSignatureMetadata(
42
42
  return signature ? { google: { thoughtSignature: signature } } : undefined;
43
43
  }
44
44
 
45
+ /**
46
+ * Repair one bounded inbound-history corruption: a JSON object literal that lost exactly
47
+ * its opening brace (observed as `code":"…}` after `{"` went missing, taking the key's
48
+ * opening quote with it). Only a text that ends with `}` and parses into an object once
49
+ * the brace is restored counts — anything looser keeps the tolerated-{} fallback so
50
+ * freeform text that merely resembles JSON is never rewritten.
51
+ */
52
+ function repairJsonObjectEnvelope(text: string): Record<string, unknown> | undefined {
53
+ if (!text.endsWith("}")) return undefined;
54
+ // A body that still opens with a quoted key lost only `{`; the observed shape lost
55
+ // `{"` together, taking the key's opening quote with it. Both restorations must parse
56
+ // into an object, so freeform text that merely resembles JSON is never rewritten.
57
+ const candidate = text.startsWith('"') ? `{${text}` : `{"${text}`;
58
+ try {
59
+ const parsed: unknown = JSON.parse(candidate);
60
+ if (isObj(parsed)) return parsed;
61
+ } catch {
62
+ /* fall through to the tolerated-{} path */
63
+ }
64
+ return undefined;
65
+ }
66
+
45
67
 
46
68
 
47
69
  function ensureAssistantPlaceholder(messages: OcxMessage[], modelId: string, now: number): OcxAssistantMessage {
@@ -94,6 +116,10 @@ function attachPendingReasoningToCallOwner(
94
116
 
95
117
  const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
96
118
 
119
+ export function hasValidatedActiveReasoningEffort(options: Pick<OcxRequestOptions, "reasoning">): boolean {
120
+ return options.reasoning !== undefined && options.reasoning !== "none";
121
+ }
122
+
97
123
 
98
124
  export function parseRequest(
99
125
  body: unknown,
@@ -333,7 +359,16 @@ export function parseRequest(
333
359
  const parsed: unknown = JSON.parse(rawArgs);
334
360
  if (isObj(parsed)) args = parsed;
335
361
  } catch {
336
- console.warn(`[parser] function_call ${call.call_id} has non-JSON arguments; defaulting to {}`);
362
+ // One observed serialization corruption loses exactly the JSON object's opening
363
+ // brace; the closed envelope is tight enough to repair back into a call the
364
+ // routed model can still see and retry, instead of replaying {} forever.
365
+ const repaired = repairJsonObjectEnvelope(rawArgs);
366
+ if (repaired === undefined) {
367
+ console.warn(`[parser] function_call ${call.call_id} has non-JSON arguments; defaulting to {}`);
368
+ } else {
369
+ args = repaired;
370
+ console.warn(`[parser] function_call ${call.call_id} arguments lost the JSON opening brace; repaired from history`);
371
+ }
337
372
  }
338
373
  }
339
374
  // Do NOT map Responses item `id` (fc_/ctc_/…) onto `thoughtSignature`. That field is
@@ -542,7 +577,8 @@ export function parseRequest(
542
577
  options.reasoning = requestedEffort;
543
578
  }
544
579
  const summaryMode = data.reasoning?.summary;
545
- if (!summaryMode || summaryMode === "none") options.hideThinkingSummary = true;
580
+ const reasoningActive = hasValidatedActiveReasoningEffort(options);
581
+ if (summaryMode === "none" || (!summaryMode && !reasoningActive)) options.hideThinkingSummary = true;
546
582
  if (data.presence_penalty !== undefined) options.presencePenalty = data.presence_penalty;
547
583
  if (data.frequency_penalty !== undefined) options.frequencyPenalty = data.frequency_penalty;
548
584
  if (data.service_tier !== undefined) options.serviceTier = data.service_tier;
@@ -192,6 +192,32 @@ export function reasoningReplayServingIdentityChanged(
192
192
  return previous !== undefined && previous.identity !== current.identity;
193
193
  }
194
194
 
195
+ /**
196
+ * Whether the conversation's last serving route kept its items in a store this route cannot
197
+ * read: a different durable destination or credential.
198
+ *
199
+ * A replayed reasoning item's `rs_*` id names an item in that store. Once the store differs, a
200
+ * stateful Responses destination resolves the id against its own store and fails the turn with
201
+ * `Item with id … not found` (#5583), so the id is as foreign as the blob beside it. A change of
202
+ * provider label, adapter or model on the same destination and credential keeps the same store,
203
+ * and there the id remains resolvable. Unknown, expired, or evicted provenance is not a change.
204
+ */
205
+ export function reasoningReplayItemStoreChanged(
206
+ scope: OcxReasoningReplayScopeRef | undefined,
207
+ ): boolean {
208
+ const current = servingIdentityFor(scope);
209
+ if (!current) return false;
210
+ const at = now();
211
+ sweepExpiredServingIdentities(at);
212
+ const previous = servingIdentities.get(current.threadId);
213
+ if (previous === undefined || previous.identity === current.identity) return false;
214
+ const storeOwner = (identity: string): string => {
215
+ const [, destination, , , credential] = JSON.parse(identity) as ReasoningReplayIdentityTuple;
216
+ return JSON.stringify([destination, credential]);
217
+ };
218
+ return storeOwner(previous.identity) !== storeOwner(current.identity);
219
+ }
220
+
195
221
  /** Record the route only after it has successfully served the conversation. */
196
222
  export function commitReasoningReplayServingIdentity(
197
223
  scope: OcxReasoningReplayScopeRef | undefined,
package/src/router.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
2
2
  import { createHash } from "node:crypto";
3
3
  import { peekAuthStore } from "./oauth/store";
4
+ import { resolveDevinApiBaseUrl, validateDevinApiBaseUrl } from "./oauth/devin/api-base";
4
5
  import {
5
6
  getCombo,
6
7
  isComboTargetInCooldown,
@@ -155,14 +156,21 @@ export function knownModelIdsForProvider(
155
156
  // This callback runs only for a scoped entry, not for each provider in an alias scan.
156
157
  const routed = routedProviderConfig(provName, prov);
157
158
  let key = routed.apiKey;
159
+ let destination = routed.baseUrl;
158
160
  if (routed.authMode === "oauth") {
159
161
  const set = peekAuthStore()[provName];
160
162
  const account = set?.accounts.find(row => row.id === set.activeAccountId);
161
163
  if (!account || account.needsReauth || !Number.isFinite(account.credential.expires)
162
164
  || account.credential.expires <= Date.now()) return undefined;
163
165
  key = account.credential.access;
166
+ if (routed.adapter === "devin") destination = validateDevinApiBaseUrl(account.credential.apiBaseUrl) ?? routed.baseUrl;
164
167
  }
165
- return key ? createHash("sha256").update(key).digest("hex") : undefined;
168
+ if (!key) return undefined;
169
+ return createHash("sha256")
170
+ .update(routed.adapter === "devin"
171
+ ? JSON.stringify([key, resolveDevinApiBaseUrl(destination)])
172
+ : key)
173
+ .digest("hex");
166
174
  });
167
175
  for (const cached of cachedModels ?? []) ids.add(cached.id);
168
176
  for (const model of config?.customModels ?? []) {
@@ -337,6 +345,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
337
345
  const preserveReasoningContentModels = staticPolicy.preserveReasoningContentModels;
338
346
  const requiresReasoningPlaceholderModels = staticPolicy.requiresReasoningPlaceholderModels;
339
347
  const reasoningSplitModels = staticPolicy.reasoningSplitModels;
348
+ const inlineThinkTagModels = staticPolicy.inlineThinkTagModels;
340
349
  const reasoningDetailsModels = staticPolicy.reasoningDetailsModels;
341
350
  const thinkingToggleModels = staticPolicy.thinkingToggleModels;
342
351
  const thinkingBudgetModels = staticPolicy.thinkingBudgetModels;
@@ -479,6 +488,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
479
488
  ...(preserveReasoningContentModels ? { preserveReasoningContentModels } : {}),
480
489
  ...(requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels } : {}),
481
490
  ...(reasoningSplitModels ? { reasoningSplitModels } : {}),
491
+ ...(inlineThinkTagModels ? { inlineThinkTagModels } : {}),
482
492
  ...(reasoningDetailsModels ? { reasoningDetailsModels } : {}),
483
493
  ...(thinkingToggleModels ? { thinkingToggleModels } : {}),
484
494
  ...(thinkingBudgetModels ? { thinkingBudgetModels } : {}),
@@ -22,6 +22,7 @@ import { getConfigDir } from "../../config";
22
22
  import { recordOwnedConfigPath } from "../../lib/config-ownership";
23
23
  import {
24
24
  currentUsageLogRevision,
25
+ encodePersistedRequestedModel,
25
26
  normalizeUsageEntryForTest,
26
27
  usageLogPath,
27
28
  type PersistedUsageEntry,
@@ -157,7 +158,9 @@ function extractRow(entry: PersistedUsageEntry): Array<string | number | null> {
157
158
  entry.timestamp,
158
159
  entry.provider,
159
160
  entry.model,
160
- entry.requestedModel ?? null,
161
+ // Old canonical JSONL rows can predate bounded selector persistence. Encode
162
+ // the disposable projection on rebuild so exact filters match across versions.
163
+ typeof entry.requestedModel === "string" ? encodePersistedRequestedModel(entry.requestedModel) : null,
161
164
  entry.status,
162
165
  entry.surface ?? null,
163
166
  entry.inboundProtocol ?? null,
@@ -510,7 +513,9 @@ function queryRows(
510
513
  };
511
514
  if (filters.provider !== undefined) add("provider = ?", filters.provider);
512
515
  if (filters.model !== undefined) add("model = ?", filters.model);
513
- if (filters.requestedModel !== undefined) add("requested_model = ?", filters.requestedModel);
516
+ // Rows store the bounded encoded form, so the lookup value must be encoded the
517
+ // same way — short selectors encode to themselves and still match verbatim.
518
+ if (filters.requestedModel !== undefined) add("requested_model = ?", encodePersistedRequestedModel(filters.requestedModel));
514
519
  if (filters.status !== undefined) add("status = ?", filters.status);
515
520
  if (filters.conversationId !== undefined) add("conversation_id = ?", filters.conversationId);
516
521
  if (filters.surface !== undefined) add("surface = ?", filters.surface);
@@ -6,7 +6,9 @@
6
6
  * (ADR-1/ADR-8 in devlog/_fin/260804_router_intelligence/000_master_plan.md).
7
7
  */
8
8
 
9
- export const HISTORY_SCHEMA_VERSION = 1;
9
+ // Version 2 rebuilds the derived projection so old raw requested-model selectors
10
+ // are replaced by the bounded encoding already used by canonical JSONL reads.
11
+ export const HISTORY_SCHEMA_VERSION = 2;
10
12
  export const HISTORY_DB_FILENAME = "routing-history.sqlite";
11
13
 
12
14
  export const HISTORY_META_KEYS = {
@@ -32,7 +32,7 @@ export function resolveWireProtocolOverride(
32
32
  ? { ...providerConfig, adapter: staticPolicy.model.adapter }
33
33
  : providerConfig;
34
34
  }
35
- const pinned = pinnedWireAdapter(providerName, modelId);
35
+ const pinned = pinnedWireAdapter(providerName, modelId, providerConfig);
36
36
  if (pinned && providerConfig.adapter !== pinned) {
37
37
  return { ...providerConfig, adapter: pinned };
38
38
  }
@@ -47,7 +47,7 @@ export function resolveWireProtocolOverride(
47
47
  if (requested
48
48
  && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(requested)
49
49
  && requested !== providerConfig.adapter
50
- && !isWirePinnedModel(providerName, modelId)
50
+ && !isWirePinnedModel(providerName, modelId, providerConfig)
51
51
  // A forward provider hands the caller's own credential upstream; the chat adapter
52
52
  // only ever sends provider.apiKey, so switching wires here would drop the auth.
53
53
  && !isCanonicalOpenAiForwardProvider(providerConfig)) {
@@ -818,6 +818,8 @@ export function providerManagementConfigError(
818
818
  if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
819
819
  const suppressSyntheticMaxError = booleanRecordConfigError(raw.modelSuppressSyntheticMax, "modelSuppressSyntheticMax");
820
820
  if (suppressSyntheticMaxError) return `provider ${name} ${suppressSyntheticMaxError}`;
821
+ const verbositySupportError = booleanRecordConfigError(raw.modelSupportsVerbosity, "modelSupportsVerbosity");
822
+ if (verbositySupportError) return `provider ${name} ${verbositySupportError}`;
821
823
  const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
822
824
  raw.modelReasoningSummaryDelivery,
823
825
  raw.modelSupportsReasoningSummaries,
@@ -1060,6 +1062,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
1060
1062
  transientRetryOn5xx: "editor",
1061
1063
  retryOnReset: "editor",
1062
1064
  reasoningSplitModels: "editor",
1065
+ inlineThinkTagModels: "editor",
1063
1066
  reasoningDetailsModels: "editor",
1064
1067
  thinkingToggleModels: "editor",
1065
1068
  thinkingBudgetModels: "editor",
@@ -199,7 +199,20 @@ async function handleChatCompletionsWithBudget(
199
199
  }
200
200
  // Combos must enter the Responses routing path so child selection, forced default
201
201
  // effort, failover, and per-attempt telemetry run before any native Chat send.
202
- if (!route.combo && !effortRow && isNativeChatRouteEligible(route, chatBody, config)) chatNativeRoute = route;
202
+ if (!route.combo && !effortRow && isNativeChatRouteEligible(route, chatBody, config)) {
203
+ chatNativeRoute = route;
204
+ // Reserve an input estimate for spend without recording it as usage: native Chat attempts
205
+ // keep the provider-reported counts, as they did before the reservation existed.
206
+ if (logCtx.usageLogInputTokens === undefined) {
207
+ const parts = [JSON.stringify(chatBody.messages ?? [])];
208
+ if (chatBody.tools !== undefined) parts.push(JSON.stringify(chatBody.tools));
209
+ logCtx.spendInputEstimateTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel));
210
+ }
211
+ const outputCeiling = chatBody.max_completion_tokens ?? chatBody.max_tokens;
212
+ if (typeof outputCeiling === "number" && outputCeiling > 0) {
213
+ logCtx.spendOutputCeilingTokens = Math.trunc(outputCeiling);
214
+ }
215
+ }
203
216
  } catch (err) {
204
217
  if (err instanceof AdmissionModelDeniedError) {
205
218
  logCtx.requestedModel = requestedModel;