@bitkyc08/opencodex 2.89.0 → 2.91.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 (242) hide show
  1. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  2. package/gui/dist/assets/App-nZ34cR5G.js +58 -0
  3. package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-BuLy7hL1.js} +1 -1
  4. package/gui/dist/assets/{index-uchD-ngf.js → index-DGXS51sp.js} +15 -15
  5. package/gui/dist/assets/quota-summary-DO1Sx5TM.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/package.json +2 -2
  8. package/src/adapters/anthropic.ts +2 -5
  9. package/src/adapters/base.ts +4 -0
  10. package/src/adapters/claude-cli/adapter.ts +13 -7
  11. package/src/adapters/claude-cli/usage-admission.ts +28 -2
  12. package/src/adapters/coding-agent/turn.ts +14 -4
  13. package/src/adapters/cursor/protobuf-request.ts +3 -0
  14. package/src/adapters/google-progress-history.ts +19 -1
  15. package/src/adapters/google.ts +28 -9
  16. package/src/adapters/image.ts +5 -3
  17. package/src/adapters/input-media-guard.ts +5 -2
  18. package/src/adapters/kiro-refusal.ts +41 -1
  19. package/src/adapters/ollama-native.ts +1 -0
  20. package/src/adapters/openai-chat/messages.ts +34 -4
  21. package/src/adapters/openai-chat/passthrough.ts +37 -4
  22. package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
  23. package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
  24. package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
  25. package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
  26. package/src/adapters/openai-chat.ts +7 -6
  27. package/src/adapters/openai-responses/opengateway-input.ts +139 -0
  28. package/src/adapters/openai-responses/passthrough.ts +7 -2
  29. package/src/adapters/openai-responses/request-strips.ts +32 -0
  30. package/src/adapters/openai-responses/transport.ts +19 -1
  31. package/src/chat/inbound.ts +20 -2
  32. package/src/chat/outbound.ts +36 -4
  33. package/src/chat/reasoning-details.ts +137 -0
  34. package/src/claude/desktop-3p-library.ts +43 -3
  35. package/src/claude/desktop-3p.ts +20 -10
  36. package/src/claude/desktop-picker-profile.ts +18 -9
  37. package/src/claude/desktop-remote-store-io.ts +2 -2
  38. package/src/cli/capabilities-agents-routing.ts +50 -0
  39. package/src/cli/capabilities-base.ts +15 -2
  40. package/src/cli/capabilities-observe-system.ts +4 -4
  41. package/src/cli/codex-shim-autorestore.ts +5 -0
  42. package/src/cli/dispatch.ts +30 -0
  43. package/src/cli/ensure-desktop-cold-start.ts +2 -2
  44. package/src/cli/fallback.ts +127 -0
  45. package/src/cli/help.ts +1 -0
  46. package/src/cli/index.ts +1 -2
  47. package/src/cli/link-apply-restart.ts +72 -0
  48. package/src/cli/link-child.ts +6 -2
  49. package/src/cli/link-cleanup.ts +95 -0
  50. package/src/cli/link-enrollment.ts +4 -0
  51. package/src/cli/link.ts +84 -9
  52. package/src/cli/provider.ts +44 -2
  53. package/src/cli/registry.ts +13 -1
  54. package/src/cli/restart-failure.ts +4 -0
  55. package/src/cli/runtime-api.ts +1 -1
  56. package/src/cli/status-codex-settings.ts +56 -0
  57. package/src/cli/status-fallback.ts +29 -0
  58. package/src/cli/status-install.ts +52 -0
  59. package/src/cli/status.ts +20 -1
  60. package/src/cli/system-restart-client.ts +32 -1
  61. package/src/cli/update-restart-child.ts +4 -3
  62. package/src/cli/update-restart-home.ts +20 -8
  63. package/src/client/connect.ts +82 -6
  64. package/src/client/home-unreachable.ts +16 -0
  65. package/src/client/hub-relay.ts +8 -2
  66. package/src/client/link-home.ts +119 -6
  67. package/src/client/link-parity-reporter.ts +107 -0
  68. package/src/client/link-relay-transport.ts +8 -1
  69. package/src/client/link-relay.ts +27 -3
  70. package/src/client/machine-listener.ts +83 -1
  71. package/src/client/runtime-restart.ts +127 -0
  72. package/src/client/runtime.ts +137 -25
  73. package/src/codex/account-lifecycle.ts +19 -6
  74. package/src/codex/account-store.ts +27 -8
  75. package/src/codex/account-usability.ts +4 -1
  76. package/src/codex/auth-api/account-list.ts +10 -5
  77. package/src/codex/auth-api/account-pause-group.ts +2 -22
  78. package/src/codex/auth-api/login-collision.ts +59 -0
  79. package/src/codex/auth-api/login-flow.ts +12 -7
  80. package/src/codex/auth-api/pool-mode-gate.ts +81 -13
  81. package/src/codex/auth-collision.ts +31 -4
  82. package/src/codex/auth-context.ts +58 -12
  83. package/src/codex/catalog/parsing.ts +4 -0
  84. package/src/codex/cli-update-plan.ts +18 -3
  85. package/src/codex/collision-identity.ts +78 -0
  86. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  87. package/src/codex/main-account-hard-lock.ts +14 -0
  88. package/src/codex/quota-history.ts +7 -0
  89. package/src/codex/quota.ts +10 -0
  90. package/src/codex/routing/cooldown-math.ts +5 -0
  91. package/src/codex/routing/failure-window.ts +61 -9
  92. package/src/codex/routing.ts +4 -3
  93. package/src/codex/shim-diagnostics.ts +2 -1
  94. package/src/codex/shim-fingerprint.ts +11 -2
  95. package/src/codex/shim-templates.ts +16 -5
  96. package/src/codex/shim-windows-refresh.ts +224 -0
  97. package/src/codex/shim.ts +45 -21
  98. package/src/codex/subagent-model-fallback.ts +24 -1
  99. package/src/combos/failover.ts +8 -1
  100. package/src/combos/index.ts +1 -0
  101. package/src/combos/resolve.ts +12 -0
  102. package/src/config/diagnostics.ts +5 -0
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/provider-validation.ts +7 -2
  105. package/src/config/schema/config-schema.ts +3 -0
  106. package/src/config/schema/fallback-policy.ts +40 -0
  107. package/src/generated/compatibility-version.json +360 -192
  108. package/src/generated/model-metadata.ts +1 -1
  109. package/src/images/loop.ts +2 -2
  110. package/src/lib/authorization-scheme.ts +95 -0
  111. package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
  112. package/src/lib/provider-admission-wait.ts +14 -0
  113. package/src/lib/provider-client-headers.ts +1 -0
  114. package/src/lib/redact.ts +2 -2
  115. package/src/lib/service-secrets.ts +37 -2
  116. package/src/lib/system-restart-contract.ts +24 -0
  117. package/src/lib/test-home-guard.ts +78 -11
  118. package/src/lib/translator-budget.ts +1 -1
  119. package/src/lib/upstream-retry.ts +15 -0
  120. package/src/link/child-parity.ts +124 -0
  121. package/src/link/codex-settings.ts +37 -0
  122. package/src/link/compatibility.ts +119 -0
  123. package/src/link/management-routes.ts +2 -0
  124. package/src/link/peer-report.ts +100 -0
  125. package/src/link/remote-platform.ts +78 -0
  126. package/src/link/ssh-argv.ts +19 -0
  127. package/src/link/status-projection.ts +24 -2
  128. package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
  129. package/src/oauth/account-import/parser.ts +15 -2
  130. package/src/oauth/account-import/types.ts +7 -0
  131. package/src/oauth/chatgpt-shared.ts +10 -1
  132. package/src/oauth/dsh.ts +45 -2
  133. package/src/oauth/index.ts +1 -1
  134. package/src/oauth/kimi.ts +2 -0
  135. package/src/oauth/kiro.ts +44 -1
  136. package/src/oauth/login-flow-state.ts +11 -2
  137. package/src/oauth/nous.ts +4 -1
  138. package/src/oauth/store.ts +2 -1
  139. package/src/protocols/codecs/chat.ts +3 -3
  140. package/src/protocols/encoders/adapter-events.ts +26 -2
  141. package/src/protocols/encoders/chat.ts +55 -3
  142. package/src/providers/claude-cli-usage.ts +20 -5
  143. package/src/providers/initial-model-selection.ts +2 -2
  144. package/src/providers/key-failover.ts +10 -2
  145. package/src/providers/key-store.ts +8 -3
  146. package/src/providers/mistral-transport.ts +240 -0
  147. package/src/providers/openai-sidecar.ts +5 -0
  148. package/src/providers/quota/anthropic-account-quota.ts +18 -6
  149. package/src/providers/quota/antigravity.ts +8 -10
  150. package/src/providers/quota/failure-codes.ts +18 -0
  151. package/src/providers/quota/vendor-probes-oauth.ts +12 -3
  152. package/src/providers/quota-routing-cache.ts +1 -1
  153. package/src/providers/quota.ts +5 -1
  154. package/src/providers/registry/entries-extended.ts +6 -1
  155. package/src/providers/routed-config.ts +1 -1
  156. package/src/providers/xai-transport.ts +2 -1
  157. package/src/quota-pool/assemble.ts +326 -0
  158. package/src/quota-pool/contract.ts +58 -0
  159. package/src/quota-pool/identity.ts +203 -0
  160. package/src/remote/features.ts +74 -0
  161. package/src/remote/protocol.ts +2 -0
  162. package/src/remote/served-by.ts +57 -0
  163. package/src/responses/audio.ts +46 -0
  164. package/src/responses/input-media.ts +37 -15
  165. package/src/responses/parser-content.ts +22 -27
  166. package/src/responses/parser.ts +7 -0
  167. package/src/responses/reasoning-envelope.ts +12 -0
  168. package/src/responses/reasoning-retention.ts +218 -11
  169. package/src/responses/schema.ts +3 -4
  170. package/src/responses/web-search-replay.ts +11 -2
  171. package/src/routing/fallback-policy.ts +290 -0
  172. package/src/routing/fallback-state.ts +179 -0
  173. package/src/server/audio-client.ts +2 -1
  174. package/src/server/audio-dictation.ts +2 -1
  175. package/src/server/audio-upstream.ts +2 -1
  176. package/src/server/auth-cors.ts +6 -5
  177. package/src/server/background-lifecycle.ts +4 -0
  178. package/src/server/chat-completions.ts +8 -1
  179. package/src/server/chat-native.ts +8 -7
  180. package/src/server/gui-session.ts +2 -1
  181. package/src/server/index/link-listener.ts +11 -1
  182. package/src/server/index/link-management.ts +47 -0
  183. package/src/server/index/link-relay-sessions.ts +11 -2
  184. package/src/server/index/optional-listeners.ts +7 -2
  185. package/src/server/index/serve-options.ts +27 -4
  186. package/src/server/index/websocket-handler.ts +10 -0
  187. package/src/server/lifecycle.ts +17 -1
  188. package/src/server/live.ts +5 -0
  189. package/src/server/management/context.ts +1 -1
  190. package/src/server/management/fallback-routes.ts +75 -0
  191. package/src/server/management/link-enrollment.ts +21 -3
  192. package/src/server/management/link-route-state.ts +2 -0
  193. package/src/server/management/link-routes.ts +12 -1
  194. package/src/server/management/oauth-account-routes.ts +2 -1
  195. package/src/server/management/quota-pool-routes.ts +24 -0
  196. package/src/server/management/route-registry.ts +3 -0
  197. package/src/server/management-api.ts +16 -0
  198. package/src/server/management-auth.ts +2 -1
  199. package/src/server/messages-native.ts +11 -0
  200. package/src/server/request-log.ts +16 -7
  201. package/src/server/responses/adapter-continuation.ts +2 -2
  202. package/src/server/responses/adapter-dispatch.ts +2 -2
  203. package/src/server/responses/agent-task-recovery.ts +1 -1
  204. package/src/server/responses/codex-ws-exchange.ts +3 -0
  205. package/src/server/responses/codex-ws-metadata.ts +44 -0
  206. package/src/server/responses/codex-ws-session.ts +21 -1
  207. package/src/server/responses/combo-stream-preflight.ts +7 -2
  208. package/src/server/responses/compact.ts +4 -2
  209. package/src/server/responses/core-auth.ts +31 -1
  210. package/src/server/responses/core-codex-account.ts +21 -14
  211. package/src/server/responses/core-combo.ts +75 -1
  212. package/src/server/responses/core-opaque-recovery.ts +20 -11
  213. package/src/server/responses/deepseek-image-files.ts +121 -0
  214. package/src/server/responses/empty-completion-guard.ts +8 -0
  215. package/src/server/responses/encrypted-payload.ts +3 -3
  216. package/src/server/responses/fetch-helpers.ts +30 -10
  217. package/src/server/responses/input-admission.ts +6 -2
  218. package/src/server/responses/main-hard-lock-routing.ts +85 -8
  219. package/src/server/responses/opengateway-completion-guard.ts +232 -0
  220. package/src/server/responses/passthrough-completion.ts +41 -0
  221. package/src/server/responses/passthrough-delivery.ts +2 -0
  222. package/src/server/responses/passthrough-dispatch.ts +13 -13
  223. package/src/server/responses/passthrough-key-rotation.ts +40 -0
  224. package/src/server/responses/policy-fallback.ts +44 -1
  225. package/src/server/responses/reasoning-archive-error.ts +21 -0
  226. package/src/server/responses/request-prepare.ts +19 -1
  227. package/src/server/responses/request-transport.ts +61 -3
  228. package/src/server/restart-replacement.ts +50 -2
  229. package/src/server/ws-bridge.ts +5 -0
  230. package/src/service/cli.ts +25 -2
  231. package/src/service/diagnostics.ts +20 -6
  232. package/src/service/orchestration.ts +17 -0
  233. package/src/service/status-json.ts +116 -0
  234. package/src/types/config.ts +7 -0
  235. package/src/types/fallback.ts +38 -0
  236. package/src/types/provider.ts +2 -0
  237. package/src/types/request.ts +16 -1
  238. package/src/types.ts +1 -0
  239. package/src/web-search/loop.ts +2 -2
  240. package/gui/dist/assets/App-BnRSujOq.js +0 -58
  241. package/gui/dist/assets/App-dnkJ85te.css +0 -1
  242. package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
@@ -12,6 +12,7 @@ import { isKeyInCooldown, getKeyCooldownUntil, setKeyCooldown } from "./api-key-
12
12
  export { clearKeyCooldowns, getKeyCooldownUntil, sweepExpiredApiKeyCooldowns } from "./api-key-cooldowns";
13
13
  import { commitProviderApiKeySelection, providerApiKeySelectionIsCurrent } from "./api-key-selection";
14
14
  import { RequestPacingQueueOverloadError } from "./request-pacing";
15
+ import { isCanonicalMistralChat } from "./mistral-transport";
15
16
  import type { ProviderApiKeySelection } from "../types/provider";
16
17
  import { routedProviderConfig } from "./routed-config";
17
18
  import { getProviderRegistryEntry } from "./registry";
@@ -585,7 +586,7 @@ function allocateDistributedKey(providerName: string, provider: OcxProviderConfi
585
586
  * (subscription traffic such as Muse Spark) falls back to a patient same-key policy so a
586
587
  * burst 429 waits and replays instead of surfacing to the client and aborting a long
587
588
  * session; key-auth Command Code at its canonical endpoints gets the same fallback (#5180), and
588
- * every other provider without the knob keeps today's fail-fast behavior.
589
+ * canonical Mistral Chat uses a slower fallback; other destinations stay fail-fast.
589
590
  * OAuth/forward/local credentials are never replayed on the same token. The returned
590
591
  * policy is fully defaulted so callers never re-check fields.
591
592
  */
@@ -609,8 +610,15 @@ export function rateLimitRetryPolicyFor(
609
610
  respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
610
611
  };
611
612
  }
612
- // No explicit knob: patient fallback for OpenCode Go and canonical Command Code only.
613
+ // No explicit knob: destination-specific fallbacks, never OAuth or local credentials.
613
614
  if (provider.authMode !== undefined && provider.authMode !== "key") return null;
615
+ // Mistral's token-throughput and temporary capacity refusals both use HTTP 429.
616
+ // Keep relays and other wire adapters opt-in, and reuse the existing send budget.
617
+ if (typeof provider.baseUrl === "string" && typeof provider.adapter === "string" && isCanonicalMistralChat({
618
+ adapter: provider.adapter, authMode: provider.authMode, baseUrl: provider.baseUrl,
619
+ })) {
620
+ return { ...DEFAULT_RATE_LIMIT_RETRY, attempts: 6, intervalMs: 10_000 };
621
+ }
614
622
  if (isOpenCodeGoDestination(provider)) return { ...OPENCODE_GO_RATE_LIMIT_RETRY };
615
623
  // Command Code's Provider API rate-limits long muse-spark turns the same way (#5180): a single
616
624
  // key cannot fail over, and the Codex client does not retry a 429, so the turn aborts. The key
@@ -32,14 +32,19 @@ export type {
32
32
  ProviderKeyStoreKind,
33
33
  } from "./api-key-resolve";
34
34
 
35
- /** Shared with routing: a key-mode override is effective only while its key resolves. */
35
+ /**
36
+ * Shared with routing: a key-mode override is operator intent, not a credential probe.
37
+ * The configured authMode alone decides the lane; a key that cannot be resolved right now
38
+ * (paused last key, missing env var, disabled pool) fails closed at the transport and must
39
+ * never implicitly reclassify the provider to the registry's OAuth lane, where a stored
40
+ * OAuth account would be billed instead of the operator's key.
41
+ */
36
42
  export function providerUsesKeyAuthOverride(
37
43
  entry: Pick<ProviderRegistryEntry, "authKind" | "allowKeyAuthOverride">,
38
44
  provider: Pick<OcxProviderConfig, "authMode">,
39
- resolvedKey: string | undefined,
40
45
  ): boolean {
41
46
  return entry.authKind === "oauth" && entry.allowKeyAuthOverride === true
42
- && provider.authMode === "key" && typeof resolvedKey === "string" && resolvedKey.trim().length > 0;
47
+ && provider.authMode === "key";
43
48
  }
44
49
 
45
50
  /**
@@ -0,0 +1,240 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { OcxProviderConfig } from "../types";
3
+ import { isEgressTransparentExecutor, markEgressTransparentExecutor } from "../lib/provider-egress";
4
+ import { configuredOutboundFetch } from "../lib/proxy-env";
5
+ import { waitOutsideProviderHeaderDeadline } from "../lib/provider-admission-wait";
6
+ import { markSameTargetRetryHold, sleepWithAbort } from "../lib/upstream-retry";
7
+ import { RequestPacingQueueOverloadError } from "./request-pacing";
8
+ import type { OcxProviderTransport } from "./xai-transport";
9
+
10
+ const MAX_IDENTITIES = 128;
11
+ /** Queued waiters per credential+model identity; one hot identity never refuses another. */
12
+ const MAX_WAITERS_PER_IDENTITY = 128;
13
+ /**
14
+ * Process-wide waiter bound. Model strings come from clients, so identities are not bounded by
15
+ * configuration; this keeps memory and pending requests finite. It is a multiple of the
16
+ * per-identity cap, so no single identity can fill it alone.
17
+ */
18
+ const MAX_WAITERS_TOTAL = 4 * MAX_WAITERS_PER_IDENTITY;
19
+ const DEFAULT_MAX_WAIT_MS = 60_000; // Existing retry wait budget, not an inferred rate window.
20
+ const MINUTE_MS = 60 * 1000; // Unit named by the observed headers, never an account quota.
21
+ type RecoveryState = { generation: number; probe?: { done: Promise<void>; finish(): void } };
22
+
23
+ /** Match the supported key-auth Chat destination, including one optional trailing slash. */
24
+ export function isCanonicalMistralChat(
25
+ provider: Pick<OcxProviderConfig, "adapter" | "authMode" | "baseUrl">,
26
+ ): boolean {
27
+ if (provider.adapter !== "openai-chat" || (provider.authMode !== undefined && provider.authMode !== "key")) return false;
28
+ try {
29
+ const url = new URL(provider.baseUrl);
30
+ return url.origin === "https://api.mistral.ai" && /^\/v1\/?$/.test(url.pathname)
31
+ && !url.username && !url.password && !url.search && !url.hash;
32
+ } catch { return false; }
33
+ }
34
+
35
+ /** Read a nonnegative integer counter; reject missing or malformed values. */
36
+ function count(headers: Headers, name: string): number | undefined {
37
+ const raw = headers.get(name)?.trim();
38
+ if (!raw || !/^\d+$/.test(raw)) return undefined;
39
+ const value = Number(raw);
40
+ return Number.isSafeInteger(value) ? value : undefined;
41
+ }
42
+
43
+ /** Read the server delay as seconds or an HTTP date; ignore invalid hints. */
44
+ function retryDelay(headers: Headers, now: number): number | undefined {
45
+ const raw = headers.get("retry-after")?.trim();
46
+ if (!raw) return undefined;
47
+ const value = /^\d+(?:\.\d+)?$/.test(raw) ? Number(raw) * 1000 : Date.parse(raw) - now;
48
+ return Number.isFinite(value) ? Math.max(0, value) : undefined;
49
+ }
50
+
51
+ /** Classify measured refusal evidence only; never predict a future cache hit or token charge. */
52
+ export function mistralMinuteRefusal(headers: Headers): "temporary" | "charge-above-allowance" | "unknown" {
53
+ const month = count(headers, "x-ratelimit-limit-tokens-month");
54
+ if (month !== undefined && count(headers, "x-ratelimit-remaining-tokens-month") === 0) return "unknown";
55
+ const limit = count(headers, "x-ratelimit-limit-tokens-minute");
56
+ const remaining = count(headers, "x-ratelimit-remaining-tokens-minute");
57
+ const cost = count(headers, "x-ratelimit-tokens-query-cost");
58
+ if (limit !== undefined && limit > 0 && cost !== undefined && cost > limit) return "charge-above-allowance";
59
+ if (limit !== undefined && limit > 0 && remaining !== undefined && remaining <= limit
60
+ && (remaining === 0 || (cost !== undefined && cost > remaining))) return "temporary";
61
+ const requests = count(headers, "x-ratelimit-limit-req-minute");
62
+ if (requests !== undefined && requests > 0 && count(headers, "x-ratelimit-remaining-req-minute") === 0) return "temporary";
63
+ return "unknown";
64
+ }
65
+
66
+ /** Process-local observations, never a fixed account/plan quota or a persisted credential. */
67
+ export class MistralBudgetPacer {
68
+ private readonly deadlines = new Map<string, number>();
69
+ private readonly recoveries = new Map<string, RecoveryState>();
70
+ /** Latest live deadline evicted at capacity; unknown identities wait it out instead of sending early. */
71
+ private evictedUntil = 0;
72
+ /**
73
+ * Parked waiters keyed by identity. A request counts only once it is about to wait, so a held
74
+ * credential or model fills only its own queue and a request that can send now is never answered
75
+ * queue_full (HTTP 429). The map holds identities with live waiters only; the total is capped by
76
+ * MAX_WAITERS_TOTAL.
77
+ */
78
+ private readonly waiting = new Map<string, number>();
79
+ private waitingTotal = 0;
80
+
81
+ constructor(
82
+ private readonly now: () => number = Date.now,
83
+ private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void> = sleepWithAbort,
84
+ ) {}
85
+
86
+ /** Record server delays and confirmed minute pressure for this credential and model. */
87
+ observe(identity: string, headers: Headers, status = 200): void {
88
+ const tokens = count(headers, "x-ratelimit-remaining-tokens-minute");
89
+ const requests = count(headers, "x-ratelimit-remaining-req-minute");
90
+ // Healthy remaining counters never predict the next request's cache/charge.
91
+ if (status !== 429 && tokens !== 0 && requests !== 0) return;
92
+ // With no explicit time, activate a conservative minute hold only AFTER a
93
+ // refusal establishes pressure. Healthy responses never predict the next cost.
94
+ const delay = retryDelay(headers, this.now())
95
+ ?? (status === 429 && mistralMinuteRefusal(headers) === "temporary" ? MINUTE_MS : undefined);
96
+ if (delay === undefined || delay === 0) return;
97
+ for (const [key, deadline] of this.deadlines) {
98
+ if (deadline <= this.now()) this.deadlines.delete(key);
99
+ }
100
+ if (!this.deadlines.has(identity) && this.deadlines.size >= MAX_IDENTITIES) {
101
+ const [evicted, until] = this.deadlines.entries().next().value!;
102
+ this.evictedUntil = Math.max(this.evictedUntil, until);
103
+ this.deadlines.delete(evicted);
104
+ }
105
+ this.deadlines.set(identity, Math.max(this.deadlines.get(identity) ?? 0, this.now() + delay));
106
+ if (status === 429) {
107
+ if (!this.recoveries.has(identity) && this.recoveries.size >= MAX_IDENTITIES) {
108
+ const idle = [...this.recoveries].find(([, state]) => !state.probe);
109
+ if (!idle) return;
110
+ this.recoveries.delete(idle[0]);
111
+ }
112
+ const state = this.recoveries.get(identity) ?? { generation: 0 };
113
+ state.generation++;
114
+ this.recoveries.set(identity, state);
115
+ }
116
+ }
117
+
118
+ /** Wait within the admission budget and return a release callback for one recovery probe. */
119
+ async acquire(identity: string, signal?: AbortSignal, maxWaitMs = DEFAULT_MAX_WAIT_MS): Promise<(success?: boolean) => void> {
120
+ signal?.throwIfAborted();
121
+ const deadline = this.now() + maxWaitMs;
122
+ let parked = false;
123
+ const park = (): void => {
124
+ if (parked) return;
125
+ const queued = this.waiting.get(identity) ?? 0;
126
+ if (queued >= MAX_WAITERS_PER_IDENTITY || this.waitingTotal >= MAX_WAITERS_TOTAL) {
127
+ throw new RequestPacingQueueOverloadError("mistral", "queue_full", 60);
128
+ }
129
+ this.waiting.set(identity, queued + 1);
130
+ this.waitingTotal++;
131
+ parked = true;
132
+ };
133
+ try {
134
+ for (;;) {
135
+ signal?.throwIfAborted();
136
+ // An evicted identity may have re-entered with a shorter delay; the evicted one still binds.
137
+ const live = this.evictedUntil > this.now() ? this.evictedUntil : undefined;
138
+ const own = this.deadlines.get(identity);
139
+ const readyAt = own === undefined ? live : live === undefined ? own : Math.max(own, live);
140
+ if (readyAt !== undefined && readyAt > deadline) {
141
+ const delay = readyAt - this.now();
142
+ throw new RequestPacingQueueOverloadError("mistral", "queue_expired", Math.ceil(delay / 1000));
143
+ }
144
+ if (readyAt !== undefined && readyAt > this.now()) {
145
+ park();
146
+ await this.sleep(readyAt - this.now(), signal);
147
+ continue;
148
+ }
149
+ const state = this.recoveries.get(identity);
150
+ if (!state) { this.deadlines.delete(identity); return () => {}; }
151
+ if (state.probe) {
152
+ const remaining = deadline - this.now();
153
+ if (remaining <= 0) throw new RequestPacingQueueOverloadError("mistral", "queue_expired", 1);
154
+ park();
155
+ const local = new AbortController();
156
+ try {
157
+ await Promise.race([state.probe.done, this.sleep(remaining, AbortSignal.any([local.signal, ...(signal ? [signal] : [])]))]);
158
+ } finally { local.abort(); }
159
+ continue;
160
+ }
161
+ let finish!: () => void;
162
+ const probe = { done: new Promise<void>(resolve => { finish = resolve; }), finish: () => finish() };
163
+ state.probe = probe;
164
+ const generation = state.generation;
165
+ let released = false;
166
+ return success => {
167
+ if (released) return;
168
+ released = true;
169
+ if (state.probe === probe) delete state.probe;
170
+ // Only this probe can settle recovery; an older concurrent success must
171
+ // not erase a more recent refusal or the server's minimum wait.
172
+ if (success && state.generation === generation) {
173
+ if (this.recoveries.get(identity) === state) this.recoveries.delete(identity);
174
+ if ((this.deadlines.get(identity) ?? 0) <= this.now()) this.deadlines.delete(identity);
175
+ }
176
+ probe.finish();
177
+ };
178
+ }
179
+ } finally {
180
+ if (parked) {
181
+ this.waitingTotal--;
182
+ const left = (this.waiting.get(identity) ?? 1) - 1;
183
+ if (left > 0) this.waiting.set(identity, left);
184
+ else this.waiting.delete(identity);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ const pacer = new MistralBudgetPacer();
191
+ const wrapped = new WeakSet<typeof globalThis.fetch>();
192
+
193
+ /** Wrap canonical Mistral Chat sends with bounded admission and server-delay protection. */
194
+ export function resolveMistralTransport(
195
+ provider: OcxProviderTransport,
196
+ budgetPacer = pacer,
197
+ ): OcxProviderTransport {
198
+ if (!isCanonicalMistralChat(provider) || (provider.fetch && wrapped.has(provider.fetch))) return provider;
199
+ const baseFetch = provider.fetch
200
+ ?? markEgressTransparentExecutor(((input, init) => configuredOutboundFetch(input, init)) as typeof globalThis.fetch);
201
+ const attemptFetch = (async (input, init) => {
202
+ const url = new URL(input instanceof Request ? input.url : String(input));
203
+ if (url.origin !== "https://api.mistral.ai" || url.pathname !== "/v1/chat/completions"
204
+ || url.search || url.hash || init?.method?.toUpperCase() !== "POST" || typeof init.body !== "string") {
205
+ return baseFetch(input, init);
206
+ }
207
+ // The Chat adapter serializes model first. Read only that bounded prefix rather
208
+ // than parsing/copying a potentially million-token conversation a second time.
209
+ const encodedModel = /^\s*\{\s*"model"\s*:\s*("(?:[^"\\]|\\.)*")\s*[,}]/.exec(init.body.slice(0, 4096))?.[1];
210
+ let model: unknown;
211
+ try { model = encodedModel === undefined ? undefined : JSON.parse(encodedModel); } catch { return baseFetch(input, init); }
212
+ const authorization = new Headers(init.headers).get("authorization");
213
+ if (!authorization || typeof model !== "string" || !model || model.length > 256) return baseFetch(input, init);
214
+ // Bind to the actual send credential, including after pool rotation; keep no raw key.
215
+ const identity = createHash("sha256").update(JSON.stringify([authorization, model])).digest("hex");
216
+ const pacing = provider.requestPacing?.enabled !== false && provider.retryOn429?.respectRetryAfter !== false;
217
+ const release = pacing ? await waitOutsideProviderHeaderDeadline(init,
218
+ signal => budgetPacer.acquire(identity, signal, provider.retryOn429?.maxIntervalMs)) : undefined;
219
+ let response: Response;
220
+ try {
221
+ init.signal?.throwIfAborted();
222
+ response = await baseFetch(input, init);
223
+ if (pacing) budgetPacer.observe(identity, response.headers, response.status);
224
+ } catch (error) { release?.(); throw error; }
225
+ release?.(response.ok);
226
+ const retry = provider.retryOn429;
227
+ if (response.status === 429 && retry?.respectRetryAfter !== false) {
228
+ const delay = retryDelay(response.headers, Date.now());
229
+ if (delay !== undefined && delay > (retry?.maxIntervalMs ?? DEFAULT_MAX_WAIT_MS)) {
230
+ // The shared retry helper caps waits. Stop same-key replays sooner than Mistral permits;
231
+ // another credential may still serve, and the original status/body/Retry-After reach the caller.
232
+ markSameTargetRetryHold(response);
233
+ }
234
+ }
235
+ return response;
236
+ }) as typeof globalThis.fetch;
237
+ if (isEgressTransparentExecutor(baseFetch)) markEgressTransparentExecutor(attemptFetch);
238
+ wrapped.add(attemptFetch);
239
+ return { ...provider, fetch: attemptFetch };
240
+ }
@@ -12,6 +12,7 @@ import {
12
12
  type CodexAuthPolicyConfig,
13
13
  } from "../codex/auth-context";
14
14
  import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
15
+ import { codexFailureWindowOutcomeMeta } from "../codex/routing/failure-window";
15
16
  import { inspectChatGptDomainClaim } from "../oauth/chatgpt";
16
17
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "../server/auth-cors";
17
18
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
@@ -205,6 +206,7 @@ export async function resolveFirstUsableOpenAiSidecar(
205
206
  releaseCodexAuthContextProbeLease(authContext);
206
207
  throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
207
208
  }
209
+ const windowMeta = codexFailureWindowOutcomeMeta(authContext);
208
210
  return {
209
211
  ...candidate,
210
212
  authContext,
@@ -220,6 +222,7 @@ export async function resolveFirstUsableOpenAiSidecar(
220
222
  probeLeaseId: authContext.probeLeaseId,
221
223
  probeQuotaScope: authContext.probeQuotaScope,
222
224
  writerGeneration: authContext.writerGeneration,
225
+ ...windowMeta,
223
226
  // 401/403 here is evidence about this exact stored credential; without the generation a
224
227
  // replacement inherits the quarantine (#2892 gap 4).
225
228
  ...(authContext.kind === "pool" ? { credentialGeneration: authContext.generation } : {}),
@@ -256,6 +259,7 @@ export async function resolveFirstUsableOpenAiSidecar(
256
259
  releaseCodexAuthContextProbeLease(authContext);
257
260
  continue;
258
261
  }
262
+ const windowMeta = codexFailureWindowOutcomeMeta(authContext);
259
263
  return {
260
264
  ...candidate,
261
265
  authContext,
@@ -271,6 +275,7 @@ export async function resolveFirstUsableOpenAiSidecar(
271
275
  threadId: authContext.affinityKey,
272
276
  probeLeaseId: authContext.probeLeaseId,
273
277
  writerGeneration: authContext.writerGeneration,
278
+ ...windowMeta,
274
279
  // Same fence as the exact-account recorder above (#2892 gap 4).
275
280
  ...(authContext.kind === "pool" ? { credentialGeneration: authContext.generation } : {}),
276
281
  },
@@ -4,7 +4,9 @@ import { getAccountSet } from "../../oauth/store";
4
4
  import { captureConfigGeneration, sweepExpiredOnWrite } from "../../lib/state-store-sweeper";
5
5
  import { configuredAnthropicInstance, type AnthropicInstanceId } from "../anthropic-instance";
6
6
  import { ACCOUNT_QUOTA_TTL_MS } from "../quota-wire";
7
- import { fetchAnthropicUsageQuotaForInstance } from "./vendor-probes-oauth";
7
+ import { fetchAnthropicUsageProbeForInstance } from "./vendor-probes-oauth";
8
+ import { thrownQuotaFailure } from "./failure-codes";
9
+ import type { QuotaFailureCode } from "../quota-types";
8
10
  import { AnthropicQuotaProbeOwnershipError, anthropicCooldownRecoveryFor, type AnthropicQuotaRecoveryResult } from "./anthropic-cooldown-recovery";
9
11
  import {
10
12
  accountCacheKey, accountQuotaCache, accountQuotaInflight, captureProviderAccountQuotaEpoch,
@@ -49,7 +51,10 @@ export async function fetchAnthropicAccountQuota(
49
51
  }
50
52
  let token: string;
51
53
  try { token = await getTokenForAccountQuotaProbe(instance, accountId); }
52
- catch { return unavailable(); }
54
+ catch {
55
+ // Missing, signed-out or unrefreshable credential: a reauthentication matter, never a quota fact.
56
+ return { ...unavailable(), quotaFailure: "account_unavailable", quotaFailureIsCurrent: ownerCurrent };
57
+ }
53
58
  if (!ownerCurrent()) return unavailable();
54
59
  const flightKey = recovery.anthropicCredentialQuotaFlightKey(key, accountId);
55
60
  const flightCurrent = () => ownerCurrent()
@@ -65,30 +70,37 @@ export async function fetchAnthropicAccountQuota(
65
70
  // in-flight observations, as the pre-instance A path did, so joined callers and later cached
66
71
  // reads agree and windows that expired during the shared probe are normalized away.
67
72
  /** Cache the settled probe result for an account and publish its decision evidence only while the flight is still current for its credential and generation. */
68
- const publish = (result: AnthropicQuotaRecoveryResult | null): AccountQuotaCacheEntry => {
73
+ const publish = (result: AnthropicQuotaRecoveryResult | null, failure?: QuotaFailureCode): AccountQuotaCacheEntry => {
69
74
  const previous = accountQuotaCache.get(key);
70
75
  const retained = previous?.isCurrent?.() === false ? undefined : previous;
71
76
  const isCurrent = result?.isCurrent ?? flightCurrent;
72
77
  const entry: AccountQuotaCacheEntry = {
73
78
  ts: Date.now(), quota: normalizeAnthropicQuota(result?.quota ?? retained?.quota, Date.now()),
74
79
  ...(!result ? { unavailable: true as const } : {}), isCurrent,
80
+ ...(!result && failure ? { quotaFailure: failure, quotaFailureIsCurrent: isCurrent } : {}),
75
81
  };
76
82
  if (isCurrent() && mayCommitAccountQuotaKey(key, writerGeneration)) { accountQuotaCache.set(key, entry); result?.publishDecisionQuota(); sweepExpiredOnWrite(entry.ts); }
77
83
  return entry;
78
84
  };
79
85
  const probe = (async (): Promise<AccountQuotaCacheEntry> => {
86
+ // The closed reason of the last usage read in this flight: an HTTP status class, an unusable
87
+ // body, or a thrown dispatch. Ownership and cooldown refusals never set it.
88
+ let failure: QuotaFailureCode | undefined;
80
89
  try {
81
90
  const result = await recovery.probeAnthropicQuotaWithRecovery(accountId, token, fresh => {
82
91
  if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("quota flight changed");
83
92
  recovery.assertAnthropicQuotaSendAllowed(accountId, token);
84
- return fetchAnthropicUsageQuotaForInstance(instance, token, fresh || freshDispatch);
93
+ return fetchAnthropicUsageProbeForInstance(instance, token, fresh || freshDispatch).then(
94
+ read => { failure = read.failure; return read.quota; },
95
+ error => { failure = thrownQuotaFailure(error); throw error; },
96
+ );
85
97
  }, () => ownerCurrent() && mayCommitAccountQuotaKey(key, writerGeneration));
86
98
  if (result && !result.isCurrent()) return unavailable();
87
99
  if (!result && !flightCurrent()) return unavailable();
88
- return publish(result);
100
+ return publish(result, failure);
89
101
  } catch (error) {
90
102
  if (error instanceof AnthropicQuotaProbeOwnershipError || !flightCurrent()) return unavailable();
91
- return publish(null);
103
+ return publish(null, failure);
92
104
  }
93
105
  })().finally(() => { if (accountQuotaInflight.get(flightKey) === probe) accountQuotaInflight.delete(flightKey); });
94
106
  accountQuotaInflight.set(flightKey, probe);
@@ -8,6 +8,7 @@ import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TI
8
8
  import { report, type ProviderQuotaReport } from "./report-cache";
9
9
  import { quotaCredentialIdentity } from "./account-cache";
10
10
  import type { ProviderQuota, ProviderQuotaWindow, QuotaFailureCode } from "../quota-types";
11
+ import { httpQuotaFailure } from "./failure-codes";
11
12
 
12
13
  export function antigravityQuotaDiagnosticIdentity(accountId: string, credential = getAccountCredential("google-antigravity", accountId)): string | undefined {
13
14
  return credential ? quotaCredentialIdentity("google-antigravity", accountId, credential, {
@@ -275,13 +276,6 @@ function quotaTransportFailure(error: unknown): QuotaFailureCode {
275
276
  return "transport_error";
276
277
  }
277
278
 
278
- function quotaHttpFailure(status: number): QuotaFailureCode {
279
- if (status >= 300 && status < 400) return "redirect_blocked";
280
- if (status === 401 || status === 403) return "access_denied";
281
- if (status === 429) return "rate_limited";
282
- return "upstream_error";
283
- }
284
-
285
279
  function unavailableAntigravityQuota(failure: QuotaFailureCode): AntigravityQuotaProbeResult {
286
280
  return { kind: "unavailable", failure, legacy: { kind: "null" } };
287
281
  }
@@ -306,13 +300,16 @@ function antigravityUnavailableFailure(
306
300
  return fallbackFailure;
307
301
  }
308
302
 
309
- export async function probeAntigravityUsageQuota(accessToken: string, projectId: string): Promise<AntigravityQuotaProbeResult> {
303
+ export async function probeAntigravityUsageQuota(accessToken: string, projectId: string, signal?: AbortSignal): Promise<AntigravityQuotaProbeResult> {
304
+ // An optional caller signal (account import) cancels every request of this probe, including the
305
+ // 403 retry and the models fallback, alongside the per-request deadline.
306
+ const requestSignal = () => signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS);
310
307
  const fetchQuota = (url: string, userAgent = antigravityUserAgent()) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
311
308
  headers: {
312
309
  Accept: "application/json", "Content-Type": "application/json",
313
310
  "User-Agent": userAgent, Authorization: `Bearer ${accessToken}`,
314
311
  },
315
- body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
312
+ body: JSON.stringify({ project: projectId }), signal: requestSignal(),
316
313
  }, antigravityOutboundDependencies);
317
314
  let summaryFailure: QuotaFailureCode | undefined;
318
315
  try {
@@ -332,13 +329,14 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId:
332
329
  // Existing behavior: summary transport/parse failure may recover through the models probe.
333
330
  summaryFailure = quotaTransportFailure(error);
334
331
  }
332
+ if (signal?.aborted) return unavailableAntigravityQuota(summaryFailure ?? "transport_error");
335
333
  try {
336
334
  const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL);
337
335
  if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) {
338
336
  return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "redirect_blocked"));
339
337
  }
340
338
  if (!response.ok) {
341
- return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, quotaHttpFailure(response.status)));
339
+ return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, httpQuotaFailure(response.status)));
342
340
  }
343
341
  const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
344
342
  if (!customWindows.length) {
@@ -0,0 +1,18 @@
1
+ import type { QuotaFailureCode } from "../quota-types";
2
+
3
+ /**
4
+ * Closed quota-probe diagnoses derived from an HTTP status class or a thrown dispatch. They carry
5
+ * no upstream text, URL, header or credential: only one of `QUOTA_FAILURE_CODES`.
6
+ */
7
+ export function httpQuotaFailure(status: number): QuotaFailureCode {
8
+ if (status >= 300 && status < 400) return "redirect_blocked";
9
+ if (status === 401 || status === 403) return "access_denied";
10
+ if (status === 429) return "rate_limited";
11
+ return "upstream_error";
12
+ }
13
+
14
+ /** A thrown fetch: the request deadline is a timeout, anything else a transport failure. */
15
+ export function thrownQuotaFailure(error: unknown): QuotaFailureCode {
16
+ const name = error && typeof error === "object" ? (error as { name?: unknown }).name : undefined;
17
+ return name === "TimeoutError" ? "timeout" : "transport_error";
18
+ }
@@ -8,7 +8,7 @@ import { captureOAuthAccountSelection, credentialGeneration, getAccountCredentia
8
8
  import { hydrateKiroAccountState, persistKiroAccountState } from "../kiro-account-state-disk";
9
9
  import { kiroProbeCurrent, kiroProbeIdentity } from "./kiro-account-probe";
10
10
  import { fetchMuseKeyQuotaSnapshot } from "../muse-key-quota";
11
- import { readAnthropicUsageQuota } from "../claude-cli-usage";
11
+ import { probeAnthropicUsageQuota, type AnthropicUsageProbe } from "../claude-cli-usage";
12
12
  import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "../xai-transport";
13
13
  import {
14
14
  commitKiroAccountUsageState,
@@ -227,7 +227,7 @@ export async function fetchXaiQuota(provider: string, context: { accessToken: st
227
227
  }
228
228
  }
229
229
 
230
- const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
230
+ const anthropicUsageInflight = new Map<string, Promise<AnthropicUsageProbe>>();
231
231
 
232
232
  /**
233
233
  * Anthropic per-credential usage.
@@ -256,6 +256,15 @@ export async function fetchAnthropicUsageQuotaForInstance(
256
256
  accessToken: string,
257
257
  requireFreshDispatch = false,
258
258
  ): Promise<ProviderQuota | null> {
259
+ return (await fetchAnthropicUsageProbeForInstance(instance, accessToken, requireFreshDispatch)).quota;
260
+ }
261
+
262
+ /** The same shared flight as `fetchAnthropicUsageQuotaForInstance`, keeping the closed failure reason. */
263
+ export async function fetchAnthropicUsageProbeForInstance(
264
+ instance: AnthropicInstanceId,
265
+ accessToken: string,
266
+ requireFreshDispatch = false,
267
+ ): Promise<AnthropicUsageProbe> {
259
268
  // Recovery evidence must be requested after the claimed cooldown. Joining an older request
260
269
  // can return after the 429 while still describing provider state from before that refusal.
261
270
  const credentialIdentity = getAccountSet(instance)?.accounts.filter(row => row.credential.access === accessToken)
@@ -264,7 +273,7 @@ export async function fetchAnthropicUsageQuotaForInstance(
264
273
  const joinable = requireFreshDispatch ? undefined : anthropicUsageInflight.get(key);
265
274
  if (joinable) return joinable;
266
275
 
267
- const probe = readAnthropicUsageQuota(accessToken).finally(() => {
276
+ const probe = probeAnthropicUsageQuota(accessToken).finally(() => {
268
277
  if (anthropicUsageInflight.get(key) === probe) anthropicUsageInflight.delete(key);
269
278
  });
270
279
  anthropicUsageInflight.set(key, probe);
@@ -29,7 +29,7 @@ export function providerQuotaRoutingBinding(
29
29
  // A key probe must not constrain that later account selection.
30
30
  const entry = getProviderRegistryEntry(name);
31
31
  if (entry && (entry.authKind === "oauth" || entry.authKind === "forward")
32
- && !providerUsesKeyAuthOverride(entry, provider, credential)) return null;
32
+ && !providerUsesKeyAuthOverride(entry, provider)) return null;
33
33
  // Static auth headers can replace or combine with the probed API-key header.
34
34
  // Its semantics belong to the adapter, so it is not provider-wide quota evidence.
35
35
  if (Object.keys(provider.headers ?? {}).some(header =>
@@ -1,6 +1,7 @@
1
1
  import { listCodexAuthAccountsSnapshot } from "../codex/auth-api";
2
2
  import { isAnthropicInstanceId, configuredAnthropicInstance } from "./anthropic-instance";
3
3
  import { fetchAnthropicAccountQuota } from "./quota/anthropic-account-quota";
4
+ import { thrownQuotaFailure } from "./quota/failure-codes";
4
5
  import { resolveEnvValue } from "../config";
5
6
  import { getAccountCredential, getAccountSet } from "../oauth/store";
6
7
  import { apiKeyPoolEntryId } from "./api-keys";
@@ -515,7 +516,10 @@ async function fetchAccountQuota(
515
516
  }
516
517
  return entry;
517
518
  } catch (error) {
518
- if (provider === "google-antigravity") quotaFailure = "account_unavailable";
519
+ // The probe itself reports HTTP and transport failures as results; what throws here is the
520
+ // credential side (missing/paused account, failed refresh, no project). A refresh that hit
521
+ // its deadline is a timeout to retry, not an account the operator must re-sign.
522
+ if (provider === "google-antigravity") quotaFailure = thrownQuotaFailure(error) === "timeout" ? "timeout" : "account_unavailable";
519
523
  return unavailable();
520
524
  }
521
525
  })().finally(() => {
@@ -1020,7 +1020,12 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1020
1020
  },
1021
1021
  },
1022
1022
  // FREEZE 2026-07-10: codestral-latest is unconfirmed behind auth. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
1023
- { id: "mistral", label: "Mistral", baseUrl: "https://api.mistral.ai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://console.mistral.ai/api-keys", defaultModel: "codestral-latest" },
1023
+ {
1024
+ id: "mistral", label: "Mistral", baseUrl: "https://api.mistral.ai/v1", adapter: "openai-chat", authKind: "key",
1025
+ dashboardUrl: "https://console.mistral.ai/api-keys", defaultModel: "codestral-latest", promptCacheKey: true,
1026
+ // Native text-only input enrolls this model in the existing vision sidecar.
1027
+ modelInputModalities: { "zai-glm-5-3": ["text"] },
1028
+ },
1024
1029
  {
1025
1030
  id: "minimax", label: "MiniMax — Coding Plan", baseUrl: "https://api.minimax.io/v1", adapter: "openai-chat", authKind: "key",
1026
1031
  dashboardUrl: "https://platform.minimax.io", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
@@ -134,7 +134,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
134
134
  const repairLegacyMimoFreeAuth = providerName === "mimo-free"
135
135
  && staticModelCatalog
136
136
  && (provider.authMode === undefined || provider.authMode === "local");
137
- const explicitKeyOverride = providerUsesKeyAuthOverride(registryEntry, provider, resolvedApiKey);
137
+ const explicitKeyOverride = providerUsesKeyAuthOverride(registryEntry, provider);
138
138
  const canonicalAuthMode = explicitKeyOverride
139
139
  ? "key"
140
140
  : repairLegacyMimoFreeAuth
@@ -3,6 +3,7 @@ import type { OcxProviderConfig } from "../types";
3
3
  import { isEgressTransparentExecutor, markEgressTransparentExecutor } from "../lib/provider-egress";
4
4
  import { configuredOutboundFetch } from "../lib/proxy-env";
5
5
  import { resolveGithubCopilotTransport } from "./github-copilot-transport";
6
+ import { resolveMistralTransport } from "./mistral-transport";
6
7
 
7
8
  export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
8
9
 
@@ -121,7 +122,7 @@ export function resolveProviderTransport(
121
122
  if (providerName === "github-copilot") {
122
123
  return resolveGithubCopilotTransport(provider, apiBaseUrl);
123
124
  }
124
- if (providerName !== "xai") return provider;
125
+ if (providerName !== "xai") return resolveMistralTransport(provider);
125
126
 
126
127
  const cacheKey = promptCacheKey?.trim();
127
128
  const affinity = cacheKey ? deriveXaiConvId(cacheKey) : undefined;