mixdog 1.2.1 → 1.2.2

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 (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -5,6 +5,7 @@ import {
5
5
  PROVIDER_RETRY_JITTER_RATIO,
6
6
  PROVIDER_RETRY_MAX_ATTEMPTS,
7
7
  } from '../stall-policy.mjs';
8
+ import { providerErrorKind } from './provider-error-kind.mjs';
8
9
 
9
10
  /** Anthropic request budget: 10 retries (11 attempts).
10
11
  * CLAUDE_CODE_MAX_RETRIES is intentionally read per request for reload/tests.
@@ -32,17 +33,23 @@ export function anthropicRequestTimeoutMs() {
32
33
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 600_000;
33
34
  }
34
35
 
36
+ // Consecutive overloads after which Anthropic switches to its fallback model
37
+ // (passed to withRetry as `overloadFallbackAfter`).
35
38
  export const ANTHROPIC_MAX_CONSECUTIVE_529 = 3;
36
39
 
37
- export class AnthropicFallbackTriggeredError extends Error {
40
+ /** Thrown by withRetry when `overloadFallbackAfter` consecutive overloads hand the send to `fallbackModel`. */
41
+ export class ModelFallbackTriggeredError extends Error {
38
42
  constructor(originalModel, fallbackModel, cause) {
39
- super(`Anthropic model fallback triggered: ${originalModel} -> ${fallbackModel}`, { cause });
40
- this.name = 'AnthropicFallbackTriggeredError';
43
+ super(`Model fallback triggered: ${originalModel} -> ${fallbackModel}`, { cause });
44
+ this.name = 'ModelFallbackTriggeredError';
41
45
  this.originalModel = originalModel;
42
46
  this.fallbackModel = fallbackModel;
43
47
  }
44
48
  }
45
49
 
50
+ // Existing provider imports keep working: the same class under its old name.
51
+ export const AnthropicFallbackTriggeredError = ModelFallbackTriggeredError;
52
+
46
53
  // Default backoff schedule used by withRetry when caller does not override.
47
54
  // Mirrors anthropic-oauth's 5-attempt curve (immediate + 1s/2s/4s/8s) so the
48
55
  // total cap stays under 15s. Total upper bound = sum = 15s.
@@ -51,9 +58,36 @@ export const DEFAULT_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
51
58
 
52
59
  const MIDSTREAM_BACKOFF_MS = [250, 1000, 2000, 4000];
53
60
 
54
- export function midstreamBackoffFor(retryNumber, schedule = MIDSTREAM_BACKOFF_MS) {
55
- const raw = schedule[Math.min(Math.max(retryNumber, 1), schedule.length) - 1];
56
- return jitterDelayMs(raw);
61
+ const ADVICE_RETRYABLE_KINDS = new Set(['overloaded', 'rate_limited', 'transient']);
62
+
63
+ /**
64
+ * The wait before a mid-stream re-issue: the server's advice (Retry-After /
65
+ * retryAfterMs) in full for a retryable kind — never capped below its value —
66
+ * else the caller's bounded backoff step. Advice never makes a terminal kind
67
+ * retryable; that is the classifier's verdict, not this wait's. The retry
68
+ * COUNT stays the caller's budget; the sleep stays the caller's abortable one.
69
+ */
70
+ export function midstreamRetryDelayMs(err, backoffMs) {
71
+ const { kind, retryAfterMs } = providerErrorKind(err);
72
+ if (retryAfterMs != null && ADVICE_RETRYABLE_KINDS.has(kind)) return Math.max(0, retryAfterMs);
73
+ return Math.max(0, Number(backoffMs) || 0);
74
+ }
75
+
76
+ /** The mid-stream wait for `retryNumber`; pass the failure as `err` so server
77
+ * advice on a retryable kind replaces the jittered step (midstreamRetryDelayMs). */
78
+ // `schedule` / `jitterRatio` default to the shared values; a provider passes
79
+ // its own (e.g. openai-retry-policy.mjs).
80
+ export function midstreamBackoffFor(
81
+ retryNumber,
82
+ schedule = MIDSTREAM_BACKOFF_MS,
83
+ err = null,
84
+ jitterRatio,
85
+ jitterMode = 'symmetric'
86
+ ) {
87
+ const steps = schedule || MIDSTREAM_BACKOFF_MS;
88
+ const raw = steps[Math.min(Math.max(retryNumber, 1), steps.length) - 1];
89
+ const step = jitterDelayMs(raw, jitterRatio ?? PROVIDER_RETRY_JITTER_RATIO, jitterMode);
90
+ return err ? midstreamRetryDelayMs(err, step) : step;
57
91
  }
58
92
 
59
93
  export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 'symmetric') {
@@ -62,7 +96,12 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
62
96
  const r = Math.min(Math.max(Number(ratio) || 0, 0), 1);
63
97
  if (!r) return Math.round(base);
64
98
  const spread = base * r;
65
- const offset = mode === 'positive' ? Math.random() * spread : (Math.random() * 2 - 1) * spread;
99
+ // 'positive': upward only; 'negative': downward only, base × (1 − r·rand);
100
+ // otherwise symmetric ±r.
101
+ let offset;
102
+ if (mode === 'positive') offset = Math.random() * spread;
103
+ else if (mode === 'negative') offset = -Math.random() * spread;
104
+ else offset = (Math.random() * 2 - 1) * spread;
66
105
  return Math.max(0, Math.round(base + offset));
67
106
  }
68
107
 
@@ -1,6 +1,30 @@
1
1
  // Structural error classification: typed status, transience, auth, context
2
2
  // overflow, retry-after and stream-close verdicts shared by every provider.
3
3
  import { readStreamOutcome } from './lib/stream-outcome.mjs';
4
+ import {
5
+ BARE_FETCH_TRANSPORT_MESSAGE_RE,
6
+ DETERMINISTIC_KINDS,
7
+ QUOTA_KINDS,
8
+ REQUEST_SIZE_PATTERNS,
9
+ TLS_RECORD_FAILURE_RE,
10
+ TOKEN_CONTEXT_OVERFLOW_PATTERNS,
11
+ TRANSIENT_SDK_NAMES,
12
+ boundedCauseChain,
13
+ isTransientErrorCode,
14
+ providerErrorKind,
15
+ typedCodeKind,
16
+ typedErrorCode,
17
+ } from './provider-error-kind.mjs';
18
+
19
+ export {
20
+ boundedCauseChain,
21
+ errorHeaderValue,
22
+ headerValue,
23
+ isTransientErrorCode,
24
+ providerErrorKind,
25
+ retryAfterMsFromError,
26
+ typedErrorCode,
27
+ } from './provider-error-kind.mjs';
4
28
 
5
29
  // HTTP statuses considered transient — safe to retry with backoff.
6
30
  // 408 — request timeout
@@ -15,22 +39,18 @@ export const TRANSIENT_STATUSES = new Set([408, 409, 425]);
15
39
  // 400/422 — bad request (deterministic)
16
40
  const AUTH_STATUSES = new Set([401, 403]);
17
41
  const PERMANENT_STATUSES = new Set([400, 404, 405, 410, 415, 422]);
18
- // Cloudflare origin-TLS pages never clear on retry (grok-build edge_client).
42
+ // Cloudflare origin-TLS pages never clear on retry.
19
43
  export const TERMINAL_EDGE_STATUSES = new Set([525, 526]);
20
44
  const GEMINI_TRANSIENT_RPC_CODES = new Set(['UNAVAILABLE', 'DEADLINE_EXCEEDED', 'ABORTED', 'INTERNAL']);
21
45
  const PREVIOUS_RESPONSE_NOT_FOUND = 'previous_response_not_found';
22
46
  export const WEBSOCKET_CONNECTION_LIMIT = 'websocket_connection_limit_reached';
23
- const RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded';
24
- const CONTEXT_OVERFLOW_CODES = new Set(['context_length_exceeded', 'context_window_exceeded', 'request_too_large']);
25
- const TRANSIENT_SDK_NAMES = new Set([
26
- 'APIConnectionError',
27
- 'APIConnectionTimeoutError',
28
- 'ConnectTimeoutError',
29
- 'HeadersTimeoutError',
30
- 'BodyTimeoutError',
47
+ // Token-context codes only: a byte-size `request_too_large` is not cured by
48
+ // compaction (see isContextOverflowError).
49
+ const CONTEXT_OVERFLOW_CODES = new Set([
50
+ 'context_length_exceeded',
51
+ 'context_window_exceeded',
52
+ 'model_context_window_exceeded',
31
53
  ]);
32
- const ANTHROPIC_RESET_HEADER = 'anthropic-ratelimit-unified-reset';
33
- const ANTHROPIC_RESET_CAP_MS = 300_000;
34
54
 
35
55
  // Structured status fields a provider error / wire event may carry. A value is
36
56
  // accepted only when it is a real numeric HTTP status; string codes
@@ -59,8 +79,11 @@ const statusOfError = (err) => Number(err?.httpStatus || err?.status || err?.res
59
79
 
60
80
  // The verdict a typed HTTP status (or a typed stream shape) gives, in
61
81
  // precedence order; null when the status decides nothing.
62
- function classifyByStatus(err, status) {
82
+ function classifyByStatus(err, status, options = {}) {
63
83
  if (AUTH_STATUSES.has(status)) return 'auth';
84
+ // Opt-in: 499 "client closed request" from Google
85
+ // frontends is retried; others keep it a deterministic 4xx.
86
+ if (status === 499 && options.retry499 === true) return 'transient';
64
87
  // Stale previous_response_id is recoverable by dropping the chain and
65
88
  // re-issuing a full frame, which is the retryable path. A typed
66
89
  // 400 here is not a deterministic payload refusal.
@@ -90,7 +113,7 @@ function classifyByStatus(err, status) {
90
113
  * 'unknown' — neither; default to permanent in safety-critical paths,
91
114
  * or retry once in best-effort paths.
92
115
  */
93
- export function classifyError(err) {
116
+ export function classifyError(err, options = {}) {
94
117
  if (!err) return 'unknown';
95
118
  // Canonical stream-outcome contract owns replay safety: exposed
96
119
  // text/reasoning or a dispatched/ambiguous complete tool call makes
@@ -105,14 +128,34 @@ export function classifyError(err) {
105
128
  // across the bounded chain before considering stale connection causes.
106
129
  const chain = boundedCauseChain(err);
107
130
  if (chain.some(isExplicitUserAbortError)) return 'permanent';
108
- // Current typed HTTP status outranks stale stream/connection annotations.
131
+ // A typed provider code outranks the HTTP status (429+server_is_overloaded
132
+ // is an overload, 403+cyber_policy a policy refusal, 5xx+insufficient_quota
133
+ // a billing refusal).
134
+ // Typed evidence only: message wording never synthesizes a verdict here.
135
+ const { kind } = providerErrorKind(err, { text: false });
109
136
  const status = statusOfError(err);
110
- return classifyByStatus(err, status) || classifyByTransport(err, chain, status) || 'unknown';
137
+ // Opt-in: a status-only 499 "client closed request" is retried instead of
138
+ // read as a 4xx; a typed code on it decides as usual.
139
+ if (status === 499 && options.retry499 === true && kind === 'invalid_request' && !typedCodeKind(err)) {
140
+ return 'transient';
141
+ }
142
+ if (kind === 'auth' || kind === 'permission') return 'auth';
143
+ if (DETERMINISTIC_KINDS.has(kind)) return 'permanent';
144
+ // An origin-TLS edge failure is final whatever code accompanies it.
145
+ if (TERMINAL_EDGE_STATUSES.has(status)) return 'permanent';
146
+ // A deterministic wire code outranks a numeric 5xx on the same event.
147
+ if (classifyWireErrorEvent(err) === 'permanent') return 'permanent';
148
+ if (kind === 'overloaded') return 'transient';
149
+ // A typed transient kind decides whatever status accompanies it (401 +
150
+ // server_error is a server fault).
151
+ if (kind === 'transient') return 'transient';
152
+ // Current typed HTTP status outranks stale stream/connection annotations.
153
+ return classifyByStatus(err, status, options) || classifyByTransport(err, chain, status) || 'unknown';
111
154
  }
112
155
 
113
156
  // A turn that ended on a malformed or unexpected tool call produced no usable
114
- // call at all (the typed finish reason says so); the reference client re-asks
115
- // the same request, bounded, instead of failing the turn.
157
+ // call at all (the typed finish reason says so); the same request is re-asked,
158
+ // bounded, instead of failing the turn.
116
159
  const INVALID_TOOL_CALL_FINISH_REASONS = new Set(['MALFORMED_FUNCTION_CALL', 'UNEXPECTED_TOOL_CALL']);
117
160
 
118
161
  function isInvalidToolCallFinish(err) {
@@ -132,9 +175,9 @@ function classifyByTransport(err, chain, status) {
132
175
  // so without this it classified as 'unknown' and no loop-level replay was
133
176
  // ever attempted (observed live: pooled WS retired by the server between
134
177
  // turns → close 1000 before response.created → the whole turn failed).
135
- // Reference behavior retries the same disconnect (codex `CodexErr::Stream`
136
- // is_retryable, and a stream fallback/retry applies); the exposure deny above
137
- // still fails closed for anything already relayed or dispatched.
178
+ // The same disconnect is retryable (a stream fallback/retry applies); the
179
+ // exposure deny above still fails closed for anything already relayed or
180
+ // dispatched.
138
181
  if (isNonTerminalStreamClose(err)) return 'transient';
139
182
 
140
183
  // Cursor's resumed tool-result stream cannot safely reopen the same wire
@@ -172,58 +215,6 @@ function classifyByTransport(err, chain, status) {
172
215
  return classifyWireErrorEvent(err) || null;
173
216
  }
174
217
 
175
- const MAX_CAUSE_CHAIN_DEPTH = 8;
176
- // 'terminated' / 'other side closed' / 'socket hang up' are the body-read
177
- // forms of the same disconnect (undici aborts the response body when the peer
178
- // closes mid-stream and may not attach a cause).
179
- const BARE_FETCH_TRANSPORT_MESSAGE_RE =
180
- /^(?:fetch failed|failed to fetch|couldn'?t fetch\.?|load failed|network error|terminated|other side closed|socket hang up)$/i;
181
- const TRANSIENT_ERROR_CODES = new Set([
182
- 'ECONNRESET',
183
- 'ETIMEDOUT',
184
- 'ESOCKETTIMEDOUT',
185
- 'EAI_AGAIN',
186
- 'ENOTFOUND',
187
- 'EAI_NODATA',
188
- 'ECONNREFUSED',
189
- 'ENETUNREACH',
190
- 'EHOSTUNREACH',
191
- 'EPIPE',
192
- 'EPROVIDERTIMEOUT',
193
- 'EGEMINITIMEOUT',
194
- 'ESTREAMSTALL',
195
- 'EWSACQUIRETIMEOUT',
196
- 'UND_ERR_SOCKET',
197
- 'UND_ERR_CONNECT_TIMEOUT',
198
- 'UND_ERR_HEADERS_TIMEOUT',
199
- 'UND_ERR_BODY_TIMEOUT',
200
- 'UND_ERR_CONNECT',
201
- 'EPROTO',
202
- 'UND_ERR_DESTROYED',
203
- 'UND_ERR_CLOSED',
204
- 'ECONNABORTED',
205
- 'ENETRESET',
206
- 'ERR_STREAM_DESTROYED',
207
- 'ERR_HTTP2_STREAM_ERROR',
208
- 'ERR_HTTP2_SESSION_ERROR',
209
- 'ERR_HTTP2_INVALID_SESSION',
210
- 'ERR_SOCKET_CONNECTION_TIMEOUT',
211
- 'ERR_SSL_WRONG_VERSION_NUMBER',
212
- 'ERR_STREAM_PREMATURE_CLOSE',
213
- ]);
214
-
215
- // TLS record corruption on a flaky link or a meddling proxy. Node builds the
216
- // ERR_SSL_* code from the OpenSSL reason string, which differs across OpenSSL
217
- // versions (…SSLV3_ALERT_BAD_RECORD_MAC, …SSL/TLS_ALERT_BAD_RECORD_MAC,
218
- // …DECRYPTION_FAILED_OR_BAD_RECORD_MAC), so the stable suffix names the class.
219
- const TLS_RECORD_FAILURE_RE = /^ERR_SSL_[A-Z0-9_/]*BAD_RECORD_MAC$/i;
220
-
221
- /** True for a socket/TLS errno that describes a transient transport fault. */
222
- export function isTransientErrorCode(code) {
223
- const value = String(code || '');
224
- return TRANSIENT_ERROR_CODES.has(value) || TLS_RECORD_FAILURE_RE.test(value);
225
- }
226
-
227
218
  // Cursor's HTTP/2 bridge preserves provider-specific codes in `cursorCode`.
228
219
  // These codes all describe an incomplete transport turn, not a model verdict.
229
220
  // Replay permission remains fail-closed in the stream-outcome gate above.
@@ -342,9 +333,8 @@ export function isToolInputCut(err) {
342
333
  // The provider was never reached: name resolution, routing, refusal or the
343
334
  // connect handshake failed, so nothing was sent or received. Only this class
344
335
  // is worth waiting out for minutes — the identical request succeeds once the
345
- // network is back (a lift, a sleeping laptop, a router reboot), and the
346
- // reference client keeps a sampling turn alive until such a connection
347
- // recovers. A stream that dropped mid-response is not this class: it keeps
336
+ // network is back (a lift, a sleeping laptop, a router reboot), so a sampling
337
+ // turn is kept alive until such a connection recovers. A stream that dropped mid-response is not this class: it keeps
348
338
  // the short, bounded retry budget.
349
339
  const CONNECT_FAILURE_CODES = new Set([
350
340
  'ENOTFOUND',
@@ -382,9 +372,14 @@ export function isServerUnavailable(err) {
382
372
  if (!err || (typeof err !== 'object' && typeof err !== 'function')) return false;
383
373
  const chain = boundedCauseChain(err);
384
374
  if (chain.some(isExplicitUserAbortError)) return false;
375
+ // Typed kind first: an overload (any status) is this class; quota, billing,
376
+ // policy, auth, request refusals and rate limits never are.
377
+ const { kind } = providerErrorKind(err);
378
+ if (kind === 'overloaded') return true;
379
+ if (kind !== 'transient' && kind !== 'unknown') return false;
385
380
  const status = statusOfError(err);
386
381
  if (status) return status === 408 || (status >= 500 && status < 600 && !TERMINAL_EDGE_STATUSES.has(status));
387
- if (isRetryableWireErrorEvent(err)) return typedErrorCode(err) !== RATE_LIMIT_EXCEEDED;
382
+ if (isRetryableWireErrorEvent(err)) return true;
388
383
  return chain.some((item) => {
389
384
  const rpc = [item?.geminiStatus, item?.error?.status, typeof item?.status === 'string' ? item.status : ''];
390
385
  if (rpc.some((field) => GEMINI_TRANSIENT_RPC_CODES.has(String(field || '').toUpperCase()))) return true;
@@ -414,21 +409,11 @@ export function canFallbackNonStreaming(err, { signal } = {}) {
414
409
  // A cut mid tool-argument stream is replayed by the loop with a split-call
415
410
  // notice, never re-issued unchanged here.
416
411
  if (isToolInputCut(err)) return false;
412
+ // A provider-terminal verdict ends recovery; no re-issue of any shape.
413
+ if (boundedCauseChain(err).some((item) => item?.providerRecoveryTerminal === true)) return false;
417
414
  return classifyError(err) === 'transient' || outcome.stallObserved === true || outcome.truncatedStream === true;
418
415
  }
419
416
 
420
- export function boundedCauseChain(err) {
421
- const chain = [];
422
- const seen = new Set();
423
- let cursor = err;
424
- while (cursor && chain.length < MAX_CAUSE_CHAIN_DEPTH && !seen.has(cursor)) {
425
- chain.push(cursor);
426
- seen.add(cursor);
427
- cursor = cursor?.cause;
428
- }
429
- return chain;
430
- }
431
-
432
417
  export function isExplicitUserAbortError(err) {
433
418
  if (!err || (typeof err !== 'object' && typeof err !== 'function')) return false;
434
419
  if (err.name === 'AbortError' || err.name === 'APIUserAbortError' || err.code === 'ABORT_ERR') return true;
@@ -462,34 +447,10 @@ export function isCursorTransientTransportError(err) {
462
447
  // Patterns cover OpenAI ("maximum context length", "reduce the length"),
463
448
  // Anthropic ("prompt is too long"), and generic "input exceeds the context
464
449
  // window" phrasing. Match is case-insensitive over err.message.
465
- const CONTEXT_OVERFLOW_PATTERNS = [
466
- /input (?:length|tokens?) exceeds? the context window/i,
467
- /exceeds? the (?:maximum )?context (?:window|length)/i,
468
- /maximum context length/i,
469
- /context[_ ]length[_ ]exceeded/i,
470
- /prompt is too long/i,
471
- /reduce the length of (?:the )?(?:messages|input|prompt)/i,
472
- // Wordings other backends use for the same refusal, each tied to a context
473
- // or input-length noun so a tokens-per-minute rate limit never matches.
474
- /prompt too long/i, // z.ai, Ollama
475
- /prompt exceeds max length/i, // z.ai CN endpoint
476
- /input is too long for requested model/i, // Amazon Bedrock
477
- /input token count.*exceeds the maximum/i, // Google
478
- /request payload size exceeds the limit/i, // Google (400, byte-size limit)
479
- // A 413 re-wrapped by a proxy or aggregator without its status.
480
- /request entity too large/i,
481
- /request exceeds the maximum size/i,
482
- /maximum prompt length is \d+/i, // xAI
483
- /exceeds (?:the )?maximum allowed input length/i, // OpenRouter relays
484
- /is longer than the model'?s context length/i, // Together AI
485
- /exceeds the available context size/i, // llama.cpp server
486
- /greater than the context length/i, // LM Studio
487
- /context window exceeds limit/i, // MiniMax
488
- /exceeded model token limit/i, // Kimi
489
- /too large for model with \d+ maximum context length/i, // Mistral
490
- /range of input length should be/i, // DashScope / Qwen
491
- /model_context_window_exceeded/i, // z.ai finish reason surfaced as text
492
- ];
450
+ // Untyped byte-size wordings (no status, no code) keep routing here: an
451
+ // unlabelled proxy refusal is most often an oversized history.
452
+ const CONTEXT_OVERFLOW_PATTERNS = [...TOKEN_CONTEXT_OVERFLOW_PATTERNS, ...REQUEST_SIZE_PATTERNS];
453
+ const REQUEST_TOO_LARGE_KIND = 'request_too_large';
493
454
 
494
455
  /**
495
456
  * True when `err` is a context-window-exceeded provider rejection. Walks
@@ -502,89 +463,33 @@ const CONTEXT_OVERFLOW_PATTERNS = [
502
463
  */
503
464
  export function isContextOverflowError(err, _depth = 0) {
504
465
  if (!err || _depth > 2) return false;
505
- if (statusOfError(err) === 413) return true;
506
466
  const code = typedErrorCode(err);
507
467
  if (code && CONTEXT_OVERFLOW_CODES.has(code)) return true;
468
+ // A typed byte/media-size refusal (a size code, or HTTP 413 without a
469
+ // token-context code) is not a token-context overflow: compaction cannot
470
+ // shrink it, so only token-context wording may still qualify it.
471
+ const sizeTyped =
472
+ typeof err === 'object' &&
473
+ boundedCauseChain(err).some(
474
+ (item) => item && typeof item === 'object' && providerErrorKind(item, { text: false }).kind === REQUEST_TOO_LARGE_KIND
475
+ );
476
+ const patterns = sizeTyped ? TOKEN_CONTEXT_OVERFLOW_PATTERNS : CONTEXT_OVERFLOW_PATTERNS;
508
477
  const msg = (typeof err === 'string' ? err : err?.message) || '';
509
- if (msg && CONTEXT_OVERFLOW_PATTERNS.some((re) => re.test(msg))) return true;
478
+ if (msg && patterns.some((re) => re.test(msg))) return true;
510
479
  if (err.cause != null && err.cause !== err) return isContextOverflowError(err.cause, _depth + 1);
511
480
  if (err.response?.data != null) return isContextOverflowError(err.response.data, _depth + 1);
512
481
  return false;
513
482
  }
514
483
 
515
- export function headerValue(headers, name) {
516
- if (!headers) return null;
517
- const lower = name.toLowerCase();
518
- if (typeof headers.get === 'function') return headers.get(name) ?? headers.get(lower);
519
- for (const [key, value] of Object.entries(headers)) {
520
- if (String(key).toLowerCase() === lower) return Array.isArray(value) ? value[0] : value;
521
- }
522
- return null;
523
- }
524
-
525
- export function retryAfterMsFromError(err) {
526
- const headers = err?.headers || err?.response?.headers || err?.data?.responseHeaders || null;
527
- const retryAfterMs = headerValue(headers, 'retry-after-ms');
528
- if (retryAfterMs != null && retryAfterMs !== '') {
529
- const n = Number(retryAfterMs);
530
- if (Number.isFinite(n) && n >= 0) return n;
531
- }
532
- const retryAfter = headerValue(headers, 'retry-after');
533
- if (retryAfter != null && retryAfter !== '') {
534
- const seconds = Number(retryAfter);
535
- if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1000);
536
- const dateMs = Date.parse(String(retryAfter));
537
- if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
538
- }
539
- // Google RPC RetryInfo encodes retryDelay as a protobuf Duration. Preserve
540
- // the same precedence as Retry-After: a server-provided retry window means
541
- // RESOURCE_EXHAUSTED is request-local, not a permanent quota refusal.
542
- const detailLists = [err?.details, err?.error?.details, err?.data?.error?.details];
543
- for (const details of detailLists) {
544
- if (!Array.isArray(details)) continue;
545
- for (const detail of details) {
546
- const delay = detail?.retryDelay;
547
- if (typeof delay === 'string') {
548
- const match = delay.trim().match(/^(\d+(?:\.\d+)?)s$/);
549
- if (match) return Math.ceil(Number(match[1]) * 1000);
550
- } else if (delay && typeof delay === 'object') {
551
- const seconds = Number(delay.seconds || 0);
552
- const nanos = Number(delay.nanos || 0);
553
- const ms = seconds * 1000 + nanos / 1_000_000;
554
- if (Number.isFinite(ms) && ms >= 0) return Math.ceil(ms);
555
- }
556
- }
557
- }
558
- const resetHeader = headerValue(headers, ANTHROPIC_RESET_HEADER);
559
- if (resetHeader != null && resetHeader !== '') {
560
- const resetUnixSec = Number(resetHeader);
561
- if (Number.isFinite(resetUnixSec) && resetUnixSec > 0) {
562
- const delayMs = Math.ceil(resetUnixSec * 1000 - Date.now());
563
- if (delayMs > 0) return Math.min(delayMs, ANTHROPIC_RESET_CAP_MS);
564
- }
565
- }
566
- // Codex parses the server-supplied window from a TYPED rate_limit_exceeded
567
- // payload. Message text never decides whether to retry — only how long to
568
- // wait after the typed code is already in hand. Headers / RetryInfo win.
569
- if (typedErrorCode(err) === RATE_LIMIT_EXCEEDED) {
570
- const delay = rateLimitRetryAfterMsFromMessage(err);
571
- if (delay != null) return delay;
572
- }
573
- return null;
574
- }
575
-
484
+ /**
485
+ * True for an explicit quota / plan / billing refusal (providerErrorKind
486
+ * quota_exhausted, entitlement or billing). No wait recovers it, so retry
487
+ * layers never replay it, even when the server attached a Retry-After.
488
+ * RESOURCE_EXHAUSTED with a 429, retry advice or a per-minute scope is a
489
+ * request-local rate limit instead.
490
+ */
576
491
  export function isPermanentQuotaError(err) {
577
- const status = statusOfError(err);
578
- // Gemini uses RESOURCE_EXHAUSTED for both daily quota and per-minute
579
- // rate limits. A 429 is request-local (Google/LiteLLM retry); without a
580
- // 429 the same code stays a deterministic quota refusal.
581
- const permanentCodes = new Set(['insufficient_quota', 'quota_exceeded']);
582
- if (status !== 429) permanentCodes.add('resource_exhausted');
583
- for (const item of boundedCauseChain(err)) {
584
- const codes = [item?.code, item?.error?.code];
585
- if (codes.some((code) => permanentCodes.has(String(code || '').toLowerCase()))) return true;
586
- }
587
- return false;
492
+ return QUOTA_KINDS.has(providerErrorKind(err).kind);
588
493
  }
589
494
 
590
495
  // ── Wire error events: default-retry with a fatal-code deny-list ────────────
@@ -595,7 +500,7 @@ export function isPermanentQuotaError(err) {
595
500
  // server_error, server_is_overloaded, slow_down, or an event with no code at
596
501
  // all — is a server-side fault and is retried under the bounded budgets
597
502
  // (observed live 2026-08-11: two turns failed on typed `server_error`
598
- // response.failed events that every reference implementation retries).
503
+ // response.failed events that are server-side faults).
599
504
  export const WIRE_ERROR_FATAL_CODES = new Set([
600
505
  'context_length_exceeded',
601
506
  'context_window_exceeded',
@@ -614,17 +519,26 @@ export const WIRE_ERROR_FATAL_CODES = new Set([
614
519
  'permission_error',
615
520
  'permission_denied',
616
521
  'billing_not_active',
522
+ // Billing / spend refusals.
523
+ 'credit_balance_exhausted',
524
+ 'organization_spend_limit_exceeded',
525
+ 'project_spend_limit_exceeded',
526
+ 'organization_usage_limit_exceeded',
527
+ 'billing_hard_limit_reached',
528
+ 'payment_required',
529
+ // Policy refusals.
530
+ 'cyber_policy_violation',
531
+ 'bio_policy_violation',
532
+ 'misalignment_policy_violation',
533
+ 'content_policy_violation',
534
+ 'content_filter',
535
+ // Flex tier capacity: the same request at the same tier is refused again.
536
+ 'flex_unavailable',
537
+ // Byte/media size and token context.
538
+ 'request_too_large',
539
+ 'model_context_window_exceeded',
617
540
  ]);
618
541
 
619
- export function typedErrorCode(err) {
620
- const failed = err?.responseFailed;
621
- const detail = failed?.response?.error || failed?.error || err?.providerError || failed || null;
622
- for (const field of [detail?.code, detail?.type, err?.providerErrorCode, err?.code, err?.error?.code]) {
623
- if (typeof field === 'string' && field.trim()) return field.trim().toLowerCase();
624
- }
625
- return '';
626
- }
627
-
628
542
  /**
629
543
  * True when the typed wire/SDK code is previous_response_not_found.
630
544
  * Callers drop lastResponseId / previous_response_id and re-issue a full frame.
@@ -657,30 +571,15 @@ export function isStaleKeepAliveError(err) {
657
571
  });
658
572
  }
659
573
 
660
- const RATE_LIMIT_RETRY_AFTER_RE = /try again in\s*(\d+(?:\.\d+)?)\s*(ms|s|seconds?)\b/i;
661
-
662
- function rateLimitRetryAfterMsFromMessage(err) {
663
- const failed = err?.responseFailed;
664
- const detail = failed?.response?.error || failed?.error || err?.providerError || err?.error || null;
665
- const messages = [detail?.message, err?.message];
666
- for (const message of messages) {
667
- if (typeof message !== 'string' || !message) continue;
668
- const match = message.match(RATE_LIMIT_RETRY_AFTER_RE);
669
- if (!match) continue;
670
- const value = Number(match[1]);
671
- if (!Number.isFinite(value) || value < 0) continue;
672
- const unit = String(match[2] || '').toLowerCase();
673
- return unit === 'ms' ? Math.ceil(value) : Math.ceil(value * 1000);
674
- }
675
- return null;
676
- }
677
-
678
574
  // 'transient' | 'permanent' for errors born from a provider wire error event;
679
575
  // null for everything else (no blanket retry for untyped local failures).
576
+ // A fatal code or a deterministic kind (quota, billing, plan, policy, auth,
577
+ // permission, context, size) outranks any numeric 5xx the event carries.
680
578
  function classifyWireErrorEvent(err) {
681
579
  if (!err || (err.responseFailed == null && err.providerWireError !== true)) return null;
682
580
  const code = typedErrorCode(err);
683
581
  if (code && WIRE_ERROR_FATAL_CODES.has(code)) return 'permanent';
582
+ if (DETERMINISTIC_KINDS.has(providerErrorKind(err, { text: false }).kind)) return 'permanent';
684
583
  return 'transient';
685
584
  }
686
585
 
@@ -696,7 +595,7 @@ export function isRetryableWireErrorEvent(err) {
696
595
  }
697
596
 
698
597
  /**
699
- * Grok Build StreamError: a mid-stream wire fault is retryable even when the
598
+ * Mid-stream wire error event: a mid-stream wire fault is retryable even when the
700
599
  * envelope type is invalid_request_error, unless a typed 4xx status or a
701
600
  * deterministic refusal code (quota/context/auth) is present.
702
601
  */
@@ -50,6 +50,7 @@ export {
50
50
  ANTHROPIC_RETRY_BACKOFF_MS,
51
51
  ANTHROPIC_RETRY_JITTER_RATIO,
52
52
  AnthropicFallbackTriggeredError,
53
+ ModelFallbackTriggeredError,
53
54
  STREAM_STALL_RETRY_BUDGET_MS,
54
55
  anthropicMaxAttempts,
55
56
  anthropicRequestTimeoutMs,
@@ -71,6 +72,7 @@ export {
71
72
  createStreamSafetyStamps,
72
73
  emitProviderRetryStage,
73
74
  isProviderRecoveryExhausted,
75
+ isProviderRecoveryTerminal,
74
76
  markProviderRecoveryExhausted,
75
77
  retryDelayLabel,
76
78
  withRetry,