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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
2
|
import { loadConfig } from '../config.mjs';
|
|
3
3
|
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
4
4
|
import { classifyError, midstreamBackoffFor, sleepWithAbort, withRetry } from './retry-classifier.mjs';
|
|
@@ -15,10 +15,27 @@ import {
|
|
|
15
15
|
_classifyMidstreamError,
|
|
16
16
|
} from './anthropic-oauth.mjs';
|
|
17
17
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
18
|
+
import {
|
|
19
|
+
applyAnthropicEffortToBody,
|
|
20
|
+
effortValuesForModel,
|
|
21
|
+
shouldIncludeEffortBeta,
|
|
22
|
+
} from './anthropic-effort.mjs';
|
|
18
23
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
19
24
|
import { enrichModels } from './model-catalog.mjs';
|
|
25
|
+
import { makeModelCache } from './model-cache.mjs';
|
|
26
|
+
import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
|
|
20
27
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
21
28
|
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
let _Anthropic = null;
|
|
31
|
+
function loadAnthropic() {
|
|
32
|
+
if (!_Anthropic) {
|
|
33
|
+
const mod = require('@anthropic-ai/sdk');
|
|
34
|
+
_Anthropic = mod.default || mod.Anthropic || mod;
|
|
35
|
+
}
|
|
36
|
+
return _Anthropic;
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
// Abort-aware mid-stream backoff sleep → shared sleepWithAbort
|
|
23
40
|
// (retry-classifier.mjs). abortMessage preserves the prior fallback text.
|
|
24
41
|
function _midstreamSleepWithAbort(ms, signal) {
|
|
@@ -117,6 +134,10 @@ function _defaultContextForModel(id, family) {
|
|
|
117
134
|
return 200000;
|
|
118
135
|
}
|
|
119
136
|
|
|
137
|
+
function _capabilitySupported(capability) {
|
|
138
|
+
return capability === true || capability?.supported === true;
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
function _normalizeAnthropicModel(raw, provider = 'anthropic') {
|
|
121
142
|
const id = raw?.id || raw?.name || raw?.model;
|
|
122
143
|
if (!id) return null;
|
|
@@ -124,35 +145,64 @@ function _normalizeAnthropicModel(raw, provider = 'anthropic') {
|
|
|
124
145
|
const family = familyMatch ? familyMatch[1].toLowerCase() : 'other';
|
|
125
146
|
const dated = /-\d{8}$/.test(String(id));
|
|
126
147
|
const versioned = !dated && /^claude-[a-z]+-\d+(?:-\d+)?$/i.test(String(id));
|
|
148
|
+
const effortValues = effortValuesForModel(raw?.capabilities, id);
|
|
127
149
|
return {
|
|
128
150
|
id,
|
|
129
151
|
display: raw?.display_name || raw?.displayName || raw?.display || _prettyName(id, family),
|
|
130
152
|
family,
|
|
131
153
|
provider,
|
|
132
|
-
contextWindow: raw?.context_window || raw?.max_context_window || raw?.input_token_limit || raw?.inputTokenLimit || _defaultContextForModel(id, family),
|
|
133
|
-
outputTokens: raw?.max_output_tokens || raw?.output_token_limit || raw?.outputTokenLimit || null,
|
|
154
|
+
contextWindow: raw?.context_window || raw?.max_context_window || raw?.max_input_tokens || raw?.input_token_limit || raw?.inputTokenLimit || _defaultContextForModel(id, family),
|
|
155
|
+
outputTokens: raw?.max_tokens || raw?.max_output_tokens || raw?.output_token_limit || raw?.outputTokenLimit || null,
|
|
134
156
|
tier: dated ? 'dated' : versioned ? 'version' : 'family',
|
|
135
157
|
latest: false,
|
|
158
|
+
supportsReasoning: effortValues.length > 0 || _capabilitySupported(raw?.capabilities?.thinking),
|
|
159
|
+
reasoningOptions: effortValues.length ? [{ type: 'effort', values: effortValues }] : [],
|
|
136
160
|
};
|
|
137
161
|
}
|
|
138
162
|
// Family-based heuristic so new model ids (including custom user-configured
|
|
139
163
|
// ones) resolve a sensible max_tokens without requiring a code change.
|
|
164
|
+
// The API-key provider has no catalog cache of its own — it reads the same
|
|
165
|
+
// anthropic-oauth-models.json disk cache (read-only) that the OAuth provider
|
|
166
|
+
// maintains. Both providers hit the same Anthropic /v1/models catalog, so a
|
|
167
|
+
// per-model outputTokens entry is valid regardless of which auth path wrote
|
|
168
|
+
// it. If this provider is ever run standalone without the OAuth provider
|
|
169
|
+
// ever having populated the cache, loadSync() simply returns null and we
|
|
170
|
+
// fall through to the shared static heuristic in anthropic-max-tokens.mjs.
|
|
171
|
+
const ANTHROPIC_OAUTH_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
172
|
+
const _sharedOAuthModelCache = makeModelCache({
|
|
173
|
+
fileName: 'anthropic-oauth-models.json',
|
|
174
|
+
ttlMs: ANTHROPIC_OAUTH_MODEL_CACHE_TTL_MS,
|
|
175
|
+
version: 1,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// In-memory mirror populated by this provider's own listModels() fetch.
|
|
179
|
+
// API-key-only installs never have the OAuth provider write the shared disk
|
|
180
|
+
// cache, so without this mirror catalog outputTokens would stay invisible to
|
|
181
|
+
// resolveMaxTokens until an OAuth session runs. listModels() results flow in
|
|
182
|
+
// here (memory only — the disk cache stays OAuth-owned/read-only for us).
|
|
183
|
+
let _apiKeyCatalogMirror = null;
|
|
184
|
+
|
|
185
|
+
function _catalogOutputTokensFromSharedCache(model) {
|
|
186
|
+
if (!model) return null;
|
|
187
|
+
try {
|
|
188
|
+
const models = Array.isArray(_apiKeyCatalogMirror)
|
|
189
|
+
? _apiKeyCatalogMirror
|
|
190
|
+
: _sharedOAuthModelCache.loadSync();
|
|
191
|
+
if (!Array.isArray(models)) return null;
|
|
192
|
+
const entry = models.find(m => m?.id === model);
|
|
193
|
+
const out = Number(entry?.outputTokens);
|
|
194
|
+
return Number.isFinite(out) && out > 0 ? out : null;
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
140
200
|
function resolveMaxTokens(model) {
|
|
141
|
-
|
|
142
|
-
if (id.includes('opus')) return 32768;
|
|
143
|
-
if (id.includes('sonnet')) return 16384;
|
|
144
|
-
if (id.includes('haiku')) return 8192;
|
|
145
|
-
return 8192;
|
|
201
|
+
return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokensFromSharedCache });
|
|
146
202
|
}
|
|
147
203
|
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
low: 1024,
|
|
151
|
-
medium: 4096,
|
|
152
|
-
high: 16384,
|
|
153
|
-
xhigh: 32768,
|
|
154
|
-
max: 32768,
|
|
155
|
-
};
|
|
204
|
+
// Test-only escape hatch for scripts/anthropic-maxtokens-test.mjs.
|
|
205
|
+
export const _test = { resolveMaxTokens };
|
|
156
206
|
|
|
157
207
|
const MIN_THINKING_BUDGET = 1024;
|
|
158
208
|
const THINKING_OUTPUT_RESERVE = 1024;
|
|
@@ -389,7 +439,7 @@ export class AnthropicProvider {
|
|
|
389
439
|
this.config = config;
|
|
390
440
|
this.name = config.name || 'anthropic';
|
|
391
441
|
const betaHeaders = config.disableBetaHeaders ? null : buildAnthropicBetaHeaders({ toolSearch: true });
|
|
392
|
-
this.client = new
|
|
442
|
+
this.client = new (loadAnthropic())({
|
|
393
443
|
apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
|
|
394
444
|
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
395
445
|
defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(config.extraHeaders || {}) },
|
|
@@ -403,7 +453,7 @@ export class AnthropicProvider {
|
|
|
403
453
|
if (newKey) {
|
|
404
454
|
this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey };
|
|
405
455
|
const betaHeaders = this.config.disableBetaHeaders ? null : buildAnthropicBetaHeaders({ toolSearch: true });
|
|
406
|
-
this.client = new
|
|
456
|
+
this.client = new (loadAnthropic())({
|
|
407
457
|
apiKey: newKey,
|
|
408
458
|
...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
|
|
409
459
|
defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(this.config.extraHeaders || {}) },
|
|
@@ -451,8 +501,7 @@ export class AnthropicProvider {
|
|
|
451
501
|
const usedSlots = toolsBpUsed + systemBpUsed;
|
|
452
502
|
// Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables
|
|
453
503
|
// message caching entirely. Any value >=1 first marks the previous
|
|
454
|
-
// user text turn; a second free slot marks the tail.
|
|
455
|
-
// anthropic-oauth.mjs for twin parity.
|
|
504
|
+
// user text turn; a second free slot marks the tail.
|
|
456
505
|
const msgSlotsCap = Number.parseInt(process.env.ANTHROPIC_MSG_SLOTS, 10);
|
|
457
506
|
const defaultMsgSlots = Math.max(0, 4 - usedSlots);
|
|
458
507
|
const msgSlots = ttls.messages
|
|
@@ -481,16 +530,21 @@ export class AnthropicProvider {
|
|
|
481
530
|
// Anthropic prefix semantics (order: tools → system → messages).
|
|
482
531
|
params.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredAnthropicTools(tools || [], opts)])];
|
|
483
532
|
}
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
?
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
533
|
+
// Known tool names for the shared parseSSEStream leaked-tool-call guard
|
|
534
|
+
// (same guard fixes both Anthropic providers). Recovered leaked calls
|
|
535
|
+
// are only synthesized when they name a tool actually offered here.
|
|
536
|
+
const knownToolNames = new Set(
|
|
537
|
+
(Array.isArray(params.tools) ? params.tools : [])
|
|
538
|
+
.map((t) => (t && typeof t.name === 'string' ? t.name : null))
|
|
539
|
+
.filter(Boolean),
|
|
540
|
+
);
|
|
541
|
+
applyAnthropicEffortToBody(params, {
|
|
542
|
+
model: useModel,
|
|
543
|
+
opts,
|
|
544
|
+
maxTokens,
|
|
545
|
+
clampThinkingBudgetTokens,
|
|
546
|
+
logTag: this.name,
|
|
547
|
+
});
|
|
494
548
|
// Fast mode → speed: "fast" on models Anthropic marks as speed-capable.
|
|
495
549
|
if (opts.fast === true && supportsAnthropicFastMode(useModel)) {
|
|
496
550
|
params.speed = 'fast';
|
|
@@ -524,12 +578,20 @@ export class AnthropicProvider {
|
|
|
524
578
|
try { totalSignal.removeEventListener('abort', handler); } catch {}
|
|
525
579
|
};
|
|
526
580
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
581
|
+
// Per-call headers override the client defaultHeaders, so the
|
|
582
|
+
// constructor-level disableBetaHeaders opt-out must be honoured here
|
|
583
|
+
// too — otherwise opencode-go's anthropic-compatible routing
|
|
584
|
+
// (disableBetaHeaders:true) would still send beta strings that a
|
|
585
|
+
// third-party endpoint may reject.
|
|
586
|
+
const betaHeaders = this.config?.disableBetaHeaders
|
|
587
|
+
? null
|
|
588
|
+
: {
|
|
589
|
+
'anthropic-beta': buildAnthropicBetaHeaders({
|
|
590
|
+
fastMode: this.fastModeBetaHeaderLatched,
|
|
591
|
+
toolSearch: true,
|
|
592
|
+
effort: shouldIncludeEffortBeta(useModel, opts),
|
|
593
|
+
}),
|
|
594
|
+
};
|
|
533
595
|
|
|
534
596
|
const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
|
|
535
597
|
let firstAttemptError = null;
|
|
@@ -623,7 +685,7 @@ export class AnthropicProvider {
|
|
|
623
685
|
async ({ signal: attemptSignal }) => {
|
|
624
686
|
const res = await this.client.messages.create(params, {
|
|
625
687
|
signal: attemptSignal,
|
|
626
|
-
headers: betaHeaders,
|
|
688
|
+
...(betaHeaders ? { headers: betaHeaders } : {}),
|
|
627
689
|
}).asResponse();
|
|
628
690
|
if (!res.ok) {
|
|
629
691
|
const text = await res.text().catch(() => '');
|
|
@@ -676,11 +738,12 @@ export class AnthropicProvider {
|
|
|
676
738
|
const parseResult = await parseSSEStream(
|
|
677
739
|
response,
|
|
678
740
|
streamController.signal,
|
|
679
|
-
() => streamController.abort(),
|
|
741
|
+
(reason) => streamController.abort(reason),
|
|
680
742
|
onStreamDelta,
|
|
681
743
|
onToolCall,
|
|
682
744
|
midState,
|
|
683
745
|
onTextDelta,
|
|
746
|
+
knownToolNames,
|
|
684
747
|
);
|
|
685
748
|
|
|
686
749
|
if (firstBytePoll) {
|
|
@@ -799,6 +862,9 @@ export class AnthropicProvider {
|
|
|
799
862
|
.map((m) => _normalizeAnthropicModel(m, this.name))
|
|
800
863
|
.filter(Boolean);
|
|
801
864
|
const enriched = await enrichModels(normalized);
|
|
865
|
+
// Feed the resolver-visible mirror so API-key-only installs get
|
|
866
|
+
// catalog outputTokens without depending on the OAuth disk cache.
|
|
867
|
+
if (enriched.length) _apiKeyCatalogMirror = enriched.slice();
|
|
802
868
|
return enriched.length ? enriched : MODELS;
|
|
803
869
|
}
|
|
804
870
|
catch (err) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import * as fsp from 'node:fs/promises';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
3
|
+
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
4
4
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
5
5
|
import { getAgentApiKey, getOpenAIUsageSessionKey } from '../../../shared/config.mjs';
|
|
6
|
+
import { num, round } from './lib/usage-primitives.mjs';
|
|
6
7
|
|
|
7
8
|
const CACHE_FILE = 'api-usage-cache.json';
|
|
8
9
|
const LIVE_TTL_MS = 5 * 60_000;
|
|
@@ -10,19 +11,6 @@ const STALE_TTL_MS = 60 * 60_000;
|
|
|
10
11
|
const DISK_JSON_MEMORY_TTL_MS = 1000;
|
|
11
12
|
let diskJsonCache = { at: 0, file: '', value: null };
|
|
12
13
|
|
|
13
|
-
function num(value, fallback = null) {
|
|
14
|
-
if (value === null || value === undefined || value === '') return fallback;
|
|
15
|
-
const n = Number(value);
|
|
16
|
-
return Number.isFinite(n) ? n : fallback;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function round(value, digits = 4) {
|
|
20
|
-
const n = Number(value);
|
|
21
|
-
if (!Number.isFinite(n)) return null;
|
|
22
|
-
const scale = 10 ** digits;
|
|
23
|
-
return Math.round(n * scale) / scale;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
14
|
function cachePath() {
|
|
27
15
|
return join(resolvePluginData(), CACHE_FILE);
|
|
28
16
|
}
|
|
@@ -41,14 +29,6 @@ function readJson(file) {
|
|
|
41
29
|
}
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
function writeJson(file, value) {
|
|
45
|
-
diskJsonCache = { at: Date.now(), file, value };
|
|
46
|
-
try {
|
|
47
|
-
mkdirSync(resolvePluginData(), { recursive: true });
|
|
48
|
-
void fsp.writeFile(file, JSON.stringify(value, null, 2), 'utf8').catch(() => {});
|
|
49
|
-
} catch {}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
32
|
function cacheKey(provider) {
|
|
53
33
|
return String(provider || '').trim().toLowerCase();
|
|
54
34
|
}
|
|
@@ -68,16 +48,31 @@ export function readCachedApiUsageSnapshot(provider, { allowStale = true } = {})
|
|
|
68
48
|
|
|
69
49
|
function writeCachedApiUsageSnapshot(provider, snapshot) {
|
|
70
50
|
const file = cachePath();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
51
|
+
// Synchronous atomic+lock write (updateJsonAtomicSync) rather than the
|
|
52
|
+
// prior fire-and-forget fsp.writeFile: the read-modify-write of
|
|
53
|
+
// `snapshots` must happen inside the same file lock as the write, or two
|
|
54
|
+
// concurrent orchestrator processes updating different providers can
|
|
55
|
+
// clobber each other's snapshot merge (last writer wins, losing the
|
|
56
|
+
// other's entry). Usage snapshot writes are infrequent (once per
|
|
57
|
+
// provider per TTL window, not per-request), so trading the async/
|
|
58
|
+
// non-blocking write for lock+fsync-dir safety has no meaningful
|
|
59
|
+
// latency impact on the request path.
|
|
60
|
+
let next = null;
|
|
61
|
+
try {
|
|
62
|
+
next = updateJsonAtomicSync(file, (curRaw) => {
|
|
63
|
+
const cur = curRaw && typeof curRaw === 'object' ? curRaw : {};
|
|
64
|
+
const snapshots = cur.snapshots && typeof cur.snapshots === 'object' ? cur.snapshots : {};
|
|
65
|
+
return {
|
|
66
|
+
version: 1,
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
snapshots: {
|
|
69
|
+
...snapshots,
|
|
70
|
+
[cacheKey(provider)]: snapshot,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}, { lock: true, fsyncDir: true, timeoutMs: 1000 }); // best-effort cache write: short lock timeout, don't block on contention
|
|
74
|
+
} catch {}
|
|
75
|
+
if (next) diskJsonCache = { at: Date.now(), file, value: next };
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
function authHeaders(key, extra = {}) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
17
17
|
import { traceHash, stableTraceStringify, summarizeTraceTools, traceTextShape } from './trace-utils.mjs';
|
|
18
18
|
import { normalizeContentForGeminiParts, splitToolContentForGemini } from './media-normalization.mjs';
|
|
19
|
+
import { scanLeakedToolCalls } from './anthropic-leaked-toolcall.mjs';
|
|
19
20
|
|
|
20
21
|
const MODELS = [
|
|
21
22
|
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
@@ -31,12 +32,19 @@ const DEFAULT_MODEL = MODELS[0].id;
|
|
|
31
32
|
// Gemini's /models has no `created` timestamp, so latest-resolution is
|
|
32
33
|
// VERSION-based (parse gemini-X.Y) rather than release-date based.
|
|
33
34
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
35
|
+
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
36
|
+
// discarded instead of misread.
|
|
37
|
+
const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 1;
|
|
34
38
|
|
|
35
39
|
// De-dupes concurrent force-refreshes so they share one HTTP round-trip,
|
|
36
40
|
// mirroring anthropic-oauth's _modelRefreshInFlight.
|
|
37
41
|
let _modelRefreshInFlight = null;
|
|
38
42
|
|
|
39
|
-
const _modelCache = makeModelCache({
|
|
43
|
+
const _modelCache = makeModelCache({
|
|
44
|
+
fileName: 'gemini-models.json',
|
|
45
|
+
ttlMs: MODEL_CACHE_TTL_MS,
|
|
46
|
+
version: GEMINI_MODEL_CACHE_SCHEMA_VERSION,
|
|
47
|
+
});
|
|
40
48
|
|
|
41
49
|
// Mirror of anthropic-oauth.mjs _compareVersion: compare two gemini ids by the
|
|
42
50
|
// X.Y version embedded in the id (gemini-3.5-flash -> [3, 5]). Falls back to a
|
|
@@ -182,6 +190,10 @@ function _geminiCachePrefixHash({ model, systemInstruction, geminiTools, content
|
|
|
182
190
|
|
|
183
191
|
const GEMINI_GLOBAL_CACHE_MIN_LIVE_MS = 6 * 60 * 1000;
|
|
184
192
|
const GEMINI_GLOBAL_CACHE_MAX_ENTRIES = 128;
|
|
193
|
+
// Grace window before deleting a superseded cachedContents name (see the
|
|
194
|
+
// cross-session race note at the L1341-1372 call site). Long enough that a
|
|
195
|
+
// concurrent session still mid-flight on the old name has time to finish.
|
|
196
|
+
const GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS = 2 * 60 * 1000;
|
|
185
197
|
const geminiGlobalCaches = new Map();
|
|
186
198
|
const geminiGlobalCacheCreates = new Map();
|
|
187
199
|
|
|
@@ -294,7 +306,7 @@ function writeGeminiCacheTrace({ opts, model, systemInstruction, tools, contents
|
|
|
294
306
|
provider: 'gemini',
|
|
295
307
|
model,
|
|
296
308
|
owner: session.owner || null,
|
|
297
|
-
|
|
309
|
+
agent: session.agent || null,
|
|
298
310
|
permission: session.permission || null,
|
|
299
311
|
toolPermission: session.toolPermission || null,
|
|
300
312
|
profileId: session.profileId || null,
|
|
@@ -412,7 +424,104 @@ function geminiChunkText(chunk) {
|
|
|
412
424
|
return text;
|
|
413
425
|
}
|
|
414
426
|
|
|
415
|
-
|
|
427
|
+
function relayGeminiStreamText(t, { onTextDelta, textLeakGuard }) {
|
|
428
|
+
if (!t) return;
|
|
429
|
+
if (textLeakGuard) textLeakGuard.feedText(t);
|
|
430
|
+
else if (onTextDelta) { try { onTextDelta(t); } catch {} }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Rolling scanner for tool calls leaked as plain XML/antml tags inside Gemini
|
|
435
|
+
* `part.text` streams. Mirrors the Anthropic OAuth guard: suppress tags from
|
|
436
|
+
* visible text, synthesize known-tool calls, dispatch via onToolCall.
|
|
437
|
+
*/
|
|
438
|
+
export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolCall, onStreamDelta }) {
|
|
439
|
+
const _knownTools = knownToolNames instanceof Set
|
|
440
|
+
? knownToolNames
|
|
441
|
+
: new Set(Array.isArray(knownToolNames) ? knownToolNames : []);
|
|
442
|
+
const _enabled = _knownTools.size > 0;
|
|
443
|
+
const _isKnownTool = (name) => _knownTools.has(name);
|
|
444
|
+
let leakBuffer = '';
|
|
445
|
+
const leakedCalls = [];
|
|
446
|
+
const dispatchedFingerprints = new Set();
|
|
447
|
+
|
|
448
|
+
const toolCallFingerprint = (name, args) => {
|
|
449
|
+
let a = args;
|
|
450
|
+
if (a === null || typeof a !== 'object' || Array.isArray(a)) a = {};
|
|
451
|
+
return traceHash(stableTraceStringify({ name: name || '', args: a }));
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const dispatchLeakedCall = (recovered) => {
|
|
455
|
+
let args = recovered?.arguments;
|
|
456
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
457
|
+
const fp = toolCallFingerprint(recovered.name, args);
|
|
458
|
+
if (dispatchedFingerprints.has(fp)) return;
|
|
459
|
+
dispatchedFingerprints.add(fp);
|
|
460
|
+
const idHash = traceHash(stableTraceStringify({
|
|
461
|
+
name: recovered.name,
|
|
462
|
+
args,
|
|
463
|
+
leak: true,
|
|
464
|
+
})).slice(0, 16);
|
|
465
|
+
const call = {
|
|
466
|
+
id: `gemini_leaked_${idHash}`,
|
|
467
|
+
name: recovered.name,
|
|
468
|
+
arguments: args,
|
|
469
|
+
};
|
|
470
|
+
leakedCalls.push(call);
|
|
471
|
+
try { onToolCall?.(call); } catch {}
|
|
472
|
+
try { onStreamDelta?.(); } catch {}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const pumpLeakBuffer = (final) => {
|
|
476
|
+
if (!_enabled) return;
|
|
477
|
+
if (!leakBuffer && !final) return;
|
|
478
|
+
const { emit, calls, rest } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final });
|
|
479
|
+
leakBuffer = rest;
|
|
480
|
+
if (emit && onTextDelta) {
|
|
481
|
+
try { onTextDelta(emit); } catch {}
|
|
482
|
+
}
|
|
483
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
get enabled() { return _enabled; },
|
|
488
|
+
feedText(text) {
|
|
489
|
+
if (!text) return;
|
|
490
|
+
if (!_enabled) {
|
|
491
|
+
try { onTextDelta?.(text); } catch {}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
leakBuffer += text;
|
|
495
|
+
pumpLeakBuffer(false);
|
|
496
|
+
},
|
|
497
|
+
finalize() {
|
|
498
|
+
pumpLeakBuffer(true);
|
|
499
|
+
},
|
|
500
|
+
scrubAssistantText(raw) {
|
|
501
|
+
if (!raw) return '';
|
|
502
|
+
if (!_enabled) return raw;
|
|
503
|
+
const { emit, calls, rest } = scanLeakedToolCalls(raw, { isKnownTool: _isKnownTool, final: true });
|
|
504
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
505
|
+
return emit + rest;
|
|
506
|
+
},
|
|
507
|
+
filterNativeToolCalls(nativeCalls) {
|
|
508
|
+
if (!_enabled || !nativeCalls?.length) return nativeCalls;
|
|
509
|
+
const kept = [];
|
|
510
|
+
for (const call of nativeCalls) {
|
|
511
|
+
const fp = toolCallFingerprint(call?.name, call?.arguments);
|
|
512
|
+
if (dispatchedFingerprints.has(fp)) continue;
|
|
513
|
+
dispatchedFingerprints.add(fp);
|
|
514
|
+
kept.push(call);
|
|
515
|
+
}
|
|
516
|
+
return kept.length ? kept : undefined;
|
|
517
|
+
},
|
|
518
|
+
getLeakedToolCalls() {
|
|
519
|
+
return leakedCalls.length ? [...leakedCalls] : [];
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
|
|
416
525
|
if (!response?.body) throw new Error(`${label}: missing response body`);
|
|
417
526
|
const reader = response.body.getReader();
|
|
418
527
|
const decoder = new TextDecoder();
|
|
@@ -509,9 +618,9 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
|
|
|
509
618
|
}
|
|
510
619
|
allChunks.push(parsed);
|
|
511
620
|
try { onStreamDelta?.(); } catch {}
|
|
512
|
-
if (onTextDelta) {
|
|
621
|
+
if (onTextDelta || textLeakGuard) {
|
|
513
622
|
const t = geminiChunkText(parsed);
|
|
514
|
-
|
|
623
|
+
relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
515
624
|
}
|
|
516
625
|
}
|
|
517
626
|
}
|
|
@@ -528,9 +637,9 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
|
|
|
528
637
|
}
|
|
529
638
|
allChunks.push(parsed);
|
|
530
639
|
try { onStreamDelta?.(); } catch {}
|
|
531
|
-
if (onTextDelta) {
|
|
640
|
+
if (onTextDelta || textLeakGuard) {
|
|
532
641
|
const t = geminiChunkText(parsed);
|
|
533
|
-
|
|
642
|
+
relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
534
643
|
}
|
|
535
644
|
} catch { /* skip malformed tail */ }
|
|
536
645
|
}
|
|
@@ -541,6 +650,7 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
|
|
|
541
650
|
if (idleTimer) clearTimeout(idleTimer);
|
|
542
651
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
543
652
|
try { reader.releaseLock(); } catch {}
|
|
653
|
+
try { textLeakGuard?.finalize(); } catch {}
|
|
544
654
|
}
|
|
545
655
|
|
|
546
656
|
const aggregated = aggregateGeminiStreamChunks(allChunks);
|
|
@@ -549,7 +659,7 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
|
|
|
549
659
|
return aggregated;
|
|
550
660
|
}
|
|
551
661
|
|
|
552
|
-
async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onTextDelta, label }) {
|
|
662
|
+
async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
|
|
553
663
|
let sawStreamChunk = false;
|
|
554
664
|
let idleTimedOut = false;
|
|
555
665
|
let idleTimer = null;
|
|
@@ -665,9 +775,9 @@ async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onT
|
|
|
665
775
|
}
|
|
666
776
|
resetIdleTimer();
|
|
667
777
|
try { onStreamDelta?.(); } catch {}
|
|
668
|
-
if (onTextDelta) {
|
|
778
|
+
if (onTextDelta || textLeakGuard) {
|
|
669
779
|
const t = geminiChunkText(step.value);
|
|
670
|
-
|
|
780
|
+
relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
671
781
|
}
|
|
672
782
|
}
|
|
673
783
|
if (idleTimedOut) {
|
|
@@ -686,6 +796,7 @@ async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onT
|
|
|
686
796
|
if (signal && onSignalAbort) {
|
|
687
797
|
try { signal.removeEventListener('abort', onSignalAbort); } catch {}
|
|
688
798
|
}
|
|
799
|
+
try { textLeakGuard?.finalize(); } catch {}
|
|
689
800
|
}
|
|
690
801
|
|
|
691
802
|
let response;
|
|
@@ -1241,11 +1352,44 @@ export class GeminiProvider {
|
|
|
1241
1352
|
// Best-effort cleanup of the previous cache so storage cost only
|
|
1242
1353
|
// accrues on the live revision. Fire-and-forget; TTL expiry covers
|
|
1243
1354
|
// any delete failures.
|
|
1355
|
+
//
|
|
1356
|
+
// Cross-session race: `_geminiGlobalCacheNameIsLive` only checks
|
|
1357
|
+
// whether `priorCacheName` still appears as *some* entry's live
|
|
1358
|
+
// cacheName in `geminiGlobalCaches`. If another session sharing the
|
|
1359
|
+
// same globalCacheKey already overwrote that map slot with a newer
|
|
1360
|
+
// cache (via `_setGeminiGlobalCache`), the check sees "not live" for
|
|
1361
|
+
// a name that a *different* in-flight session still holds in its own
|
|
1362
|
+
// `providerState.gemini.cacheName` (captured earlier via
|
|
1363
|
+
// `_attachGeminiCacheState` and possibly already in-flight inside a
|
|
1364
|
+
// `generateContent`/`streamGenerateContent` call at L1470-1473).
|
|
1365
|
+
// Deleting immediately can 404 that concurrent request server-side.
|
|
1366
|
+
//
|
|
1367
|
+
// Fix chosen: delay the DELETE by a grace period instead of adding
|
|
1368
|
+
// refcounting/last-used-session tracking. Rationale (minimal-change,
|
|
1369
|
+
// matches the module's existing "best-effort, TTL is the backstop"
|
|
1370
|
+
// posture at L1342-1343):
|
|
1371
|
+
// - Any session that captured `priorCacheName` did so before this
|
|
1372
|
+
// create finished, so its in-flight (or next) turn using that
|
|
1373
|
+
// name almost certainly completes within a couple of minutes;
|
|
1374
|
+
// a short grace window is enough for it to either finish or move
|
|
1375
|
+
// on to a fresh cache attach.
|
|
1376
|
+
// - The server-side cache TTL (1h) already reclaims any cache we
|
|
1377
|
+
// fail to delete, so skipping/delaying deletion is safe — it
|
|
1378
|
+
// only costs a little extra storage for at most the grace
|
|
1379
|
+
// window, never correctness.
|
|
1380
|
+
// - Refcounting/session tracking would need to plumb per-session
|
|
1381
|
+
// liveness into a shared map across concurrent providers, which
|
|
1382
|
+
// is a much larger change for a purely cosmetic cost saving.
|
|
1383
|
+
// Re-check liveness right before firing the DELETE too, in case the
|
|
1384
|
+
// name became live again (e.g. re-attached) during the wait.
|
|
1244
1385
|
const priorCacheName = state?.cacheName || null;
|
|
1245
|
-
if (priorCacheName && priorCacheName !== cacheName
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1386
|
+
if (priorCacheName && priorCacheName !== cacheName) {
|
|
1387
|
+
setTimeout(() => {
|
|
1388
|
+
if (_geminiGlobalCacheNameIsLive(priorCacheName)) return;
|
|
1389
|
+
const delUrl = `https://generativelanguage.googleapis.com/v1beta/${priorCacheName}?key=${encodeURIComponent(apiKey)}`;
|
|
1390
|
+
fetch(delUrl, { method: 'DELETE', signal: AbortSignal.timeout(10_000), dispatcher: getLlmDispatcher() })
|
|
1391
|
+
.catch(() => { /* TTL expiry will reclaim it */ });
|
|
1392
|
+
}, GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS).unref?.();
|
|
1249
1393
|
}
|
|
1250
1394
|
const createdAt = Date.now();
|
|
1251
1395
|
const entry = {
|
|
@@ -1322,6 +1466,14 @@ export class GeminiProvider {
|
|
|
1322
1466
|
const toolConfig = functionGeminiTools.length ? toGeminiToolConfig(opts.toolChoice) : undefined;
|
|
1323
1467
|
try { opts.onStageChange?.('requesting'); } catch {}
|
|
1324
1468
|
|
|
1469
|
+
const buildTextLeakGuard = () => createGeminiTextLeakGuard({
|
|
1470
|
+
knownToolNames: tools?.map((t) => t.name).filter(Boolean) ?? [],
|
|
1471
|
+
onTextDelta,
|
|
1472
|
+
onToolCall,
|
|
1473
|
+
onStreamDelta,
|
|
1474
|
+
});
|
|
1475
|
+
let textLeakGuard = null;
|
|
1476
|
+
|
|
1325
1477
|
// Explicit cachedContents (system + tools + prior-turn transcript).
|
|
1326
1478
|
// Per Google docs, `tools` must be supplied on BOTH the cache create
|
|
1327
1479
|
// call AND every subsequent generate_content call — the cache stores
|
|
@@ -1401,10 +1553,12 @@ export class GeminiProvider {
|
|
|
1401
1553
|
err.status = res.status;
|
|
1402
1554
|
throw err;
|
|
1403
1555
|
}
|
|
1556
|
+
textLeakGuard = buildTextLeakGuard();
|
|
1404
1557
|
return await consumeGeminiRestStreamResponse(res, {
|
|
1405
1558
|
signal: attemptSignal,
|
|
1406
1559
|
onStreamDelta,
|
|
1407
1560
|
onTextDelta,
|
|
1561
|
+
textLeakGuard,
|
|
1408
1562
|
label: 'Gemini REST streamGenerateContent',
|
|
1409
1563
|
});
|
|
1410
1564
|
},
|
|
@@ -1485,10 +1639,12 @@ export class GeminiProvider {
|
|
|
1485
1639
|
// timer but KEEP the parent link attached so a later
|
|
1486
1640
|
// abort during streaming still reaches the request.
|
|
1487
1641
|
clearConnectTimer();
|
|
1642
|
+
textLeakGuard = buildTextLeakGuard();
|
|
1488
1643
|
return await consumeGeminiSdkStream(streamResult, {
|
|
1489
1644
|
signal: attemptSignal,
|
|
1490
1645
|
onStreamDelta,
|
|
1491
1646
|
onTextDelta,
|
|
1647
|
+
textLeakGuard,
|
|
1492
1648
|
label: 'Gemini SDK streamGenerateContent',
|
|
1493
1649
|
});
|
|
1494
1650
|
} finally {
|
|
@@ -1519,10 +1675,21 @@ export class GeminiProvider {
|
|
|
1519
1675
|
});
|
|
1520
1676
|
const candidate = response.candidates?.[0] || null;
|
|
1521
1677
|
const textParts = candidate?.content?.parts?.filter(p => 'text' in p) ?? [];
|
|
1522
|
-
const
|
|
1523
|
-
const
|
|
1678
|
+
const rawContent = textParts.map(p => 'text' in p ? p.text : '').join('');
|
|
1679
|
+
const content = textLeakGuard?.enabled
|
|
1680
|
+
? textLeakGuard.scrubAssistantText(rawContent)
|
|
1681
|
+
: rawContent;
|
|
1682
|
+
const leakedToolCalls = textLeakGuard?.getLeakedToolCalls() ?? [];
|
|
1683
|
+
let nativeToolCalls = parseToolCalls(candidate?.content?.parts ?? []);
|
|
1684
|
+
if (textLeakGuard?.enabled) {
|
|
1685
|
+
nativeToolCalls = textLeakGuard.filterNativeToolCalls(nativeToolCalls);
|
|
1686
|
+
}
|
|
1687
|
+
let toolCalls = nativeToolCalls;
|
|
1688
|
+
if (leakedToolCalls.length) {
|
|
1689
|
+
toolCalls = toolCalls?.length ? [...toolCalls, ...leakedToolCalls] : leakedToolCalls;
|
|
1690
|
+
}
|
|
1524
1691
|
const citations = collectGeminiGroundingSources(candidate);
|
|
1525
|
-
emitGeminiToolCalls(
|
|
1692
|
+
emitGeminiToolCalls(nativeToolCalls, onToolCall);
|
|
1526
1693
|
// Inspect candidate.finishReason — Gemini reports terminal status here.
|
|
1527
1694
|
// Only STOP (and the legacy "FINISH_REASON_STOP") plus tool/function-
|
|
1528
1695
|
// call paths represent a fully delivered turn. MAX_TOKENS / SAFETY /
|