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
@@ -1,6 +1,11 @@
1
1
  import { GoogleGenerativeAI } from '@google/generative-ai';
2
2
  import { reloadProviderApiKey, resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
3
- import { canFallbackNonStreaming, emitProviderRetryStage, withRetry } from './retry-classifier.mjs';
3
+ import {
4
+ canFallbackNonStreaming,
5
+ emitProviderRetryStage,
6
+ markProviderRecoveryExhausted,
7
+ withRetry,
8
+ } from './retry-classifier.mjs';
4
9
  import { noteErrorUsage } from './lib/note-error-usage.mjs';
5
10
  import { traceNonStreamingFallback } from './lib/transport-fallback-trace.mjs';
6
11
  import {
@@ -12,6 +17,13 @@ import {
12
17
  } from '../stall-policy.mjs';
13
18
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
14
19
  import { GEMINI_FIRST_BYTE_TIMEOUT_MS, consumeGeminiRestStreamResponse } from './gemini-stream.mjs';
20
+ import { applyGeminiErrorContract } from './gemini-error-contract.mjs';
21
+ import {
22
+ GEMINI_MIDSTREAM_MAX_ATTEMPTS,
23
+ geminiEndsWithFunctionResponse,
24
+ geminiInvalidStreamReason,
25
+ runGeminiStreamAttempts,
26
+ } from './gemini-retry-policy.mjs';
15
27
  import { geminiTextLeakGuardFor, streamGeminiSdkAttempt } from './gemini-sdk-request.mjs';
16
28
  import { emitGeminiToolCalls } from './gemini-schema.mjs';
17
29
  import { buildGeminiRequest, geminiCachedRestBody } from './gemini-request-body.mjs';
@@ -32,6 +44,8 @@ import {
32
44
  GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS,
33
45
  _geminiCredentialFingerprint,
34
46
  _invalidateGeminiCachesForCredentialFingerprint,
47
+ geminiExplicitCacheRefused,
48
+ markGeminiExplicitCacheRefused,
35
49
  } from './gemini-cache.mjs';
36
50
  import {
37
51
  awaitSharedCreate,
@@ -59,7 +73,7 @@ import {
59
73
  let _modelRefreshInFlight = null;
60
74
  const GEMINI_AVAILABILITY_TIMEOUT_MS = 1_000;
61
75
 
62
- function geminiRestError(res, text, label) {
76
+ export function geminiRestError(res, text, label) {
63
77
  let payload = null;
64
78
  try {
65
79
  payload = text ? JSON.parse(text) : null;
@@ -78,16 +92,11 @@ function geminiRestError(res, text, label) {
78
92
  err.error = detail;
79
93
  err.data = payload;
80
94
  if (detail.status) err.geminiStatus = detail.status;
81
- if (Array.isArray(detail.details)) err.details = detail.details;
82
- const retryAfter = res.headers?.get?.('retry-after') ?? res.headers?.get?.('retry-after-ms');
83
- // RESOURCE_EXHAUSTED without a server retry window is deterministic
84
- // quota exhaustion. With Retry-After present, leave it request-local
85
- // so withRetry can honor the mandated delay.
86
- // A 429 RESOURCE_EXHAUSTED is a request-local rate limit even without
87
- // Retry-After (Google/LiteLLM retry). Do not stamp it as a quota code.
88
- if (detail.status && (retryAfter == null || retryAfter === '') && res.status !== 429) {
89
- err.code = detail.status;
90
- }
95
+ const details = detail.details ?? detail.errorDetails;
96
+ if (Array.isArray(details)) err.details = details;
97
+ // Typed contract (code/retryAfterMs/resetAt/quotaScope); the core decides
98
+ // whether a 429 RESOURCE_EXHAUSTED is request-local or a quota refusal.
99
+ applyGeminiErrorContract(err, { details, googleStatus: detail.status, message: detail.message });
91
100
  }
92
101
  return err;
93
102
  }
@@ -139,6 +148,9 @@ export class GeminiProvider {
139
148
  // input already includes cache. See registry.mjs.
140
149
  static inputExcludesCache = false;
141
150
  name = 'gemini';
151
+ // The two-phase ladder (gemini-retry-policy.mjs) is the whole recovery:
152
+ // a spent ladder surfaces instead of parking on loop recovery cycles.
153
+ retryPolicy = Object.freeze({ recoveryCycles: false });
142
154
  genAI;
143
155
  config;
144
156
  _fetch;
@@ -263,7 +275,7 @@ export class GeminiProvider {
263
275
  if (decision.canReuseState) {
264
276
  return state.cacheName;
265
277
  }
266
- if (!apiKey) return null;
278
+ if (!apiKey || geminiExplicitCacheRefused(model)) return null;
267
279
  const { canAttachState } = decision;
268
280
  if (geminiPrefixBelowMinimum({ model, systemInstruction, geminiTools, contents, opts, currentIter })) {
269
281
  return canAttachState ? state.cacheName : null;
@@ -443,6 +455,7 @@ export class GeminiProvider {
443
455
  _setGeminiGlobalCache(globalCacheKey, entry);
444
456
  return entry;
445
457
  } catch (err) {
458
+ markGeminiExplicitCacheRefused(model, err);
446
459
  process.stderr.write(`[gemini] cachedContents.create error: ${err?.message || err}\n`);
447
460
  return null;
448
461
  }
@@ -574,7 +587,18 @@ export class GeminiProvider {
574
587
  emitGeminiToolCalls(parsed.nativeToolCalls, callbacks.onToolCall);
575
588
  const usage = resolveGeminiUsage(response, opts, cachedContent, useModel);
576
589
  const incomplete = geminiIncompleteError(response, parsed, useModel, usage);
577
- if (incomplete) throw incomplete;
590
+ if (incomplete) {
591
+ // A malformed / unexpected tool call that survived the stream ladder
592
+ // (4 attempts) is spent; the loop must not multiply that budget.
593
+ if (
594
+ geminiInvalidStreamReason(response, textLeakGuard, {
595
+ afterToolResult: geminiEndsWithFunctionResponse(request.contents),
596
+ })
597
+ ) {
598
+ markProviderRecoveryExhausted(incomplete, { owner: 'gemini-stream', attempts: GEMINI_MIDSTREAM_MAX_ATTEMPTS });
599
+ }
600
+ throw incomplete;
601
+ }
578
602
  return geminiSendResult(parsed, useModel, opts, usage);
579
603
  }
580
604
 
@@ -643,12 +667,19 @@ export class GeminiProvider {
643
667
  const restPassthrough = createPassthroughSignal(signal);
644
668
  const totalSignal = restPassthrough.signal;
645
669
  try {
646
- return await withRetry(
647
- async ({ signal: attemptSignal }) => {
670
+ const afterToolResult = geminiEndsWithFunctionResponse(contents);
671
+ return await runGeminiStreamAttempts({
672
+ signal: totalSignal,
673
+ sleepFn: this.config?.sleepFn,
674
+ onRetry: geminiRetryLogger(opts, '[gemini-rest]'),
675
+ afterToolResult,
676
+ run: async ({ signal: attemptSignal, connected }) => {
648
677
  signalRequesting(opts);
649
678
  const res = await this._openRestStream(genUrl, body, attemptSignal);
679
+ connected();
650
680
  const textLeakGuard = geminiTextLeakGuardFor(stream);
651
681
  const response = await consumeGeminiRestStreamResponse(res, {
682
+ acceptEmptyNoFinish: afterToolResult,
652
683
  signal: attemptSignal,
653
684
  onStreamDelta: callbacks.onStreamDelta,
654
685
  onTextDelta: callbacks.onTextDelta,
@@ -658,8 +689,7 @@ export class GeminiProvider {
658
689
  });
659
690
  return { response, textLeakGuard };
660
691
  },
661
- { signal: totalSignal, onRetry: geminiRetryLogger(opts, '[gemini-rest]') }
662
- );
692
+ });
663
693
  } catch (err) {
664
694
  if (
665
695
  !internal.skipExplicitCache &&
@@ -698,13 +728,16 @@ export class GeminiProvider {
698
728
  const sdkPassthrough = createPassthroughSignal(signal);
699
729
  const totalSignal = sdkPassthrough.signal;
700
730
  try {
701
- return await withRetry(
702
- async ({ signal: attemptSignal }) => {
731
+ return await runGeminiStreamAttempts({
732
+ signal: totalSignal,
733
+ sleepFn: this.config?.sleepFn,
734
+ onRetry: geminiRetryLogger(opts, '[gemini]'),
735
+ afterToolResult: geminiEndsWithFunctionResponse(contents),
736
+ run: async ({ signal: attemptSignal, connected }) => {
703
737
  signalRequesting(opts);
704
- return await streamGeminiSdkAttempt(genModel, stream, attemptSignal);
738
+ return await streamGeminiSdkAttempt(genModel, stream, attemptSignal, connected);
705
739
  },
706
- { signal: totalSignal, onRetry: geminiRetryLogger(opts, '[gemini]') }
707
- );
740
+ });
708
741
  } catch (err) {
709
742
  const recovered = await this._recoverGeminiNonStreaming({
710
743
  streamErr: err,
@@ -333,6 +333,9 @@ async function _postRefresh(tokens) {
333
333
  isTerminalRefresh,
334
334
  reauthRequired: isTerminalRefresh,
335
335
  oauthError: oauthError || null,
336
+ status: res.status,
337
+ headers: res.headers,
338
+ ...(oauthError ? { code: oauthError } : {}),
336
339
  });
337
340
  }
338
341
  const accessToken = json?.access_token;
@@ -18,6 +18,8 @@ import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
18
18
  import { sanitizeModelList } from './model-list-sanitize.mjs';
19
19
  import { makeModelCache } from './model-cache.mjs';
20
20
  import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
21
+ import { xaiModelSupportsReasoningEffort } from './openai-compat-xai.mjs';
22
+ import { XAI_COMPAT_RETRY_POLICY } from './openai-compat-retry-profile.mjs';
21
23
  import { createTimeoutSignal } from '../stall-policy.mjs';
22
24
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
23
25
  import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
@@ -48,19 +50,44 @@ export {
48
50
  } from './grok-oauth-tokens.mjs';
49
51
  export { beginOAuthLogin, loginOAuth } from './grok-oauth-login.mjs';
50
52
 
53
+ // Typed wire codes that say the bearer itself expired or was revoked. An
54
+ // ordinary permission 403 carries none of them and is never a refresh signal.
55
+ const EXPIRED_TOKEN_CODES = new Set([
56
+ 'token_expired',
57
+ 'expired_token',
58
+ 'access_token_expired',
59
+ 'token_revoked',
60
+ 'revoked_token',
61
+ 'invalid_token',
62
+ ]);
63
+
64
+ function hasExpiredTokenSignature(err) {
65
+ return [err?.code, err?.type, err?.providerErrorCode, err?.error?.code, err?.error?.type].some(
66
+ (field) => typeof field === 'string' && EXPIRED_TOKEN_CODES.has(field.trim().toLowerCase())
67
+ );
68
+ }
69
+
51
70
  const _modelCache = makeModelCache({
52
71
  fileName: 'grok-oauth-models.json',
53
72
  ttlMs: MODEL_CACHE_TTL_MS,
54
73
  version: GROK_MODEL_CACHE_SCHEMA_VERSION,
55
74
  });
56
75
 
57
- function _grokModelSupportsEffort(id) {
76
+ // A model takes a reasoning effort when its own roster row or the catalog
77
+ // publishes effort values; otherwise the shared xAI rule decides (catalog row
78
+ // first, then pass-through). Two id tokens are kept: xAI's own
79
+ // `non-reasoning` variant marker, and the Build endpoint family. Neither
80
+ // publishes capabilities, and both 400 on any effort.
81
+ function _grokModelSupportsEffort(id, model = null) {
58
82
  const text = String(id || '').toLowerCase();
59
83
  if (!text) return false;
60
84
  if (NON_CHAT_MODEL_RE.test(text)) return false;
61
- if (text.includes('non-reasoning')) return false;
62
- if (text === 'grok-build' || text.startsWith('grok-build-')) return false;
63
- return text.includes('reasoning') || /^grok-\d/.test(text);
85
+ if (_grokCatalogEffortValues(model).length || _grokApiEffortValues(model).length) return true;
86
+ if (/non[-_]?reasoning/.test(text)) return false;
87
+ // Build endpoint models reject reasoning_effort outright; a catalog or
88
+ // roster that publishes effort values has already answered above.
89
+ if (/^grok[-_]build/.test(text)) return false;
90
+ return xaiModelSupportsReasoningEffort(text);
64
91
  }
65
92
 
66
93
  // Pre-catalog fallback matches Grok 4.5 (low/medium/high). Grok 4.6 adds
@@ -86,7 +113,8 @@ function _grokApiEffortValues(model) {
86
113
  model?.reasoningEfforts ??
87
114
  model?.reasoning_efforts ??
88
115
  model?._meta?.reasoningEfforts ??
89
- model?._meta?.reasoning_efforts;
116
+ model?._meta?.reasoning_efforts ??
117
+ model?.capabilities?.reasoning_effort;
90
118
  return Array.isArray(raw) ? _uniqueKnownGrokEfforts(raw) : [];
91
119
  }
92
120
 
@@ -103,7 +131,7 @@ function _grokCatalogEffortValues(model) {
103
131
  }
104
132
 
105
133
  function _grokReasoningLevels(id, model) {
106
- if (!_grokModelSupportsEffort(id)) return [];
134
+ if (!_grokModelSupportsEffort(id, model)) return [];
107
135
  const catalog = _grokCatalogEffortValues(model);
108
136
  if (catalog.length) return catalog;
109
137
  const api = _grokApiEffortValues(model);
@@ -144,13 +172,14 @@ function _normalizeGrokModel(m) {
144
172
  const contextWindow = _grokApiContextWindow(m);
145
173
  const apiEfforts = _grokApiEffortValues(m);
146
174
  const reasoningOptions = apiEfforts.length ? [{ type: 'effort', values: apiEfforts }] : undefined;
175
+ const row = { ...m, reasoningOptions };
147
176
  return {
148
177
  id,
149
178
  name: id,
150
179
  display: _displayGrokModel(m),
151
180
  provider: 'grok-oauth',
152
- family: _normalizeGrokFamily(id),
153
- reasoningLevels: _grokReasoningLevels(id, { ...m, reasoningOptions }),
181
+ family: _normalizeGrokFamily(id, row),
182
+ reasoningLevels: _grokReasoningLevels(id, row),
154
183
  ...(reasoningOptions ? { reasoningOptions } : {}),
155
184
  tier: 'version',
156
185
  latest: false,
@@ -165,7 +194,7 @@ function _sanitizeGrokModels(models) {
165
194
  const next = models.map((m) => {
166
195
  let out = m;
167
196
  const display = _displayGrokModel(m);
168
- const family = _normalizeGrokFamily(m?.id);
197
+ const family = _normalizeGrokFamily(m?.id, out);
169
198
  const reasoningLevels = _grokReasoningLevels(m?.id, out);
170
199
  // api.x.ai /models does not expose a max-output field. Historical
171
200
  // enrichment could persist contextWindow as outputTokens (for example
@@ -248,6 +277,15 @@ function resolveLatestGrokModel() {
248
277
  return _newestDatedGrokChatModel(cached)?.id || null;
249
278
  }
250
279
 
280
+ // Whether a request for this model may carry an effort: the levels its cached
281
+ // roster row already resolved from the roster and catalog answer, else the id
282
+ // alone follows the catalog rule.
283
+ function _grokSendSupportsEffort(id) {
284
+ const cached = _modelCache.loadSync();
285
+ const row = Array.isArray(cached) ? cached.find((m) => m?.id === id) : null;
286
+ return Array.isArray(row?.reasoningLevels) ? row.reasoningLevels.length > 0 : _grokModelSupportsEffort(id);
287
+ }
288
+
251
289
  async function ensureLatestGrokModel(provider) {
252
290
  let m = resolveLatestGrokModel();
253
291
  if (m) return m;
@@ -265,6 +303,8 @@ export class GrokOAuthProvider {
265
303
  // OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
266
304
  static inputExcludesCache = false;
267
305
  name = 'grok-oauth';
306
+ // The inner xai profile's limits are final: no loop-level recovery cycles on top.
307
+ retryPolicy = XAI_COMPAT_RETRY_POLICY;
268
308
  config;
269
309
  tokens = null;
270
310
  _inner = null;
@@ -395,11 +435,12 @@ export class GrokOAuthProvider {
395
435
  });
396
436
  const inner = this._ensureInner(tokens.access_token, useModel, requestHeaders);
397
437
  const grokTools = normalizeGrokToolSchemas(tools);
398
- // Effort-less models (grok-build, composer): the service 400s on ANY
399
- // reasoningEffort parameter, and pinned model settings can carry one
400
- // (e.g. the bench harness pins effort per route). Strip it here so a
401
- // saved effort never breaks a model that cannot accept it.
402
- if (!_grokModelSupportsEffort(useModel) && sendOpts) {
438
+ // Effort-less models: the service 400s on ANY reasoningEffort parameter,
439
+ // and pinned model settings can carry one (e.g. the bench harness pins
440
+ // effort per route). Strip it here so a saved effort never breaks a model
441
+ // that cannot accept it. The model's own roster row decides; a model not
442
+ // on the roster yet follows the catalog rule.
443
+ if (sendOpts && !_grokSendSupportsEffort(useModel)) {
403
444
  // 'none' (not null) so the ?? fallback chain in the xai builder
404
445
  // cannot fall through to a config/env effort; the normalizer maps
405
446
  // it to "omit the parameter".
@@ -414,9 +455,10 @@ export class GrokOAuthProvider {
414
455
  return await inner._doSend(messages, useModel, grokTools, sendOpts);
415
456
  } catch (err) {
416
457
  noteErrorUsage(err);
417
- // Refresh-and-retry on a server-rejected OAuth session (401/403).
418
- // TYPED status only: a rejection whose only evidence is message
419
- // text is never treated as an auth failure worth re-issuing.
458
+ // Refresh-and-retry on a server-rejected OAuth session: 401, or a 403
459
+ // carrying an explicit expired/revoked-token code. TYPED evidence only:
460
+ // message text is never an auth verdict, and a plain permission 403 is
461
+ // surfaced as-is.
420
462
  const rejectedStatus = Number(err?.httpStatus || err?.status);
421
463
  // 426 = client version gate. Retry once only when the body stated a
422
464
  // minimum we did not already satisfy; nothing was emitted before it.
@@ -434,7 +476,7 @@ export class GrokOAuthProvider {
434
476
  sendOpts
435
477
  );
436
478
  }
437
- if (rejectedStatus === 401 || rejectedStatus === 403) {
479
+ if (rejectedStatus === 401 || (rejectedStatus === 403 && hasExpiredTokenSignature(err))) {
438
480
  // A stream-level rejection after text/tool dispatch cannot be safely
439
481
  // replayed: the client has already observed output or may have
440
482
  // executed a side-effecting tool.
@@ -444,7 +486,14 @@ export class GrokOAuthProvider {
444
486
  process.stderr.write(`[grok-oauth] ${rejectedStatus}, force-refreshing token...\n`);
445
487
  const fresh = await this.ensureAuth({ forceRefresh: true });
446
488
  const retryInner = this._ensureInner(fresh.access_token, useModel, requestHeaders);
447
- return await retryInner._doSend(messages, useModel, grokTools, sendOpts);
489
+ try {
490
+ return await retryInner._doSend(messages, useModel, grokTools, sendOpts);
491
+ } catch (retryErr) {
492
+ // Rejected again on a token that was just refreshed: re-login is the
493
+ // only remaining path.
494
+ if (Number(retryErr?.httpStatus || retryErr?.status) === 401) retryErr.reauthRequired = true;
495
+ throw retryErr;
496
+ }
448
497
  }
449
498
  throw err;
450
499
  }
@@ -9,6 +9,7 @@
9
9
  import { noteErrorUsage } from './lib/note-error-usage.mjs';
10
10
  import { fileMtimeMs, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
11
11
  import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
12
+ import { COMPAT_RETRY_POLICY } from './openai-compat-retry-profile.mjs';
12
13
  import { fetchCompatModelItems, listCompatModels } from './openai-compat-models.mjs';
13
14
  import { makeModelCache } from './model-cache.mjs';
14
15
  import {
@@ -114,6 +115,8 @@ export class MimoOAuthProvider {
114
115
  // OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
115
116
  static inputExcludesCache = false;
116
117
  name = 'mimo-oauth';
118
+ // The retry profile's limits are final: no loop-level recovery cycles on top.
119
+ retryPolicy = COMPAT_RETRY_POLICY;
117
120
  config;
118
121
  tokens = null;
119
122
  _inner = null;
@@ -208,7 +208,8 @@ function _stalenessFamily(row, id) {
208
208
  const modelId = String(id || row?.id || row?.name || '')
209
209
  .trim()
210
210
  .toLowerCase();
211
- const claude = modelId.match(/(?:^|\/)claude-(opus|sonnet|haiku|fable)(?:-|$)/);
211
+ // Claude ids name their family right after the brand (`claude-<family>-<version>`).
212
+ const claude = modelId.match(/(?:^|\/)claude-([a-z]+)(?:-|$)/);
212
213
  if (claude) return `claude-${claude[1]}`;
213
214
  const family = row && typeof row.family === 'string' ? row.family.trim().toLowerCase() : '';
214
215
  return family || null;
@@ -10,8 +10,10 @@ import { createHash } from 'node:crypto';
10
10
  import { noteErrorUsage } from './lib/note-error-usage.mjs';
11
11
  import { fileMtimeMs } from './lib/oauth-token-utils.mjs';
12
12
  import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
13
+ import { COMPAT_RETRY_POLICY } from './openai-compat-retry-profile.mjs';
13
14
  import { getModelMetadataSync } from './model-catalog.mjs';
14
15
  import { makeModelCache } from './model-cache.mjs';
16
+ import { normalizeCacheKey } from '../agent-runtime/cache-strategy.mjs';
15
17
  import { PRICING_RATE_KEYS } from '../../../shared/llm/model-pricing-rates.mjs';
16
18
  import {
17
19
  MUSE_DEFAULT_EFFORT_LEVELS as FIVE_LEVELS,
@@ -100,15 +102,6 @@ function museRow(id, discovered = null) {
100
102
  const EFFORT_DOWNGRADE = { max: 'xhigh' };
101
103
  const SUMMARY_MODES = new Set(['auto', 'detailed', 'concise']);
102
104
 
103
- // Cache identities beyond the wire limit become a bounded hash identity.
104
- function normalizeCacheKey(value) {
105
- const text = String(value ?? '').trim();
106
- if (!text) return '';
107
- const wellFormed = text.toWellFormed();
108
- if (wellFormed.length <= 64) return wellFormed;
109
- return `pc_${createHash('sha256').update(wellFormed).digest('hex').slice(0, 32)}`;
110
- }
111
-
112
105
  // Final request shaping on the Responses wire (installed through the shared
113
106
  // transport's opt-in params hook).
114
107
  function shapeResponsesParams(params, { opts, modelInfo }, config = {}) {
@@ -321,6 +314,8 @@ export class MuseCodeOAuthProvider {
321
314
  nativePdf = false;
322
315
  static inputExcludesCache = false;
323
316
  name = 'muse-code-oauth';
317
+ // The retry profile's limits are final: no loop-level recovery cycles on top.
318
+ retryPolicy = COMPAT_RETRY_POLICY;
324
319
  config;
325
320
  tokens = null;
326
321
  _inner = null;
@@ -21,6 +21,7 @@ import { normalizeCompatChatResponse } from './openai-compat-response-normalizat
21
21
  import { applyCompatToolChoice, compatStreamRetryReporter } from './compat-request-policy.mjs';
22
22
  import { ensureChatToolPairs } from './lib/wire-pairing.mjs';
23
23
  import { xaiCacheRouting } from './openai-compat-xai.mjs';
24
+ import { compatRetryOptions, withCompatRetryProfile } from './openai-compat-retry-profile.mjs';
24
25
  import { preparePdfTextForProvider } from './media-normalization.mjs';
25
26
  import { isCustomProviderName, resolveCustomReasoning } from './custom-reasoning.mjs';
26
27
 
@@ -85,7 +86,7 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
85
86
  try {
86
87
  try {
87
88
  assembled = await withRetry(
88
- refineAttemptErrors(profile, async ({ signal: attemptSignal }) => {
89
+ withCompatRetryProfile(provider.name, refineAttemptErrors(profile, async ({ signal: attemptSignal }) => {
89
90
  try {
90
91
  opts.onStageChange?.('requesting');
91
92
  } catch {
@@ -139,11 +140,12 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
139
140
  }
140
141
  throw error;
141
142
  }
142
- }),
143
+ })),
143
144
  {
144
145
  signal: totalSignal.signal,
145
146
  onRetry: compatStreamRetryReporter(provider.name, opts),
146
147
  retry529: opts.retry529,
148
+ ...compatRetryOptions(provider.name),
147
149
  }
148
150
  );
149
151
  } catch (streamErr) {
@@ -8,6 +8,7 @@ import { truncatedCompatStreamError } from '../lib/openai-tool-args.mjs';
8
8
  import {
9
9
  emitCompatToolCallOnce,
10
10
  firstByteCompatStreamError,
11
+ failingFinishReasonError,
11
12
  markErrorLiveTextEmitted,
12
13
  markUnsafeRetryIfToolEmitted,
13
14
  } from '../openai-compat-stream-common.mjs';
@@ -61,7 +62,28 @@ function parseAccumulatedToolCalls(state, { label, parseToolCalls }) {
61
62
  }
62
63
  }
63
64
 
65
+ // A gateway-reported failing finish_reason (`network_error`, `error`) is an
66
+ // interruption: the stream carries no terminal, so exposure keeps its replay
67
+ // gates.
68
+ function finishReasonInterruption(state, err) {
69
+ attachPartial(state, err);
70
+ if (state.emittedText) {
71
+ markErrorLiveTextEmitted(err);
72
+ try {
73
+ err.streamStalled = true;
74
+ } catch {
75
+ /* best-effort */
76
+ }
77
+ }
78
+ return stampCompatOutcome(state, markUnsafeRetryIfToolEmitted(err, state.streamEmitState), {
79
+ terminalObserved: false,
80
+ continuation: true,
81
+ });
82
+ }
83
+
64
84
  export function settleCompatStream(state, { label, parseToolCalls }) {
85
+ const failing = failingFinishReasonError(state.stopReason, label);
86
+ if (failing) throw finishReasonInterruption(state, failing);
65
87
  const response = buildResponse(state);
66
88
  let toolCalls = parseAccumulatedToolCalls(state, { label, parseToolCalls });
67
89
  if (Array.isArray(toolCalls) && toolCalls.length) {
@@ -71,6 +71,7 @@ export async function consumeCompatChatCompletionStream(
71
71
  idleDeadlineAt: state.semanticIdleDeadlineAt,
72
72
  idleEnabled: state.sawFirstEvent && idleEnabled && state.semanticIdleDeadlineAt > 0,
73
73
  idleLabel: `${label} SSE idle`,
74
+ streamLabel: label,
74
75
  emittedToolCall: () => toolWorkStarted(state),
75
76
  });
76
77
  if (done) {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * openai-compat-error-contract.mjs — the error contract of the OpenAI-compatible
3
+ * provider boundary: `status`, lowercased `code` (from the wire error's `code`
4
+ * or `type`), `headers`, `retryAfterMs` and `resetAt` when the provider sent
5
+ * them. Evidence is structural only; message text is never parsed.
6
+ */
7
+ import { retryAfterMsFromError, typedStatusFrom } from './retry-classifier.mjs';
8
+
9
+ const lowered = (value) => (typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '');
10
+
11
+ // Epoch seconds, epoch ms or an ISO date → epoch ms; `resets_in_seconds` is relative.
12
+ function resetAtFrom(wire, now) {
13
+ const inSeconds = Number(wire?.resets_in_seconds);
14
+ if (wire?.resets_in_seconds != null && Number.isFinite(inSeconds) && inSeconds >= 0) {
15
+ return now + Math.ceil(inSeconds * 1000);
16
+ }
17
+ const raw = wire?.resets_at ?? wire?.reset_at;
18
+ if (raw == null || raw === '') return null;
19
+ const numeric = Number(raw);
20
+ if (Number.isFinite(numeric) && numeric > 0) return numeric < 1e12 ? Math.round(numeric * 1000) : Math.round(numeric);
21
+ const parsed = Date.parse(String(raw));
22
+ return Number.isNaN(parsed) ? null : parsed;
23
+ }
24
+
25
+ /** Stamp the boundary contract onto a provider error in place and return it. */
26
+ export function applyCompatErrorContract(err, detail = err?.error ?? err?.providerError, now = Date.now()) {
27
+ if (!err || typeof err !== 'object') return err;
28
+ try {
29
+ const wire = detail && typeof detail === 'object' ? detail : null;
30
+ const status = typedStatusFrom(err, err.response, wire);
31
+ if (status && !(Number(err.status) >= 100)) err.status = status;
32
+ const wireCode = lowered(wire?.code) || lowered(wire?.type) || lowered(err.type);
33
+ // Keep an errno / SDK-local code (ECONNRESET, ...) that is not the wire code.
34
+ if (wireCode && (typeof err.code !== 'string' || !err.code.trim() || lowered(err.code) === wireCode)) {
35
+ err.code = wireCode;
36
+ }
37
+ if (!err.headers && err.response?.headers) err.headers = err.response.headers;
38
+ if (!Number.isFinite(err.retryAfterMs)) {
39
+ const retryAfterMs = retryAfterMsFromError(err);
40
+ if (retryAfterMs != null) err.retryAfterMs = retryAfterMs;
41
+ }
42
+ if (!Number.isFinite(err.resetAt)) {
43
+ const resetAt = resetAtFrom(wire, now);
44
+ if (resetAt) err.resetAt = resetAt;
45
+ }
46
+ } catch {
47
+ /* the contract is best-effort; the original error stays authoritative */
48
+ }
49
+ return err;
50
+ }
@@ -7,6 +7,8 @@ import {
7
7
  writeXaiResponsesCacheTrace,
8
8
  } from './openai-compat-xai.mjs';
9
9
  import { createProviderReplay } from './lib/provider-replay.mjs';
10
+ import { stampStreamOutcome, STREAM_TRANSPORTS } from './lib/stream-outcome.mjs';
11
+ import { failingFinishReasonError } from './openai-compat-stream-common.mjs';
10
12
 
11
13
  const reportedNumber = (value) =>
12
14
  value == null || (typeof value === 'string' && value.trim() === '') ? Number.NaN : Number(value);
@@ -141,6 +143,27 @@ export function normalizeCompatChatResponse({
141
143
  const choice = response.choices[0];
142
144
  const toolCalls = assembled.toolCalls;
143
145
  const stopReason = choice?.finish_reason || null;
146
+ const failingFinish = failingFinishReasonError(stopReason, providerName);
147
+ if (failingFinish) {
148
+ const err = failingFinish;
149
+ const partialContent = choice?.message?.content || '';
150
+ Object.assign(err, {
151
+ partialContent,
152
+ partialToolCalls: Array.isArray(toolCalls) && toolCalls.length ? toolCalls : undefined,
153
+ pendingToolUse: false,
154
+ partialModel: response.model || useModel,
155
+ ...(response.usage ? { partialUsage: chatCompletionUsage(providerName, response.usage) } : {}),
156
+ });
157
+ stampStreamOutcome(err, {
158
+ transport: STREAM_TRANSPORTS.NON_STREAMING,
159
+ provider: 'openai-compat',
160
+ terminalObserved: false,
161
+ continuation: true,
162
+ textObservedChars: partialContent.length,
163
+ toolCallsComplete: err.partialToolCalls?.length || 0,
164
+ });
165
+ throw err;
166
+ }
144
167
  if (
145
168
  (stopReason === 'length' && Array.isArray(toolCalls) && toolCalls.length > 0) ||
146
169
  stopReason === 'content_filter'