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.
- package/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- 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
|
|
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
|
-
|
|
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
|
|
379
|
-
//
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
818
|
-
//
|
|
819
|
-
const stallRetryBudget =
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
throw
|
|
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'
|
|
1169
|
-
|
|
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
|
+
});
|