mixdog 0.9.52 → 0.9.54
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 +1 -1
- package/scripts/agent-model-liveness-test.mjs +68 -0
- package/scripts/anthropic-transport-policy-test.mjs +260 -0
- package/scripts/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/gemini-provider-test.mjs +396 -1
- package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +144 -4
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +91 -22
- package/scripts/provider-admission-scheduler-test.mjs +4 -5
- package/scripts/provider-contract-test.mjs +326 -11
- package/scripts/provider-toolcall-test.mjs +269 -27
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/repl.mjs +11 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +76 -325
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +10 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +68 -289
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +33 -5
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +113 -144
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +101 -190
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +263 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +96 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +81 -81
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +58 -20
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +65 -12
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +245 -0
- package/src/runtime/shared/process-lifecycle.mjs +184 -34
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +233 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/engine/turn.mjs +31 -0
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -28,6 +28,7 @@ export const LEGACY_EFFORT_BUDGET = Object.freeze({
|
|
|
28
28
|
});
|
|
29
29
|
const _LOGGED_EFFORT_NORMALIZATION = new Set();
|
|
30
30
|
const _LOGGED_UNKNOWN_EFFORT = new Set();
|
|
31
|
+
const MAX_LOGGED_UNKNOWN_EFFORTS = 100;
|
|
31
32
|
|
|
32
33
|
function normalizeModelId(model) {
|
|
33
34
|
return String(model || '').toLowerCase().replace(/\./g, '-');
|
|
@@ -223,8 +224,12 @@ export function applyAnthropicEffortToBody(
|
|
|
223
224
|
return;
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
|
|
227
|
-
|
|
227
|
+
const unknownEffort = String(opts.effort ?? '');
|
|
228
|
+
if (opts.effort && normalized === undefined && !_LOGGED_UNKNOWN_EFFORT.has(unknownEffort)) {
|
|
229
|
+
if (_LOGGED_UNKNOWN_EFFORT.size >= MAX_LOGGED_UNKNOWN_EFFORTS) {
|
|
230
|
+
_LOGGED_UNKNOWN_EFFORT.delete(_LOGGED_UNKNOWN_EFFORT.values().next().value);
|
|
231
|
+
}
|
|
232
|
+
_LOGGED_UNKNOWN_EFFORT.add(unknownEffort);
|
|
228
233
|
try {
|
|
229
234
|
process.stderr.write(
|
|
230
235
|
`[${logTag}] unknown effort=${opts.effort} ignored (known legacy: ${Object.keys(LEGACY_EFFORT_BUDGET).join(', ')})\n`,
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
ensureLatestAnthropicModel,
|
|
26
26
|
} from './anthropic-model-resolve.mjs';
|
|
27
27
|
import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
|
|
28
|
-
import { providerNativeToolPrefixCount } from '../../../../session-runtime/provider-request-tools.mjs';
|
|
29
28
|
import {
|
|
30
29
|
TOKEN_REFRESH_SKEW_MS,
|
|
31
30
|
isAnthropicOAuthRefreshDisabled,
|
|
@@ -70,8 +69,19 @@ import {
|
|
|
70
69
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
71
70
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
72
71
|
import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
|
|
72
|
+
import {
|
|
73
|
+
ANTHROPIC_CACHE_TTL_STABLE as CACHE_TTL_STABLE,
|
|
74
|
+
ANTHROPIC_CACHE_TTL_VOLATILE as CACHE_TTL_VOLATILE,
|
|
75
|
+
applyAnthropicCacheMarkers,
|
|
76
|
+
clampAnthropicThinkingBudget as clampThinkingBudgetTokens,
|
|
77
|
+
deferredAnthropicTools as sharedDeferredAnthropicTools,
|
|
78
|
+
requestAnthropicTools as sharedRequestAnthropicTools,
|
|
79
|
+
normalizeAnthropicNonStreamingResponse,
|
|
80
|
+
resolveAnthropicCacheTtls as resolveCacheTtls,
|
|
81
|
+
sanitizeAnthropicInputSchema,
|
|
82
|
+
toAnthropicToolChoice,
|
|
83
|
+
} from './lib/anthropic-request-utils.mjs';
|
|
73
84
|
|
|
74
|
-
// --- Model catalog cache helpers: extracted to anthropic-model-resolve.mjs ---
|
|
75
85
|
// SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
|
|
76
86
|
const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
|
|
77
87
|
|
|
@@ -202,113 +212,12 @@ function resolveMaxTokens(model) {
|
|
|
202
212
|
return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokens });
|
|
203
213
|
}
|
|
204
214
|
|
|
205
|
-
const MIN_THINKING_BUDGET = 1024;
|
|
206
|
-
const THINKING_OUTPUT_RESERVE = 1024;
|
|
207
|
-
|
|
208
|
-
function clampThinkingBudgetTokens(value, maxTokens) {
|
|
209
|
-
const desired = Math.floor(Number(value));
|
|
210
|
-
const max = Math.floor(Number(maxTokens));
|
|
211
|
-
if (!Number.isFinite(desired) || desired <= 0 || !Number.isFinite(max)) return null;
|
|
212
|
-
const ceiling = max - THINKING_OUTPUT_RESERVE;
|
|
213
|
-
if (ceiling < MIN_THINKING_BUDGET) return null;
|
|
214
|
-
return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Layered cache TTLs — stable layers get 1h, volatile layers get 5m.
|
|
218
|
-
// Anthropic requires 1h entries to appear before 5m entries in the request.
|
|
219
|
-
const CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' }; // tools, system, tier3, messages
|
|
220
|
-
const CACHE_TTL_VOLATILE = { type: 'ephemeral' }; // explicit 5m override
|
|
221
|
-
|
|
222
215
|
// --- Message conversion ---
|
|
223
216
|
|
|
224
|
-
function withCacheControl(block, ttl = CACHE_TTL_VOLATILE) {
|
|
225
|
-
if (!block || typeof block !== 'object' || block.cache_control) return block;
|
|
226
|
-
return { ...block, cache_control: ttl };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function appendCacheControl(content, ttl = CACHE_TTL_VOLATILE) {
|
|
230
|
-
if (Array.isArray(content)) {
|
|
231
|
-
if (content.length === 0) return content;
|
|
232
|
-
const next = [...content];
|
|
233
|
-
next[next.length - 1] = withCacheControl(next[next.length - 1], ttl);
|
|
234
|
-
return next;
|
|
235
|
-
}
|
|
236
|
-
if (typeof content === 'string') {
|
|
237
|
-
return [withCacheControl({ type: 'text', text: content }, ttl)];
|
|
238
|
-
}
|
|
239
|
-
return content;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
217
|
// Anthropic's tool spec forbids oneOf / allOf / anyOf at the TOP level of
|
|
243
218
|
// input_schema (nested usage inside properties is allowed). External MCP
|
|
244
219
|
// servers sometimes emit such schemas.
|
|
245
220
|
// Convert them to a flat object schema so the API never sees a 400.
|
|
246
|
-
function _sanitizeInputSchema(schema, toolName) {
|
|
247
|
-
if (!schema || typeof schema !== 'object') {
|
|
248
|
-
return { type: 'object', properties: {} };
|
|
249
|
-
}
|
|
250
|
-
const compound = schema.oneOf || schema.anyOf || schema.allOf;
|
|
251
|
-
if (!compound) return structuredClone(schema);
|
|
252
|
-
// Merge all branch properties into one permissive object schema.
|
|
253
|
-
// None of the branches' required lists are hoisted — callers that relied
|
|
254
|
-
// on discriminated-union semantics will still function; the model simply
|
|
255
|
-
// receives a union of the property surface with no hard-required constraint.
|
|
256
|
-
const mergedProps = { ...(schema.properties && typeof schema.properties === 'object' ? schema.properties : {}) };
|
|
257
|
-
const branchDescs = [];
|
|
258
|
-
for (const branch of Array.isArray(compound) ? compound : []) {
|
|
259
|
-
if (branch && typeof branch === 'object' && branch.properties) {
|
|
260
|
-
Object.assign(mergedProps, branch.properties);
|
|
261
|
-
}
|
|
262
|
-
if (branch && typeof branch === 'object') {
|
|
263
|
-
const parts = [];
|
|
264
|
-
if (branch.description) parts.push(branch.description);
|
|
265
|
-
else if (branch.type) parts.push(`type:${branch.type}`);
|
|
266
|
-
if (parts.length) branchDescs.push(parts.join(' '));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const compoundKey = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'allOf';
|
|
270
|
-
let description = schema.description || '';
|
|
271
|
-
if (branchDescs.length) {
|
|
272
|
-
const parts = [];
|
|
273
|
-
let used = 0;
|
|
274
|
-
for (let i = 0; i < branchDescs.length; i++) {
|
|
275
|
-
const v = `(variant ${i + 1}: ${branchDescs[i]})`;
|
|
276
|
-
if (used + v.length + (parts.length ? 1 : 0) > 500) break;
|
|
277
|
-
parts.push(v);
|
|
278
|
-
used += v.length + (parts.length > 1 ? 1 : 0);
|
|
279
|
-
}
|
|
280
|
-
const addition = parts.join(' ');
|
|
281
|
-
if (addition) description = description ? `${description} ${addition}` : addition;
|
|
282
|
-
}
|
|
283
|
-
const mergedPropsCount = Object.keys(mergedProps).length;
|
|
284
|
-
if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
285
|
-
process.stderr.write(
|
|
286
|
-
`[anthropic-oauth-sanitizer] tool="${toolName ?? ''}" compound="${compoundKey}" branches=${Array.isArray(compound) ? compound.length : 0} mergedProps=${mergedPropsCount}\n`
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
type: 'object',
|
|
291
|
-
...(description ? { description } : {}),
|
|
292
|
-
properties: mergedProps,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function toAnthropicTools(tools) {
|
|
297
|
-
return tools.map(t => {
|
|
298
|
-
const out = {
|
|
299
|
-
name: t.name,
|
|
300
|
-
description: t.description,
|
|
301
|
-
input_schema: _sanitizeInputSchema(t.inputSchema, t.name),
|
|
302
|
-
};
|
|
303
|
-
if (t.deferLoading === true || t.defer_loading === true) out.defer_loading = true;
|
|
304
|
-
return out;
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
function nativeAnthropicTools(opts) {
|
|
308
|
-
return Array.isArray(opts?.nativeTools)
|
|
309
|
-
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
310
|
-
: [];
|
|
311
|
-
}
|
|
312
221
|
// Map the orchestrator-level opts.toolChoice into Anthropic's tool_choice.
|
|
313
222
|
// Only 'none' is activated: it lets the hard-cap final turn keep the tool
|
|
314
223
|
// DEFINITIONS in-request (so the tools->system->messages prefix — and its
|
|
@@ -321,59 +230,11 @@ function nativeAnthropicTools(opts) {
|
|
|
321
230
|
// effort/thinking active on reasoning models — activating it would convert a
|
|
322
231
|
// previously-harmless no-op into a hard 400 on exactly that turn. Attached
|
|
323
232
|
// only when the request actually carries tools (see buildRequestBody).
|
|
324
|
-
function toAnthropicToolChoice(toolChoice) {
|
|
325
|
-
return toolChoice === 'none' ? { type: 'none' } : undefined;
|
|
326
|
-
}
|
|
327
|
-
function discoveredAnthropicToolNames(messages, opts, provider) {
|
|
328
|
-
const anthropicNative = new Set(['anthropic', 'anthropic-oauth']);
|
|
329
|
-
const discovered = new Set(
|
|
330
|
-
Array.isArray(opts?.session?.deferredDiscoveredTools)
|
|
331
|
-
? opts.session.deferredDiscoveredTools.map((name) => String(name || '').trim()).filter(Boolean)
|
|
332
|
-
: [],
|
|
333
|
-
);
|
|
334
|
-
for (const message of Array.isArray(messages) ? messages : []) {
|
|
335
|
-
const native = message?.nativeToolSearch;
|
|
336
|
-
const source = String(native?.provider || '').toLowerCase();
|
|
337
|
-
if (source && source !== provider
|
|
338
|
-
&& !(anthropicNative.has(source) && anthropicNative.has(provider))) continue;
|
|
339
|
-
for (const name of Array.isArray(native?.toolReferences) ? native.toolReferences : []) {
|
|
340
|
-
const key = String(name || '').trim();
|
|
341
|
-
if (key) discovered.add(key);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return discovered;
|
|
345
|
-
}
|
|
346
233
|
function deferredAnthropicTools(activeTools, messages, opts) {
|
|
347
|
-
|
|
348
|
-
// A request whose ONLY tools are deferred is rejected by the API with
|
|
349
|
-
// `400: At least one tool must have defer_loading=false` — happens on the
|
|
350
|
-
// iteration-cap final turn (loop sends tools: [] to force a text answer).
|
|
351
|
-
// No active tools ⇒ send no deferred catalog either.
|
|
352
|
-
if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
|
|
353
|
-
const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
354
|
-
const discovered = discoveredAnthropicToolNames(messages, opts, 'anthropic-oauth');
|
|
355
|
-
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
356
|
-
return catalog
|
|
357
|
-
.filter((tool) => tool?.name && discovered.has(String(tool.name)) && !active.has(String(tool.name)))
|
|
358
|
-
.map((tool) => ({ ...tool, deferLoading: true }));
|
|
234
|
+
return sharedDeferredAnthropicTools(activeTools, messages, opts, 'anthropic-oauth');
|
|
359
235
|
}
|
|
360
236
|
function requestAnthropicTools(tools, messages, opts) {
|
|
361
|
-
|
|
362
|
-
if (opts?.providerToolSnapshotAuthoritative === true) {
|
|
363
|
-
const nativePrefixCount = providerNativeToolPrefixCount(
|
|
364
|
-
activeTools,
|
|
365
|
-
opts.providerNativeToolPrefixCount,
|
|
366
|
-
);
|
|
367
|
-
return [
|
|
368
|
-
...activeTools.slice(0, nativePrefixCount),
|
|
369
|
-
...toAnthropicTools(activeTools.slice(nativePrefixCount)),
|
|
370
|
-
];
|
|
371
|
-
}
|
|
372
|
-
const deferredTools = deferredAnthropicTools(activeTools, messages, opts);
|
|
373
|
-
return [
|
|
374
|
-
...nativeAnthropicTools(opts),
|
|
375
|
-
...toAnthropicTools([...activeTools, ...deferredTools]),
|
|
376
|
-
];
|
|
237
|
+
return sharedRequestAnthropicTools(tools, messages, opts, 'anthropic-oauth');
|
|
377
238
|
}
|
|
378
239
|
|
|
379
240
|
function toAnthropicMessages(messages) {
|
|
@@ -475,154 +336,8 @@ function toAnthropicMessages(messages) {
|
|
|
475
336
|
// messageTtl === null disables the tail. BP3 (tier3) now rides a system block,
|
|
476
337
|
// so it is no longer marked here.
|
|
477
338
|
// ANTHROPIC_MSG_SLOTS=0 is honoured upstream by passing messageTtl = null.
|
|
478
|
-
function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1 } = {}) {
|
|
479
|
-
if (!Array.isArray(sanitizedMessages) || sanitizedMessages.length === 0) {
|
|
480
|
-
return sanitizedMessages;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const firstText = (content) => {
|
|
484
|
-
if (typeof content === 'string') return content;
|
|
485
|
-
if (Array.isArray(content)) {
|
|
486
|
-
const first = content.find((b) => b?.type === 'text');
|
|
487
|
-
return first && typeof first.text === 'string' ? first.text : '';
|
|
488
|
-
}
|
|
489
|
-
return '';
|
|
490
|
-
};
|
|
491
|
-
const isSystemReminder = (content) => firstText(content).startsWith('<system-reminder>');
|
|
492
|
-
|
|
493
|
-
const markLast = (msg, ttl) => {
|
|
494
|
-
if (!msg) return;
|
|
495
|
-
msg.content = appendCacheControl(msg.content, ttl);
|
|
496
|
-
};
|
|
497
|
-
const ttlRank = (ttl) => ttl?.ttl === '1h' ? 2 : 1;
|
|
498
|
-
const canMarkMessageIdx = (idx) => {
|
|
499
|
-
// System-reminder messages (volatileTail / roleSpecific BP4) vary
|
|
500
|
-
// per-call, so never pin them with a 1h marker. The 1h system blocks
|
|
501
|
-
// (BP1/BP2/BP3) already satisfy Anthropic's "1h before 5m" ordering.
|
|
502
|
-
if (idx < 0) return false;
|
|
503
|
-
const msg = sanitizedMessages[idx];
|
|
504
|
-
if (ttlRank(messageTtl) > ttlRank(CACHE_TTL_VOLATILE)
|
|
505
|
-
&& isSystemReminder(msg?.content)) {
|
|
506
|
-
return false;
|
|
507
|
-
}
|
|
508
|
-
return true;
|
|
509
|
-
};
|
|
510
|
-
const hasUserText = (msg) => {
|
|
511
|
-
if (msg?.role !== 'user') return false;
|
|
512
|
-
if (isSystemReminder(msg.content)) return false;
|
|
513
|
-
if (typeof msg.content === 'string') return msg.content.trim().length > 0;
|
|
514
|
-
if (!Array.isArray(msg.content)) return false;
|
|
515
|
-
return msg.content.some(b => b?.type === 'text' && typeof b.text === 'string' && b.text.trim().length > 0);
|
|
516
|
-
};
|
|
517
|
-
const previousUserTextAnchorIdx = () => {
|
|
518
|
-
// Prefer the user text turn before the current tail. In a normal
|
|
519
|
-
// user->assistant->tool loop this is the last prompt that was already
|
|
520
|
-
// present in the previous API request, so its prefix can overlap.
|
|
521
|
-
const tailIdx = sanitizedMessages.length - 1;
|
|
522
|
-
for (let i = tailIdx - 1; i >= 0; i--) {
|
|
523
|
-
if (hasUserText(sanitizedMessages[i])) return i;
|
|
524
|
-
}
|
|
525
|
-
return -1;
|
|
526
|
-
};
|
|
527
|
-
const latestToolResultTailIdx = () => {
|
|
528
|
-
// Claude/pi refs allow cache_control on tool_result blocks. Keep this
|
|
529
|
-
// narrower than "last message" so a fresh user prompt or steering text
|
|
530
|
-
// never becomes a 1h breakpoint.
|
|
531
|
-
for (let i = sanitizedMessages.length - 1; i >= 0; i--) {
|
|
532
|
-
const msg = sanitizedMessages[i];
|
|
533
|
-
if (msg?.role !== 'user' || !Array.isArray(msg.content) || msg.content.length === 0) continue;
|
|
534
|
-
const lastBlock = msg.content[msg.content.length - 1];
|
|
535
|
-
if (lastBlock?.type === 'tool_result') return i;
|
|
536
|
-
}
|
|
537
|
-
return -1;
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
const firstRequestUserPromptIdx = () => {
|
|
541
|
-
// Iteration-1 fallback: on the very first request a session has only the
|
|
542
|
-
// current user prompt — no tool_result tail and no earlier user turn, so
|
|
543
|
-
// both anchors above return -1 and NO message breakpoint is placed. That
|
|
544
|
-
// left the whole tools+system prefix (~4.2k) uncached on iter1: nothing
|
|
545
|
-
// was written, so iter2 re-sent it as a fresh full write instead of a
|
|
546
|
-
// read hit. Anchor the current prompt's tail so the stable prefix is
|
|
547
|
-
// cache-written on the first ask and read back on the next. Only used
|
|
548
|
-
// when neither real anchor exists, so later turns still prefer the
|
|
549
|
-
// tool_result / previous-user-text anchors (never the volatile new
|
|
550
|
-
// prompt). Synthetic <system-reminder> turns are excluded by hasUserText.
|
|
551
|
-
if (latestToolResultTailIdx() !== -1 || previousUserTextAnchorIdx() !== -1) return -1;
|
|
552
|
-
const tailIdx = sanitizedMessages.length - 1;
|
|
553
|
-
return hasUserText(sanitizedMessages[tailIdx]) ? tailIdx : -1;
|
|
554
|
-
};
|
|
555
|
-
if (messageTtl !== null) {
|
|
556
|
-
const slots = Math.max(0, Math.min(4, Number(messageSlots) || 0));
|
|
557
|
-
const marked = new Set();
|
|
558
|
-
const candidates = [latestToolResultTailIdx(), previousUserTextAnchorIdx(), firstRequestUserPromptIdx()];
|
|
559
|
-
for (const idx of candidates) {
|
|
560
|
-
if (slots <= 0) break;
|
|
561
|
-
if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx)) continue;
|
|
562
|
-
markLast(sanitizedMessages[idx], messageTtl);
|
|
563
|
-
marked.add(idx);
|
|
564
|
-
if (marked.size >= slots) break;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return sanitizedMessages;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// --- SSE parser + midstream retry policy: extracted to anthropic-sse.mjs ---
|
|
572
|
-
|
|
573
339
|
// --- Build request body ---
|
|
574
340
|
|
|
575
|
-
function resolveCacheTtls(opts) {
|
|
576
|
-
// Layered cache strategy — caller may override per-layer via opts.cacheStrategy.
|
|
577
|
-
// Anthropic enforces: 1h entries must appear before 5m entries in the request.
|
|
578
|
-
const strategy = opts.cacheStrategy || {};
|
|
579
|
-
const pick = (layer, fallback) => {
|
|
580
|
-
const v = strategy[layer];
|
|
581
|
-
if (v === '1h') return CACHE_TTL_STABLE;
|
|
582
|
-
if (v === '5m') return CACHE_TTL_VOLATILE;
|
|
583
|
-
if (v === 'none') return null;
|
|
584
|
-
return fallback;
|
|
585
|
-
};
|
|
586
|
-
// BP budget (4 total):
|
|
587
|
-
// BP1 baseRules — 1h (shared tool policy + compact skill manifest)
|
|
588
|
-
// BP2 stableSystem — 1h (role/system rules)
|
|
589
|
-
// BP3 tier3 — 1h (sessionMarker: stable memory/meta body)
|
|
590
|
-
// BP4 messages — 1h sliding tail (tool_result cache across iter)
|
|
591
|
-
// tools BP is dropped — system BP covers the tools prefix via
|
|
592
|
-
// Anthropic's prompt cache prefix semantics (order: tools → system
|
|
593
|
-
// → messages).
|
|
594
|
-
// tier3 defaults to 1h (stable) — sessionMarker content is stable per
|
|
595
|
-
// memory/meta tuple and the BP slot is only spent when a 3rd
|
|
596
|
-
// (cacheTier:'tier3') system block is actually present, so this default is
|
|
597
|
-
// free for sessions that don't carry one. Previously null here meant any
|
|
598
|
-
// caller that skipped agent runtime resolve (CLI, raw agent spawn)
|
|
599
|
-
// silently lost the tier3 cache layer even though it supported one.
|
|
600
|
-
const resolved = {
|
|
601
|
-
tools: pick('tools', null),
|
|
602
|
-
system: pick('system', CACHE_TTL_STABLE),
|
|
603
|
-
tier3: pick('tier3', CACHE_TTL_STABLE),
|
|
604
|
-
messages: pick('messages', CACHE_TTL_STABLE),
|
|
605
|
-
};
|
|
606
|
-
// A partial cacheStrategy override (e.g. {system:'5m'} while tier3/
|
|
607
|
-
// messages default to '1h') can put a longer TTL after a shorter one in
|
|
608
|
-
// request order, which Anthropic rejects: 1h breakpoints must all appear
|
|
609
|
-
// before any 5m breakpoint. Normalize left-to-right in wire order
|
|
610
|
-
// (system -> tier3 -> messages; tools is emitted before system and is
|
|
611
|
-
// excluded from the run) so a later layer is downgraded to the earliest
|
|
612
|
-
// shorter TTL seen so far — never re-promoted. Layers set to null ('none')
|
|
613
|
-
// emit no breakpoint at all, so they neither violate nor constrain
|
|
614
|
-
// ordering and are skipped.
|
|
615
|
-
const ttlRank = (ttl) => (ttl === CACHE_TTL_STABLE ? 2 : 1); // 1h=2, 5m=1
|
|
616
|
-
let minRank = Infinity;
|
|
617
|
-
for (const layer of ['system', 'tier3', 'messages']) {
|
|
618
|
-
if (!resolved[layer]) continue;
|
|
619
|
-
const rank = ttlRank(resolved[layer]);
|
|
620
|
-
if (rank > minRank) resolved[layer] = CACHE_TTL_VOLATILE;
|
|
621
|
-
else minRank = rank;
|
|
622
|
-
}
|
|
623
|
-
return resolved;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
341
|
// BP3 (tier3) is injected by session/manager as its own `system` role block —
|
|
627
342
|
// the 3rd system block, tagged `cacheTier:'tier3'`. buildSystemBlocks applies
|
|
628
343
|
// the tier3 1h cache_control to that block; BP1/BP2 take the system TTL. No
|
|
@@ -848,6 +563,7 @@ export class AnthropicOAuthProvider {
|
|
|
848
563
|
const onStreamDelta = typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null;
|
|
849
564
|
const onToolCall = typeof opts.onToolCall === 'function' ? opts.onToolCall : null;
|
|
850
565
|
const onTextDelta = typeof opts.onTextDelta === 'function' ? opts.onTextDelta : null;
|
|
566
|
+
const onTextReset = typeof opts.onTextReset === 'function' ? opts.onTextReset : null;
|
|
851
567
|
const externalSignal = opts.signal || null;
|
|
852
568
|
// Test seam: lets the retry harness drive stream outcomes without a
|
|
853
569
|
// live OAuth session.
|
|
@@ -898,7 +614,7 @@ export class AnthropicOAuthProvider {
|
|
|
898
614
|
try { totalSignal.removeEventListener('abort', handler); } catch {}
|
|
899
615
|
};
|
|
900
616
|
|
|
901
|
-
const doRequest = async (accessToken, requestSignal = null) => {
|
|
617
|
+
const doRequest = async (accessToken, requestSignal = null, requestBody = body) => {
|
|
902
618
|
const controller = createAbortController();
|
|
903
619
|
const fetchStartedAt = Date.now();
|
|
904
620
|
|
|
@@ -951,7 +667,7 @@ export class AnthropicOAuthProvider {
|
|
|
951
667
|
'x-app': 'cli',
|
|
952
668
|
'Content-Type': 'application/json',
|
|
953
669
|
},
|
|
954
|
-
body: JSON.stringify(
|
|
670
|
+
body: JSON.stringify(requestBody),
|
|
955
671
|
signal: controller.signal,
|
|
956
672
|
dispatcher: getLlmDispatcher(),
|
|
957
673
|
});
|
|
@@ -993,8 +709,8 @@ export class AnthropicOAuthProvider {
|
|
|
993
709
|
// Test seam: injectable request factory for retry-path tests.
|
|
994
710
|
const doRequestImpl = typeof opts._doRequestFn === 'function' ? opts._doRequestFn : doRequest;
|
|
995
711
|
|
|
996
|
-
const requestWithRetry = async (accessToken) => withRetry(async ({ signal: attemptSignal }) => {
|
|
997
|
-
const result = await doRequestImpl(accessToken, attemptSignal);
|
|
712
|
+
const requestWithRetry = async (accessToken, requestBody = body) => withRetry(async ({ signal: attemptSignal }) => {
|
|
713
|
+
const result = await doRequestImpl(accessToken, attemptSignal, requestBody);
|
|
998
714
|
const status = Number(result?.response?.status || 0);
|
|
999
715
|
const transientStatus = classifyError({ httpStatus: status }) === 'transient';
|
|
1000
716
|
if (transientStatus || status === 429) {
|
|
@@ -1024,8 +740,14 @@ export class AnthropicOAuthProvider {
|
|
|
1024
740
|
backoffMs: ANTHROPIC_RETRY_BACKOFF_MS,
|
|
1025
741
|
retryJitterRatio: ANTHROPIC_RETRY_JITTER_RATIO,
|
|
1026
742
|
retryJitterMode: 'positive',
|
|
743
|
+
// Max/Pro OAuth sessions use subscription quota windows. Claude
|
|
744
|
+
// Code fails their 429s immediately rather than waiting through
|
|
745
|
+
// the API-key/PAYG retry budget (which may carry hours-long
|
|
746
|
+
// Retry-After values).
|
|
747
|
+
retry429: false,
|
|
1027
748
|
perAttemptTimeoutMs: requestTimeoutMs,
|
|
1028
749
|
perAttemptLabel: 'Anthropic OAuth initial response',
|
|
750
|
+
provider: 'anthropic',
|
|
1029
751
|
model: useModel,
|
|
1030
752
|
fallbackModel: opts._fallbackTriggered ? undefined : opts.fallbackModel,
|
|
1031
753
|
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
@@ -1046,6 +768,51 @@ export class AnthropicOAuthProvider {
|
|
|
1046
768
|
let firstAttemptError = null;
|
|
1047
769
|
let firstAttemptClassifier = null;
|
|
1048
770
|
|
|
771
|
+
const recoverNonStreaming = async (midState, streamingError, controller) => {
|
|
772
|
+
const exposedChars = Number(midState?.emittedTextChars) || 0;
|
|
773
|
+
if (!onTextReset || exposedChars <= 0
|
|
774
|
+
|| midState.emittedToolCall || midState.partialToolCall || midState.emittedThinking) {
|
|
775
|
+
try { streamingError.liveTextEmitted = true; streamingError.unsafeToRetry = true; } catch {}
|
|
776
|
+
throw streamingError;
|
|
777
|
+
}
|
|
778
|
+
let resetAccepted = false;
|
|
779
|
+
try {
|
|
780
|
+
resetAccepted = await onTextReset({
|
|
781
|
+
chars: exposedChars,
|
|
782
|
+
reason: 'anthropic-streaming-fallback',
|
|
783
|
+
}) === true;
|
|
784
|
+
} catch {}
|
|
785
|
+
if (!resetAccepted) {
|
|
786
|
+
try { streamingError.liveTextEmitted = true; streamingError.unsafeToRetry = true; } catch {}
|
|
787
|
+
throw streamingError;
|
|
788
|
+
}
|
|
789
|
+
try { controller?.abort?.(streamingError); } catch {}
|
|
790
|
+
try { onStageChange?.('requesting', { transport: 'non-streaming-fallback' }); } catch {}
|
|
791
|
+
let fallback = await requestWithRetry(creds.accessToken, { ...body, stream: false });
|
|
792
|
+
if (fallback.response.status === 401) {
|
|
793
|
+
cleanupCancelHandler(fallback.cancelHandler);
|
|
794
|
+
try { fallback.controller?.abort?.(); } catch {}
|
|
795
|
+
creds = await this.ensureAuth({ forceRefresh: true, reason: '401' });
|
|
796
|
+
fallback = await requestWithRetry(creds.accessToken, { ...body, stream: false });
|
|
797
|
+
}
|
|
798
|
+
if (!fallback.response.ok) {
|
|
799
|
+
const text = await fallback.response.text().catch(() => '');
|
|
800
|
+
cleanupCancelHandler(fallback.cancelHandler);
|
|
801
|
+
try { fallback.controller?.abort?.(); } catch {}
|
|
802
|
+
const fallbackError = new Error(`Anthropic OAuth API ${fallback.response.status}: ${this.scrubTokens(text).slice(0, 200)}`);
|
|
803
|
+
fallbackError.status = fallback.response.status;
|
|
804
|
+
fallbackError.httpStatus = fallback.response.status;
|
|
805
|
+
throw fallbackError;
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
const message = await fallback.response.json();
|
|
809
|
+
return normalizeAnthropicNonStreamingResponse(message, useModel);
|
|
810
|
+
} finally {
|
|
811
|
+
cleanupCancelHandler(fallback.cancelHandler);
|
|
812
|
+
try { fallback.controller?.abort?.('Anthropic non-streaming fallback complete'); } catch {}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
|
|
1049
816
|
try {
|
|
1050
817
|
for (let attemptIndex = 0; attemptIndex <= MAX_MIDSTREAM_RETRIES; attemptIndex++) {
|
|
1051
818
|
let response, controller, cancelHandler;
|
|
@@ -1205,28 +972,12 @@ export class AnthropicOAuthProvider {
|
|
|
1205
972
|
} catch { /* ignore non-extensible result */ }
|
|
1206
973
|
return result;
|
|
1207
974
|
} catch (err) {
|
|
1208
|
-
//
|
|
1209
|
-
//
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
// tag the error so upstream layers refuse to retry as well.
|
|
975
|
+
// Acknowledged reset semantics let the owner tombstone this
|
|
976
|
+
// attempt before the full request is restarted non-streaming.
|
|
977
|
+
// Without that acknowledgement, recoverNonStreaming stamps
|
|
978
|
+
// the error unsafe and preserves the no-concatenation rule.
|
|
1213
979
|
if (midState.emittedText) {
|
|
1214
|
-
|
|
1215
|
-
try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
|
|
1216
|
-
if (attemptIndex > 0 && firstAttemptError) {
|
|
1217
|
-
try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
|
|
1218
|
-
try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
|
|
1219
|
-
// firstAttemptError is what actually propagates here when
|
|
1220
|
-
// live text was emitted this attempt — stamp the unsafe
|
|
1221
|
-
// flags onto IT too, else upstream sees an unmarked error
|
|
1222
|
-
// and retries, duplicating already-streamed output.
|
|
1223
|
-
try {
|
|
1224
|
-
firstAttemptError.liveTextEmitted = true;
|
|
1225
|
-
firstAttemptError.unsafeToRetry = true;
|
|
1226
|
-
} catch {}
|
|
1227
|
-
throw err;
|
|
1228
|
-
}
|
|
1229
|
-
throw err;
|
|
980
|
+
return await recoverNonStreaming(midState, err, controller);
|
|
1230
981
|
}
|
|
1231
982
|
// Completed/partial tools and thinking blocks are also replay
|
|
1232
983
|
// boundaries. Stamp the current (latest) error so no upstream
|
|
@@ -1444,5 +1195,5 @@ export const _test = {
|
|
|
1444
1195
|
resolveMaxTokens,
|
|
1445
1196
|
deferredAnthropicTools,
|
|
1446
1197
|
requestAnthropicTools,
|
|
1447
|
-
sanitizeInputSchema:
|
|
1198
|
+
sanitizeInputSchema: (schema, toolName) => sanitizeAnthropicInputSchema(schema, toolName, 'anthropic-oauth'),
|
|
1448
1199
|
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* anthropic-sse.mjs — Anthropic SSE stream parser + mid-stream retry policy.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* parseSSEStream / _classifyMidstreamError / ANTHROPIC_MAX_MIDSTREAM_RETRIES
|
|
7
|
-
* from here (via the anthropic-oauth.mjs re-export for legacy paths).
|
|
4
|
+
* Shared by both Anthropic providers. anthropic-oauth.mjs retains its public
|
|
5
|
+
* re-exports for provider test and integration entry points.
|
|
8
6
|
*/
|
|
9
7
|
import { randomBytes } from 'crypto';
|
|
10
8
|
import {
|
|
@@ -176,7 +174,10 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
176
174
|
content += emit;
|
|
177
175
|
try { onStreamDelta?.('text'); } catch {}
|
|
178
176
|
if (onTextDelta) {
|
|
179
|
-
if (state)
|
|
177
|
+
if (state) {
|
|
178
|
+
state.emittedText = true;
|
|
179
|
+
state.emittedTextChars = (Number(state.emittedTextChars) || 0) + emit.length;
|
|
180
|
+
}
|
|
180
181
|
try { onTextDelta(emit); } catch {}
|
|
181
182
|
}
|
|
182
183
|
}
|
|
@@ -460,7 +461,10 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
460
461
|
} else {
|
|
461
462
|
content += delta.text || '';
|
|
462
463
|
if (delta.text && onTextDelta) {
|
|
463
|
-
if (state)
|
|
464
|
+
if (state) {
|
|
465
|
+
state.emittedText = true;
|
|
466
|
+
state.emittedTextChars = (Number(state.emittedTextChars) || 0) + delta.text.length;
|
|
467
|
+
}
|
|
464
468
|
try { onTextDelta(delta.text); } catch {}
|
|
465
469
|
}
|
|
466
470
|
if (delta.text) {
|