@bitkyc08/opencodex 2.64.0-preview.20260923 → 2.65.0-preview.20260925

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 (208) hide show
  1. package/README.md +28 -25
  2. package/bin/ocx.mjs +16 -2
  3. package/gui/dist/assets/App-8NMiZxT0.css +1 -0
  4. package/gui/dist/assets/App-CsYvvpr3.js +50 -0
  5. package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-BEq4OCOz.js → index-BB0iHG8-.js} +12 -12
  8. package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -4
  11. package/src/adapters/anthropic.ts +30 -1
  12. package/src/adapters/claude-cli/adapter.ts +218 -0
  13. package/src/adapters/claude-cli/profiles.ts +38 -0
  14. package/src/adapters/codebuddy/profiles.ts +2 -0
  15. package/src/adapters/coding-agent/profile.ts +12 -4
  16. package/src/adapters/coding-agent/turn.ts +16 -6
  17. package/src/adapters/command-code-tool-text.ts +133 -23
  18. package/src/adapters/cursor/catalog.ts +4 -8
  19. package/src/adapters/cursor/discovery.ts +10 -5
  20. package/src/adapters/cursor/effort-map.ts +2 -6
  21. package/src/adapters/cursor/envelope-echo.ts +10 -5
  22. package/src/adapters/cursor/request-builder.ts +9 -4
  23. package/src/adapters/cursor/thread-continuity.ts +53 -0
  24. package/src/adapters/cursor.ts +29 -13
  25. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  26. package/src/adapters/google-tool-schema.ts +26 -3
  27. package/src/adapters/identity.ts +214 -2
  28. package/src/adapters/openai-chat/passthrough.ts +1 -0
  29. package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
  30. package/src/adapters/openai-chat.ts +43 -15
  31. package/src/adapters/openai-responses/canonical-forward.ts +17 -10
  32. package/src/adapters/openai-responses/passthrough.ts +19 -4
  33. package/src/adapters/openai-responses/request-strips.ts +25 -0
  34. package/src/adapters/openai-responses/web-search.ts +22 -4
  35. package/src/adapters/qoder/profiles.ts +2 -0
  36. package/src/adapters/registry.ts +8 -0
  37. package/src/adapters/run-turn-queue.ts +7 -0
  38. package/src/bridge/response-json.ts +6 -4
  39. package/src/bridge/sse.ts +7 -5
  40. package/src/claude/alias.ts +73 -26
  41. package/src/claude/context-windows.ts +15 -6
  42. package/src/claude/desktop-3p-library.ts +3 -2
  43. package/src/claude/desktop-3p.ts +1 -1
  44. package/src/claude/desktop-first-party.ts +63 -22
  45. package/src/claude/desktop-picker-profile.ts +302 -0
  46. package/src/claude/desktop-picker.ts +365 -0
  47. package/src/claude/desktop-risk.ts +20 -0
  48. package/src/claude/gateway-cache.ts +13 -4
  49. package/src/claude/inbound.ts +5 -2
  50. package/src/claude/intercept/connect-proxy.ts +106 -29
  51. package/src/claude/intercept/local-ca.ts +76 -18
  52. package/src/claude/intercept/model-bindings.ts +145 -0
  53. package/src/claude/intercept/picker-bootstrap.ts +141 -0
  54. package/src/claude/intercept/picker-ca.ts +126 -0
  55. package/src/claude/intercept/picker-listener.ts +213 -0
  56. package/src/claude/intercept/picker-models.ts +101 -0
  57. package/src/claude/intercept/picker-runtime.ts +330 -0
  58. package/src/claude/intercept/picker-trust.ts +125 -0
  59. package/src/claude/intercept/runtime.ts +149 -2
  60. package/src/claude/model-info.ts +18 -4
  61. package/src/cli/agent.ts +16 -2
  62. package/src/cli/capabilities.ts +71 -2
  63. package/src/cli/claude-desktop.ts +319 -33
  64. package/src/cli/claude.ts +15 -7
  65. package/src/cli/codex-shim-autorestore.ts +1 -0
  66. package/src/cli/dispatch.ts +29 -23
  67. package/src/cli/effort.ts +16 -6
  68. package/src/cli/ensure-desired-integrations.ts +29 -4
  69. package/src/cli/ready.ts +1 -1
  70. package/src/cli/registry.ts +8 -0
  71. package/src/cli/restart-scope.ts +9 -1
  72. package/src/cli/runtime-api.ts +17 -0
  73. package/src/cli/system-command.ts +11 -12
  74. package/src/client/connect.ts +12 -2
  75. package/src/client/state.ts +39 -1
  76. package/src/clients/config-export.ts +57 -9
  77. package/src/codex/auth-context.ts +51 -11
  78. package/src/codex/catalog/derive-entry.ts +5 -5
  79. package/src/codex/catalog/gather-capture.ts +25 -6
  80. package/src/codex/catalog/metadata.ts +3 -3
  81. package/src/codex/catalog/parsing.ts +39 -1
  82. package/src/codex/catalog/provider-models.ts +8 -6
  83. package/src/codex/catalog/retained-sync.ts +17 -7
  84. package/src/codex/catalog/sync.ts +2 -2
  85. package/src/codex/desktop-app-restart.ts +8 -0
  86. package/src/codex/desktop-switches.ts +9 -1
  87. package/src/codex/home.ts +43 -4
  88. package/src/codex/inject/config-toml.ts +137 -0
  89. package/src/codex/inject/plan.ts +23 -0
  90. package/src/codex/inject/remove.ts +21 -1
  91. package/src/codex/inject.ts +63 -8
  92. package/src/codex/journal.ts +36 -0
  93. package/src/codex/main-account-hard-lock.ts +14 -1
  94. package/src/codex/main-account-policy-wait.ts +109 -0
  95. package/src/codex/native-profile-startup.ts +24 -6
  96. package/src/codex/native-residue.ts +28 -10
  97. package/src/codex/quota-types.ts +8 -1
  98. package/src/codex/quota.ts +49 -7
  99. package/src/codex/runtime.ts +31 -10
  100. package/src/combos/failover.ts +7 -6
  101. package/src/combos/resolve.ts +91 -14
  102. package/src/combos/types.ts +18 -1
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/schema/config-schema.ts +21 -2
  105. package/src/config/schema/leaf-validators.ts +6 -3
  106. package/src/config/subagent-models.ts +3 -1
  107. package/src/generated/compatibility-version.json +275 -163
  108. package/src/images/artifacts.ts +14 -0
  109. package/src/integrations/owned-refresh.ts +1 -1
  110. package/src/integrations/ownership-policy.ts +32 -1
  111. package/src/integrations/state.ts +3 -1
  112. package/src/integrations/writer.ts +9 -0
  113. package/src/lib/config-ownership.ts +3 -0
  114. package/src/lib/errors.ts +87 -0
  115. package/src/lib/package-tree-integrity.ts +1 -1
  116. package/src/lib/request-execution-budget.ts +42 -14
  117. package/src/lib/request-resend-gate.ts +4 -3
  118. package/src/lib/retry-delay.ts +7 -1
  119. package/src/lib/tool-envelope-echo-filter.ts +236 -0
  120. package/src/lib/upstream-retry.ts +20 -12
  121. package/src/oauth/index.ts +1 -0
  122. package/src/oauth/kiro-credentials.ts +14 -1
  123. package/src/oauth/login-cli.ts +1 -0
  124. package/src/providers/derive.ts +4 -0
  125. package/src/providers/fast-opt-in.ts +31 -0
  126. package/src/providers/model-rename-fields.ts +2 -0
  127. package/src/providers/provider-id-rewrite.ts +2 -0
  128. package/src/providers/quota/vendor-probes-key.ts +8 -2
  129. package/src/providers/registry/entries-core.ts +45 -2
  130. package/src/providers/registry/entries-extended.ts +99 -12
  131. package/src/providers/registry/model-ids.ts +2 -0
  132. package/src/providers/registry/types.ts +7 -1
  133. package/src/providers/resolved-model-policy.ts +11 -4
  134. package/src/providers/service-tier.ts +10 -3
  135. package/src/responses/code-mode-helper-compat.ts +3 -0
  136. package/src/responses/parser.ts +45 -3
  137. package/src/router.ts +6 -0
  138. package/src/server/auth-cors.ts +8 -0
  139. package/src/server/chat-completions.ts +3 -1
  140. package/src/server/claude-messages.ts +69 -14
  141. package/src/server/grok-upstream-envelope-echo.ts +120 -0
  142. package/src/server/index/claude-intercept-lifecycle.ts +24 -0
  143. package/src/server/index/serve-options.ts +2 -2
  144. package/src/server/index.ts +7 -0
  145. package/src/server/management/agent-settings-routes.ts +204 -112
  146. package/src/server/management/claude-desktop-picker-routes.ts +128 -0
  147. package/src/server/management/combo-routes.ts +59 -2
  148. package/src/server/management/config-routes.ts +81 -39
  149. package/src/server/management/context.ts +3 -0
  150. package/src/server/management/native-integration-routes.ts +169 -93
  151. package/src/server/management/provider-routes.ts +46 -4
  152. package/src/server/management/route-registry.ts +10 -0
  153. package/src/server/management/routing-profile-routes.ts +9 -0
  154. package/src/server/management/sidebar-routes.ts +52 -0
  155. package/src/server/management-api.ts +2 -0
  156. package/src/server/proxy-liveness.ts +38 -3
  157. package/src/server/request-log-account-rotation.ts +53 -0
  158. package/src/server/request-log.ts +3 -16
  159. package/src/server/responses/adapter-dispatch.ts +17 -1
  160. package/src/server/responses/agent-task-recovery.ts +49 -26
  161. package/src/server/responses/codex-ws-exchange.ts +18 -7
  162. package/src/server/responses/codex-ws-wire.ts +27 -4
  163. package/src/server/responses/core-combo.ts +21 -3
  164. package/src/server/responses/core-normalize.ts +7 -0
  165. package/src/server/responses/core-opaque-recovery.ts +3 -2
  166. package/src/server/responses/encrypted-payload.ts +15 -14
  167. package/src/server/responses/fetch-helpers.ts +1 -1
  168. package/src/server/responses/input-admission.ts +10 -6
  169. package/src/server/responses/native-response-control.ts +2 -2
  170. package/src/server/responses/passthrough-delivery.ts +182 -12
  171. package/src/server/responses/passthrough-dispatch.ts +102 -44
  172. package/src/server/responses/policy-fallback.ts +3 -0
  173. package/src/server/responses/policy-refusal.ts +67 -0
  174. package/src/server/responses/request-send-budget.ts +4 -0
  175. package/src/server/responses/run-turn-execution.ts +42 -11
  176. package/src/server/responses/ws-upstream.ts +2 -1
  177. package/src/server/system-env-shell.ts +0 -1
  178. package/src/server/system-env.ts +25 -4
  179. package/src/service/cli.ts +34 -3
  180. package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
  181. package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
  182. package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
  183. package/src/tray/windows-tray.ps1 +188 -7
  184. package/src/tray/windows.ts +23 -2
  185. package/src/types/config.ts +52 -12
  186. package/src/types/provider.ts +22 -5
  187. package/src/types/tools.ts +11 -5
  188. package/src/types.ts +1 -0
  189. package/src/update/async-check.ts +109 -0
  190. package/src/update/badge.ts +11 -17
  191. package/src/update/check-types.ts +9 -0
  192. package/src/update/desktop-badge.ts +102 -0
  193. package/src/update/index.ts +48 -8
  194. package/src/update/install-detection.d.mts +29 -2
  195. package/src/update/install-detection.mjs +213 -7
  196. package/src/update/job.ts +17 -12
  197. package/src/update/notify.ts +16 -11
  198. package/src/update/pnpm-owner-worker.ts +12 -0
  199. package/src/update/refresh-scheduler.ts +141 -0
  200. package/src/usage/cost.ts +31 -4
  201. package/src/usage/expected-prices.ts +57 -0
  202. package/assets/download-linux.svg +0 -10
  203. package/assets/download-macos.svg +0 -10
  204. package/assets/download-windows.svg +0 -10
  205. package/gui/dist/assets/App-D5eN1TID.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
@@ -440,6 +440,25 @@ function invitesResendAfterReplacement(status: number): boolean {
440
440
  || status === 307 || status === 308 || status === 413 || status >= 500;
441
441
  }
442
442
 
443
+ /**
444
+ * The answer a request keeps once its one operator replacement has gone out.
445
+ *
446
+ * A status that invites another send settles as the refusal. Any other answer keeps its real
447
+ * status: no client retries it, and the caller needs the evidence (a 400 names the request
448
+ * defect). The marker still stops this process from using it as a recovery trigger, such as the
449
+ * opaque-blob rebuild of a 400 or a combo hop on a context overflow, because each of those checks
450
+ * it before sending again.
451
+ */
452
+ export function settleOperatorReplacement(response: Response): Response {
453
+ if (response.ok) return response;
454
+ if (invitesResendAfterReplacement(response.status)) {
455
+ cancelResponseBodyBestEffort(response);
456
+ return replayRefusalResponse();
457
+ }
458
+ markResponseNonReplayable(response);
459
+ return response;
460
+ }
461
+
443
462
  export async function fetchWithAttemptDeadline(
444
463
  url: string,
445
464
  init: RequestInit,
@@ -624,18 +643,7 @@ export async function fetchWithResetRetry(
624
643
  opts.onSendsConsumed?.(1);
625
644
  try {
626
645
  const response = await doFetch(attempt === 0 ? firstRecovery : "connection-reset");
627
- if (spentOperatorReplacement && !response.ok) {
628
- if (invitesResendAfterReplacement(response.status)) {
629
- cancelResponseBodyBestEffort(response);
630
- return replayRefusalResponse();
631
- }
632
- // Any other answer keeps its real status: no client retries it, and the caller needs the
633
- // evidence (a 400 names the request defect). The marker still stops this process from
634
- // using it as a recovery trigger, such as the opaque-blob rebuild of a 400 or a combo hop
635
- // on a context overflow, because each of those checks it before sending again.
636
- markResponseNonReplayable(response);
637
- }
638
- return response;
646
+ return spentOperatorReplacement ? settleOperatorReplacement(response) : response;
639
647
  } catch (err) {
640
648
  if (opts.abortSignal?.aborted) throw err;
641
649
  if (!isConnectionResetError(err)) {
@@ -1270,6 +1270,7 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
1270
1270
  "modelReasoningEffortMap",
1271
1271
  "noTemperatureModels",
1272
1272
  "noTopPModels",
1273
+ "noStopModels",
1273
1274
  "noPenaltyModels",
1274
1275
  "autoToolChoiceOnlyModels",
1275
1276
  "preserveReasoningContentModels",
@@ -179,6 +179,12 @@ export function resolveKiroCliNativeSessionEntries(
179
179
  * Windows: official MSI installs to `C:\Program Files\Kiro-Cli\kiro-cli.exe`, while some local
180
180
  * installs keep the binary next to `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3`.
181
181
  * macOS/Linux: prefer PATH, then the usual user-local bin directories.
182
+ *
183
+ * The canonical `kiro-cli` name is exhausted everywhere first. Only then, and only on Windows, does
184
+ * the short `kiro.exe` name count, and only inside the two dedicated `Kiro-Cli` install folders
185
+ * already trusted for `kiro-cli.exe`, resolved from an absolute base. A short name is never looked
186
+ * up on PATH or in shared POSIX bin directories (`~/.local/bin`, `/usr/local/bin`, `/opt/homebrew/bin`):
187
+ * an unrelated `kiro` there, such as the Kiro IDE launcher, must not be run for credential commands.
182
188
  */
183
189
  export function resolveKiroCliExecutable(
184
190
  inputs: KiroCliNativeInputs & {
@@ -212,6 +218,8 @@ export function resolveKiroCliExecutable(
212
218
  : pathEntries.map(entry => posix.join(entry, "kiro-cli"));
213
219
 
214
220
  const installCandidates: string[] = [];
221
+ // Windows only: kiro.exe inside the dedicated Kiro-Cli folders, tried after every canonical name.
222
+ const shortInstallCandidates: string[] = [];
215
223
  if (inputs.platform === "win32") {
216
224
  const localBase = inputs.env.LOCALAPPDATA?.trim()
217
225
  || (inputs.env.USERPROFILE?.trim() ? win32.join(inputs.env.USERPROFILE.trim(), "AppData", "Local") : "")
@@ -221,6 +229,11 @@ export function resolveKiroCliExecutable(
221
229
  win32.join(localBase, "Kiro-Cli", "kiro-cli.exe"),
222
230
  win32.join(programFiles, "Kiro-Cli", "kiro-cli.exe"),
223
231
  );
232
+ // A relative or drive-relative base would make the short-name lookup depend on the process
233
+ // working directory or current drive, so only a fully qualified drive path qualifies.
234
+ for (const base of [localBase, programFiles]) {
235
+ if (/^[A-Za-z]:[\\/]/.test(base)) shortInstallCandidates.push(win32.join(base, "Kiro-Cli", "kiro.exe"));
236
+ }
224
237
  } else if (inputs.platform === "darwin") {
225
238
  installCandidates.push(
226
239
  posix.join(inputs.home, ".local", "bin", "kiro-cli"),
@@ -234,7 +247,7 @@ export function resolveKiroCliExecutable(
234
247
  );
235
248
  }
236
249
 
237
- for (const candidate of [...pathCandidates, ...installCandidates]) {
250
+ for (const candidate of [...pathCandidates, ...installCandidates, ...shortInstallCandidates]) {
238
251
  if (exists(candidate) && isFile(candidate)) return candidate;
239
252
  }
240
253
  return inputs.platform === "win32" ? "kiro-cli.exe" : "kiro-cli";
@@ -189,6 +189,7 @@ export function providerConfigFromKeyLoginProvider(def: KeyLoginProvider, key: s
189
189
  ...(def.noReasoningModels ? { noReasoningModels: [...def.noReasoningModels] } : {}),
190
190
  ...(def.noTemperatureModels ? { noTemperatureModels: [...def.noTemperatureModels] } : {}),
191
191
  ...(def.noTopPModels ? { noTopPModels: [...def.noTopPModels] } : {}),
192
+ ...(def.noStopModels ? { noStopModels: [...def.noStopModels] } : {}),
192
193
  ...(def.noPenaltyModels ? { noPenaltyModels: [...def.noPenaltyModels] } : {}),
193
194
  ...(def.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...def.autoToolChoiceOnlyModels] } : {}),
194
195
  ...(def.preserveReasoningContentModels ? { preserveReasoningContentModels: [...def.preserveReasoningContentModels] } : {}),
@@ -41,6 +41,7 @@ export interface DerivedKeyLoginProvider {
41
41
  noReasoningModels?: string[];
42
42
  noTemperatureModels?: string[];
43
43
  noTopPModels?: string[];
44
+ noStopModels?: string[];
44
45
  noPenaltyModels?: string[];
45
46
  autoToolChoiceOnlyModels?: string[];
46
47
  preserveReasoningContentModels?: string[];
@@ -269,6 +270,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
269
270
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
270
271
  ...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
271
272
  ...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
273
+ ...(entry.noStopModels ? { noStopModels: [...entry.noStopModels] } : {}),
272
274
  ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
273
275
  ...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
274
276
  ...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}),
@@ -336,6 +338,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
336
338
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
337
339
  ...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
338
340
  ...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
341
+ ...(entry.noStopModels ? { noStopModels: [...entry.noStopModels] } : {}),
339
342
  ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
340
343
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
341
344
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
@@ -552,6 +555,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
552
555
  if (!prov.noReasoningModels && seed.noReasoningModels) prov.noReasoningModels = [...seed.noReasoningModels];
553
556
  if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
554
557
  if (!prov.noTopPModels && seed.noTopPModels) prov.noTopPModels = [...seed.noTopPModels];
558
+ if (!prov.noStopModels && seed.noStopModels) prov.noStopModels = [...seed.noStopModels];
555
559
  if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels];
556
560
  if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
557
561
  if (prov.promptCacheKey === undefined && seed.promptCacheKey !== undefined) prov.promptCacheKey = seed.promptCacheKey;
@@ -0,0 +1,31 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import { getProviderRegistryEntry } from "./registry";
3
+ import type { ProviderRegistryEntry } from "./registry/types";
4
+
5
+ /**
6
+ * Whether a provider's Fast lane is switched off.
7
+ *
8
+ * `fastEnabled: false` turns Fast off on any provider. A registry entry marked `fastOptIn` bills its
9
+ * Fast lane beyond the plan (Anthropic fast mode draws usage credits at 2x price), so it stays off
10
+ * until the operator sets `fastEnabled: true`. Off is expressed as provider capability `false`,
11
+ * which every Fast consumer already treats as a global denial.
12
+ *
13
+ * The registry entry is matched by name without a transport check on purpose: this can only turn
14
+ * Fast off, so a custom endpoint that reuses the name loses nothing it could safely keep.
15
+ */
16
+ export function fastSwitchOff(
17
+ provider: Pick<OcxProviderConfig, "fastEnabled">,
18
+ entry: Pick<ProviderRegistryEntry, "fastOptIn"> | undefined,
19
+ ): boolean {
20
+ if (provider.fastEnabled === false) return true;
21
+ if (provider.fastEnabled === true) return false;
22
+ return entry?.fastOptIn === true;
23
+ }
24
+
25
+ /** `fastSwitchOff` with the registry entry looked up by provider name. */
26
+ export function providerFastSwitchOff(
27
+ providerName: string | undefined,
28
+ provider: Pick<OcxProviderConfig, "fastEnabled">,
29
+ ): boolean {
30
+ return fastSwitchOff(provider, providerName ? getProviderRegistryEntry(providerName) : undefined);
31
+ }
@@ -25,6 +25,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
25
25
  requiresPairedResponsesToolResults: "none",
26
26
  annotateEmptyToolOutputs: "none",
27
27
  supportsServiceTier: "none",
28
+ fastEnabled: "none",
28
29
  modelSupportsServiceTier: "record",
29
30
  preserveResponsesReasoningContent: "none",
30
31
  modelReasoningEffortsAuthoritative: "none",
@@ -101,6 +102,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
101
102
  noReasoningModels: "list",
102
103
  noTemperatureModels: "list",
103
104
  noTopPModels: "list",
105
+ noStopModels: "list",
104
106
  noPenaltyModels: "list",
105
107
  noStructuredOutputModels: "list",
106
108
  noJsonSchemaModels: "list",
@@ -95,6 +95,8 @@ export function rewriteProviderReferences(config: OcxConfig, from: string, to: s
95
95
 
96
96
  routeRecordValues(config.claudeCode?.tierModels as Record<string, string> | undefined);
97
97
  routeRecordValues(config.claudeCode?.modelMap as Record<string, string> | undefined);
98
+ // First-party picker bindings hold routes too; their keys are Anthropic picker ids.
99
+ routeRecordValues(config.claudeCode?.intercept?.modelMap);
98
100
 
99
101
  // Bare provider ids.
100
102
  for (const model of config.customModels ?? []) {
@@ -371,9 +371,15 @@ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig):
371
371
  const toppedUp = toFiniteNumber(preferred.topped_up_balance);
372
372
  const balance = totalBalance ?? grantedBalance ?? toppedUp;
373
373
  if (balance === undefined || balance < 0) return null;
374
+ // The rows are currency-scoped, so the symbol has to follow the row that was
375
+ // picked: the two glyph currencies keep their sign, any other ISO code
376
+ // prefixes the amount, and a row without one keeps the legacy dollar.
377
+ const currency = String(preferred.currency ?? "").trim().toUpperCase();
378
+ const sign = currency === "CNY" ? "¥" : currency === "" || currency === "USD" ? "$" : `${currency} `;
379
+ const amount = (value: number) => `${sign}${value.toFixed(2)}`;
374
380
  const label = grantedBalance !== undefined && grantedBalance > 0
375
- ? `API balance ($${balance.toFixed(2)} total, $${grantedBalance.toFixed(2)} granted)`
376
- : `API balance ($${balance.toFixed(2)})`;
381
+ ? `API balance (${amount(balance)} total, ${amount(grantedBalance)} granted)`
382
+ : `API balance (${amount(balance)})`;
377
383
  return report(provider, "deepseek:balance", {
378
384
  customWindows: [{ label, percent: 0 }],
379
385
  updatedAt: Date.now(),
@@ -267,6 +267,29 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
267
267
  // 260813: grok-4.6 added per docs.x.ai/developers/grok-4-6. Context/vision still match
268
268
  // grok-4.5; the reasoning ladder does not — 4.6 adds the documented xhigh rung.
269
269
  models: XAI_MODELS,
270
+ // grok-4.7-build-fast arrives only through OAuth discovery. We read it as the Grok Build id of
271
+ // what xAI documents as Grok 4.7 Fast: "the same model served on faster infrastructure",
272
+ // offered in Cursor and Grok Build only, not on the public xAI API (docs.x.ai/developers/grok-4-7,
273
+ // fetched 2026-09-24). It therefore inherits grok-4.7's documented facts in the lists below.
274
+ // Its wire pin and service tier stay unclaimed until probed, which is why it is absent from
275
+ // XAI_MODELS, modelWireDefaults and modelSupportsServiceTier.
276
+ // Live 2026-09-20: Chat Completions rejects `stop` on grok-4.6
277
+ // (`400 invalid-argument "Model grok-4.6 does not support parameter stop."`).
278
+ // xAI documents `stop` as unsupported for reasoning models. Claude Code
279
+ // auto-mode always sends stop_sequences; forwarding that as `stop` makes
280
+ // the classifier treat Grok as temporarily unavailable while chat turns
281
+ // still work. Keep caller stop sequences on non-reasoning ids.
282
+ // Live 2026-09-23: grok-4.7 answers the same 400.
283
+ noStopModels: [
284
+ "grok-4.7",
285
+ "grok-4.7-build-fast",
286
+ "grok-4.6",
287
+ "grok-4.5",
288
+ "grok-4.3",
289
+ "grok-4.20-multi-agent-0309",
290
+ "grok-4.20-0309-reasoning",
291
+ "grok-build-0.1",
292
+ ],
270
293
  // Measured only on grok-4.6 against cli-chat-proxy.grok.com: even an invalid
271
294
  // `text.verbosity` value is accepted and low/high/omitted output length is non-monotonic.
272
295
  // Apply the resulting opt-out to the whole xAI lineup because `text.verbosity` is an OpenAI
@@ -278,6 +301,20 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
278
301
  // absent from xAI's documented API, so a model discovered later has no more support for it
279
302
  // than the seeded ones do.
280
303
  supportsVerbosity: false,
304
+ // docs.x.ai/docs/guides/reasoning: presencePenalty and frequencyPenalty "cannot be used with
305
+ // reasoning models. Requests that include them return an error." Live 2026-09-23: grok-4.7
306
+ // answers 400 invalid-argument "Model grok-4.7 does not support parameter presencePenalty."
307
+ // Non-reasoning ids keep caller penalties.
308
+ noPenaltyModels: [
309
+ "grok-4.7",
310
+ "grok-4.7-build-fast",
311
+ "grok-4.6",
312
+ "grok-4.5",
313
+ "grok-4.3",
314
+ "grok-4.20-multi-agent-0309",
315
+ "grok-4.20-0309-reasoning",
316
+ "grok-build-0.1",
317
+ ],
281
318
  defaultModel: "grok-4.5",
282
319
  // Grok 4.7/4.6/4.5 subscription Responses callers use the native wire with the existing
283
320
  // namespace/web-search/replay normalization. Chat remains an explicit modelAdapters
@@ -335,6 +372,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
335
372
  // (they are already listed in noVisionModels below).
336
373
  modelInputModalities: {
337
374
  "grok-4.7": ["text", "image"],
375
+ "grok-4.7-build-fast": ["text", "image"],
338
376
  "grok-4.6": ["text", "image"],
339
377
  "grok-4.5": ["text", "image"],
340
378
  "grok-4.3": ["text", "image"],
@@ -347,7 +385,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
347
385
  // reasoning_content as the top cause of prompt-cache misses on multi-turn conversations
348
386
  // (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13 — devlog/_plan/260713_grok_caching).
349
387
  // Models that never emit reasoning simply have no thinking parts to replay (no-op).
350
- preserveReasoningContentModels: ["grok-4.7", "grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
388
+ preserveReasoningContentModels: ["grok-4.7", "grok-4.7-build-fast", "grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
351
389
  // grok-4.5 reasoning is always-on with low/medium/high (no off tier, no xhigh).
352
390
  // grok-4.6 adds xhigh per docs.x.ai/developers/model-capabilities/text/reasoning;
353
391
  // multi-agent accepts the same four wire values to select 4 or 16 collaborators. xAI
@@ -356,15 +394,17 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
356
394
  // 2026-09-23 live probe accepted low..xhigh and rejected max on both wires;
357
395
  // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
358
396
  "grok-4.7": ["low", "medium", "high", "xhigh"],
397
+ "grok-4.7-build-fast": ["low", "medium", "high", "xhigh"],
359
398
  "grok-4.6": ["low", "medium", "high", "xhigh"],
360
399
  "grok-4.5": ["low", "medium", "high"],
361
400
  "grok-4.20-multi-agent-0309": ["low", "medium", "high", "xhigh"],
362
401
  },
363
- modelDefaultReasoningEfforts: { "grok-4.7": "high", "grok-4.6": "high" },
402
+ modelDefaultReasoningEfforts: { "grok-4.7": "high", "grok-4.7-build-fast": "high", "grok-4.6": "high" },
364
403
  modelContextWindows: {
365
404
  // 500k confirmed by context_length_exceeded:
366
405
  // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
367
406
  "grok-4.7": 500_000,
407
+ "grok-4.7-build-fast": 500_000,
368
408
  "grok-4.6": 500_000,
369
409
  "grok-4.5": 500_000,
370
410
  "grok-4.3": 1_000_000,
@@ -446,9 +486,11 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
446
486
  // Claude fast mode on the subscription lane (Claude Code `/fast`): the OAuth route accepts
447
487
  // `speed` and gates it on account entitlement (usage credits / org enablement), probed live
448
488
  // 2026-09-23 (devlog/_plan/260923_anthropic_fast_speed/020_probe-evidence.md).
489
+ // Off until the operator opts in: fast mode draws usage credits at 2x price.
449
490
  fastWire: ANTHROPIC_FAST_WIRE,
450
491
  modelSupportsServiceTier: { ...ANTHROPIC_FAST_MODELS },
451
492
  fastTierDescription: ANTHROPIC_FAST_TIER_DESCRIPTION,
493
+ fastOptIn: true,
452
494
  },
453
495
  {
454
496
  id: "anthropic-apikey",
@@ -471,6 +513,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
471
513
  fastWire: ANTHROPIC_FAST_WIRE,
472
514
  modelSupportsServiceTier: { ...ANTHROPIC_FAST_MODELS },
473
515
  fastTierDescription: ANTHROPIC_FAST_TIER_DESCRIPTION,
516
+ fastOptIn: true,
474
517
  },
475
518
  {
476
519
  id: "kimi",
@@ -108,6 +108,12 @@ import {
108
108
  STEPFUN_MODEL_INPUT_MODALITIES,
109
109
  STEPFUN_NO_VISION_MODELS,
110
110
  STEPFUN_REASONING_EFFORTS,
111
+ ANTHROPIC_MODELS,
112
+ ANTHROPIC_MODEL_CONTEXT_WINDOWS,
113
+ ANTHROPIC_MODEL_INPUT_MODALITIES,
114
+ ANTHROPIC_MODEL_REASONING_EFFORTS,
115
+ ANTHROPIC_REASONING_EFFORTS,
116
+ ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
111
117
  } from "./model-seeds";
112
118
 
113
119
  export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
@@ -824,15 +830,30 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
824
830
  // glm-5.3 carry live end-to-end evidence there (custom tools, reasoning replay, streaming,
825
831
  // multi-turn continuation).
826
832
  //
827
- // That is deliberately NOT expressed as a modelWireDefaults pin. Pinning would move every
828
- // existing Codex user of those models onto a different upstream with no config change, and
829
- // one delta is unresolved: preserveReasoningContentModels below is read by the CHAT adapter,
830
- // while the Responses serializer reads preserveResponsesReasoningContent, which this entry
831
- // does not set. On the Responses wire those models would replay with blanked reasoning
832
- // content -- less state than they carry today. Z.AI and DeepSeek set both flags together for
833
- // exactly this reason. Until that flag is justified against this gateway, Responses stays a
834
- // documented per-model modelAdapters opt-in;
835
- // tests/providers/alibaba-token-plan-responses-optin.test.ts holds both halves.
833
+ // That evidence is now expressed as a modelWireDefaults pin scoped to Responses inbound
834
+ // only: Codex clients ride the native wire with zero translation hops, while chat and
835
+ // anthropic inbound keep the provider-wide chat wire and its measured prefix-cache
836
+ // behavior. The pin was held back until the one open delta was closed with its own live
837
+ // evidence: the Responses serializer replays reasoning content through the separate
838
+ // preserveResponsesReasoningContent flag, which the Chat-side preserveReasoningContentModels
839
+ // list does not cover. Measured 260922 on this gateway (#5188): a two-turn replay that
840
+ // round-trips a reasoning item WITH its plaintext content array is accepted (HTTP 200) and
841
+ // the model continues from it, so the flag is set beside the pins — the same pairing Z.AI
842
+ // and DeepSeek use. qwen3.7-plus is the one pinned model in thinkingBudgetModels, and its
843
+ // full low/medium/high/xhigh/max effort ladder is accepted as reasoning.effort strings on
844
+ // this wire (measured same day), so the Responses path does not need the numeric
845
+ // thinking_budget translation the Chat wire applies. The rest of the family stays a
846
+ // documented per-model modelAdapters opt-in; modelAdapters always wins over the pin in
847
+ // both directions.
848
+ // tests/providers/alibaba-token-plan-responses-optin.test.ts holds the opt-in half and the
849
+ // flag guard; tests/providers/alibaba-token-plan-wire-defaults.test.ts holds the pins.
850
+ // The intl sibling stays unpinned until the same four-axis verification runs against its
851
+ // gateway (its /responses route is registered, #5097).
852
+ modelWireDefaults: {
853
+ "qwen3.8-flash": { wire: "openai-responses", inbound: ["responses"] },
854
+ "qwen3.7-plus": { wire: "openai-responses", inbound: ["responses"] },
855
+ "glm-5.3": { wire: "openai-responses", inbound: ["responses"] },
856
+ },
836
857
  note: "Token Plan Personal Edition · China (Beijing)",
837
858
  modelInputModalities: ALIBABA_TOKEN_PLAN_INPUT_MODALITIES,
838
859
  modelContextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
@@ -862,6 +883,10 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
862
883
  directReasoningEffortModels: QWEN38_FAMILY,
863
884
  thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS.filter(id => !QWEN38_FAMILY.includes(id)),
864
885
  preserveReasoningContentModels: ALIBABA_TOKEN_PLAN_PRESERVE_REASONING,
886
+ // Responses replay uses this provider-level flag, not the Chat-path model list above;
887
+ // measured live on this gateway (see the pin comment). The model list still covers a
888
+ // caller who opts back into Chat.
889
+ preserveResponsesReasoningContent: true,
865
890
  noVisionModels: ALIBABA_TOKEN_PLAN_NO_VISION,
866
891
  // The gateway accepts prompt_cache_key on every Token Plan chat model (probed 260902).
867
892
  promptCacheKey: true,
@@ -1199,11 +1224,23 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1199
1224
  adapter: "openai-chat",
1200
1225
  authKind: "key",
1201
1226
  dashboardUrl: "https://xiaomimimo.com",
1202
- // Token-plan roster per Xiaomi's token-plan model list (V2.6 Pro and Flash). No jawcodeBundle,
1203
- // so no plan-specific facts are claimed; usage estimates still come from the model-level vendor
1204
- // price fallback (the pay-as-you-go equivalent), exactly as they did for V2.5.
1227
+ // Token-plan roster per Xiaomi's token-plan model list (V2.6 Pro and Flash). Model-level facts
1228
+ // come from Xiaomi's model pages (mimo.mi.com/models/en-US/<id>, fetched 2026-09-24): 1M context,
1229
+ // 128K max output; V2.6 Pro/Flash and V2.5 take text/image/video/audio, V2.5 Pro text only. The
1230
+ // catalog vocabulary has no video or audio, so only text/image are claimed. The token plan speaks
1231
+ // the same API format as pay-as-you-go, so these are model facts rather than plan facts. No
1232
+ // jawcodeBundle: pricing and entitlement stay unclaimed, and usage estimates still come from the
1233
+ // model-level vendor price fallback, exactly as they did for V2.5.
1205
1234
  defaultModel: "mimo-v2.6-pro",
1206
1235
  models: ["mimo-v2.6-pro", "mimo-v2.6-flash", "mimo-v2.5-pro", "mimo-v2.5"],
1236
+ modelContextWindows: { "mimo-v2.6-pro": 1_048_576, "mimo-v2.6-flash": 1_048_576, "mimo-v2.5-pro": 1_048_576, "mimo-v2.5": 1_048_576 },
1237
+ modelMaxOutputTokens: { "mimo-v2.6-pro": 131_072, "mimo-v2.6-flash": 131_072, "mimo-v2.5-pro": 131_072, "mimo-v2.5": 131_072 },
1238
+ modelInputModalities: {
1239
+ "mimo-v2.6-pro": ["text", "image"],
1240
+ "mimo-v2.6-flash": ["text", "image"],
1241
+ "mimo-v2.5": ["text", "image"],
1242
+ "mimo-v2.5-pro": ["text"],
1243
+ },
1207
1244
  // The gateway validates the ladder strictly and rejects anything above `high`.
1208
1245
  reasoningEfforts: ["low", "medium", "high"],
1209
1246
  reasoningEffortMap: { xhigh: "high", max: "high", ultra: "high" },
@@ -1401,4 +1438,54 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1401
1438
  reasoningEfforts: STEPFUN_REASONING_EFFORTS,
1402
1439
  note: "StepFun (阶跃星辰) official OpenAI-compatible API.",
1403
1440
  },
1441
+ {
1442
+ // Official Claude Code CLI as the transport for a Claude subscription (§三十一). The CLI owns
1443
+ // the account: this row stores no token and the adapter reads and injects none, so the request
1444
+ // path is Anthropic's own harness rather than a replayed Claude Code identity against the
1445
+ // Messages API. `baseUrl` is the destination the subscription's traffic reaches; OpenCodex
1446
+ // never sends it. Fails closed if the row's base URL is overridden.
1447
+ // v1 runs tools-disabled (`--tools ""`, no `--mcp-config`) so the client keeps tool ownership:
1448
+ // text/reasoning only until the shared capture-only tool bridge lands. Requires the CLI:
1449
+ // `npm i -g @anthropic-ai/claude-code`, plus a signed-in session (`claude` -> /login).
1450
+ // GOVERNANCE: whether a subscription login may be driven through a proxy for a third-party
1451
+ // agent is Anthropic's call rather than OpenCodex's — flagged for maintainer review, as with
1452
+ // the CodeBuddy rows above.
1453
+ id: "claude-cli",
1454
+ label: "Claude Code CLI (subscription)",
1455
+ adapter: "claude-cli",
1456
+ baseUrl: "https://api.anthropic.com",
1457
+ // `key` + `keyOptional`, deliberately not `local`. "local" (Ollama, vLLM, LM Studio) means the
1458
+ // traffic never leaves the machine and there is no credential to classify; this row reaches
1459
+ // api.anthropic.com, so `local` misreported it wherever auth is classified — the account
1460
+ // surface answered "local provider ... has no credentials" (`classifyAccount`,
1461
+ // src/cli/account-api.ts) and the dashboard filed the row as a local runtime. What IS true is
1462
+ // keyless: the CLI reads the operator's own sign-in, so `keyOptional` is the existing flag that
1463
+ // exempts a row from key enforcement without claiming a key exists. Key rows are also what
1464
+ // `deriveProviderPresets` lists, so this entry needs no `dashboardPreset` flag to stay
1465
+ // reachable from the Providers page.
1466
+ authKind: "key",
1467
+ keyOptional: true,
1468
+ // There is no key console for a keyless row: the link that helps an operator is the one that
1469
+ // documents the install and sign-in this provider requires.
1470
+ dashboardUrl: "https://docs.claude.com/en/docs/claude-code/setup",
1471
+ defaultModel: "claude-sonnet-5",
1472
+ models: [...ANTHROPIC_MODELS],
1473
+ // Static roster, exactly like the CodeBuddy rows. Without this the catalog treats the row as a
1474
+ // live-discovery candidate and requests a model list the CLI route never serves: a real start
1475
+ // logged `Provider model discovery for "claude-cli" failed with HTTP 404` and then fell back to
1476
+ // these ids anyway. `liveModels: false` makes the configured roster authoritative and skips the
1477
+ // request entirely (src/codex/catalog/provider-models.ts).
1478
+ liveModels: false,
1479
+ modelContextWindows: { ...ANTHROPIC_MODEL_CONTEXT_WINDOWS },
1480
+ // Text-only for v1, not the image modality the Messages API rows publish. The CLI parses an
1481
+ // image frame (verified against 2.1.270), but a headless turn has no verified contract that the
1482
+ // harness hands those bytes to the model, and advertising a modality the route cannot honour
1483
+ // makes a route selection pick this row for a picture it then answers blind. The adapter refuses
1484
+ // direct image input for the same reason; the vision sidecar still captions images into text.
1485
+ noVisionModels: [...ANTHROPIC_MODELS],
1486
+ reasoningEfforts: ANTHROPIC_REASONING_EFFORTS,
1487
+ modelReasoningEfforts: { ...ANTHROPIC_MODEL_REASONING_EFFORTS },
1488
+ defaultMaxOutputTokens: ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
1489
+ note: "Runs Claude subscription traffic through Anthropic's own harness: the official Claude Code CLI headlessly (`claude -p`), one turn per request. OpenCodex stores no Claude token, reads none and injects none — the CLI signs in and bills the account itself, which is why this row is keyless and an API key saved here never reaches the harness (use `anthropic-apikey` for key billing). The sign-in is the one of the user this proxy runs as, so every request served through this row — by any client of this proxy — spends that same account; OpenCodex neither pools nor multiplexes Claude sign-ins. Requires the CLI (`npm i -g @anthropic-ai/claude-code`) and a signed-in session (`claude` -> /login). v1 disables CLI tools (--tools \"\", --strict-mcp-config) so the client retains tool ownership: text/reasoning only for now. Subscription routing authorization flagged for maintainer review.",
1490
+ },
1404
1491
  ];
@@ -83,6 +83,7 @@ export const REGISTRY_FIELD_MODEL_ID_ROLES = {
83
83
  modelSupportsServiceTier: RECORD_KEYS,
84
84
  keyAuthServiceTier: KEY_AUTH_SERVICE_TIER,
85
85
  fastTierDescription: NONE,
86
+ fastOptIn: NONE,
86
87
  modelServiceTierCapabilityBaseUrlGuard: NONE,
87
88
  preserveResponsesReasoningContent: NONE,
88
89
  dropResponsesReasoningItems: NONE,
@@ -107,6 +108,7 @@ export const REGISTRY_FIELD_MODEL_ID_ROLES = {
107
108
  noReasoningModels: NONE,
108
109
  noTemperatureModels: NONE,
109
110
  noTopPModels: NONE,
111
+ noStopModels: NONE,
110
112
  noPenaltyModels: NONE,
111
113
  noJsonSchemaModels: NONE,
112
114
  parallelToolCalls: NONE,
@@ -257,6 +257,11 @@ export interface ProviderRegistryEntry {
257
257
  };
258
258
  /** Provider-specific copy for the Codex catalog's Fast tier. */
259
259
  fastTierDescription?: string;
260
+ /**
261
+ * The Fast lane is billed beyond the plan, so it stays off until the operator sets
262
+ * `providers.<name>.fastEnabled: true` (see `providerFastSwitchOff`).
263
+ */
264
+ fastOptIn?: boolean;
260
265
  /**
261
266
  * Registry-only destination guard for `modelSupportsServiceTier`. This scopes vendor evidence
262
267
  * without changing provider ownership, routing, authentication, or config validation.
@@ -308,6 +313,7 @@ export interface ProviderRegistryEntry {
308
313
  noReasoningModels?: string[];
309
314
  noTemperatureModels?: string[];
310
315
  noTopPModels?: string[];
316
+ noStopModels?: string[];
311
317
  noPenaltyModels?: string[];
312
318
  /**
313
319
  * Registry-only seed for `OcxProviderConfig.noJsonSchemaModels`. Merged into the
@@ -359,7 +365,7 @@ export type ProviderConfigSeed = Pick<
359
365
  | "modelDisplayNames"
360
366
  | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
361
367
  | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat"
362
- | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
368
+ | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noStopModels" | "noPenaltyModels"
363
369
  | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "inlineThinkTagModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" | "showThinkingSummary"
364
370
  | "googleMode" | "project" | "location" | "headers"
365
371
  >;
@@ -2,6 +2,7 @@ import type { ModelCapabilities, OcxProviderConfig } from "../types";
2
2
  import { MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
3
3
  import { isCanonicalOpenAiForwardProvider } from "./openai-tiers";
4
4
  import { resolveProviderAuthTransport } from "./fastwire";
5
+ import { fastSwitchOff } from "./fast-opt-in";
5
6
  import { registryEntrySupportsLiveModelDiscovery } from "./static-model-discovery";
6
7
  import type { InboundWire, ProviderRegistryEntry, ResponsesTerminalRepairPolicy } from "./registry/types";
7
8
  import {
@@ -30,7 +31,7 @@ export type StaticProviderPolicyField =
30
31
  | "modelMaxOutputTokens" | "reasoningEfforts" | "modelReasoningEfforts" | "modelReasoningEffortsAuthoritative"
31
32
  | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
32
33
  | "reasoningWireFormat" | "noVisionModels" | "noReasoningModels" | "noTemperatureModels"
33
- | "noTopPModels" | "noPenaltyModels" | "noJsonSchemaModels" | "parallelToolCalls"
34
+ | "noTopPModels" | "noStopModels" | "noPenaltyModels" | "noJsonSchemaModels" | "parallelToolCalls"
34
35
  | "promptCacheKey" | "chatServiceTier" | "openaiChatEofTolerance" | "statelessResponses"
35
36
  | "requiresAdjacentResponsesToolResults" | "requiresPairedResponsesToolResults" | "annotateEmptyToolOutputs"
36
37
  | "fastWire" | "supportsServiceTier" | "modelSupportsServiceTier" | "supportsOpenAiWebSearchToolFields"
@@ -187,7 +188,10 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
187
188
  legacyClinePassLadder || provider.reasoningEfforts === undefined ? entry?.reasoningEfforts ? "registry" : "unknown" : "operator");
188
189
  put("chatServiceTier", provider.chatServiceTier ?? keyAuthDefaults?.chatServiceTier ?? entry?.chatServiceTier,
189
190
  provider.chatServiceTier !== undefined ? "operator" : keyAuthDefaults?.chatServiceTier !== undefined ? "registry" : entry?.chatServiceTier !== undefined ? "registry" : "unknown");
190
- put("supportsServiceTier", provider.supportsServiceTier ?? keyAuthDefaults?.supportsServiceTier ?? entry?.supportsServiceTier,
191
+ // The Fast switch overrides every capability source; see providerFastSwitchOff.
192
+ const fastOff = fastSwitchOff(provider, input.registryEntry);
193
+ if (fastOff) put("supportsServiceTier", false, provider.fastEnabled === false ? "operator" : "registry");
194
+ else put("supportsServiceTier", provider.supportsServiceTier ?? keyAuthDefaults?.supportsServiceTier ?? entry?.supportsServiceTier,
191
195
  provider.supportsServiceTier !== undefined ? "operator" : keyAuthDefaults?.supportsServiceTier !== undefined ? "registry" : entry?.supportsServiceTier !== undefined ? "registry" : "unknown");
192
196
  if (entry && !registryEntrySupportsLiveModelDiscovery(entry)) put("liveModels", false, "registry");
193
197
  putMergedMap("modelDisplayNames", entry?.modelDisplayNames, provider.modelDisplayNames);
@@ -226,6 +230,7 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
226
230
  put("modelReasoningEffortMap", modelEffortMap, modelEffortMapSource);
227
231
  for (const key of [
228
232
  "noVisionModels", "noReasoningModels", "noTemperatureModels", "noTopPModels",
233
+ "noStopModels",
229
234
  "noPenaltyModels", "noJsonSchemaModels", "autoToolChoiceOnlyModels",
230
235
  "preserveReasoningContentModels", "requiresReasoningPlaceholderModels",
231
236
  "reasoningSplitModels", "reasoningDetailsModels", "thinkingToggleModels", "thinkingBudgetModels",
@@ -324,7 +329,9 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
324
329
  const modelSupportsReasoningSummaries = modelValue(providerPolicy.modelSupportsReasoningSummaries)
325
330
  ?? (modelValue(providerPolicy.modelReasoningSummaryDelivery) !== undefined ? true : undefined);
326
331
  const modelSupportsVerbosity = modelValue(providerPolicy.modelSupportsVerbosity) ?? providerPolicy.supportsVerbosity;
327
- const modelSupportsServiceTier = modelValue(providerPolicy.modelSupportsServiceTier) ?? providerPolicy.supportsServiceTier;
332
+ const modelSupportsServiceTier = fastOff
333
+ ? false
334
+ : modelValue(providerPolicy.modelSupportsServiceTier) ?? providerPolicy.supportsServiceTier;
328
335
  const model: ResolvedPerModelStaticPolicy = {
329
336
  adapter,
330
337
  ...(modelContextWindow !== undefined ? { contextWindow: modelContextWindow } : {}),
@@ -379,7 +386,7 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
379
386
  }
380
387
  modelProvenance.supportsVerbosity = modelOrProviderSource(provider.modelSupportsVerbosity, entry?.modelSupportsVerbosity, provider.supportsVerbosity, entry?.supportsVerbosity);
381
388
  const exactServiceTierSource = modelSource(provider.modelSupportsServiceTier, registryServiceTierDefaults);
382
- modelProvenance.supportsServiceTier = exactServiceTierSource !== "unknown" ? exactServiceTierSource
389
+ modelProvenance.supportsServiceTier = !fastOff && exactServiceTierSource !== "unknown" ? exactServiceTierSource
383
390
  : providerProvenance.supportsServiceTier ?? "unknown";
384
391
  modelProvenance.responsesUpstreamStreaming = model.responsesUpstreamStreaming === undefined ? "unknown" : "registry";
385
392
  modelProvenance.responsesTerminalRepair = model.responsesTerminalRepair === undefined ? "unknown" : "registry";
@@ -15,6 +15,7 @@ import {
15
15
  type FastPolicyAuthority,
16
16
  type ResolvedFastPolicy,
17
17
  } from "./fastwire";
18
+ import { providerFastSwitchOff } from "./fast-opt-in";
18
19
 
19
20
  /** OpenAI-compatible adapters that can carry the standard `service_tier` field. */
20
21
  export const SERVICE_TIER_ADAPTERS = new Set(["openai-chat", "openai-responses"]);
@@ -35,6 +36,7 @@ type ServiceTierCapabilityProvider = Pick<
35
36
  | "apiKeyTransport"
36
37
  | "chatServiceTier"
37
38
  | "fastWire"
39
+ | "fastEnabled"
38
40
  >;
39
41
 
40
42
  function cloneRegistryWireDefaults(
@@ -83,9 +85,14 @@ function buildFastPolicyAuthority(
83
85
  && registryModelServiceTierCapabilityApplies(registry, capabilityProvider)
84
86
  ? registry.modelSupportsServiceTier
85
87
  : undefined;
86
- const providerCapability = capabilityProvider.supportsServiceTier
87
- ?? keyAuthDefaults?.supportsServiceTier
88
- ?? registry?.supportsServiceTier;
88
+ const fastSwitchOff = providerFastSwitchOff(providerName, {
89
+ fastEnabled: capabilityProvider.fastEnabled ?? provider.fastEnabled,
90
+ });
91
+ const providerCapability = fastSwitchOff
92
+ ? false
93
+ : capabilityProvider.supportsServiceTier
94
+ ?? keyAuthDefaults?.supportsServiceTier
95
+ ?? registry?.supportsServiceTier;
89
96
  const authority: FastPolicyAuthority = Object.freeze({
90
97
  providerAdapter: provider.adapter,
91
98
  providerAuthMode: provider.authMode ?? registry?.authKind ?? "key",
@@ -92,6 +92,9 @@ export function compileCodeModeHelperInput(
92
92
  }
93
93
  return `const result = await tools.view_image(${JSON.stringify(viewArgs)});\nif (result && result.image_url) { image(result.image_url); } else { text(result); }`;
94
94
  }
95
+ if (helperName === "create_goal" || helperName === "get_goal" || helperName === "update_goal") {
96
+ return `const result = await tools.${helperName}(${JSON.stringify(args)});\ntext(result);`;
97
+ }
95
98
  return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
96
99
  }
97
100