@bitkyc08/opencodex 2.75.0 → 2.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -0,0 +1,134 @@
1
+ import type { OAuthController, OAuthCredentials } from "./types";
2
+ import {
3
+ createZedNativeAuthData,
4
+ decryptZedAccessToken,
5
+ parseZedCallbackPayload,
6
+ } from "../providers/zed";
7
+
8
+ const LOGIN_TIMEOUT_MS = 300_000;
9
+
10
+ type ZedServer = ReturnType<typeof Bun.serve>;
11
+
12
+ function closingResponse(body: string, status: number): Response {
13
+ return new Response(body, {
14
+ status,
15
+ headers: { "Content-Type": "text/html; charset=utf-8", Connection: "close" },
16
+ });
17
+ }
18
+
19
+ function callbackSuccess(): Response {
20
+ return closingResponse(
21
+ "<!doctype html><html><body><h2>Login complete</h2><p>You can close this tab.</p></body></html>",
22
+ 200,
23
+ );
24
+ }
25
+
26
+ function callbackFailure(message: string): Response {
27
+ // Do not reflect the callback query, encrypted token, or decryption error details into HTML.
28
+ void message;
29
+ return closingResponse(
30
+ "<!doctype html><html><body><h2>Login failed</h2><p>Return to OpenCodex and retry the login.</p></body></html>",
31
+ 400,
32
+ );
33
+ }
34
+
35
+ async function parseManualCallback(input: string, privateKeyVerifier: string): Promise<OAuthCredentials> {
36
+ const payload = parseZedCallbackPayload(input);
37
+ const access = decryptZedAccessToken(payload.encryptedAccessToken, privateKeyVerifier);
38
+ return {
39
+ access,
40
+ refresh: access,
41
+ expires: Number.MAX_SAFE_INTEGER,
42
+ accountId: payload.userId,
43
+ source: "oauth",
44
+ };
45
+ }
46
+
47
+ /** Zed native-app login: local RSA callback, not OAuth2/PKCE and not refreshable. */
48
+ export async function loginZed(ctrl: OAuthController): Promise<OAuthCredentials> {
49
+ let resolveCallback: ((credential: OAuthCredentials) => void) | undefined;
50
+ const callback = new Promise<OAuthCredentials>(resolve => {
51
+ resolveCallback = resolve;
52
+ });
53
+
54
+ let privateKeyVerifier: string | undefined;
55
+ let server: ZedServer | undefined;
56
+ // Ends the manual-paste branch once the race settles, so a late paste can neither re-prompt
57
+ // nor keep the private key alive after the callback (or cancellation) already won.
58
+ const manualStop = new AbortController();
59
+ const manualStopped = new Promise<undefined>(resolve => {
60
+ manualStop.signal.addEventListener("abort", () => resolve(undefined), { once: true });
61
+ });
62
+ try {
63
+ server = Bun.serve({
64
+ hostname: "127.0.0.1",
65
+ port: 0,
66
+ fetch: async request => {
67
+ const url = new URL(request.url);
68
+ if (request.method !== "GET" || url.pathname !== "/") return closingResponse("Not found", 404);
69
+ try {
70
+ if (!privateKeyVerifier) throw new Error("Zed login callback is not initialized");
71
+ const credential = await parseManualCallback(url.toString(), privateKeyVerifier);
72
+ resolveCallback?.(credential);
73
+ return callbackSuccess();
74
+ } catch (error) {
75
+ return callbackFailure(error instanceof Error ? error.message : "invalid callback");
76
+ }
77
+ },
78
+ });
79
+ if (!server.port) throw new Error("Zed login callback port was not allocated");
80
+ const authData = createZedNativeAuthData(server.port);
81
+ privateKeyVerifier = authData.privateKeyVerifier;
82
+ ctrl.onAuth?.({
83
+ url: authData.authUrl,
84
+ instructions: "Sign in with your Zed account in the browser, then return here after Zed redirects to the local callback.",
85
+ });
86
+ ctrl.onProgress?.("Waiting for Zed login callback…");
87
+
88
+ const timeout = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
89
+ const signal = ctrl.signal ? AbortSignal.any([ctrl.signal, timeout]) : timeout;
90
+ const cancelled = new Promise<never>((_, reject) => {
91
+ if (signal.aborted) {
92
+ reject(new Error("Zed login cancelled"));
93
+ return;
94
+ }
95
+ signal.addEventListener("abort", () => reject(new Error("Zed login cancelled")), { once: true });
96
+ });
97
+ const manual = ctrl.onManualCodeInput
98
+ ? (async (): Promise<OAuthCredentials> => {
99
+ let verifier: string | undefined = authData.privateKeyVerifier;
100
+ while (!manualStop.signal.aborted && !signal.aborted) {
101
+ const input = await Promise.race([ctrl.onManualCodeInput?.(), manualStopped]);
102
+ if (manualStop.signal.aborted || signal.aborted) break;
103
+ if (!input) {
104
+ await new Promise(resolve => setTimeout(resolve, 0));
105
+ continue;
106
+ }
107
+ try { return await parseManualCallback(input, verifier); } catch { /* keep waiting */ }
108
+ }
109
+ verifier = undefined;
110
+ // Never settles: the race already has its answer, and this branch must not override it.
111
+ return new Promise<never>(() => {});
112
+ })()
113
+ : undefined;
114
+ return await Promise.race([callback, cancelled, ...(manual ? [manual] : [])]);
115
+ } catch (error) {
116
+ throw error;
117
+ } finally {
118
+ manualStop.abort();
119
+ server?.stop(true);
120
+ }
121
+ }
122
+
123
+ /** Zed's long-lived native token has no refresh endpoint. */
124
+ export async function refreshZedToken(refresh: string, _signal?: AbortSignal, credential?: OAuthCredentials): Promise<OAuthCredentials> {
125
+ const accountId = credential?.accountId;
126
+ if (!accountId || !refresh) throw new Error("Zed credential is missing its user id");
127
+ return {
128
+ access: refresh,
129
+ refresh,
130
+ expires: Number.MAX_SAFE_INTEGER,
131
+ accountId,
132
+ source: credential?.source ?? "oauth",
133
+ };
134
+ }
@@ -78,6 +78,8 @@ function discoveryPredicateError(predicate: ProviderModelDiscoveryPredicate): st
78
78
  if (predicate.path.some(segment => typeof segment !== "string" || !segment.trim() || segment.length > 64)) {
79
79
  return "predicate path segments must be nonblank strings up to 64 characters";
80
80
  }
81
+ // One projection is sufficient for providers[].id and avoids combinatorial traversal.
82
+ if (predicate.path.filter(segment => segment === "*").length > 1) return "predicate path may contain at most one array projection";
81
83
  const values = "equalsAny" in predicate
82
84
  ? predicate.equalsAny
83
85
  : "containsAny" in predicate
@@ -145,7 +147,7 @@ export function providerModelDiscoverySpecError(spec: ProviderModelDiscoverySpec
145
147
  return `${field} must be a positive integer no greater than ${hardLimit}`;
146
148
  }
147
149
  }
148
- for (const [group, predicates] of Object.entries(spec.filter ?? {})) {
150
+ for (const [group, predicates] of Object.entries({ ...spec.filter, ...(spec.preferFirst === undefined ? {} : { preferFirst: spec.preferFirst }) })) {
149
151
  if (!Array.isArray(predicates) || predicates.length === 0 || predicates.length > 32) {
150
152
  return `${group} must contain 1-32 predicates`;
151
153
  }
@@ -349,12 +351,17 @@ export async function readBoundedDiscoveryJson(
349
351
  }
350
352
 
351
353
  function valueAtPath(item: Record<string, unknown>, path: readonly string[]): unknown {
352
- let current: unknown = item;
353
- for (const segment of path) {
354
+ const visit = (current: unknown, index: number): unknown => {
355
+ if (index === path.length) return current;
356
+ const segment = path[index];
357
+ if (segment === "*") {
358
+ if (!Array.isArray(current)) return undefined;
359
+ return current.slice(0, MODEL_DISCOVERY_MAX_FILTER_VALUES).map(value => visit(value, index + 1));
360
+ }
354
361
  if (current === null || typeof current !== "object" || Array.isArray(current)) return undefined;
355
- current = (current as Record<string, unknown>)[segment];
356
- }
357
- return current;
362
+ return visit((current as Record<string, unknown>)[segment!], index + 1);
363
+ };
364
+ return visit(item, 0);
358
365
  }
359
366
 
360
367
  function comparableScalar(value: unknown, caseInsensitive: boolean): ProviderModelDiscoveryScalar | undefined {
@@ -522,6 +529,7 @@ export function extractProviderModelItems(
522
529
  }
523
530
 
524
531
  const items: ProviderModelsApiItem[] = [];
532
+ const preferred: ProviderModelsApiItem[] = [];
525
533
  const seen = new Set<string>();
526
534
  const idField = discovery.spec?.idField ?? "id";
527
535
  for (const raw of data) {
@@ -547,7 +555,10 @@ export function extractProviderModelItems(
547
555
  // are published, only what is known about an already-admitted one.
548
556
  if (!providerModelMatchesDiscoveryFilter(item, discovery.spec?.filter) || seen.has(finalId)) continue;
549
557
  seen.add(finalId);
550
- items.push(siblings ? enrichAdmittedModel(item, siblings) : item);
558
+ // Preference, like admission, must not be supplied by sibling enrichment.
559
+ const target = discovery.spec?.preferFirst?.some(predicate => predicateMatches(raw as ProviderModelsApiItem, predicate))
560
+ ? preferred : items;
561
+ target.push(siblings ? enrichAdmittedModel(item, siblings) : item);
551
562
  }
552
- return { ok: true, items, rawCount: data.length };
563
+ return { ok: true, items: [...preferred, ...items], rawCount: data.length };
553
564
  }
@@ -142,6 +142,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
142
142
  desktopExecutor: "none",
143
143
  unsafeAllowNativeLocalExec: "none",
144
144
  nativeLocalExec: "none",
145
+ tlsProfile: "none",
145
146
  } as const satisfies Record<keyof OcxProviderConfig, ModelRenameRole>;
146
147
 
147
148
  function fieldsWithRole(role: ModelRenameRole): string[] {
@@ -19,8 +19,8 @@ export interface ProviderRewriteResult {
19
19
  * Re-point every config reference from one provider id to another.
20
20
  *
21
21
  * Three shapes exist and the difference matters: routed model strings
22
- * (`"<provider>/<model>"`), bare provider ids (`customModels[].provider`,
23
- * `combos[*].targets[].provider`, `routingProfiles[*].candidates[].provider`),
22
+ * (`"<provider>/<model>"`, including `combos[*].decisionModel`), bare provider ids (`customModels[].provider`,
23
+ * `combos[*].targets[].provider`, `combos[*].decisionProvider`, `routingProfiles[*].candidates[].provider`),
24
24
  * and keys that ARE provider ids or routes (`providerContextCaps`,
25
25
  * `claudeCode.desktopProfile.assignments`). A rewrite that handles only the
26
26
  * first leaves an orphaned context cap and — worse — a combo target or routing
@@ -106,12 +106,19 @@ export function rewriteProviderReferences(config: OcxConfig, from: string, to: s
106
106
  }
107
107
  }
108
108
  for (const combo of Object.values(config.combos ?? {})) {
109
+ const decisionModel = route(typeof combo.decisionModel === "string" ? combo.decisionModel.trim() : combo.decisionModel);
110
+ if (decisionModel) combo.decisionModel = decisionModel;
109
111
  for (const target of combo.targets ?? []) {
110
112
  if (target.provider === from) {
111
113
  target.provider = to;
112
114
  changed += 1;
113
115
  }
114
116
  }
117
+ // A JEV Combo's decision service is validated against configured providers too.
118
+ if (typeof combo.decisionProvider === "string" && combo.decisionProvider.trim() === from) {
119
+ combo.decisionProvider = to;
120
+ changed += 1;
121
+ }
115
122
  }
116
123
 
117
124
  // Routing-profile candidates carry a bare provider id next to a bare model
@@ -1,6 +1,8 @@
1
+ import { parseAnthropicFamilyHeaders, mergeAnthropicFamilyWindows } from "./anthropic-family-headers";
2
+ import { observeAnthropicFamilyQuota, clearAnthropicRequestedFamilyQuota, ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS } from "../../oauth/anthropic-model-quota";
1
3
  import { createHash } from "node:crypto";
2
4
  import { getValidAccessTokenForAccount } from "../../oauth";
3
- import { getAccountCredential, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store";
5
+ import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store";
4
6
  import type { GenerationContext } from "../../lib/state-store-sweeper";
5
7
  import { ACCOUNT_QUOTA_TTL_MS, toFiniteNumber } from "../quota-wire";
6
8
  import { clearKiroAccountUsageState, hydrateKiroUsageVerdict, kiroPersistableVerdicts, reconcileKiroAccountUsageState } from "../kiro-usage";
@@ -34,6 +36,8 @@ export type AccountQuotaCacheEntry = {
34
36
  quotaFailureIsCurrent?: () => boolean;
35
37
  /** Private new-reader identity; never persisted or serialized. */
36
38
  identity?: string;
39
+ /** Private passive Anthropic ownership fence; not serialized with quota bars. */
40
+ anthropicCredentialGeneration?: string;
37
41
  isCurrent?: () => boolean;
38
42
  };
39
43
  /** Expired measurements become unknown; missing reset evidence never implies a fresh allowance. */
@@ -67,7 +71,10 @@ export function normalizeAnthropicQuota(quota: ProviderQuota | null | undefined,
67
71
  changed = true;
68
72
  continue;
69
73
  }
70
- if (validReset(window.resetAt) && window.resetAt <= now) {
74
+ if ((window.passiveObservedAt !== undefined && (typeof window.passiveObservedAt !== "number"
75
+ || !Number.isFinite(window.passiveObservedAt) || window.passiveObservedAt <= 0 || window.passiveObservedAt > now
76
+ || now - window.passiveObservedAt >= ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS))
77
+ || validReset(window.resetAt) && window.resetAt <= now) {
71
78
  changed = true;
72
79
  continue;
73
80
  }
@@ -202,6 +209,10 @@ export function getCachedProviderAccountQuota(provider: string, accountId: strin
202
209
  const account = getAccountSet("kiro")?.accounts.find(row => row.id === accountId);
203
210
  if (!account || entry?.identity !== kiroEvidenceIdentity(account)) return null;
204
211
  }
212
+ if (provider === "anthropic" && entry?.anthropicCredentialGeneration) {
213
+ const credential = getAccountCredential(provider, accountId);
214
+ if (!credential || credentialGeneration(credential) !== entry.anthropicCredentialGeneration) return null;
215
+ }
205
216
  if (entry?.isCurrent && !entry.isCurrent()) return null;
206
217
  return provider === "anthropic" ? normalizeAnthropicQuota(entry?.quota, Date.now()) : entry?.quota ?? null;
207
218
  }
@@ -230,10 +241,11 @@ function anthropicHeaderResetAt(value: string | null): number | undefined {
230
241
  return Number.isFinite(new Date(timestamp).getTime()) ? timestamp : undefined;
231
242
  }
232
243
 
233
- export function parseAnthropicRateLimitHeaders(headers: Headers): ProviderQuota | null {
244
+ export function parseAnthropicRateLimitHeaders(headers: Headers, status?: number): ProviderQuota | null {
234
245
  const fiveHourPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-5h-utilization"));
235
246
  const weeklyPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-7d-utilization"));
236
- if (fiveHourPercent === undefined && weeklyPercent === undefined) return null;
247
+ const customWindows = parseAnthropicFamilyHeaders(headers, Date.now(), status);
248
+ if (fiveHourPercent === undefined && weeklyPercent === undefined && !customWindows.length) return null;
237
249
  const fiveHourResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-5h-reset"));
238
250
  const weeklyResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-7d-reset"));
239
251
  return {
@@ -241,6 +253,7 @@ export function parseAnthropicRateLimitHeaders(headers: Headers): ProviderQuota
241
253
  ...(fiveHourPercent !== undefined && fiveHourResetAt !== undefined ? { fiveHourResetAt } : {}),
242
254
  ...(weeklyPercent !== undefined ? { weeklyPercent } : {}),
243
255
  ...(weeklyPercent !== undefined && weeklyResetAt !== undefined ? { weeklyResetAt } : {}),
256
+ ...(customWindows.length ? { customWindows } : {}),
244
257
  updatedAt: Date.now(),
245
258
  };
246
259
  }
@@ -261,25 +274,34 @@ export function recordAnthropicAccountQuotaFromHeaders(
261
274
  accountId: string,
262
275
  headers: Headers,
263
276
  writerGeneration: number,
277
+ status?: number,
278
+ model?: string,
264
279
  ): void {
265
280
  if (!accountId) return;
266
- const observed = parseAnthropicRateLimitHeaders(headers);
267
- if (!observed) return;
268
281
  const key = accountCacheKey("anthropic", accountId);
269
282
  if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
283
+ if (status !== undefined && status >= 200 && status < 300) clearAnthropicRequestedFamilyQuota(accountId, model);
284
+ const observed = parseAnthropicRateLimitHeaders(headers, status);
285
+ if (!observed) return;
270
286
  // Hydrate before writing, for the same reason `recordPassiveAccountQuota` does: this write
271
287
  // arrives unprompted from the request path, and `persistAccountQuotaCache` serializes the
272
288
  // whole map. Landing before any reader has hydrated would persist this single row and erase
273
289
  // every other provider's saved row.
274
290
  hydrateAccountQuotaCache();
275
291
  const candidate = accountQuotaCache.get(key);
276
- const previous = candidate?.isCurrent?.() === false ? undefined : candidate;
292
+ const credential = getAccountCredential("anthropic", accountId);
293
+ const generation = credential && credentialGeneration(credential);
294
+ const previous = candidate?.isCurrent?.() === false
295
+ || candidate?.anthropicCredentialGeneration && candidate.anthropicCredentialGeneration !== generation ? undefined : candidate;
296
+ if (observed.customWindows) observeAnthropicFamilyQuota(accountId, observed.customWindows, observed.updatedAt);
277
297
  accountQuotaCache.set(key, {
278
298
  ...previous,
299
+ ...(generation ? { anthropicCredentialGeneration: generation, isCurrent: undefined } : {}),
279
300
  // Headers do not prove that the last usage probe succeeded.
280
301
  ts: previous?.ts ?? 0,
281
302
  quota: normalizeAnthropicQuota({
282
303
  ...normalizeAnthropicQuota(previous?.quota, observed.updatedAt), ...observed,
304
+ ...(observed.customWindows ? { customWindows: mergeAnthropicFamilyWindows(previous?.quota?.customWindows, observed.customWindows) } : {}),
283
305
  }, observed.updatedAt),
284
306
  });
285
307
  persistAccountQuotaCache();
@@ -1,3 +1,6 @@
1
+ import { mergeAnthropicFamilyWindows, hasAnthropicFamilyEnumeration } from "./anthropic-family-headers";
2
+ import { getCachedProviderAccountQuota } from "./account-cache";
3
+ import { observeAnthropicFamilyQuota, anthropicFamilyQuotaGeneration } from "../../oauth/anthropic-model-quota";
1
4
  import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus } from "../../oauth/store";
2
5
  import type { ProviderQuota } from "../quota-types";
3
6
 
@@ -42,6 +45,7 @@ export function clearAnthropicCooldownGenerations(): void {
42
45
  export type AnthropicCooldownRecoveryProbe = Readonly<{
43
46
  requiresFreshDispatch: boolean;
44
47
  isCurrentCredential(): boolean;
48
+ isCurrentFamily(): boolean;
45
49
  isCurrent(): boolean;
46
50
  settle(quota: ProviderQuota): "cleared" | "retained" | "superseded";
47
51
  }>;
@@ -67,6 +71,7 @@ export async function captureAnthropicCooldownRecoveryProbe(
67
71
  // Lazy because anthropic-routing reads the quota cache on normal request routing.
68
72
  const routing = await import("../../oauth/anthropic-routing");
69
73
  const cooldownGeneration = anthropicCooldownGeneration(accountId);
74
+ const familyGeneration = anthropicFamilyQuotaGeneration(accountId);
70
75
  const claim = routing.captureAnthropicCooldownRecovery(accountId);
71
76
  const isCurrentCredential = () => {
72
77
  const current = getAccountCredential("anthropic", accountId);
@@ -75,6 +80,7 @@ export async function captureAnthropicCooldownRecoveryProbe(
75
80
  return {
76
81
  requiresFreshDispatch: claim !== null,
77
82
  isCurrentCredential,
83
+ isCurrentFamily: () => anthropicFamilyQuotaGeneration(accountId) === familyGeneration,
78
84
  isCurrent: () => isCurrentCredential()
79
85
  && anthropicCooldownGeneration(accountId) === cooldownGeneration,
80
86
  settle: quota => claim === null ? "retained"
@@ -107,12 +113,24 @@ export async function probeAnthropicQuotaWithRecovery(
107
113
  }
108
114
  // Clearing the claimed cooldown intentionally advances the fence. Adopt that exact new
109
115
  // generation; any later observation/429 then invalidates publication before a cache write.
116
+ const ownsFamily = probe.isCurrentFamily();
117
+ const authoritative = ownsFamily && hasAnthropicFamilyEnumeration(quota);
118
+ if (ownsFamily) observeAnthropicFamilyQuota(accountId, quota.customWindows ?? [], quota.updatedAt, authoritative);
119
+ if (!authoritative) {
120
+ const cached = getCachedProviderAccountQuota("anthropic", accountId)?.customWindows;
121
+ const windows = ownsFamily ? mergeAnthropicFamilyWindows(cached, quota.customWindows)
122
+ : cached ?? [];
123
+ // A superseded family enumeration has no authority, including over absent families.
124
+ if (!ownsFamily || windows.length) quota = { ...quota, customWindows: windows };
125
+ }
110
126
  const publicationGeneration = anthropicCooldownGeneration(accountId);
127
+ const publicationFamilyGeneration = anthropicFamilyQuotaGeneration(accountId);
111
128
  afterSettlementForTests?.();
112
129
  return {
113
130
  quota,
114
131
  isCurrent: () => probe.isCurrentCredential()
115
132
  && anthropicCooldownGeneration(accountId) === publicationGeneration
133
+ && anthropicFamilyQuotaGeneration(accountId) === publicationFamilyGeneration
116
134
  && mayPublish(),
117
135
  };
118
136
  }
@@ -0,0 +1,25 @@
1
+ /** 7d_oi attribution is fixture-confirmed for Fable; it is not a future-family wildcard. */
2
+ import type { ProviderQuotaWindow } from "../quota-types";
3
+ export function parseAnthropicFamilyHeaders(headers: Headers, now: number, status?: number): ProviderQuotaWindow[] {
4
+ const value = headers.get("anthropic-ratelimit-unified-7d_oi-utilization")?.trim();
5
+ const rejected = status === 429 && headers.get("anthropic-ratelimit-unified-7d_oi-status")?.trim() === "rejected";
6
+ const numeric = value && /^\d+(?:\.\d+)?$/.test(value) ? Number(value) : undefined;
7
+ // The observed overage fixture is 1.01; spent evidence clamps to the display ceiling.
8
+ const percent = numeric !== undefined && Number.isFinite(numeric) && numeric >= 0 && numeric <= 1.01
9
+ ? Math.min(100, Math.round(numeric * 10_000) / 100) : rejected ? 100 : undefined;
10
+ if (percent === undefined) return [];
11
+ const rawReset = headers.get("anthropic-ratelimit-unified-7d_oi-reset")?.trim();
12
+ const resetAt = rawReset && /^\d+(?:\.\d+)?$/.test(rawReset) ? Number(rawReset) * 1000 : undefined;
13
+ return [{ label: "Fable", scope: "model", percent, passiveObservedAt: now, ...(rejected ? { rejected: true as const } : {}),
14
+ ...(resetAt !== undefined && resetAt > 0 && Number.isFinite(new Date(resetAt).getTime()) ? { resetAt } : {}) }];
15
+ }
16
+ export function mergeAnthropicFamilyWindows(previous: ProviderQuotaWindow[] = [], observed: ProviderQuotaWindow[] = []): ProviderQuotaWindow[] {
17
+ return [...previous.filter(window => !observed.some(update => update.label === window.label && update.scope === window.scope)), ...observed];
18
+ }
19
+
20
+ const enumerated = new WeakSet<object>();
21
+ export function markAnthropicFamilyEnumeration<T extends object>(quota: T, authoritative: boolean): T {
22
+ if (authoritative) enumerated.add(quota);
23
+ return quota;
24
+ }
25
+ export function hasAnthropicFamilyEnumeration(quota: object): boolean { return enumerated.has(quota); }
@@ -1,3 +1,4 @@
1
+ import { markAnthropicFamilyEnumeration } from "./anthropic-family-headers";
1
2
  import { effectiveCodexAuthAccountId, fetchMainAccountInfoSnapshot, listCodexAuthAccountsSnapshot } from "../../codex/auth-api";
2
3
  import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
3
4
  import { getValidAccessToken } from "../../oauth";
@@ -311,7 +312,10 @@ async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuo
311
312
  updatedAt: Date.now(),
312
313
  };
313
314
  // Empty / schema-changed payloads must not cache as "success with no bars".
314
- return hasQuotaRows(quota) ? quota : null;
315
+ return hasQuotaRows(quota) ? markAnthropicFamilyEnumeration(quota, Array.isArray(body.limits) && body.limits.every(raw => {
316
+ const kind = asRecord(raw)?.kind;
317
+ return kind === "session" || kind === "weekly_all" || kind === "weekly_scoped" && parseClaudeLimit(raw) !== null;
318
+ })) : null;
315
319
  }
316
320
 
317
321
  /**
@@ -30,6 +30,10 @@ export interface ProviderQuotaWindow {
30
30
  * provider-wide group named "Opus" there would otherwise be mistaken for a per-model window.
31
31
  */
32
32
  scope?: "model";
33
+ /** Passive family readings have their own freshness clock, independent of active probes. */
34
+ passiveObservedAt?: number;
35
+ /** Upstream refusal evidence, distinct from a measured utilization counter. */
36
+ rejected?: true;
33
37
  }
34
38
 
35
39
  export interface ProviderQuotaCreditsUsd {
@@ -157,6 +157,24 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
157
157
  // still advertises image for noVision members so Codex can attach (sidecar option B).
158
158
  noVisionModels: [...CURSOR_NO_VISION_MODELS],
159
159
  },
160
+ {
161
+ id: "zed",
162
+ label: "Zed Hosted AI (experimental)",
163
+ adapter: "zed",
164
+ baseUrl: "https://cloud.zed.dev",
165
+ authKind: "oauth",
166
+ featured: false,
167
+ dashboardPreset: true,
168
+ models: [],
169
+ liveModels: true,
170
+ defaultModel: "auto",
171
+ modelDiscovery: {
172
+ url: "https://cloud.zed.dev/models",
173
+ maxResponseBytes: 4 * 1024 * 1024,
174
+ maxModels: 2_000,
175
+ },
176
+ note: "Experimental and unofficial Zed Hosted AI bridge, not endorsed by Zed. Use at your own risk: it consumes the signed-in Zed account's hosted-model entitlement and may be outside Zed's terms, and Zed may limit or suspend the account.",
177
+ },
160
178
  {
161
179
  // The canonical Cognition account provider, after absorbing `devin-cli`
162
180
  // (devlog/_plan/260913_devin_provider_merge). The two ids were the same
@@ -1143,6 +1143,61 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1143
1143
  modelInputModalities: OPPER_MODEL_INPUT_MODALITIES,
1144
1144
  note: "EU-hosted AI gateway: one OpenAI-compatible endpoint and one key in front of 30+ providers. Bare model ids are pools (claude-sonnet-4-6, gpt-5.5) and Opper picks the route per request; vendor/model ids (anthropic/claude-sonnet-4-6) pin one provider. The catalogue is discovered live from /v3/compat/models with your key; the public list is at opper.ai/models. Token rates are the model providers' rates with no markup; Opper charges a 3% fee when you buy credits.",
1145
1145
  },
1146
+ {
1147
+ // Sionic AI public contract and live streams/tools/images checked 2026-10-02:
1148
+ // https://opengateway.ai/docs/reference/endpoints/models
1149
+ // https://opengateway.ai/docs/reference/endpoints/responses
1150
+ id: "opengateway",
1151
+ label: "OpenGateway",
1152
+ adapter: "openai-chat",
1153
+ baseUrl: "https://apis.opengateway.ai/v1",
1154
+ authKind: "key",
1155
+ dashboardUrl: "https://opengateway.ai/api-keys",
1156
+ liveModels: true,
1157
+ // GET /v1/models is public and answers 200 for any Bearer value, so it cannot verify a key.
1158
+ apiKeyValidation: "unknown",
1159
+ preserveCustomDestination: true,
1160
+ // Both cold-start seeds are active Sionic-served vision models with 1M context.
1161
+ // GLM is the default, not a quality ranking: a verified, large-output coding seed.
1162
+ defaultModel: "z-ai/glm-5.3-flash-ultrafast",
1163
+ models: ["z-ai/glm-5.3-flash-ultrafast", "deepseek/deepseek-v4.1-flash-ultrafast", "openai/o3-pro"],
1164
+ modelContextWindows: {
1165
+ "z-ai/glm-5.3-flash-ultrafast": 1_000_000,
1166
+ "deepseek/deepseek-v4.1-flash-ultrafast": 1_000_000,
1167
+ "openai/o3-pro": 200_000,
1168
+ },
1169
+ modelMaxOutputTokens: { "z-ai/glm-5.3-flash-ultrafast": 131_072 },
1170
+ modelInputModalities: {
1171
+ "z-ai/glm-5.3-flash-ultrafast": ["text", "image"],
1172
+ "deepseek/deepseek-v4.1-flash-ultrafast": ["text", "image"],
1173
+ "openai/o3-pro": ["text"],
1174
+ },
1175
+ noVisionModels: ["openai/o3-pro"],
1176
+ // Chat is the broad wire (77/80 active rows). o3-pro is Responses-only, on all
1177
+ // inbounds. Only the two live-verified Sionic models bypass translation for Codex.
1178
+ // Other OpenAI/Anthropic/Gemini rows stay on Chat until their Responses tool replay
1179
+ // is independently verified; endpoint availability alone is not conformance evidence.
1180
+ modelWireDefaults: {
1181
+ "openai/o3-pro": "openai-responses",
1182
+ "z-ai/glm-5.3-flash-ultrafast": { wire: "openai-responses", inbound: ["responses"] },
1183
+ "deepseek/deepseek-v4.1-flash-ultrafast": { wire: "openai-responses", inbound: ["responses"] },
1184
+ },
1185
+ modelDiscovery: {
1186
+ path: "models", // Public GET; lower-case ?status=active returns 400, so filter locally.
1187
+ // Admit a row only on a wire it is served on: the Chat default needs chat_completions,
1188
+ // and Responses-only rows are admitted only when pinned to Responses above. A future
1189
+ // Responses-only row stays hidden until it is pinned, rather than 404ing on Chat.
1190
+ filter: {
1191
+ allOf: [{ path: ["status"], equalsAny: ["active"] }],
1192
+ anyOf: [
1193
+ { path: ["endpoints"], containsAny: ["chat_completions"] },
1194
+ { path: ["id"], equalsAny: ["openai/o3-pro"] },
1195
+ ],
1196
+ },
1197
+ preferFirst: [{ path: ["providers", "*", "id"], containsAny: ["sionic-ai"] }],
1198
+ },
1199
+ note: "Sionic AI's OpenAI-compatible gateway. Public live discovery lists active models served on Chat Completions (plus pinned Responses-only rows), with Sionic-served ultrafast models first. The public catalog cannot validate keys.",
1200
+ },
1146
1201
  {
1147
1202
  // Public contract checked 2026-09-29: https://docs.tokenlab.sh/api-reference/models/list-models
1148
1203
  // A supplied key is validated and scopes the catalog; anonymous discovery is also public.
@@ -30,6 +30,7 @@ export interface ResponsesTerminalRepairPolicy {
30
30
 
31
31
  export type ProviderModelDiscoveryScalar = string | number | boolean;
32
32
 
33
+ /** Path segment `*` projects at most 256 array elements; containsAny/All match projected scalars exactly. */
33
34
  export type ProviderModelDiscoveryPredicate =
34
35
  | {
35
36
  path: readonly string[];
@@ -70,6 +71,8 @@ interface ProviderModelDiscoverySharedSpec {
70
71
  idField?: string;
71
72
  /** Declarative eligibility rules evaluated against each untrusted model row. */
72
73
  filter?: ProviderModelDiscoveryFilter;
74
+ /** Stable partition: admitted original rows matching ANY predicate precede other rows. Registry-only. */
75
+ preferFirst?: readonly ProviderModelDiscoveryPredicate[];
73
76
  /** Optional lower byte ceiling; the process-wide hard ceiling still wins. */
74
77
  maxResponseBytes?: number;
75
78
  /** Optional lower raw-row ceiling; the process-wide hard ceiling still wins. */
@@ -1,4 +1,4 @@
1
- import type { FastWire, OcxParsedRequest, OcxProviderConfig } from "../types";
1
+ import type { FastWire, OcxParsedRequest, OcxProviderConfig, TierDecision } from "../types";
2
2
 
3
3
  /**
4
4
  * Grok OAuth Fast is a serving lane, not a tier.
@@ -35,6 +35,16 @@ function modelVariantFastWire(variant: string): FastWire {
35
35
  return { kind: "model-variant", canonicalToWire: { priority: variant }, foreignCallerTiers: "drop" };
36
36
  }
37
37
 
38
+ /** Shared by admission preview and final serialization; explicit operator wires stay authoritative. */
39
+ export function xaiOauthFastModelForDecision(
40
+ route: { providerName: string; provider: Pick<OcxProviderConfig, "authMode" | "fastWire">; modelId: string },
41
+ decision: TierDecision | undefined,
42
+ ): string | undefined {
43
+ return decision?.kind === "set" && route.provider.fastWire === undefined
44
+ ? xaiOauthFastModel(route.providerName, route.provider, route.modelId)
45
+ : undefined;
46
+ }
47
+
38
48
  function isRecord(value: unknown): value is Record<string, unknown> {
39
49
  return !!value && typeof value === "object" && !Array.isArray(value);
40
50
  }
@@ -58,9 +68,7 @@ export function applyXaiOauthFastModel(
58
68
  // The lane switch replaces the registry's service-tier Fast only. The xai registry entry declares no
59
69
  // FastWire, so a provider-level `fastWire` is always the operator's own (service-tier.ts reads it
60
70
  // first): that decision carries a wire value they verified, and it is sent unchanged.
61
- const variant = parsed.options.tierDecision?.kind === "set" && route.provider.fastWire === undefined
62
- ? xaiOauthFastModel(route.providerName, route.provider, route.modelId)
63
- : undefined;
71
+ const variant = xaiOauthFastModelForDecision(route, parsed.options.tierDecision);
64
72
  if (!variant) {
65
73
  if (previous === undefined) return;
66
74
  delete parsed._wireModelOverride;