mixdog 0.9.1 → 0.9.3
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 +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -19,13 +19,16 @@ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
|
19
19
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
20
20
|
import { enrichModels } from './model-catalog.mjs';
|
|
21
21
|
import { makeModelCache } from './model-cache.mjs';
|
|
22
|
+
import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
|
|
22
23
|
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
23
24
|
import {
|
|
25
|
+
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
24
26
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
25
27
|
PROVIDER_RETRY_BACKOFF_MS,
|
|
26
28
|
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
27
|
-
PROVIDER_SSE_IDLE_TIMEOUT_MS,
|
|
28
29
|
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
30
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
31
|
+
streamStalledError,
|
|
29
32
|
createPassthroughSignal,
|
|
30
33
|
} from '../stall-policy.mjs';
|
|
31
34
|
import {
|
|
@@ -38,14 +41,23 @@ import {
|
|
|
38
41
|
withRetry,
|
|
39
42
|
} from './retry-classifier.mjs';
|
|
40
43
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
44
|
+
import {
|
|
45
|
+
applyAnthropicEffortToBody,
|
|
46
|
+
effortValuesForModel,
|
|
47
|
+
shouldIncludeEffortBeta,
|
|
48
|
+
} from './anthropic-effort.mjs';
|
|
41
49
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
42
50
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
43
51
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
52
|
+
import { scanLeakedToolCalls, createToolCallDedupe } from './anthropic-leaked-toolcall.mjs';
|
|
44
53
|
|
|
45
54
|
// --- Model catalog cache helpers ---
|
|
46
55
|
// Disk-backed cache so repeated process starts (cron, tool calls) don't
|
|
47
56
|
// hammer /v1/models. 24h TTL matches the upstream client cadence.
|
|
48
57
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
58
|
+
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
59
|
+
// discarded instead of misread.
|
|
60
|
+
const ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION = 1;
|
|
49
61
|
// SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
|
|
50
62
|
const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
|
|
51
63
|
|
|
@@ -64,6 +76,7 @@ const SSE_MIDSTREAM_POLICY = {
|
|
|
64
76
|
};
|
|
65
77
|
|
|
66
78
|
function formatRetryAfter(ms) {
|
|
79
|
+
if (ms == null) return '';
|
|
67
80
|
const n = Number(ms);
|
|
68
81
|
if (!Number.isFinite(n) || n < 0) return '';
|
|
69
82
|
if (n >= 60_000 && n % 60_000 === 0) return `${Math.round(n / 60_000)}m`;
|
|
@@ -93,6 +106,7 @@ function anthropicQuotaError(status, headers, bodyText = '') {
|
|
|
93
106
|
const _modelCache = makeModelCache({
|
|
94
107
|
fileName: 'anthropic-oauth-models.json',
|
|
95
108
|
ttlMs: MODEL_CACHE_TTL_MS,
|
|
109
|
+
version: ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION,
|
|
96
110
|
onSave: (m) => { _inMemoryCatalog = Array.isArray(m) ? m.slice() : null; },
|
|
97
111
|
});
|
|
98
112
|
|
|
@@ -134,6 +148,10 @@ function _displayModel(id) {
|
|
|
134
148
|
return `claude-${m[1].toLowerCase()}-${m[2]}${m[3] ? `.${m[3]}` : ''}`;
|
|
135
149
|
}
|
|
136
150
|
|
|
151
|
+
function _capabilitySupported(capability) {
|
|
152
|
+
return capability === true || capability?.supported === true;
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
// Classify a model id into our common tier/family shape. Anthropic's catalog
|
|
138
156
|
// mixes dated ids (claude-opus-4-5-20251101), versioned aliases
|
|
139
157
|
// (claude-opus-4-6), and the raw family tokens resolved via env vars.
|
|
@@ -150,15 +168,19 @@ function _normalizeAnthropicModel(raw) {
|
|
|
150
168
|
const releaseDate = dated
|
|
151
169
|
? id.match(/-(\d{4})(\d{2})(\d{2})$/)
|
|
152
170
|
: null;
|
|
171
|
+
const effortValues = effortValuesForModel(raw?.capabilities, id);
|
|
153
172
|
return {
|
|
154
173
|
id,
|
|
155
174
|
display: raw?.display_name || _prettyName(id, family),
|
|
156
175
|
family,
|
|
157
176
|
provider: 'anthropic-oauth',
|
|
158
|
-
contextWindow: raw?.context_window || raw?.max_context_window || _defaultContextForModel(id, family),
|
|
177
|
+
contextWindow: raw?.context_window || raw?.max_context_window || raw?.max_input_tokens || _defaultContextForModel(id, family),
|
|
178
|
+
outputTokens: raw?.max_tokens || raw?.max_output_tokens || null,
|
|
159
179
|
tier,
|
|
160
180
|
latest: false, // assigned in a second pass once full list is known
|
|
161
181
|
releaseDate: releaseDate ? `${releaseDate[1]}-${releaseDate[2]}-${releaseDate[3]}` : null,
|
|
182
|
+
supportsReasoning: effortValues.length > 0 || _capabilitySupported(raw?.capabilities?.thinking),
|
|
183
|
+
reasoningOptions: effortValues.length ? [{ type: 'effort', values: effortValues }] : [],
|
|
162
184
|
};
|
|
163
185
|
}
|
|
164
186
|
|
|
@@ -201,8 +223,7 @@ function _compareVersion(a, b) {
|
|
|
201
223
|
}
|
|
202
224
|
|
|
203
225
|
// Newest HIGH-TIER chat model by version, read from the SYNC in-memory catalog
|
|
204
|
-
// mirror.
|
|
205
|
-
// Anthropic ships three families: opus / sonnet / haiku. "Latest" is the
|
|
226
|
+
// mirror. Anthropic ships three families: opus / sonnet / haiku. "Latest" is the
|
|
206
227
|
// highest version across opus + sonnet only — haiku is the cheap tier and is
|
|
207
228
|
// never the flagship default. Returns null until listModels() populates the
|
|
208
229
|
// mirror; callers must warm the catalog (ensureLatestAnthropicModel) when null.
|
|
@@ -284,12 +305,11 @@ const OAUTH_SUCCESS_REDIRECT_URL = process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_
|
|
|
284
305
|
const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
285
306
|
const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
|
|
286
307
|
|
|
287
|
-
// Anthropic OAuth contract for first-party OAuth clients
|
|
288
|
-
//
|
|
289
|
-
// Mixdog keeps that upstream client contract for OAuth routing. Haiku is not
|
|
308
|
+
// Anthropic OAuth contract for first-party OAuth clients: Opus/Sonnet
|
|
309
|
+
// requests are gated on this exact system-prompt prefix. Haiku is not
|
|
290
310
|
// gated and ignores this prefix.
|
|
291
311
|
const CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
292
|
-
const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11
|
|
312
|
+
const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11';
|
|
293
313
|
const DEFAULT_CLI_VERSION = '2.1.77';
|
|
294
314
|
|
|
295
315
|
function resolveCliVersion() {
|
|
@@ -298,9 +318,11 @@ function resolveCliVersion() {
|
|
|
298
318
|
}
|
|
299
319
|
|
|
300
320
|
function requiresSystemPrefix(model) {
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
321
|
+
// High-tier Claude OAuth models require the first-party system prefix for
|
|
322
|
+
// OAuth pool routing. Haiku does not; keep every other Claude family (Opus,
|
|
323
|
+
// Sonnet, Fable, and future non-Haiku families) on the prefixed path.
|
|
324
|
+
const id = String(model || '').toLowerCase();
|
|
325
|
+
return /^claude-/.test(id) && !/^claude-haiku(?:-|$)/.test(id);
|
|
304
326
|
}
|
|
305
327
|
|
|
306
328
|
// OAuth rate-limit pool routing is gated by the server inspecting the first
|
|
@@ -351,35 +373,39 @@ function buildSystemBlocks(systemMsgs, model, systemTtl, tier3Ttl) {
|
|
|
351
373
|
return blocks;
|
|
352
374
|
}
|
|
353
375
|
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
376
|
+
// Catalog-reported outputTokens for a model id, read from the in-memory
|
|
377
|
+
// catalog mirror (lazily populated from the disk cache if the mirror hasn't
|
|
378
|
+
// been warmed yet by listModels()). Never throws — any failure just means
|
|
379
|
+
// "no catalog data", and callers fall back to the static heuristics below.
|
|
380
|
+
function _catalogOutputTokens(model) {
|
|
381
|
+
if (!model) return null;
|
|
382
|
+
try {
|
|
383
|
+
if (!Array.isArray(_inMemoryCatalog)) {
|
|
384
|
+
const cached = _modelCache.loadSync();
|
|
385
|
+
if (Array.isArray(cached)) _inMemoryCatalog = cached.slice();
|
|
386
|
+
}
|
|
387
|
+
if (!Array.isArray(_inMemoryCatalog)) return null;
|
|
388
|
+
const entry = _inMemoryCatalog.find(m => m?.id === model);
|
|
389
|
+
const out = Number(entry?.outputTokens);
|
|
390
|
+
return Number.isFinite(out) && out > 0 ? out : null;
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
365
395
|
|
|
396
|
+
// resolveMaxTokens: catalog-driven max_tokens for a model id. Thin wrapper
|
|
397
|
+
// around the shared anthropic-max-tokens helper (also used by the API-key
|
|
398
|
+
// twin in anthropic.mjs) — this provider supplies its own in-memory-mirror-
|
|
399
|
+
// first catalog lookup strategy.
|
|
400
|
+
// 1. MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS env override, if set, wins outright.
|
|
401
|
+
// 2. Catalog outputTokens (trusted over hardcoded heuristics when present),
|
|
402
|
+
// clamped to [MAX_TOKENS_FLOOR, safetyCap].
|
|
403
|
+
// 3. Static MAX_TOKENS table / family heuristic fallback when the catalog
|
|
404
|
+
// has no entry for this model, also clamped to the safety cap.
|
|
366
405
|
function resolveMaxTokens(model) {
|
|
367
|
-
|
|
368
|
-
const id = String(model || '').toLowerCase();
|
|
369
|
-
if (id.includes('opus')) return 65536;
|
|
370
|
-
if (id.includes('sonnet')) return 16384;
|
|
371
|
-
if (id.includes('haiku')) return 8192;
|
|
372
|
-
return 8192;
|
|
406
|
+
return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokens });
|
|
373
407
|
}
|
|
374
408
|
|
|
375
|
-
const EFFORT_BUDGET = {
|
|
376
|
-
low: 1024,
|
|
377
|
-
medium: 4096,
|
|
378
|
-
high: 16384,
|
|
379
|
-
xhigh: 32768,
|
|
380
|
-
max: 32768,
|
|
381
|
-
};
|
|
382
|
-
|
|
383
409
|
const MIN_THINKING_BUDGET = 1024;
|
|
384
410
|
const THINKING_OUTPUT_RESERVE = 1024;
|
|
385
411
|
|
|
@@ -392,10 +418,6 @@ function clampThinkingBudgetTokens(value, maxTokens) {
|
|
|
392
418
|
return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
|
|
393
419
|
}
|
|
394
420
|
|
|
395
|
-
// Tracks which unknown effort labels we've already logged so a repeated
|
|
396
|
-
// session-level misconfig doesn't flood stderr with the same warning.
|
|
397
|
-
const _LOGGED_UNKNOWN_EFFORT = new Set();
|
|
398
|
-
|
|
399
421
|
// Layered cache TTLs — stable layers get 1h, volatile layers get 5m.
|
|
400
422
|
// Anthropic requires 1h entries to appear before 5m entries in the request.
|
|
401
423
|
const CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' }; // tools, system, tier3, messages
|
|
@@ -418,8 +440,8 @@ function credentialCandidates() {
|
|
|
418
440
|
// Fallback expiry from the access_token's JWT `exp` claim (epoch ms) when the
|
|
419
441
|
// credentials file carries no explicit expiresAt — without it expiresAt stays 0,
|
|
420
442
|
// which ensureAuth reads as "never expires", disabling proactive refresh. Claude
|
|
421
|
-
// OAuth tokens are opaque so this returns 0 and the file's expiresAt governs
|
|
422
|
-
//
|
|
443
|
+
// OAuth tokens are opaque so this returns 0 and the file's expiresAt governs.
|
|
444
|
+
// JWT `exp` is epoch SECONDS (RFC 7519).
|
|
423
445
|
function _expiryFromAccessToken(token) {
|
|
424
446
|
try {
|
|
425
447
|
const parts = String(token || '').split('.');
|
|
@@ -644,7 +666,7 @@ export class ReauthRequired extends Error {
|
|
|
644
666
|
}
|
|
645
667
|
}
|
|
646
668
|
|
|
647
|
-
// --- Message conversion
|
|
669
|
+
// --- Message conversion ---
|
|
648
670
|
|
|
649
671
|
function withCacheControl(block, ttl = CACHE_TTL_VOLATILE) {
|
|
650
672
|
if (!block || typeof block !== 'object' || block.cache_control) return block;
|
|
@@ -938,10 +960,23 @@ function _anthropicSseError(event) {
|
|
|
938
960
|
return err;
|
|
939
961
|
}
|
|
940
962
|
|
|
941
|
-
async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta) {
|
|
963
|
+
async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta, knownToolNames) {
|
|
942
964
|
const reader = response.body.getReader();
|
|
943
965
|
const decoder = new TextDecoder();
|
|
944
|
-
|
|
966
|
+
// SEMANTIC idle window: reset only by real model events (message/content/
|
|
967
|
+
// tool deltas), NOT by raw keepalive bytes. A ping-only wedge therefore
|
|
968
|
+
// trips this within the window instead of hanging until the 30-min agent
|
|
969
|
+
// watchdog. See resetIdleTimer + the per-event reset in the loop below.
|
|
970
|
+
// state.semanticIdleTimeoutMs is a test/override seam (same shape as
|
|
971
|
+
// firstMessageTimeoutMs); production uses the shared env-backed default.
|
|
972
|
+
const SSE_IDLE_TIMEOUT_MS = Number.isFinite(Number(state?.semanticIdleTimeoutMs))
|
|
973
|
+
&& Number(state.semanticIdleTimeoutMs) > 0
|
|
974
|
+
? Number(state.semanticIdleTimeoutMs)
|
|
975
|
+
: PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
976
|
+
const SSE_FIRST_MESSAGE_TIMEOUT_MS = Number.isFinite(Number(state?.firstMessageTimeoutMs))
|
|
977
|
+
&& Number(state.firstMessageTimeoutMs) > 0
|
|
978
|
+
? Number(state.firstMessageTimeoutMs)
|
|
979
|
+
: PROVIDER_FIRST_BYTE_TIMEOUT_MS;
|
|
945
980
|
let content = '';
|
|
946
981
|
let hasThinkingContent = false;
|
|
947
982
|
const contentBlockTypes = new Set();
|
|
@@ -951,16 +986,137 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
951
986
|
let stopReason = null;
|
|
952
987
|
let buffer = '';
|
|
953
988
|
let idleTimedOut = false;
|
|
989
|
+
let firstMessageTimedOut = false;
|
|
954
990
|
let idleTimer = null;
|
|
991
|
+
let firstMessageTimer = null;
|
|
955
992
|
let currentEvent = '';
|
|
956
993
|
|
|
957
994
|
const pendingToolInputs = new Map();
|
|
958
995
|
|
|
996
|
+
// Leaked tool-call guard. The model (esp. Opus via OAuth) occasionally
|
|
997
|
+
// emits a tool call as plain text tags inside `text_delta` instead of a
|
|
998
|
+
// native `tool_use` block. `leakBuffer` is a minimal rolling window that
|
|
999
|
+
// only holds back text when a partial sentinel prefix is present, so a
|
|
1000
|
+
// tag split across chunk boundaries is still detected while ordinary text
|
|
1001
|
+
// still streams promptly. The guard is additive: the native tool_use path
|
|
1002
|
+
// (content_block_start/input_json_delta/content_block_stop) is untouched.
|
|
1003
|
+
const _knownTools = knownToolNames instanceof Set
|
|
1004
|
+
? knownToolNames
|
|
1005
|
+
: new Set(Array.isArray(knownToolNames) ? knownToolNames : []);
|
|
1006
|
+
const _leakGuardEnabled = _knownTools.size > 0;
|
|
1007
|
+
const _isKnownTool = (name) => _knownTools.has(name);
|
|
1008
|
+
let leakBuffer = '';
|
|
1009
|
+
// Running markdown fence/inline-code state threaded across text_delta
|
|
1010
|
+
// chunks (Fix 1): a tool-call tag inside a ```code fence``` or inline span
|
|
1011
|
+
// is a doc example, not a real call — the guard emits it as text.
|
|
1012
|
+
let leakFenceState = undefined;
|
|
1013
|
+
// Cross-path fingerprint dedupe (Fix 2): a synthesized text-leaked call and
|
|
1014
|
+
// an identical native tool_use block must dispatch onToolCall exactly once.
|
|
1015
|
+
const _toolDedupe = createToolCallDedupe();
|
|
1016
|
+
|
|
1017
|
+
// Synthesize + dispatch a recovered leaked call exactly like the native
|
|
1018
|
+
// content_block_stop path (push into toolCalls, flag state, eager
|
|
1019
|
+
// onToolCall). A generated id uses the same `toolu_`-prefixed shape as
|
|
1020
|
+
// Anthropic's native tool-call ids.
|
|
1021
|
+
const dispatchLeakedCall = (recovered) => {
|
|
1022
|
+
let args = recovered?.arguments;
|
|
1023
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
1024
|
+
// Skip if an identical native (or prior synthetic) call already fired.
|
|
1025
|
+
if (!_toolDedupe.shouldDispatch(recovered.name, args)) return;
|
|
1026
|
+
const call = {
|
|
1027
|
+
id: `toolu_leaked_${randomBytes(8).toString('hex')}`,
|
|
1028
|
+
name: recovered.name,
|
|
1029
|
+
arguments: args,
|
|
1030
|
+
};
|
|
1031
|
+
toolCalls.push(call);
|
|
1032
|
+
if (state) state.emittedToolCall = true;
|
|
1033
|
+
try { onToolCall?.(call); } catch {}
|
|
1034
|
+
try { onStreamDelta?.(); } catch {}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// Feed accumulated text through the scanner. On `final` nothing is held
|
|
1038
|
+
// back so legitimate text is never lost at stream end.
|
|
1039
|
+
const pumpLeakBuffer = (final) => {
|
|
1040
|
+
if (!_leakGuardEnabled) return;
|
|
1041
|
+
if (!leakBuffer && !final) return;
|
|
1042
|
+
const { emit, calls, rest, fenceState } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final, fenceState: leakFenceState });
|
|
1043
|
+
leakBuffer = rest;
|
|
1044
|
+
leakFenceState = fenceState;
|
|
1045
|
+
if (emit) {
|
|
1046
|
+
content += emit;
|
|
1047
|
+
if (onTextDelta) {
|
|
1048
|
+
if (state) state.emittedText = true;
|
|
1049
|
+
try { onTextDelta(emit); } catch {}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1053
|
+
};
|
|
1054
|
+
|
|
959
1055
|
// Holds the in-flight reader.read() race rejector so the idle timer can
|
|
960
1056
|
// force-unblock the loop even when reader.cancel() fails to settle the
|
|
961
1057
|
// pending read (undici half-open socket). See resetIdleTimer below.
|
|
962
1058
|
let idleReject = null;
|
|
963
1059
|
|
|
1060
|
+
const firstMessageTimeoutError = () => {
|
|
1061
|
+
const err = new Error(`Anthropic OAuth SSE stream produced no message_start within ${SSE_FIRST_MESSAGE_TIMEOUT_MS}ms`);
|
|
1062
|
+
err.code = 'EEMPTYSTREAM';
|
|
1063
|
+
err.isEmptyStream = true;
|
|
1064
|
+
err.firstByteTimeout = true;
|
|
1065
|
+
return err;
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
const clearFirstMessageTimer = () => {
|
|
1069
|
+
if (firstMessageTimer) {
|
|
1070
|
+
clearTimeout(firstMessageTimer);
|
|
1071
|
+
firstMessageTimer = null;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
const armFirstMessageTimer = () => {
|
|
1076
|
+
if (!(SSE_FIRST_MESSAGE_TIMEOUT_MS > 0)) return;
|
|
1077
|
+
clearFirstMessageTimer();
|
|
1078
|
+
firstMessageTimer = setTimeout(() => {
|
|
1079
|
+
if (state?.sawMessageStart) return;
|
|
1080
|
+
firstMessageTimedOut = true;
|
|
1081
|
+
const err = firstMessageTimeoutError();
|
|
1082
|
+
try { abortStream?.(err); } catch (abortErr) {
|
|
1083
|
+
try { process.stderr.write(`[anthropic-oauth] sse first-message abortStream failed: ${abortErr?.message ?? String(abortErr)}\n`); } catch {}
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
const _c = reader.cancel('SSE first message timeout');
|
|
1087
|
+
if (_c && typeof _c.catch === 'function') _c.catch(() => {});
|
|
1088
|
+
} catch (cancelErr) {
|
|
1089
|
+
try { process.stderr.write(`[anthropic-oauth] sse first-message cancel failed: ${cancelErr?.message ?? String(cancelErr)}\n`); } catch {}
|
|
1090
|
+
}
|
|
1091
|
+
if (idleReject) {
|
|
1092
|
+
const r = idleReject; idleReject = null; r(err);
|
|
1093
|
+
}
|
|
1094
|
+
}, SSE_FIRST_MESSAGE_TIMEOUT_MS);
|
|
1095
|
+
try { firstMessageTimer.unref?.(); } catch {}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// Attach the partial stream state to a mid-stream stall error so the agent
|
|
1099
|
+
// loop can decide SUCCESS vs FAILURE. The recurring "worker finished but
|
|
1100
|
+
// owner never notified" case is a FINAL no-tool summary stream that wedges
|
|
1101
|
+
// ping-only after the real work (tool calls) already completed in earlier
|
|
1102
|
+
// iterations: there is streamed `content`, no pending tool_use, and no
|
|
1103
|
+
// emitted tool call this iteration. The loop treats that as a successful
|
|
1104
|
+
// partial-final (deliver the summary we have) instead of dropping it. A
|
|
1105
|
+
// stall WITH a pending/emitted tool call stays a hard failure (a tool whose
|
|
1106
|
+
// input never completed must never be reported as done).
|
|
1107
|
+
const _attachStallPartial = (err) => {
|
|
1108
|
+
try {
|
|
1109
|
+
err.partialContent = content;
|
|
1110
|
+
err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
1111
|
+
err.pendingToolUse = pendingToolInputs.size > 0;
|
|
1112
|
+
err.partialModel = model || undefined;
|
|
1113
|
+
err.partialUsage = usage;
|
|
1114
|
+
err.partialStopReason = stopReason || undefined;
|
|
1115
|
+
err.partialHasThinking = hasThinkingContent;
|
|
1116
|
+
} catch { /* best-effort enrichment */ }
|
|
1117
|
+
return err;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
964
1120
|
const resetIdleTimer = () => {
|
|
965
1121
|
// OFF by default. When disabled the
|
|
966
1122
|
// idle timer never arms, so the stream is never killed on inactivity;
|
|
@@ -983,13 +1139,14 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
983
1139
|
// pending forever and the SSE idle timeout never unblocks the loop —
|
|
984
1140
|
// the 391s-hang root cause.
|
|
985
1141
|
if (idleReject) {
|
|
986
|
-
const e =
|
|
987
|
-
e.code = 'ETIMEDOUT';
|
|
1142
|
+
const e = _attachStallPartial(streamStalledError('Anthropic OAuth SSE', SSE_IDLE_TIMEOUT_MS, { emittedToolCall: !!state?.emittedToolCall }));
|
|
988
1143
|
const r = idleReject; idleReject = null; r(e);
|
|
989
1144
|
}
|
|
990
|
-
// Shared provider policy: short
|
|
991
|
-
//
|
|
1145
|
+
// Shared provider policy: short SEMANTIC-event inactivity catches the
|
|
1146
|
+
// ping-only wedge where SSE starts, emits some deltas, then goes silent
|
|
1147
|
+
// while `:ping` keepalives keep the transport socket warm.
|
|
992
1148
|
}, SSE_IDLE_TIMEOUT_MS);
|
|
1149
|
+
try { idleTimer.unref?.(); } catch {}
|
|
993
1150
|
};
|
|
994
1151
|
|
|
995
1152
|
const onAbort = () => {
|
|
@@ -1009,7 +1166,13 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1009
1166
|
}
|
|
1010
1167
|
|
|
1011
1168
|
try {
|
|
1012
|
-
|
|
1169
|
+
// Part A / reviewer fix: do NOT arm the SEMANTIC idle timer before the
|
|
1170
|
+
// stream has produced its first event. A slow first response is governed
|
|
1171
|
+
// by armFirstMessageTimer() (first-byte window) alone; arming the
|
|
1172
|
+
// semantic idle here could let it win and mis-abort a legitimately slow
|
|
1173
|
+
// first response as a stall. The semantic idle is first armed at
|
|
1174
|
+
// `message_start` (see below), so it only ever guards MID-stream silence.
|
|
1175
|
+
armFirstMessageTimer();
|
|
1013
1176
|
streamLoop: while (true) {
|
|
1014
1177
|
let chunk;
|
|
1015
1178
|
try {
|
|
@@ -1021,9 +1184,10 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1021
1184
|
});
|
|
1022
1185
|
} catch (err) {
|
|
1023
1186
|
if (idleTimedOut) {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1187
|
+
throw _attachStallPartial(streamStalledError('Anthropic OAuth SSE', SSE_IDLE_TIMEOUT_MS, { emittedToolCall: !!state?.emittedToolCall }));
|
|
1188
|
+
}
|
|
1189
|
+
if (firstMessageTimedOut) {
|
|
1190
|
+
throw firstMessageTimeoutError();
|
|
1027
1191
|
}
|
|
1028
1192
|
if (signal?.aborted) {
|
|
1029
1193
|
_captureMidstreamAbort(state, signal.reason);
|
|
@@ -1036,21 +1200,25 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1036
1200
|
const { done, value } = chunk;
|
|
1037
1201
|
if (done) break;
|
|
1038
1202
|
|
|
1039
|
-
resetIdleTimer();
|
|
1040
1203
|
buffer += decoder.decode(value, { stream: true });
|
|
1041
1204
|
const lines = buffer.split('\n');
|
|
1042
1205
|
buffer = lines.pop() || '';
|
|
1043
1206
|
|
|
1044
1207
|
for (const line of lines) {
|
|
1045
1208
|
if (line.startsWith(':')) {
|
|
1046
|
-
// SSE comment frame (Anthropic `:ping` keepalive).
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
// the
|
|
1051
|
-
|
|
1209
|
+
// SSE comment frame (Anthropic `:ping` keepalive). Keep it
|
|
1210
|
+
// at transport level only: comments must not refresh the
|
|
1211
|
+
// agent's semantic progress timestamp, or a ping-only 200
|
|
1212
|
+
// can look alive forever without message_start/content.
|
|
1213
|
+
// Crucially it also does NOT reset the SEMANTIC idle timer
|
|
1214
|
+
// below — a ping-only wedge must trip the idle abort.
|
|
1052
1215
|
continue;
|
|
1053
1216
|
}
|
|
1217
|
+
// Blank lines are SSE record separators — emitted after EVERY
|
|
1218
|
+
// frame, including `:ping` keepalives — so they are NOT semantic
|
|
1219
|
+
// progress and must not reset the idle timer (else a ping frame's
|
|
1220
|
+
// trailing blank would keep a wedge alive forever).
|
|
1221
|
+
if (line === '') continue;
|
|
1054
1222
|
if (line.startsWith('event: ')) {
|
|
1055
1223
|
currentEvent = line.slice(7).trim();
|
|
1056
1224
|
continue;
|
|
@@ -1062,11 +1230,24 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1062
1230
|
try {
|
|
1063
1231
|
const event = JSON.parse(data);
|
|
1064
1232
|
|
|
1233
|
+
// SEMANTIC idle reset (Part A): reset the idle timer ONLY for
|
|
1234
|
+
// real progress events, NOT for Anthropic keepalives. Anthropic
|
|
1235
|
+
// sends pings as a NAMED event (`event: ping` /
|
|
1236
|
+
// `data: {"type":"ping"}`), not just `:` comment frames, so a
|
|
1237
|
+
// named ping must be excluded here or a ping-only wedge keeps
|
|
1238
|
+
// the timer alive forever. Everything that is not a ping is a
|
|
1239
|
+
// genuine server event (message_start/content/tool/thinking
|
|
1240
|
+
// deltas, message_delta/stop, errors) and counts as progress.
|
|
1241
|
+
if (currentEvent !== 'ping' && event?.type !== 'ping') {
|
|
1242
|
+
resetIdleTimer();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1065
1245
|
if (currentEvent === 'error' || event?.type === 'error' || event?.error) {
|
|
1066
1246
|
throw _anthropicSseError(event);
|
|
1067
1247
|
}
|
|
1068
1248
|
|
|
1069
1249
|
if (event.type === 'message_start' && event.message) {
|
|
1250
|
+
clearFirstMessageTimer();
|
|
1070
1251
|
if (state) state.sawMessageStart = true;
|
|
1071
1252
|
if (event.message.model) model = event.message.model;
|
|
1072
1253
|
if (event.message.usage) {
|
|
@@ -1091,8 +1272,13 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1091
1272
|
if (event.type === 'content_block_delta') {
|
|
1092
1273
|
const delta = event.delta;
|
|
1093
1274
|
if (delta?.type) contentBlockTypes.add(delta.type);
|
|
1275
|
+
// Time-to-first-token: stamp the first content delta
|
|
1276
|
+
// (text / thinking / tool input_json) exactly once so
|
|
1277
|
+
// the SSE trace can separate first-byte latency from
|
|
1278
|
+
// total stream/generation time. Without this stamp
|
|
1279
|
+
// ttftMs was always null and reported as 0ms.
|
|
1280
|
+
if (state && !state.ttftAt) state.ttftAt = Date.now();
|
|
1094
1281
|
if (delta?.type === 'text_delta') {
|
|
1095
|
-
content += delta.text || '';
|
|
1096
1282
|
try { onStreamDelta?.(); } catch {}
|
|
1097
1283
|
// Live text relay (gateway): forward the explicit
|
|
1098
1284
|
// text chunk. thinking/signature/input_json deltas
|
|
@@ -1101,9 +1287,21 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1101
1287
|
// live it cannot be withdrawn, so flag the attempt so
|
|
1102
1288
|
// the mid-stream retry loop treats any later failure
|
|
1103
1289
|
// as final (a retry would concatenate attempts).
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1106
|
-
|
|
1290
|
+
if (_leakGuardEnabled) {
|
|
1291
|
+
// Route text through the leaked-tool-call guard.
|
|
1292
|
+
// It appends to `content`, forwards visible text
|
|
1293
|
+
// via onTextDelta, and synthesizes/dispatches any
|
|
1294
|
+
// recovered known-tool call — suppressing the
|
|
1295
|
+
// tags from the visible stream. A partial sentinel
|
|
1296
|
+
// is held in leakBuffer until the next chunk.
|
|
1297
|
+
leakBuffer += delta.text || '';
|
|
1298
|
+
pumpLeakBuffer(false);
|
|
1299
|
+
} else {
|
|
1300
|
+
content += delta.text || '';
|
|
1301
|
+
if (delta.text && onTextDelta) {
|
|
1302
|
+
if (state) state.emittedText = true;
|
|
1303
|
+
try { onTextDelta(delta.text); } catch {}
|
|
1304
|
+
}
|
|
1107
1305
|
}
|
|
1108
1306
|
}
|
|
1109
1307
|
if (delta?.type === 'thinking_delta' || delta?.type === 'signature_delta') {
|
|
@@ -1159,13 +1357,25 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1159
1357
|
name: pending.name,
|
|
1160
1358
|
arguments: parsedArgs,
|
|
1161
1359
|
};
|
|
1162
|
-
toolCalls.push(call);
|
|
1163
1360
|
pendingToolInputs.delete(event.index);
|
|
1164
|
-
if (state) state.emittedToolCall = true;
|
|
1165
1361
|
// Eager dispatch: let the loop start this tool
|
|
1166
1362
|
// before message_stop arrives. The loop keys
|
|
1167
1363
|
// pending promises by call.id so order is safe.
|
|
1168
|
-
|
|
1364
|
+
// Fix 2: skip the ENTIRE call (push + dispatch) when a
|
|
1365
|
+
// text-leaked synthetic of the same (name,args) already
|
|
1366
|
+
// fired — otherwise the duplicate stays in `toolCalls`
|
|
1367
|
+
// and the loop executes the side-effecting tool twice.
|
|
1368
|
+
// An invalid-args marker never fingerprint-collides with
|
|
1369
|
+
// a real recovered call, so malformed native calls still
|
|
1370
|
+
// dispatch (the marker path is unaffected).
|
|
1371
|
+
if (_toolDedupe.shouldDispatch(call.name, call.arguments)) {
|
|
1372
|
+
toolCalls.push(call);
|
|
1373
|
+
if (state) state.emittedToolCall = true;
|
|
1374
|
+
// Eager dispatch: let the loop start this tool
|
|
1375
|
+
// before message_stop arrives. The loop keys
|
|
1376
|
+
// pending promises by call.id so order is safe.
|
|
1377
|
+
try { onToolCall?.(call); } catch {}
|
|
1378
|
+
}
|
|
1169
1379
|
try { onStreamDelta?.(); } catch {}
|
|
1170
1380
|
}
|
|
1171
1381
|
}
|
|
@@ -1206,6 +1416,12 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1206
1416
|
}
|
|
1207
1417
|
}
|
|
1208
1418
|
|
|
1419
|
+
// Stream ended: flush any held-back leaked-tool-call buffer. `final`
|
|
1420
|
+
// holds nothing back, so a trailing partial sentinel that never
|
|
1421
|
+
// resolved into a real call is surfaced as ordinary text — legitimate
|
|
1422
|
+
// user-visible content is never lost on the failure path.
|
|
1423
|
+
pumpLeakBuffer(true);
|
|
1424
|
+
|
|
1209
1425
|
// Truncated-stream guard: if the reader loop exited (EOF or break)
|
|
1210
1426
|
// after message_start but without seeing message_stop / a tool_use
|
|
1211
1427
|
// stop_reason, the assistant turn was cut off mid-flight. Returning
|
|
@@ -1242,6 +1458,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1242
1458
|
};
|
|
1243
1459
|
} finally {
|
|
1244
1460
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1461
|
+
clearFirstMessageTimer();
|
|
1245
1462
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
1246
1463
|
try { reader.releaseLock(); } catch (err) {
|
|
1247
1464
|
try { process.stderr.write(`[anthropic-oauth] reader releaseLock failed: ${err?.message ?? String(err)}\n`); } catch {}
|
|
@@ -1365,21 +1582,13 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
1365
1582
|
body.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredTools])];
|
|
1366
1583
|
}
|
|
1367
1584
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
|
|
1376
|
-
} else if (!_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
|
|
1377
|
-
_LOGGED_UNKNOWN_EFFORT.add(opts.effort);
|
|
1378
|
-
try {
|
|
1379
|
-
process.stderr.write(`[anthropic-oauth] unknown effort=${opts.effort} ignored (known: ${Object.keys(EFFORT_BUDGET).join(',')})\n`);
|
|
1380
|
-
} catch {}
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1585
|
+
applyAnthropicEffortToBody(body, {
|
|
1586
|
+
model,
|
|
1587
|
+
opts,
|
|
1588
|
+
maxTokens,
|
|
1589
|
+
clampThinkingBudgetTokens,
|
|
1590
|
+
logTag: 'anthropic-oauth',
|
|
1591
|
+
});
|
|
1383
1592
|
|
|
1384
1593
|
if (opts.fast === true && supportsAnthropicFastMode(model)) {
|
|
1385
1594
|
body.speed = 'fast';
|
|
@@ -1530,6 +1739,20 @@ export class AnthropicOAuthProvider {
|
|
|
1530
1739
|
if (body.speed === 'fast') {
|
|
1531
1740
|
this.fastModeBetaHeaderLatched = true;
|
|
1532
1741
|
}
|
|
1742
|
+
// advanced-tool-use-2025-11-20 beta is only needed when this request
|
|
1743
|
+
// actually carries deferred (defer_loading) tools — gate the header on
|
|
1744
|
+
// that instead of sending it unconditionally on every request.
|
|
1745
|
+
const hasDeferredTools = Array.isArray(body.tools)
|
|
1746
|
+
&& body.tools.some((t) => t && t.defer_loading === true);
|
|
1747
|
+
// Known tool names for the leaked-tool-call guard in parseSSEStream:
|
|
1748
|
+
// recovered leaked calls are only synthesized when they name a tool
|
|
1749
|
+
// actually offered to this request (native + lowered). Derived from the
|
|
1750
|
+
// final request body so it matches exactly what the model was given.
|
|
1751
|
+
const knownToolNames = new Set(
|
|
1752
|
+
(Array.isArray(body.tools) ? body.tools : [])
|
|
1753
|
+
.map((t) => (t && typeof t.name === 'string' ? t.name : null))
|
|
1754
|
+
.filter(Boolean),
|
|
1755
|
+
);
|
|
1533
1756
|
const sessionId = opts.sessionId || null;
|
|
1534
1757
|
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
1535
1758
|
// Option A: no absolute wall-clock cap on streaming generation. A stream
|
|
@@ -1597,7 +1820,8 @@ export class AnthropicOAuthProvider {
|
|
|
1597
1820
|
'anthropic-beta': buildAnthropicBetaHeaders({
|
|
1598
1821
|
base: OAUTH_BETA_HEADERS,
|
|
1599
1822
|
fastMode: this.fastModeBetaHeaderLatched,
|
|
1600
|
-
toolSearch:
|
|
1823
|
+
toolSearch: hasDeferredTools,
|
|
1824
|
+
effort: shouldIncludeEffortBeta(useModel, opts),
|
|
1601
1825
|
}),
|
|
1602
1826
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
1603
1827
|
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
@@ -1758,11 +1982,12 @@ export class AnthropicOAuthProvider {
|
|
|
1758
1982
|
const result = await parseSSEFn(
|
|
1759
1983
|
response,
|
|
1760
1984
|
controller.signal,
|
|
1761
|
-
() => controller.abort(),
|
|
1985
|
+
(reason) => controller.abort(reason),
|
|
1762
1986
|
onStreamDelta,
|
|
1763
1987
|
onToolCall,
|
|
1764
1988
|
midState,
|
|
1765
1989
|
onTextDelta,
|
|
1990
|
+
knownToolNames,
|
|
1766
1991
|
);
|
|
1767
1992
|
|
|
1768
1993
|
const ttftMs = midState.ttftAt ? midState.ttftAt - sseStartedAt : null;
|
|
@@ -2214,3 +2439,7 @@ export async function loginOAuth() {
|
|
|
2214
2439
|
// Lets the SSE parser be exercised in isolation against a synthetic
|
|
2215
2440
|
// ReadableStream without needing a live OAuth session.
|
|
2216
2441
|
export { parseSSEStream };
|
|
2442
|
+
|
|
2443
|
+
// Test-only escape hatch for scripts/tool-smoke.mjs to verify the
|
|
2444
|
+
// catalog-driven max-tokens resolution without duplicating its logic.
|
|
2445
|
+
export const _test = { resolveMaxTokens };
|