mixdog 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -3
- 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/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- 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/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- 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/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- 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 +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -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 +131 -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 +94 -16
- 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-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- 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/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- 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.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- 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/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- 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-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 +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- 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 +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- 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/child-spawn-gate.mjs +0 -6
- 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/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- 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 +146 -9
- 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 +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- 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 +0 -11
- 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 -647
- 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 +81 -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 +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- 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 +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
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
|
-
import { classifyError, midstreamBackoffFor, withRetry } from './retry-classifier.mjs';
|
|
4
|
+
import { classifyError, midstreamBackoffFor, sleepWithAbort, withRetry } from './retry-classifier.mjs';
|
|
5
5
|
import { traceAgentUsage } from '../agent-trace.mjs';
|
|
6
6
|
import {
|
|
7
7
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
@@ -15,29 +15,29 @@ 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';
|
|
20
25
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
let _Anthropic = null;
|
|
29
|
+
function loadAnthropic() {
|
|
30
|
+
if (!_Anthropic) {
|
|
31
|
+
const mod = require('@anthropic-ai/sdk');
|
|
32
|
+
_Anthropic = mod.default || mod.Anthropic || mod;
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const reason = signal.reason;
|
|
36
|
-
reject(reason instanceof Error ? reason : new Error('Anthropic mid-stream retry backoff aborted'));
|
|
37
|
-
};
|
|
38
|
-
if (signal.aborted) { onAbort(); return; }
|
|
39
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
40
|
-
});
|
|
34
|
+
return _Anthropic;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Abort-aware mid-stream backoff sleep → shared sleepWithAbort
|
|
38
|
+
// (retry-classifier.mjs). abortMessage preserves the prior fallback text.
|
|
39
|
+
function _midstreamSleepWithAbort(ms, signal) {
|
|
40
|
+
return sleepWithAbort(ms, signal, undefined, 'Anthropic mid-stream retry backoff aborted');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// 4-BP cache policy aligned with anthropic-oauth — system + tier3 +
|
|
@@ -132,6 +132,10 @@ function _defaultContextForModel(id, family) {
|
|
|
132
132
|
return 200000;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function _capabilitySupported(capability) {
|
|
136
|
+
return capability === true || capability?.supported === true;
|
|
137
|
+
}
|
|
138
|
+
|
|
135
139
|
function _normalizeAnthropicModel(raw, provider = 'anthropic') {
|
|
136
140
|
const id = raw?.id || raw?.name || raw?.model;
|
|
137
141
|
if (!id) return null;
|
|
@@ -139,15 +143,18 @@ function _normalizeAnthropicModel(raw, provider = 'anthropic') {
|
|
|
139
143
|
const family = familyMatch ? familyMatch[1].toLowerCase() : 'other';
|
|
140
144
|
const dated = /-\d{8}$/.test(String(id));
|
|
141
145
|
const versioned = !dated && /^claude-[a-z]+-\d+(?:-\d+)?$/i.test(String(id));
|
|
146
|
+
const effortValues = effortValuesForModel(raw?.capabilities, id);
|
|
142
147
|
return {
|
|
143
148
|
id,
|
|
144
149
|
display: raw?.display_name || raw?.displayName || raw?.display || _prettyName(id, family),
|
|
145
150
|
family,
|
|
146
151
|
provider,
|
|
147
|
-
contextWindow: raw?.context_window || raw?.max_context_window || raw?.input_token_limit || raw?.inputTokenLimit || _defaultContextForModel(id, family),
|
|
148
|
-
outputTokens: raw?.max_output_tokens || raw?.output_token_limit || raw?.outputTokenLimit || null,
|
|
152
|
+
contextWindow: raw?.context_window || raw?.max_context_window || raw?.max_input_tokens || raw?.input_token_limit || raw?.inputTokenLimit || _defaultContextForModel(id, family),
|
|
153
|
+
outputTokens: raw?.max_tokens || raw?.max_output_tokens || raw?.output_token_limit || raw?.outputTokenLimit || null,
|
|
149
154
|
tier: dated ? 'dated' : versioned ? 'version' : 'family',
|
|
150
155
|
latest: false,
|
|
156
|
+
supportsReasoning: effortValues.length > 0 || _capabilitySupported(raw?.capabilities?.thinking),
|
|
157
|
+
reasoningOptions: effortValues.length ? [{ type: 'effort', values: effortValues }] : [],
|
|
151
158
|
};
|
|
152
159
|
}
|
|
153
160
|
// Family-based heuristic so new model ids (including custom user-configured
|
|
@@ -160,15 +167,6 @@ function resolveMaxTokens(model) {
|
|
|
160
167
|
return 8192;
|
|
161
168
|
}
|
|
162
169
|
|
|
163
|
-
// Effort → thinking budget tokens (Anthropic extended thinking)
|
|
164
|
-
const EFFORT_BUDGET = {
|
|
165
|
-
low: 1024,
|
|
166
|
-
medium: 4096,
|
|
167
|
-
high: 16384,
|
|
168
|
-
xhigh: 32768,
|
|
169
|
-
max: 32768,
|
|
170
|
-
};
|
|
171
|
-
|
|
172
170
|
const MIN_THINKING_BUDGET = 1024;
|
|
173
171
|
const THINKING_OUTPUT_RESERVE = 1024;
|
|
174
172
|
|
|
@@ -404,7 +402,7 @@ export class AnthropicProvider {
|
|
|
404
402
|
this.config = config;
|
|
405
403
|
this.name = config.name || 'anthropic';
|
|
406
404
|
const betaHeaders = config.disableBetaHeaders ? null : buildAnthropicBetaHeaders({ toolSearch: true });
|
|
407
|
-
this.client = new
|
|
405
|
+
this.client = new (loadAnthropic())({
|
|
408
406
|
apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
|
|
409
407
|
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
410
408
|
defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(config.extraHeaders || {}) },
|
|
@@ -418,7 +416,7 @@ export class AnthropicProvider {
|
|
|
418
416
|
if (newKey) {
|
|
419
417
|
this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey };
|
|
420
418
|
const betaHeaders = this.config.disableBetaHeaders ? null : buildAnthropicBetaHeaders({ toolSearch: true });
|
|
421
|
-
this.client = new
|
|
419
|
+
this.client = new (loadAnthropic())({
|
|
422
420
|
apiKey: newKey,
|
|
423
421
|
...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
|
|
424
422
|
defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(this.config.extraHeaders || {}) },
|
|
@@ -496,16 +494,21 @@ export class AnthropicProvider {
|
|
|
496
494
|
// Anthropic prefix semantics (order: tools → system → messages).
|
|
497
495
|
params.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredAnthropicTools(tools || [], opts)])];
|
|
498
496
|
}
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
?
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
497
|
+
// Known tool names for the shared parseSSEStream leaked-tool-call guard
|
|
498
|
+
// (same guard fixes both Anthropic providers). Recovered leaked calls
|
|
499
|
+
// are only synthesized when they name a tool actually offered here.
|
|
500
|
+
const knownToolNames = new Set(
|
|
501
|
+
(Array.isArray(params.tools) ? params.tools : [])
|
|
502
|
+
.map((t) => (t && typeof t.name === 'string' ? t.name : null))
|
|
503
|
+
.filter(Boolean),
|
|
504
|
+
);
|
|
505
|
+
applyAnthropicEffortToBody(params, {
|
|
506
|
+
model: useModel,
|
|
507
|
+
opts,
|
|
508
|
+
maxTokens,
|
|
509
|
+
clampThinkingBudgetTokens,
|
|
510
|
+
logTag: this.name,
|
|
511
|
+
});
|
|
509
512
|
// Fast mode → speed: "fast" on models Anthropic marks as speed-capable.
|
|
510
513
|
if (opts.fast === true && supportsAnthropicFastMode(useModel)) {
|
|
511
514
|
params.speed = 'fast';
|
|
@@ -539,12 +542,20 @@ export class AnthropicProvider {
|
|
|
539
542
|
try { totalSignal.removeEventListener('abort', handler); } catch {}
|
|
540
543
|
};
|
|
541
544
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
545
|
+
// Per-call headers override the client defaultHeaders, so the
|
|
546
|
+
// constructor-level disableBetaHeaders opt-out must be honoured here
|
|
547
|
+
// too — otherwise opencode-go's anthropic-compatible routing
|
|
548
|
+
// (disableBetaHeaders:true) would still send beta strings that a
|
|
549
|
+
// third-party endpoint may reject.
|
|
550
|
+
const betaHeaders = this.config?.disableBetaHeaders
|
|
551
|
+
? null
|
|
552
|
+
: {
|
|
553
|
+
'anthropic-beta': buildAnthropicBetaHeaders({
|
|
554
|
+
fastMode: this.fastModeBetaHeaderLatched,
|
|
555
|
+
toolSearch: true,
|
|
556
|
+
effort: shouldIncludeEffortBeta(useModel, opts),
|
|
557
|
+
}),
|
|
558
|
+
};
|
|
548
559
|
|
|
549
560
|
const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
|
|
550
561
|
let firstAttemptError = null;
|
|
@@ -638,7 +649,7 @@ export class AnthropicProvider {
|
|
|
638
649
|
async ({ signal: attemptSignal }) => {
|
|
639
650
|
const res = await this.client.messages.create(params, {
|
|
640
651
|
signal: attemptSignal,
|
|
641
|
-
headers: betaHeaders,
|
|
652
|
+
...(betaHeaders ? { headers: betaHeaders } : {}),
|
|
642
653
|
}).asResponse();
|
|
643
654
|
if (!res.ok) {
|
|
644
655
|
const text = await res.text().catch(() => '');
|
|
@@ -691,11 +702,12 @@ export class AnthropicProvider {
|
|
|
691
702
|
const parseResult = await parseSSEStream(
|
|
692
703
|
response,
|
|
693
704
|
streamController.signal,
|
|
694
|
-
() => streamController.abort(),
|
|
705
|
+
(reason) => streamController.abort(reason),
|
|
695
706
|
onStreamDelta,
|
|
696
707
|
onToolCall,
|
|
697
708
|
midState,
|
|
698
709
|
onTextDelta,
|
|
710
|
+
knownToolNames,
|
|
699
711
|
);
|
|
700
712
|
|
|
701
713
|
if (firstBytePoll) {
|
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
|
|
@@ -41,14 +41,6 @@ function readJson(file) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
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
44
|
function cacheKey(provider) {
|
|
53
45
|
return String(provider || '').trim().toLowerCase();
|
|
54
46
|
}
|
|
@@ -68,16 +60,31 @@ export function readCachedApiUsageSnapshot(provider, { allowStale = true } = {})
|
|
|
68
60
|
|
|
69
61
|
function writeCachedApiUsageSnapshot(provider, snapshot) {
|
|
70
62
|
const file = cachePath();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
63
|
+
// Synchronous atomic+lock write (updateJsonAtomicSync) rather than the
|
|
64
|
+
// prior fire-and-forget fsp.writeFile: the read-modify-write of
|
|
65
|
+
// `snapshots` must happen inside the same file lock as the write, or two
|
|
66
|
+
// concurrent orchestrator processes updating different providers can
|
|
67
|
+
// clobber each other's snapshot merge (last writer wins, losing the
|
|
68
|
+
// other's entry). Usage snapshot writes are infrequent (once per
|
|
69
|
+
// provider per TTL window, not per-request), so trading the async/
|
|
70
|
+
// non-blocking write for lock+fsync-dir safety has no meaningful
|
|
71
|
+
// latency impact on the request path.
|
|
72
|
+
let next = null;
|
|
73
|
+
try {
|
|
74
|
+
next = updateJsonAtomicSync(file, (curRaw) => {
|
|
75
|
+
const cur = curRaw && typeof curRaw === 'object' ? curRaw : {};
|
|
76
|
+
const snapshots = cur.snapshots && typeof cur.snapshots === 'object' ? cur.snapshots : {};
|
|
77
|
+
return {
|
|
78
|
+
version: 1,
|
|
79
|
+
updatedAt: Date.now(),
|
|
80
|
+
snapshots: {
|
|
81
|
+
...snapshots,
|
|
82
|
+
[cacheKey(provider)]: snapshot,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}, { lock: true, fsyncDir: true, timeoutMs: 1000 }); // best-effort cache write: short lock timeout, don't block on contention
|
|
86
|
+
} catch {}
|
|
87
|
+
if (next) diskJsonCache = { at: Date.now(), file, value: next };
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
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 (mirrors openai-oauth's schema-version gate).
|
|
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 /
|
|
@@ -113,6 +113,9 @@ export function normalizeGrokModelId(id) {
|
|
|
113
113
|
return (id && RETIRED_MODEL_ALIASES[id]) || id;
|
|
114
114
|
}
|
|
115
115
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
116
|
+
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
117
|
+
// discarded instead of misread (mirrors openai-oauth's schema-version gate).
|
|
118
|
+
const GROK_MODEL_CACHE_SCHEMA_VERSION = 1;
|
|
116
119
|
const DISCOVERY_TIMEOUT_MS = 15_000;
|
|
117
120
|
const TOKEN_TIMEOUT_MS = 30_000;
|
|
118
121
|
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -362,7 +365,11 @@ async function refreshTokens(tokens) {
|
|
|
362
365
|
}
|
|
363
366
|
|
|
364
367
|
// --- Model catalog cache (24h disk TTL) ---
|
|
365
|
-
const _modelCache = makeModelCache({
|
|
368
|
+
const _modelCache = makeModelCache({
|
|
369
|
+
fileName: 'grok-oauth-models.json',
|
|
370
|
+
ttlMs: MODEL_CACHE_TTL_MS,
|
|
371
|
+
version: GROK_MODEL_CACHE_SCHEMA_VERSION,
|
|
372
|
+
});
|
|
366
373
|
const PROXY_MODEL_METADATA = {
|
|
367
374
|
'grok-build': { display: 'Grok Build', contextWindow: 512000 },
|
|
368
375
|
'grok-composer-2.5-fast': { display: 'Composer 2.5 Fast', contextWindow: 200000 },
|