mixdog 0.9.50 → 0.9.52
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 +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -49,8 +49,8 @@ function _captureMidstreamAbort(state, reason) {
|
|
|
49
49
|
|
|
50
50
|
// Abort-aware mid-stream backoff sleep → shared sleepWithAbort
|
|
51
51
|
// (retry-classifier.mjs). abortMessage preserves the prior fallback text.
|
|
52
|
-
export function _midstreamSleepWithAbort(ms, signal) {
|
|
53
|
-
return sleepWithAbort(ms, signal,
|
|
52
|
+
export function _midstreamSleepWithAbort(ms, signal, sleepFn) {
|
|
53
|
+
return sleepWithAbort(ms, signal, sleepFn, 'Anthropic OAuth mid-stream retry backoff aborted');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function _statusForAnthropicSseError(type, message) {
|
|
@@ -83,9 +83,8 @@ function _anthropicSseError(event) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta, knownToolNames) {
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
try { onStreamDelta?.('transport'); } catch {}
|
|
86
|
+
// onStreamDelta is a semantic-progress callback. HTTP headers and raw SSE
|
|
87
|
+
// bytes prove transport health but must not refresh semantic watchdogs.
|
|
89
88
|
const reader = response.body.getReader();
|
|
90
89
|
const decoder = new TextDecoder();
|
|
91
90
|
// SEMANTIC idle window: reset only by real model events (message/content/
|
|
@@ -287,17 +286,18 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
287
286
|
if (_c && typeof _c.catch === 'function') _c.catch(() => {});
|
|
288
287
|
} catch {}
|
|
289
288
|
};
|
|
290
|
-
if (signal) {
|
|
291
|
-
if (signal.aborted) {
|
|
292
|
-
_captureMidstreamAbort(state, signal.reason);
|
|
293
|
-
throw signal.reason instanceof Error
|
|
294
|
-
? signal.reason
|
|
295
|
-
: new Error('Anthropic OAuth SSE stream aborted');
|
|
296
|
-
}
|
|
297
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
289
|
try {
|
|
290
|
+
// Reader ownership begins at getReader() above, so even a signal that
|
|
291
|
+
// was already aborted must pass through this try/finally cleanup path.
|
|
292
|
+
if (signal) {
|
|
293
|
+
if (signal.aborted) {
|
|
294
|
+
_captureMidstreamAbort(state, signal.reason);
|
|
295
|
+
throw signal.reason instanceof Error
|
|
296
|
+
? signal.reason
|
|
297
|
+
: new Error('Anthropic OAuth SSE stream aborted');
|
|
298
|
+
}
|
|
299
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
300
|
+
}
|
|
301
301
|
// Part A / reviewer fix: do NOT arm the SEMANTIC idle timer before the
|
|
302
302
|
// stream has produced its first event. A slow first response is governed
|
|
303
303
|
// by armFirstMessageTimer() (first-byte window) alone; arming the
|
|
@@ -331,8 +331,6 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
331
331
|
}
|
|
332
332
|
const { done, value } = chunk;
|
|
333
333
|
if (done) break;
|
|
334
|
-
try { onStreamDelta?.('transport'); } catch {}
|
|
335
|
-
|
|
336
334
|
buffer += decoder.decode(value, { stream: true });
|
|
337
335
|
const lines = buffer.split('\n');
|
|
338
336
|
buffer = lines.pop() || '';
|
|
@@ -382,6 +380,12 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
382
380
|
if (event.type === 'message_start' && event.message) {
|
|
383
381
|
clearFirstMessageTimer();
|
|
384
382
|
if (state) state.sawMessageStart = true;
|
|
383
|
+
// The first protocol event proves the response transport
|
|
384
|
+
// is live. Report it only here (never for raw bytes,
|
|
385
|
+
// comments, or ping events), then separately report the
|
|
386
|
+
// semantic message boundary. Content kinds remain
|
|
387
|
+
// reasoning/text/tool-specific below.
|
|
388
|
+
try { onStreamDelta?.('transport'); } catch {}
|
|
385
389
|
try { onStreamDelta?.('semantic'); } catch {}
|
|
386
390
|
if (event.message.model) model = event.message.model;
|
|
387
391
|
if (event.message.usage) {
|
|
@@ -395,6 +399,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
395
399
|
if (event.type === 'content_block_start') {
|
|
396
400
|
const block = event.content_block;
|
|
397
401
|
if (block?.type === 'tool_use') {
|
|
402
|
+
if (state) state.partialToolCall = true;
|
|
398
403
|
pendingToolInputs.set(event.index, {
|
|
399
404
|
id: block.id || '',
|
|
400
405
|
name: block.name || '',
|
|
@@ -402,6 +407,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
402
407
|
});
|
|
403
408
|
}
|
|
404
409
|
if (block?.type === 'thinking' || block?.type === 'redacted_thinking') {
|
|
410
|
+
if (state) state.emittedThinking = true;
|
|
405
411
|
if (block.type === 'redacted_thinking') {
|
|
406
412
|
// Redacted blocks round-trip EXACTLY as
|
|
407
413
|
// {type:'redacted_thinking',data} — no thinking/
|
|
@@ -463,6 +469,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
463
469
|
}
|
|
464
470
|
}
|
|
465
471
|
if (delta?.type === 'thinking_delta' || delta?.type === 'signature_delta') {
|
|
472
|
+
if (state) state.emittedThinking = true;
|
|
466
473
|
// Extended-thinking block: provider reasoning without
|
|
467
474
|
// user-visible text. Track presence so a final turn
|
|
468
475
|
// that emitted ONLY thinking (no text_delta, no
|
|
@@ -490,6 +497,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
490
497
|
}
|
|
491
498
|
}
|
|
492
499
|
if (delta?.type === 'input_json_delta') {
|
|
500
|
+
if (state) state.partialToolCall = true;
|
|
493
501
|
const pending = pendingToolInputs.get(event.index);
|
|
494
502
|
if (pending) {
|
|
495
503
|
pending.inputJson += delta.partial_json || '';
|
|
@@ -658,6 +666,10 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
658
666
|
if (idleTimer) clearTimeout(idleTimer);
|
|
659
667
|
clearFirstMessageTimer();
|
|
660
668
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
669
|
+
// message_stop deliberately exits before EOF because Anthropic may keep
|
|
670
|
+
// sending pings. Cancel the reader so the successful response body and
|
|
671
|
+
// underlying keep-alive connection are not stranded.
|
|
672
|
+
try { await reader.cancel('Anthropic SSE complete'); } catch {}
|
|
661
673
|
try { reader.releaseLock(); } catch (err) {
|
|
662
674
|
try { process.stderr.write(`[anthropic-oauth] reader releaseLock failed: ${err?.message ?? String(err)}\n`); } catch {}
|
|
663
675
|
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
3
3
|
import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ANTHROPIC_RETRY_BACKOFF_MS,
|
|
6
|
+
ANTHROPIC_RETRY_JITTER_RATIO,
|
|
7
|
+
AnthropicFallbackTriggeredError,
|
|
8
|
+
anthropicMaxAttempts,
|
|
9
|
+
anthropicRequestTimeoutMs,
|
|
10
|
+
classifyError,
|
|
11
|
+
midstreamBackoffFor,
|
|
12
|
+
sleepWithAbort,
|
|
13
|
+
withRetry,
|
|
14
|
+
retryAfterMsFromError,
|
|
15
|
+
} from './retry-classifier.mjs';
|
|
5
16
|
import { traceAgentUsage } from '../agent-trace.mjs';
|
|
6
17
|
import {
|
|
7
18
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
@@ -23,9 +34,11 @@ import {
|
|
|
23
34
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
24
35
|
import { enrichModels } from './model-catalog.mjs';
|
|
25
36
|
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
37
|
+
import { providerNativeToolPrefixCount } from '../../../../session-runtime/provider-request-tools.mjs';
|
|
26
38
|
import { makeModelCache } from './model-cache.mjs';
|
|
27
39
|
import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
|
|
28
40
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
41
|
+
import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
|
|
29
42
|
|
|
30
43
|
const require = createRequire(import.meta.url);
|
|
31
44
|
let _Anthropic = null;
|
|
@@ -231,7 +244,12 @@ function resolveMaxTokens(model) {
|
|
|
231
244
|
}
|
|
232
245
|
|
|
233
246
|
// Test-only escape hatch for scripts/anthropic-maxtokens-test.mjs.
|
|
234
|
-
export const _test = {
|
|
247
|
+
export const _test = {
|
|
248
|
+
resolveMaxTokens,
|
|
249
|
+
deferredAnthropicTools,
|
|
250
|
+
requestAnthropicTools,
|
|
251
|
+
sanitizeInputSchema: _sanitizeInputSchema,
|
|
252
|
+
};
|
|
235
253
|
|
|
236
254
|
const MIN_THINKING_BUDGET = 1024;
|
|
237
255
|
const THINKING_OUTPUT_RESERVE = 1024;
|
|
@@ -356,6 +374,24 @@ function deferredAnthropicTools(activeTools, messages, opts) {
|
|
|
356
374
|
.filter((tool) => tool?.name && discovered.has(String(tool.name)) && !active.has(String(tool.name)))
|
|
357
375
|
.map((tool) => ({ ...tool, deferLoading: true }));
|
|
358
376
|
}
|
|
377
|
+
function requestAnthropicTools(tools, messages, opts) {
|
|
378
|
+
const activeTools = Array.isArray(tools) ? tools : [];
|
|
379
|
+
if (opts?.providerToolSnapshotAuthoritative === true) {
|
|
380
|
+
const nativePrefixCount = providerNativeToolPrefixCount(
|
|
381
|
+
activeTools,
|
|
382
|
+
opts.providerNativeToolPrefixCount,
|
|
383
|
+
);
|
|
384
|
+
return [
|
|
385
|
+
...activeTools.slice(0, nativePrefixCount),
|
|
386
|
+
...toAnthropicTools(activeTools.slice(nativePrefixCount)),
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
const deferredTools = deferredAnthropicTools(activeTools, messages, opts);
|
|
390
|
+
return [
|
|
391
|
+
...nativeAnthropicTools(opts),
|
|
392
|
+
...toAnthropicTools([...activeTools, ...deferredTools]),
|
|
393
|
+
];
|
|
394
|
+
}
|
|
359
395
|
function toAnthropicMessages(messages) {
|
|
360
396
|
// Marker-free lowering. cache_control is applied AFTER sanitization by
|
|
361
397
|
// applyAnthropicCacheMarkers() so that block drops/inserts/reorders
|
|
@@ -409,6 +445,7 @@ function toAnthropicMessages(messages) {
|
|
|
409
445
|
content: references.length
|
|
410
446
|
? references.map((tool_name) => ({ type: 'tool_reference', tool_name }))
|
|
411
447
|
: normalizeContentForAnthropic(m.content),
|
|
448
|
+
...((m.toolKind === 'error' || m.isError === true) ? { is_error: true } : {}),
|
|
412
449
|
};
|
|
413
450
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
414
451
|
last.content.push(block);
|
|
@@ -549,15 +586,18 @@ export class AnthropicProvider {
|
|
|
549
586
|
name = 'anthropic';
|
|
550
587
|
client;
|
|
551
588
|
config;
|
|
589
|
+
apiKey;
|
|
552
590
|
fastModeBetaHeaderLatched = false;
|
|
553
591
|
constructor(config) {
|
|
554
|
-
this.config = config;
|
|
555
|
-
this.name = config.name || 'anthropic';
|
|
556
|
-
|
|
592
|
+
this.config = config || {};
|
|
593
|
+
this.name = this.config.name || 'anthropic';
|
|
594
|
+
this.apiKey = this.config.apiKey || (this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
|
|
595
|
+
const betaHeaders = this.config.disableBetaHeaders ? null : buildAnthropicBetaHeaders();
|
|
557
596
|
this.client = new (loadAnthropic())({
|
|
558
|
-
apiKey:
|
|
559
|
-
...(config.baseURL ? { baseURL: config.baseURL } : {}),
|
|
560
|
-
defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(config.extraHeaders || {}) },
|
|
597
|
+
apiKey: this.apiKey,
|
|
598
|
+
...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
|
|
599
|
+
defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(this.config.extraHeaders || {}) },
|
|
600
|
+
maxRetries: 0,
|
|
561
601
|
});
|
|
562
602
|
}
|
|
563
603
|
reloadApiKey() {
|
|
@@ -567,11 +607,16 @@ export class AnthropicProvider {
|
|
|
567
607
|
|| (this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
|
|
568
608
|
if (newKey) {
|
|
569
609
|
this.config = { ...(this.config || {}), apiKey: newKey };
|
|
570
|
-
|
|
610
|
+
this.apiKey = newKey;
|
|
611
|
+
// Tool-search is a request capability, not an account
|
|
612
|
+
// capability. Keep it off the client defaults and add it only
|
|
613
|
+
// on turns that actually serialize defer_loading tools.
|
|
614
|
+
const betaHeaders = this.config.disableBetaHeaders ? null : buildAnthropicBetaHeaders();
|
|
571
615
|
this.client = new (loadAnthropic())({
|
|
572
616
|
apiKey: newKey,
|
|
573
617
|
...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
|
|
574
618
|
defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(this.config.extraHeaders || {}) },
|
|
619
|
+
maxRetries: 0,
|
|
575
620
|
});
|
|
576
621
|
}
|
|
577
622
|
} catch { /* best effort */ }
|
|
@@ -586,8 +631,16 @@ export class AnthropicProvider {
|
|
|
586
631
|
try {
|
|
587
632
|
return await this._doSend(messages, model, tools, sendOpts);
|
|
588
633
|
} catch (err) {
|
|
589
|
-
|
|
590
|
-
|
|
634
|
+
const status = Number(err?.status || err?.httpStatus || err?.response?.status || 0);
|
|
635
|
+
const outputWasExposed = err?.liveTextEmitted === true
|
|
636
|
+
|| err?.emittedText === true
|
|
637
|
+
|| err?.emittedToolCall === true
|
|
638
|
+
|| err?.toolCallEmitted === true
|
|
639
|
+
|| err?.partialToolCall === true
|
|
640
|
+
|| err?.emittedThinking === true
|
|
641
|
+
|| err?.unsafeToRetry === true;
|
|
642
|
+
if (status === 401
|
|
643
|
+
&& !outputWasExposed) {
|
|
591
644
|
process.stderr.write(`[provider] Auth error, re-reading provider authentication...\n`);
|
|
592
645
|
this.reloadApiKey();
|
|
593
646
|
return await this._doSend(messages, model, tools, sendOpts);
|
|
@@ -640,11 +693,11 @@ export class AnthropicProvider {
|
|
|
640
693
|
system: systemBlocks.length ? systemBlocks : undefined,
|
|
641
694
|
messages: anthropicMessages,
|
|
642
695
|
};
|
|
643
|
-
const
|
|
644
|
-
if (
|
|
696
|
+
const requestTools = requestAnthropicTools(tools, chatMsgs, opts);
|
|
697
|
+
if (requestTools.length) {
|
|
645
698
|
// No cache_control on tools — the system BP covers tools via
|
|
646
699
|
// Anthropic prefix semantics (order: tools → system → messages).
|
|
647
|
-
params.tools =
|
|
700
|
+
params.tools = requestTools;
|
|
648
701
|
}
|
|
649
702
|
// tool_choice only when tools are actually present (Anthropic rejects
|
|
650
703
|
// tool_choice without tools). 'none' rides the hard-cap final turn to
|
|
@@ -653,6 +706,8 @@ export class AnthropicProvider {
|
|
|
653
706
|
const toolChoice = toAnthropicToolChoice(opts.toolChoice);
|
|
654
707
|
if (toolChoice) params.tool_choice = toolChoice;
|
|
655
708
|
}
|
|
709
|
+
const hasDeferredTools = Array.isArray(params.tools)
|
|
710
|
+
&& params.tools.some((tool) => tool?.defer_loading === true);
|
|
656
711
|
// Known tool names for the shared parseSSEStream leaked-tool-call guard
|
|
657
712
|
// (same guard fixes both Anthropic providers). Recovered leaked calls
|
|
658
713
|
// are only synthesized when they name a tool actually offered here.
|
|
@@ -711,7 +766,7 @@ export class AnthropicProvider {
|
|
|
711
766
|
: {
|
|
712
767
|
'anthropic-beta': buildAnthropicBetaHeaders({
|
|
713
768
|
fastMode: this.fastModeBetaHeaderLatched,
|
|
714
|
-
toolSearch:
|
|
769
|
+
toolSearch: hasDeferredTools,
|
|
715
770
|
effort: shouldIncludeEffortBeta(useModel, opts),
|
|
716
771
|
}),
|
|
717
772
|
};
|
|
@@ -795,6 +850,8 @@ export class AnthropicProvider {
|
|
|
795
850
|
sawMessageStart: false,
|
|
796
851
|
sawCompleted: false,
|
|
797
852
|
emittedToolCall: false,
|
|
853
|
+
partialToolCall: false,
|
|
854
|
+
emittedThinking: false,
|
|
798
855
|
// Gateway live-text relay invariant: set by parseSSEStream
|
|
799
856
|
// once a non-empty text chunk has been forwarded. A later
|
|
800
857
|
// failure is non-retryable (rendered text cannot be
|
|
@@ -806,11 +863,12 @@ export class AnthropicProvider {
|
|
|
806
863
|
|
|
807
864
|
let firstBytePoll = null;
|
|
808
865
|
let firstByteTimeout = null;
|
|
866
|
+
let response = null;
|
|
809
867
|
|
|
810
868
|
try {
|
|
811
869
|
try { onStageChange?.('requesting'); } catch {}
|
|
812
870
|
|
|
813
|
-
|
|
871
|
+
response = await withRetry(
|
|
814
872
|
async ({ signal: attemptSignal }) => {
|
|
815
873
|
const res = await this.client.messages.create(params, {
|
|
816
874
|
signal: attemptSignal,
|
|
@@ -821,13 +879,14 @@ export class AnthropicProvider {
|
|
|
821
879
|
const err = new Error(`Anthropic API ${res.status}: ${text.slice(0, 200)}`);
|
|
822
880
|
err.status = res.status;
|
|
823
881
|
err.httpStatus = res.status;
|
|
882
|
+
err.initialResponseError = true;
|
|
824
883
|
// Carry response headers so withRetry can honor a
|
|
825
884
|
// short Retry-After and upstream can read quota hints.
|
|
826
885
|
err.headers = res.headers;
|
|
827
886
|
err.response = { status: res.status, headers: res.headers };
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
887
|
+
// This is an initial-response 429, before SSE
|
|
888
|
+
// output/tool exposure, so the request-local
|
|
889
|
+
// withRetry loop may retry it with jitter.
|
|
831
890
|
if (res.status === 429) {
|
|
832
891
|
const retryAfterMs = retryAfterMsFromError({ headers: res.headers, response: { headers: res.headers } });
|
|
833
892
|
err.name = 'ProviderQuotaError';
|
|
@@ -835,7 +894,6 @@ export class AnthropicProvider {
|
|
|
835
894
|
err.retryAfterMs = retryAfterMs;
|
|
836
895
|
err.providerQuota = true;
|
|
837
896
|
err.quotaExceeded = true;
|
|
838
|
-
err.unsafeToRetry = true;
|
|
839
897
|
}
|
|
840
898
|
throw err;
|
|
841
899
|
}
|
|
@@ -846,9 +904,17 @@ export class AnthropicProvider {
|
|
|
846
904
|
},
|
|
847
905
|
{
|
|
848
906
|
signal: totalSignal,
|
|
849
|
-
|
|
907
|
+
maxAttempts: anthropicMaxAttempts(),
|
|
908
|
+
backoffMs: ANTHROPIC_RETRY_BACKOFF_MS,
|
|
909
|
+
retryJitterRatio: ANTHROPIC_RETRY_JITTER_RATIO,
|
|
910
|
+
retryJitterMode: 'positive',
|
|
911
|
+
perAttemptTimeoutMs: anthropicRequestTimeoutMs(),
|
|
850
912
|
perAttemptLabel: `${this.name} Anthropic streaming response`,
|
|
913
|
+
model: useModel,
|
|
914
|
+
fallbackModel: opts._fallbackTriggered ? undefined : opts.fallbackModel,
|
|
851
915
|
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
916
|
+
const status = Number(lastErr?.httpStatus || lastErr?.status || lastErr?.response?.status || 0);
|
|
917
|
+
if (status === 429) notifyCurrentAnthropicRateLimit(lastErr);
|
|
852
918
|
const delayLabel = Number.isFinite(Number(delayMs))
|
|
853
919
|
? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}`
|
|
854
920
|
: '';
|
|
@@ -890,6 +956,7 @@ export class AnthropicProvider {
|
|
|
890
956
|
onTextDelta,
|
|
891
957
|
knownToolNames,
|
|
892
958
|
);
|
|
959
|
+
try { streamController.abort?.('Anthropic SSE complete'); } catch {}
|
|
893
960
|
|
|
894
961
|
if (firstBytePoll) {
|
|
895
962
|
clearInterval(firstBytePoll);
|
|
@@ -913,6 +980,14 @@ export class AnthropicProvider {
|
|
|
913
980
|
|
|
914
981
|
return buildReturnFromParse(parseResult);
|
|
915
982
|
} catch (err) {
|
|
983
|
+
if (err instanceof AnthropicFallbackTriggeredError) {
|
|
984
|
+
process.stderr.write(`[${this.name}] ${err.message}\n`);
|
|
985
|
+
return this._doSend(messages, err.fallbackModel, tools, {
|
|
986
|
+
...opts,
|
|
987
|
+
fallbackModel: undefined,
|
|
988
|
+
_fallbackTriggered: true,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
916
991
|
// Live-text invariant: once a non-empty text chunk has been
|
|
917
992
|
// relayed to the client (gateway live mode), the rendered
|
|
918
993
|
// output cannot be withdrawn and re-issuing would concatenate
|
|
@@ -934,10 +1009,24 @@ export class AnthropicProvider {
|
|
|
934
1009
|
firstAttemptError.liveTextEmitted = true;
|
|
935
1010
|
firstAttemptError.unsafeToRetry = true;
|
|
936
1011
|
} catch {}
|
|
937
|
-
throw
|
|
1012
|
+
throw err;
|
|
938
1013
|
}
|
|
939
1014
|
throw err;
|
|
940
1015
|
}
|
|
1016
|
+
if (midState.emittedToolCall || midState.partialToolCall || midState.emittedThinking) {
|
|
1017
|
+
try {
|
|
1018
|
+
err.emittedToolCall = !!midState.emittedToolCall;
|
|
1019
|
+
err.partialToolCall = !!midState.partialToolCall;
|
|
1020
|
+
err.emittedThinking = !!midState.emittedThinking;
|
|
1021
|
+
err.unsafeToRetry = true;
|
|
1022
|
+
} catch {}
|
|
1023
|
+
try { streamController.abort?.(err); } catch {}
|
|
1024
|
+
throw err;
|
|
1025
|
+
}
|
|
1026
|
+
// withRetry already exhausted the full request-level budget.
|
|
1027
|
+
// Do not accidentally grant an additional SSE retry budget
|
|
1028
|
+
// to an initial HTTP 429 that never produced a stream.
|
|
1029
|
+
if (err?.initialResponseError) throw err;
|
|
941
1030
|
if (err?.isEmptyStream && attemptIndex < MAX_MIDSTREAM_RETRIES) {
|
|
942
1031
|
firstAttemptError = err;
|
|
943
1032
|
firstAttemptClassifier = 'empty_stream';
|
|
@@ -953,6 +1042,8 @@ export class AnthropicProvider {
|
|
|
953
1042
|
if (classifyError(err) === 'transient'
|
|
954
1043
|
&& !midState.sawMessageStart
|
|
955
1044
|
&& !midState.emittedToolCall
|
|
1045
|
+
&& !midState.partialToolCall
|
|
1046
|
+
&& !midState.emittedThinking
|
|
956
1047
|
&& attemptIndex < MAX_MIDSTREAM_RETRIES) {
|
|
957
1048
|
firstAttemptError = err;
|
|
958
1049
|
firstAttemptClassifier = err?.providerErrorType || 'sse_transient';
|
|
@@ -968,6 +1059,8 @@ export class AnthropicProvider {
|
|
|
968
1059
|
if ((err?.truncatedStream === true || err?.code === 'TRUNCATED_STREAM')
|
|
969
1060
|
&& classifyError(err) === 'transient'
|
|
970
1061
|
&& !midState.emittedToolCall
|
|
1062
|
+
&& !midState.partialToolCall
|
|
1063
|
+
&& !midState.emittedThinking
|
|
971
1064
|
&& attemptIndex < MAX_MIDSTREAM_RETRIES) {
|
|
972
1065
|
firstAttemptError = err;
|
|
973
1066
|
firstAttemptClassifier = 'truncated_stream';
|
|
@@ -984,19 +1077,31 @@ export class AnthropicProvider {
|
|
|
984
1077
|
if (classifier && attemptIndex < MAX_MIDSTREAM_RETRIES) {
|
|
985
1078
|
firstAttemptError = err;
|
|
986
1079
|
firstAttemptClassifier = classifier;
|
|
1080
|
+
const status = Number(err?.httpStatus || err?.status || 0);
|
|
1081
|
+
let retryDelayMs = null;
|
|
1082
|
+
if (status === 429) {
|
|
1083
|
+
if (!err.headers && response?.headers) err.headers = response.headers;
|
|
1084
|
+
if (!err.response && response) err.response = { status, headers: response.headers };
|
|
1085
|
+
retryDelayMs = retryAfterMsFromError(err);
|
|
1086
|
+
if (retryDelayMs != null) err.retryAfterMs = retryDelayMs;
|
|
1087
|
+
notifyCurrentAnthropicRateLimit(err);
|
|
1088
|
+
}
|
|
987
1089
|
try { streamController.abort?.(err); } catch {}
|
|
988
1090
|
try {
|
|
989
1091
|
process.stderr.write(
|
|
990
1092
|
`[${this.name}] mid-stream recovered: retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES} (cause: ${classifier})\n`,
|
|
991
1093
|
);
|
|
992
1094
|
} catch {}
|
|
993
|
-
await _midstreamSleepWithAbort(
|
|
1095
|
+
await _midstreamSleepWithAbort(
|
|
1096
|
+
retryDelayMs ?? midstreamBackoffFor(attemptIndex + 1),
|
|
1097
|
+
totalSignal,
|
|
1098
|
+
);
|
|
994
1099
|
continue;
|
|
995
1100
|
}
|
|
996
1101
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
997
|
-
try {
|
|
998
|
-
try {
|
|
999
|
-
throw
|
|
1102
|
+
try { err.midstreamRetries = attemptIndex; } catch {}
|
|
1103
|
+
try { err.midstreamClassifier = firstAttemptClassifier; } catch {}
|
|
1104
|
+
throw err;
|
|
1000
1105
|
}
|
|
1001
1106
|
throw err;
|
|
1002
1107
|
} finally {
|
|
@@ -1011,7 +1116,7 @@ export class AnthropicProvider {
|
|
|
1011
1116
|
}
|
|
1012
1117
|
}
|
|
1013
1118
|
async listModels() {
|
|
1014
|
-
const apiKey = this.config?.apiKey || (this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
|
|
1119
|
+
const apiKey = this.apiKey || this.config?.apiKey || (this.name === 'anthropic' ? process.env.ANTHROPIC_API_KEY : null);
|
|
1015
1120
|
if (!apiKey) return MODELS;
|
|
1016
1121
|
try {
|
|
1017
1122
|
const base = String(this.config?.baseURL || 'https://api.anthropic.com').replace(/\/+$/, '');
|
|
@@ -1043,16 +1148,9 @@ export class AnthropicProvider {
|
|
|
1043
1148
|
}
|
|
1044
1149
|
}
|
|
1045
1150
|
async isAvailable() {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
1051
|
-
});
|
|
1052
|
-
return true;
|
|
1053
|
-
}
|
|
1054
|
-
catch {
|
|
1055
|
-
return false;
|
|
1056
|
-
}
|
|
1151
|
+
// Availability probes must not spend tokens or depend on a live
|
|
1152
|
+
// network. Dispatch owns authentication validation and 401 reload.
|
|
1153
|
+
return !!(this.apiKey || this.config?.apiKey
|
|
1154
|
+
|| (this.name === 'anthropic' && process.env.ANTHROPIC_API_KEY));
|
|
1057
1155
|
}
|
|
1058
1156
|
}
|
|
@@ -90,16 +90,20 @@ export function _geminiCachePrefixContents(contents, prefixCount) {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function _geminiCachePrefixHash({ model, systemInstruction, geminiTools, contents, prefixCount }) {
|
|
93
|
+
export function _geminiCachePrefixHash({ model, systemInstruction, geminiTools, toolConfig, contents, prefixCount }) {
|
|
94
94
|
return traceHash(stableTraceStringify({
|
|
95
95
|
model: model || null,
|
|
96
96
|
systemInstruction: systemInstruction || '',
|
|
97
97
|
tools: geminiTools || [],
|
|
98
|
+
toolConfig: toolConfig || null,
|
|
98
99
|
contents: _geminiCachePrefixContents(contents, prefixCount),
|
|
99
100
|
}));
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
// The provider's default explicit-cache TTL is five minutes. A six-minute
|
|
104
|
+
// global-cache floor made every freshly-created default cache ineligible for
|
|
105
|
+
// cross-session reuse. Ten seconds matches the per-session minimum headroom.
|
|
106
|
+
export const GEMINI_GLOBAL_CACHE_MIN_LIVE_MS = 10 * 1000;
|
|
103
107
|
export const GEMINI_GLOBAL_CACHE_MAX_ENTRIES = 128;
|
|
104
108
|
// Grace window before deleting a superseded cachedContents name (see the
|
|
105
109
|
// cross-session race note at the delete call site in gemini.mjs). Long enough
|
|
@@ -108,15 +112,30 @@ export const GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS = 2 * 60 * 1000;
|
|
|
108
112
|
export const geminiGlobalCaches = new Map();
|
|
109
113
|
export const geminiGlobalCacheCreates = new Map();
|
|
110
114
|
|
|
111
|
-
export function
|
|
115
|
+
export function _geminiCredentialFingerprint(apiKey) {
|
|
116
|
+
return traceHash(String(apiKey || ''));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function _geminiGlobalCacheKey({ credentialFingerprint, model, cachePrefixHash, cachePrefixContentCount }) {
|
|
112
120
|
return traceHash(stableTraceStringify({
|
|
113
|
-
|
|
121
|
+
credentialFingerprint: credentialFingerprint || null,
|
|
114
122
|
model: model || null,
|
|
115
123
|
cachePrefixHash,
|
|
116
124
|
cachePrefixContentCount,
|
|
117
125
|
}));
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
export function _invalidateGeminiCachesForCredentialFingerprint(credentialFingerprint) {
|
|
129
|
+
if (!credentialFingerprint) return 0;
|
|
130
|
+
let removed = 0;
|
|
131
|
+
for (const [key, entry] of geminiGlobalCaches) {
|
|
132
|
+
if (entry?.cacheCredentialFingerprint !== credentialFingerprint) continue;
|
|
133
|
+
geminiGlobalCaches.delete(key);
|
|
134
|
+
removed += 1;
|
|
135
|
+
}
|
|
136
|
+
return removed;
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
export function _pruneGeminiGlobalCaches(now = Date.now()) {
|
|
121
140
|
for (const [key, entry] of geminiGlobalCaches) {
|
|
122
141
|
if (!entry?.cacheName || Number(entry.cacheExpiresAt || 0) <= now) {
|
|
@@ -172,6 +191,7 @@ export function _attachGeminiCacheState(opts, entry, currentIter) {
|
|
|
172
191
|
cacheTokenSize: entry.cacheTokenSize,
|
|
173
192
|
cachePrefixContentCount: entry.cachePrefixContentCount,
|
|
174
193
|
cachePrefixHash: entry.cachePrefixHash,
|
|
194
|
+
cacheCredentialFingerprint: entry.cacheCredentialFingerprint,
|
|
175
195
|
},
|
|
176
196
|
};
|
|
177
197
|
}
|