mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -24,7 +24,7 @@ import {
24
24
  resolveAnthropicModelAfter404,
25
25
  ensureLatestAnthropicModel,
26
26
  } from './anthropic-model-resolve.mjs';
27
- import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
27
+ import { sanitizeToolPairs } from '../session/context-utils.mjs';
28
28
  import {
29
29
  TOKEN_REFRESH_SKEW_MS,
30
30
  isAnthropicOAuthRefreshDisabled,
@@ -51,8 +51,9 @@ import {
51
51
  AnthropicFallbackTriggeredError,
52
52
  anthropicRequestTimeoutMs,
53
53
  classifyError,
54
+ markProviderRecoveryExhausted,
54
55
  anthropicMaxAttempts,
55
- createStallRetryBudget,
56
+ resolveStallRetryBudget,
56
57
  midstreamBackoffFor,
57
58
  retryAfterMsFromError,
58
59
  STREAM_STALL_RETRY_BUDGET_MS,
@@ -66,6 +67,8 @@ import {
66
67
  stampAnthropicStreamOutcome,
67
68
  } from './anthropic-sse.mjs';
68
69
  import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
70
+ import { applyAnthropicServerFallback } from './anthropic-server-fallback.mjs';
71
+ import { fastModeAvailable, noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
69
72
  import { gzipSync } from 'node:zlib';
70
73
 
71
74
  // Request-body gzip gate (see the fetch site below). Env kill-switch
@@ -84,7 +87,6 @@ import {
84
87
  shouldIncludeEffortBeta,
85
88
  } from './anthropic-effort.mjs';
86
89
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
87
- import { normalizeContentForAnthropic } from './media-normalization.mjs';
88
90
  import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
89
91
  import {
90
92
  ANTHROPIC_CACHE_TTL_STABLE as CACHE_TTL_STABLE,
@@ -95,7 +97,9 @@ import {
95
97
  requestAnthropicTools as sharedRequestAnthropicTools,
96
98
  normalizeAnthropicNonStreamingResponse,
97
99
  resolveAnthropicCacheTtls as resolveCacheTtls,
100
+ resolveAnthropicMessageCacheSlots,
98
101
  sanitizeAnthropicInputSchema,
102
+ toAnthropicMessages,
99
103
  toAnthropicToolChoice,
100
104
  } from './lib/anthropic-request-utils.mjs';
101
105
 
@@ -258,92 +262,6 @@ function requestAnthropicTools(tools, messages, opts) {
258
262
  return sharedRequestAnthropicTools(tools, messages, opts, 'anthropic-oauth');
259
263
  }
260
264
 
261
- function toAnthropicMessages(messages) {
262
- // Marker-free lowering. cache_control is applied AFTER sanitization by
263
- // applyAnthropicCacheMarkers() so that block drops/inserts/reorders
264
- // performed by sanitizeAnthropicContentPairs cannot move or delete a
265
- // marked block (the root cause of the sporadic COLD-turn cache miss:
266
- // pre-sanitize markers landed on blocks the sanitizer then rewrote, so
267
- // the provider-visible breakpoint diverged from the cached one).
268
- const result = [];
269
- for (let idx = 0; idx < messages.length; idx++) {
270
- const m = messages[idx];
271
- if (m.role === 'system') continue;
272
-
273
- if (m.role === 'assistant' && (m.toolCalls?.length || m.assistantBlocks?.length || m.thinkingBlocks?.length)) {
274
- let content;
275
- if (m.assistantBlocks?.length) {
276
- content = m.assistantBlocks.slice();
277
- } else {
278
- content = [];
279
- // Adaptive-thinking round-trip: prior-turn thinking blocks are
280
- // REQUIRED back, unmodified (signature intact; empty thinking
281
- // field allowed), and MUST precede tool_use blocks. Emit them
282
- // first, verbatim as received from the SSE parser.
283
- if (Array.isArray(m.thinkingBlocks)) {
284
- for (const tb of m.thinkingBlocks) {
285
- if (tb && typeof tb === 'object') content.push(tb);
286
- }
287
- }
288
- if (m.content) content.push({ type: 'text', text: m.content });
289
- for (const tc of m.toolCalls || []) {
290
- content.push({
291
- type: 'tool_use',
292
- id: tc.id,
293
- name: tc.name,
294
- input: tc.arguments,
295
- });
296
- }
297
- }
298
- result.push({ role: 'assistant', content });
299
- continue;
300
- }
301
-
302
- if (m.role === 'tool') {
303
- const last = result[result.length - 1];
304
- const native = m.nativeToolSearch;
305
- const nativeProvider = String(native?.provider || '').toLowerCase();
306
- const anthropicNative = new Set(['anthropic', 'anthropic-oauth']);
307
- const references = (!nativeProvider || anthropicNative.has(nativeProvider))
308
- && Array.isArray(native?.toolReferences)
309
- ? native.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
310
- : [];
311
- const block = {
312
- type: 'tool_result',
313
- tool_use_id: m.toolCallId || '',
314
- content: references.length
315
- ? references.map((tool_name) => ({ type: 'tool_reference', tool_name }))
316
- : normalizeContentForAnthropic(m.content),
317
- ...((m.toolKind === 'error' || m.isError === true) ? { is_error: true } : {}),
318
- };
319
- if (last?.role === 'user' && Array.isArray(last.content)) {
320
- last.content.push(block);
321
- } else {
322
- result.push({ role: 'user', content: [block] });
323
- }
324
- continue;
325
- }
326
-
327
- // First-party client parity: fold a user text turn that directly follows a
328
- // tool_result turn into that tool_result's content. A sibling text
329
- // turn after tool_result renders as `</function_results>\n\nHuman:`
330
- // on the wire and trains the model toward 3-token empty end_turn
331
- // completions (see foldUserTextIntoToolResultTail).
332
- // EXCEPTION: steering-origin user messages (human/TUI interjections)
333
- // must keep their own user turn so their provenance survives — folding
334
- // them into the preceding tool_result would disguise user input as
335
- // tool output. Anthropic accepts a user text message after a
336
- // tool_result message, so the distinct turn stays request-valid.
337
- const isSteering = m.role === 'user' && m.meta?.source === 'steering';
338
- if (m.role === 'user' && !isSteering
339
- && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
340
- continue;
341
- }
342
- result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
343
- }
344
- return sanitizeAnthropicContentPairs(result);
345
- }
346
-
347
265
  // Applies cache_control markers to the FINAL, already-sanitized Anthropic
348
266
  // message array — by INVARIANT, never by pre-sanitize index. Because
349
267
  // sanitizeAnthropicContentPairs has already run (and must NOT run again
@@ -375,32 +293,16 @@ function buildRequestBody(messages, model, tools, sendOpts) {
375
293
  // sessionMarker (cacheTier:'tier3') at ttls.tier3.
376
294
  const systemBlocks = buildSystemBlocks(systemMsgs, model, ttls?.system, ttls?.tier3);
377
295
 
378
- // 4-BP budget layout. tools BP is dropped — system BP covers the
379
- // tools prefix via Anthropic's prompt cache prefix semantics
380
- // (order: tools → system → messages). That frees slots for the
381
- // messages-tail. The system blocks now hold BP1/BP2/BP3 (tier3), so the
382
- // tier3 breakpoint is accounted for inside systemBpUsed.
383
- const systemBpUsed = systemBlocks.filter(b => b.cache_control).length;
384
- const toolsBpUsed = 0;
385
- const usedSlots = toolsBpUsed + systemBpUsed;
386
- // Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables message
387
- // caching entirely. Any value >=1 first marks the previous user text turn
388
- // so consecutive requests share a breakpoint; a second free slot marks the
389
- // tail for the newest delta.
390
- const msgSlotsCap = Number.parseInt(process.env.ANTHROPIC_MSG_SLOTS, 10);
391
- const defaultMsgSlots = Math.max(0, 4 - usedSlots);
392
- const msgSlots = ttls.messages
393
- ? (Number.isFinite(msgSlotsCap) && msgSlotsCap >= 0 ? Math.min(msgSlotsCap, defaultMsgSlots) : defaultMsgSlots)
394
- : 0;
296
+ // Message-tail cache budget (4-BP layout + ANTHROPIC_MSG_SLOTS) is shared
297
+ // with anthropic.mjs — see resolveAnthropicMessageCacheSlots.
298
+ const messageCacheSlots = resolveAnthropicMessageCacheSlots(systemBlocks, ttls);
395
299
  // Build → sanitize (once, inside toAnthropicMessages) → mark. Markers are
396
300
  // applied to the FINAL sanitized array by invariant, so block drops /
397
301
  // inserts / reorders performed by the sanitizer can never move or delete a
398
302
  // marked block. NEVER sanitize again after this (see send path).
399
- // msgSlots === 0 (ANTHROPIC_MSG_SLOTS=0, or no free slot) → tail disabled.
400
- const tailTtl = msgSlots > 0 ? ttls.messages : null;
401
303
  const anthropicMessages = applyAnthropicCacheMarkers(
402
304
  toAnthropicMessages(chatMsgs),
403
- { messageTtl: tailTtl, messageSlots: msgSlots },
305
+ messageCacheSlots,
404
306
  );
405
307
 
406
308
  const body = {
@@ -409,6 +311,9 @@ function buildRequestBody(messages, model, tools, sendOpts) {
409
311
  messages: anthropicMessages,
410
312
  stream: true,
411
313
  };
314
+ applyAnthropicServerFallback(body, model, {
315
+ enabled: opts.serverFallback !== false,
316
+ });
412
317
 
413
318
  if (systemBlocks.length) body.system = systemBlocks;
414
319
 
@@ -436,7 +341,9 @@ function buildRequestBody(messages, model, tools, sendOpts) {
436
341
  logTag: 'anthropic-oauth',
437
342
  });
438
343
 
439
- if (opts.fast === true && supportsAnthropicFastMode(model)) {
344
+ // Skipped while the fast capacity pool is in cooldown (or permanently
345
+ // unavailable) so a drained pool is not re-probed every turn.
346
+ if (opts.fast === true && supportsAnthropicFastMode(model) && fastModeAvailable()) {
440
347
  body.speed = 'fast';
441
348
  }
442
349
 
@@ -691,6 +598,7 @@ export class AnthropicOAuthProvider {
691
598
  fastMode: this.fastModeBetaHeaderLatched,
692
599
  toolSearch: hasDeferredTools,
693
600
  effort: shouldIncludeEffortBeta(useModel, opts),
601
+ serverFallback: body.fallbacks === 'default',
694
602
  }),
695
603
  'anthropic-dangerous-direct-browser-access': 'true',
696
604
  'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
@@ -761,6 +669,12 @@ export class AnthropicOAuthProvider {
761
669
  try { result.controller?.abort?.(); } catch {}
762
670
  // Initial-response failure: nothing was sampled, so the
763
671
  // typed retry rules upstream own the decision.
672
+ // Subscription 429s never retry in-loop, so the cooldown is
673
+ // what keeps the NEXT turn off the drained fast pool.
674
+ noteFastModeCapacityError(
675
+ { httpStatus: status, headers: result?.response?.headers },
676
+ { fast: requestBody?.speed === 'fast' },
677
+ );
764
678
  throw Object.assign(
765
679
  anthropicQuotaError(status, result?.response?.headers, this.scrubTokens(quotaText)),
766
680
  { initialResponseError: true },
@@ -800,6 +714,12 @@ export class AnthropicOAuthProvider {
800
714
  onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
801
715
  const status = Number(lastErr?.httpStatus || lastErr?.status || lastErr?.response?.status || 0) || null;
802
716
  if (status === 429) notifyCurrentAnthropicRateLimit(lastErr);
717
+ // Fast capacity exhausted: drop `speed` so the replay runs at
718
+ // standard speed instead of re-hitting the drained pool.
719
+ if (requestBody?.speed === 'fast'
720
+ && noteFastModeCapacityError(lastErr, { fast: true }) !== 'retry-fast') {
721
+ delete requestBody.speed;
722
+ }
803
723
  const reason = status || lastErr?.code || lastErr?.message || 'network error';
804
724
  const suffix = delayReason ? ` (${delayReason})` : '';
805
725
  try {
@@ -814,9 +734,21 @@ export class AnthropicOAuthProvider {
814
734
  const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
815
735
  let firstAttemptError = null;
816
736
  let firstAttemptClassifier = null;
817
- // Send-scoped stall window: in-place stall retries share one wall
818
- // clock starting at the first stall (see createStallRetryBudget).
819
- const stallRetryBudget = createStallRetryBudget();
737
+ // Shared logical-send window: provider fallback and loop replay consume
738
+ // the same recovery budget.
739
+ const stallRetryBudget = resolveStallRetryBudget(opts);
740
+ const requireTransportRecoveryBudget = (error, controller) => {
741
+ if (stallRetryBudget.allowStallRetry()) return;
742
+ try {
743
+ process.stderr.write(
744
+ `[anthropic-oauth] transport recovery budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first failure)\n`,
745
+ );
746
+ } catch {}
747
+ try { controller?.abort?.(error); } catch {}
748
+ throw markProviderRecoveryExhausted(error, {
749
+ owner: 'anthropic-oauth-transport-budget',
750
+ });
751
+ };
820
752
 
821
753
  // Core non-streaming re-issue: abort the dead stream and repeat the
822
754
  // SAME request with stream:false. Shared by the exposed-text recovery
@@ -882,10 +814,13 @@ export class AnthropicOAuthProvider {
882
814
  const message = await fallback.response.json();
883
815
  return normalizeAnthropicNonStreamingResponse(message, useModel);
884
816
  } catch (err) {
885
- if (lifetime.signal.aborted && lifetime.signal.reason instanceof Error) {
886
- throw lifetime.signal.reason;
887
- }
888
- throw err;
817
+ const failure = lifetime.signal.aborted && lifetime.signal.reason instanceof Error
818
+ ? lifetime.signal.reason
819
+ : err;
820
+ if (failure instanceof AnthropicFallbackTriggeredError || totalSignal?.aborted) throw failure;
821
+ throw markProviderRecoveryExhausted(failure, {
822
+ owner: 'anthropic-oauth-nonstreaming-fallback',
823
+ });
889
824
  } finally {
890
825
  releaseFallback('Anthropic non-streaming fallback complete');
891
826
  lifetime.cleanup();
@@ -910,6 +845,7 @@ export class AnthropicOAuthProvider {
910
845
  try { streamingError.liveTextEmitted = true; streamingError.unsafeToRetry = true; } catch {}
911
846
  throw streamingError;
912
847
  }
848
+ requireTransportRecoveryBudget(streamingError, controller);
913
849
  return issueNonStreamingFallback(controller, streamingError);
914
850
  };
915
851
 
@@ -1162,13 +1098,12 @@ export class AnthropicOAuthProvider {
1162
1098
  && !midState.emittedToolCall
1163
1099
  && !midState.partialToolCall
1164
1100
  && !midState.emittedThinking) {
1101
+ requireTransportRecoveryBudget(err, controller);
1165
1102
  try { process.stderr.write('[anthropic-oauth] stream stalled with no exposure — retrying non-streaming\n'); } catch {}
1166
1103
  return await issueNonStreamingFallback(controller, err);
1167
1104
  }
1168
- if (classifier === 'stream_stalled' && !stallRetryBudget.allowStallRetry()) {
1169
- try { process.stderr.write(`[anthropic-oauth] stall retry budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first stall) — surfacing for fresh-request retry\n`); } catch {}
1170
- try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
1171
- throw err;
1105
+ if (classifier === 'stream_stalled') {
1106
+ requireTransportRecoveryBudget(err, controller);
1172
1107
  }
1173
1108
  if (classifier && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1174
1109
  firstAttemptError = err;
@@ -1195,6 +1130,12 @@ export class AnthropicOAuthProvider {
1195
1130
  );
1196
1131
  continue;
1197
1132
  }
1133
+ if (classifier && attemptIndex >= MAX_MIDSTREAM_RETRIES) {
1134
+ markProviderRecoveryExhausted(err, {
1135
+ owner: 'anthropic-oauth-midstream',
1136
+ attempts: attemptIndex + 1,
1137
+ });
1138
+ }
1198
1139
  if (attemptIndex > 0 && firstAttemptError) {
1199
1140
  try { err.midstreamRetries = attemptIndex; } catch {}
1200
1141
  try { err.midstreamClassifier = firstAttemptClassifier; } catch {}
@@ -0,0 +1,48 @@
1
+ function normalizedModelId(value) {
2
+ const raw = typeof value === 'string'
3
+ ? value
4
+ : value && typeof value === 'object'
5
+ ? value.model
6
+ : '';
7
+ return typeof raw === 'string' ? raw.trim() : '';
8
+ }
9
+
10
+ export function supportsAnthropicServerFallback(model) {
11
+ const id = normalizedModelId(model).toLowerCase().replace(/\./g, '-');
12
+ return /^claude-(?:opus|fable)-5(?:$|[-@])/.test(id);
13
+ }
14
+
15
+ export function applyAnthropicServerFallback(body, model, { enabled = true } = {}) {
16
+ if (!body || typeof body !== 'object' || !enabled || !supportsAnthropicServerFallback(model)) {
17
+ return false;
18
+ }
19
+ body.fallbacks = 'default';
20
+ return true;
21
+ }
22
+
23
+ export function parseAnthropicFallbackBlock(block) {
24
+ if (!block || typeof block !== 'object' || block.type !== 'fallback') return null;
25
+ const originalModel = normalizedModelId(block.from);
26
+ const fallbackModel = normalizedModelId(block.to);
27
+ if (!originalModel || !fallbackModel) return null;
28
+ const trigger = block.trigger && typeof block.trigger === 'object'
29
+ ? block.trigger
30
+ : null;
31
+ const triggerType = typeof trigger?.type === 'string' && trigger.type.trim()
32
+ ? trigger.type.trim()
33
+ : 'refusal';
34
+ const category = typeof trigger?.category === 'string' && trigger.category.trim()
35
+ ? trigger.category.trim()
36
+ : null;
37
+ return {
38
+ trigger: triggerType,
39
+ originalModel,
40
+ fallbackModel,
41
+ ...(category ? { category } : {}),
42
+ };
43
+ }
44
+
45
+ export function anthropicFallbackProviderMetadata(events) {
46
+ const fallbacks = Array.isArray(events) ? events.filter(Boolean) : [];
47
+ return fallbacks.length ? { anthropicFallbacks: fallbacks } : undefined;
48
+ }
@@ -0,0 +1,215 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ SERVER_SIDE_FALLBACK_BETA_HEADER,
6
+ buildAnthropicBetaHeaders,
7
+ } from './anthropic-betas.mjs';
8
+ import { _buildRequestBodyForCacheSmoke } from './anthropic-oauth.mjs';
9
+ import {
10
+ applyAnthropicServerFallback,
11
+ parseAnthropicFallbackBlock,
12
+ supportsAnthropicServerFallback,
13
+ } from './anthropic-server-fallback.mjs';
14
+ import { parseSSEStream } from './anthropic-sse.mjs';
15
+ import { normalizeAnthropicNonStreamingResponse } from './lib/anthropic-request-utils.mjs';
16
+ import { agentLoop } from '../session/agent-loop.mjs';
17
+
18
+ const fallbackBlock = {
19
+ type: 'fallback',
20
+ from: { model: 'claude-opus-5' },
21
+ to: { model: 'claude-opus-4-8' },
22
+ trigger: { type: 'refusal', category: 'cyber' },
23
+ };
24
+
25
+ test('server fallback defaults only eligible Anthropic model families', () => {
26
+ assert.equal(supportsAnthropicServerFallback('claude-opus-5'), true);
27
+ assert.equal(supportsAnthropicServerFallback('claude-fable-5-20260801'), true);
28
+ assert.equal(supportsAnthropicServerFallback('claude-opus-4-8'), false);
29
+
30
+ const body = {};
31
+ assert.equal(applyAnthropicServerFallback(body, 'claude-opus-5'), true);
32
+ assert.equal(body.fallbacks, 'default');
33
+
34
+ const disabled = {};
35
+ assert.equal(
36
+ applyAnthropicServerFallback(disabled, 'claude-opus-5', { enabled: false }),
37
+ false,
38
+ );
39
+ assert.equal('fallbacks' in disabled, false);
40
+ });
41
+
42
+ test('server fallback beta header is request-gated and deduplicated', () => {
43
+ assert.equal(
44
+ buildAnthropicBetaHeaders({ base: '', serverFallback: false }).includes(SERVER_SIDE_FALLBACK_BETA_HEADER),
45
+ false,
46
+ );
47
+ const headers = buildAnthropicBetaHeaders({
48
+ base: SERVER_SIDE_FALLBACK_BETA_HEADER,
49
+ serverFallback: true,
50
+ }).split(',');
51
+ assert.equal(headers.filter((item) => item === SERVER_SIDE_FALLBACK_BETA_HEADER).length, 1);
52
+ });
53
+
54
+ test('OAuth request body opts eligible models into Anthropic default fallback', () => {
55
+ const opus5 = _buildRequestBodyForCacheSmoke([], 'claude-opus-5');
56
+ assert.equal(opus5.fallbacks, 'default');
57
+
58
+ const opus48 = _buildRequestBodyForCacheSmoke([], 'claude-opus-4-8');
59
+ assert.equal('fallbacks' in opus48, false);
60
+
61
+ const disabled = _buildRequestBodyForCacheSmoke(
62
+ [],
63
+ 'claude-opus-5',
64
+ [],
65
+ { serverFallback: false },
66
+ );
67
+ assert.equal('fallbacks' in disabled, false);
68
+ });
69
+
70
+ test('fallback block parser uses structured model and refusal category fields', () => {
71
+ assert.deepEqual(parseAnthropicFallbackBlock(fallbackBlock), {
72
+ trigger: 'refusal',
73
+ originalModel: 'claude-opus-5',
74
+ fallbackModel: 'claude-opus-4-8',
75
+ category: 'cyber',
76
+ });
77
+ assert.equal(parseAnthropicFallbackBlock({ type: 'text', text: 'fallback' }), null);
78
+ });
79
+
80
+ const encoder = new TextEncoder();
81
+ function sseResponse(events) {
82
+ const chunks = events.map((event) => (
83
+ encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`)
84
+ ));
85
+ let index = 0;
86
+ return {
87
+ body: {
88
+ getReader() {
89
+ return {
90
+ read() {
91
+ if (index < chunks.length) {
92
+ return Promise.resolve({ done: false, value: chunks[index++] });
93
+ }
94
+ return Promise.resolve({ done: true, value: undefined });
95
+ },
96
+ cancel() { return Promise.resolve(); },
97
+ releaseLock() {},
98
+ };
99
+ },
100
+ },
101
+ };
102
+ }
103
+
104
+ test('SSE parser records server fallback and attributes the turn to the serving model', async () => {
105
+ const state = {
106
+ attemptIndex: 0,
107
+ sawMessageStart: false,
108
+ sawCompleted: false,
109
+ emittedToolCall: false,
110
+ partialToolCall: false,
111
+ emittedThinking: false,
112
+ emittedText: false,
113
+ };
114
+ const result = await parseSSEStream(
115
+ sseResponse([
116
+ {
117
+ type: 'message_start',
118
+ message: {
119
+ model: 'claude-opus-5',
120
+ usage: { input_tokens: 10 },
121
+ },
122
+ },
123
+ { type: 'content_block_start', index: 0, content_block: fallbackBlock },
124
+ { type: 'content_block_stop', index: 0 },
125
+ { type: 'content_block_start', index: 1, content_block: { type: 'text', text: '' } },
126
+ { type: 'content_block_delta', index: 1, delta: { type: 'text_delta', text: 'done' } },
127
+ { type: 'content_block_stop', index: 1 },
128
+ {
129
+ type: 'message_delta',
130
+ delta: { stop_reason: 'end_turn' },
131
+ usage: { output_tokens: 2 },
132
+ },
133
+ { type: 'message_stop' },
134
+ ]),
135
+ null,
136
+ () => {},
137
+ () => {},
138
+ () => {},
139
+ state,
140
+ () => {},
141
+ );
142
+
143
+ assert.equal(result.content, 'done');
144
+ assert.equal(result.model, 'claude-opus-4-8');
145
+ assert.equal(result.stopReason, 'end_turn');
146
+ assert.equal(result.contentBlockTypes.includes('fallback'), true);
147
+ assert.deepEqual(result.providerMetadata, {
148
+ anthropicFallbacks: [{
149
+ trigger: 'refusal',
150
+ originalModel: 'claude-opus-5',
151
+ fallbackModel: 'claude-opus-4-8',
152
+ category: 'cyber',
153
+ }],
154
+ });
155
+ });
156
+
157
+ test('agent loop reports the actual serving model in per-request usage deltas', async () => {
158
+ const deltas = [];
159
+ const provider = {
160
+ name: 'anthropic-oauth',
161
+ async send() {
162
+ return {
163
+ content: 'done',
164
+ model: 'claude-opus-4-8',
165
+ stopReason: 'end_turn',
166
+ usage: {
167
+ inputTokens: 1,
168
+ outputTokens: 2,
169
+ cachedTokens: 3,
170
+ cacheWriteTokens: 4,
171
+ promptTokens: 8,
172
+ },
173
+ };
174
+ },
175
+ };
176
+
177
+ const result = await agentLoop(
178
+ provider,
179
+ [{ role: 'user', content: 'test' }],
180
+ 'claude-opus-5',
181
+ [],
182
+ async () => {},
183
+ process.cwd(),
184
+ {
185
+ sessionId: 'server-fallback-usage-test',
186
+ onUsageDelta: (delta) => deltas.push(delta),
187
+ },
188
+ );
189
+
190
+ assert.equal(result.model, 'claude-opus-4-8');
191
+ assert.equal(deltas.length, 1);
192
+ assert.equal(deltas[0].requestedModel, 'claude-opus-5');
193
+ assert.equal(deltas[0].model, 'claude-opus-4-8');
194
+ });
195
+
196
+ test('non-streaming normalizer excludes fallback blocks and preserves metadata', () => {
197
+ const result = normalizeAnthropicNonStreamingResponse({
198
+ model: 'claude-opus-5',
199
+ content: [
200
+ fallbackBlock,
201
+ { type: 'text', text: 'done' },
202
+ ],
203
+ stop_reason: 'end_turn',
204
+ usage: { input_tokens: 10, output_tokens: 2 },
205
+ });
206
+
207
+ assert.equal(result.content, 'done');
208
+ assert.equal(result.model, 'claude-opus-4-8');
209
+ assert.deepEqual(result.providerMetadata?.anthropicFallbacks, [{
210
+ trigger: 'refusal',
211
+ originalModel: 'claude-opus-5',
212
+ fallbackModel: 'claude-opus-4-8',
213
+ category: 'cyber',
214
+ }]);
215
+ });