mixdog 0.9.52 → 0.9.53
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/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/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 +63 -3
- 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 +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- 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 +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -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 +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- 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 +5 -7
- 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 +12 -5
- 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/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/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 +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- 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/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 +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- 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
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
ANTHROPIC_MAX_MIDSTREAM_RETRIES,
|
|
25
25
|
parseSSEStream,
|
|
26
26
|
_classifyMidstreamError,
|
|
27
|
-
} from './anthropic-
|
|
27
|
+
} from './anthropic-sse.mjs';
|
|
28
28
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
29
29
|
import {
|
|
30
30
|
applyAnthropicEffortToBody,
|
|
@@ -34,11 +34,21 @@ import {
|
|
|
34
34
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
35
35
|
import { enrichModels } from './model-catalog.mjs';
|
|
36
36
|
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
37
|
-
import { providerNativeToolPrefixCount } from '../../../../session-runtime/provider-request-tools.mjs';
|
|
38
37
|
import { makeModelCache } from './model-cache.mjs';
|
|
39
38
|
import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
|
|
40
39
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
41
40
|
import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
|
|
41
|
+
import {
|
|
42
|
+
ANTHROPIC_CACHE_TTL_STABLE as CACHE_TTL_STABLE,
|
|
43
|
+
ANTHROPIC_CACHE_TTL_VOLATILE as CACHE_TTL_VOLATILE,
|
|
44
|
+
applyAnthropicCacheMarkers,
|
|
45
|
+
clampAnthropicThinkingBudget as clampThinkingBudgetTokens,
|
|
46
|
+
deferredAnthropicTools as sharedDeferredAnthropicTools,
|
|
47
|
+
requestAnthropicTools as sharedRequestAnthropicTools,
|
|
48
|
+
resolveAnthropicCacheTtls as resolveCacheTtls,
|
|
49
|
+
sanitizeAnthropicInputSchema,
|
|
50
|
+
toAnthropicToolChoice,
|
|
51
|
+
} from './lib/anthropic-request-utils.mjs';
|
|
42
52
|
|
|
43
53
|
const require = createRequire(import.meta.url);
|
|
44
54
|
let _Anthropic = null;
|
|
@@ -61,66 +71,11 @@ function _midstreamSleepWithAbort(ms, signal) {
|
|
|
61
71
|
// breakpoint, so they do not spend a separate cache_control slot. 1h TTL
|
|
62
72
|
// requires the extended-cache-ttl beta header, which we set on the client via
|
|
63
73
|
// defaultHeaders below.
|
|
64
|
-
const CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' };
|
|
65
|
-
const CACHE_TTL_VOLATILE = { type: 'ephemeral' };
|
|
66
|
-
|
|
67
|
-
function withCacheControl(block, ttl = CACHE_TTL_VOLATILE) {
|
|
68
|
-
if (!block || typeof block !== 'object' || block.cache_control) return block;
|
|
69
|
-
return { ...block, cache_control: ttl };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function appendCacheControl(content, ttl = CACHE_TTL_VOLATILE) {
|
|
73
|
-
if (Array.isArray(content)) {
|
|
74
|
-
if (content.length === 0) return content;
|
|
75
|
-
const next = [...content];
|
|
76
|
-
next[next.length - 1] = withCacheControl(next[next.length - 1], ttl);
|
|
77
|
-
return next;
|
|
78
|
-
}
|
|
79
|
-
if (typeof content === 'string') {
|
|
80
|
-
return [withCacheControl({ type: 'text', text: content }, ttl)];
|
|
81
|
-
}
|
|
82
|
-
return content;
|
|
83
|
-
}
|
|
84
74
|
|
|
85
75
|
// BP3 (tier3) rides its own `system` role block (the 3rd system block, tagged
|
|
86
76
|
// cacheTier:'tier3'). buildSystemBlocks applies the tier3 1h cache_control to
|
|
87
77
|
// that block; BP1/BP2 take the system TTL. Mirrors anthropic-oauth.mjs.
|
|
88
78
|
|
|
89
|
-
function resolveCacheTtls(opts) {
|
|
90
|
-
const strategy = opts?.cacheStrategy || {};
|
|
91
|
-
const pick = (layer, fallback) => {
|
|
92
|
-
const v = strategy[layer];
|
|
93
|
-
if (v === '1h') return CACHE_TTL_STABLE;
|
|
94
|
-
if (v === '5m') return CACHE_TTL_VOLATILE;
|
|
95
|
-
if (v === 'none') return null;
|
|
96
|
-
return fallback;
|
|
97
|
-
};
|
|
98
|
-
const resolved = {
|
|
99
|
-
tools: pick('tools', null),
|
|
100
|
-
system: pick('system', CACHE_TTL_STABLE),
|
|
101
|
-
tier3: pick('tier3', CACHE_TTL_STABLE),
|
|
102
|
-
messages: pick('messages', CACHE_TTL_STABLE),
|
|
103
|
-
};
|
|
104
|
-
// A partial cacheStrategy override (e.g. {system:'5m'} while tier3/
|
|
105
|
-
// messages default to '1h') can put a longer TTL after a shorter one in
|
|
106
|
-
// request order, which Anthropic rejects: 1h breakpoints must all appear
|
|
107
|
-
// before any 5m breakpoint. Normalize left-to-right in wire order
|
|
108
|
-
// (system -> tier3 -> messages; tools is emitted before system and is
|
|
109
|
-
// excluded from the run) so a later layer is downgraded to the earliest
|
|
110
|
-
// shorter TTL seen so far — never re-promoted. Layers set to null ('none')
|
|
111
|
-
// emit no breakpoint at all, so they neither violate nor constrain
|
|
112
|
-
// ordering and are skipped. Mirrors anthropic-oauth.mjs.
|
|
113
|
-
const ttlRank = (ttl) => (ttl === CACHE_TTL_STABLE ? 2 : 1); // 1h=2, 5m=1
|
|
114
|
-
let minRank = Infinity;
|
|
115
|
-
for (const layer of ['system', 'tier3', 'messages']) {
|
|
116
|
-
if (!resolved[layer]) continue;
|
|
117
|
-
const rank = ttlRank(resolved[layer]);
|
|
118
|
-
if (rank > minRank) resolved[layer] = CACHE_TTL_VOLATILE;
|
|
119
|
-
else minRank = rank;
|
|
120
|
-
}
|
|
121
|
-
return resolved;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
79
|
function buildSystemBlocks(systemMsgs, systemTtl, tier3Ttl) {
|
|
125
80
|
// systemMsgs is an array of { content, cacheTier }. Each non-empty element
|
|
126
81
|
// becomes its own content block: cacheTier:'tier3' (BP3 sessionMarker) gets
|
|
@@ -248,84 +203,11 @@ export const _test = {
|
|
|
248
203
|
resolveMaxTokens,
|
|
249
204
|
deferredAnthropicTools,
|
|
250
205
|
requestAnthropicTools,
|
|
251
|
-
sanitizeInputSchema:
|
|
206
|
+
sanitizeInputSchema: (schema, toolName) => sanitizeAnthropicInputSchema(schema, toolName, 'anthropic'),
|
|
252
207
|
};
|
|
253
208
|
|
|
254
|
-
const MIN_THINKING_BUDGET = 1024;
|
|
255
|
-
const THINKING_OUTPUT_RESERVE = 1024;
|
|
256
|
-
|
|
257
|
-
function clampThinkingBudgetTokens(value, maxTokens) {
|
|
258
|
-
const desired = Math.floor(Number(value));
|
|
259
|
-
const max = Math.floor(Number(maxTokens));
|
|
260
|
-
if (!Number.isFinite(desired) || desired <= 0 || !Number.isFinite(max)) return null;
|
|
261
|
-
const ceiling = max - THINKING_OUTPUT_RESERVE;
|
|
262
|
-
if (ceiling < MIN_THINKING_BUDGET) return null;
|
|
263
|
-
return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
|
|
264
|
-
}
|
|
265
209
|
// Anthropic forbids oneOf / allOf / anyOf at the TOP level of input_schema.
|
|
266
210
|
// Mirror the same sanitizer as anthropic-oauth.mjs so both providers are safe.
|
|
267
|
-
function _sanitizeInputSchema(schema, toolName) {
|
|
268
|
-
if (!schema || typeof schema !== 'object') {
|
|
269
|
-
return { type: 'object', properties: {} };
|
|
270
|
-
}
|
|
271
|
-
const compound = schema.oneOf || schema.anyOf || schema.allOf;
|
|
272
|
-
if (!compound) return structuredClone(schema);
|
|
273
|
-
const mergedProps = { ...(schema.properties && typeof schema.properties === 'object' ? schema.properties : {}) };
|
|
274
|
-
const branchDescs = [];
|
|
275
|
-
for (const branch of Array.isArray(compound) ? compound : []) {
|
|
276
|
-
if (branch && typeof branch === 'object' && branch.properties) {
|
|
277
|
-
Object.assign(mergedProps, branch.properties);
|
|
278
|
-
}
|
|
279
|
-
if (branch && typeof branch === 'object') {
|
|
280
|
-
const parts = [];
|
|
281
|
-
if (branch.description) parts.push(branch.description);
|
|
282
|
-
else if (branch.type) parts.push(`type:${branch.type}`);
|
|
283
|
-
if (parts.length) branchDescs.push(parts.join(' '));
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const compoundKey = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'allOf';
|
|
287
|
-
let description = schema.description || '';
|
|
288
|
-
if (branchDescs.length) {
|
|
289
|
-
const parts = [];
|
|
290
|
-
let used = 0;
|
|
291
|
-
for (let i = 0; i < branchDescs.length; i++) {
|
|
292
|
-
const v = `(variant ${i + 1}: ${branchDescs[i]})`;
|
|
293
|
-
if (used + v.length + (parts.length ? 1 : 0) > 500) break;
|
|
294
|
-
parts.push(v);
|
|
295
|
-
used += v.length + (parts.length > 1 ? 1 : 0);
|
|
296
|
-
}
|
|
297
|
-
const addition = parts.join(' ');
|
|
298
|
-
if (addition) description = description ? `${description} ${addition}` : addition;
|
|
299
|
-
}
|
|
300
|
-
const mergedPropsCount = Object.keys(mergedProps).length;
|
|
301
|
-
if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
302
|
-
process.stderr.write(
|
|
303
|
-
`[anthropic-sanitizer] tool="${toolName ?? ''}" compound="${compoundKey}" branches=${Array.isArray(compound) ? compound.length : 0} mergedProps=${mergedPropsCount}\n`
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
return {
|
|
307
|
-
type: 'object',
|
|
308
|
-
...(description ? { description } : {}),
|
|
309
|
-
properties: mergedProps,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function toAnthropicTools(tools) {
|
|
314
|
-
return tools.map((t) => {
|
|
315
|
-
const out = {
|
|
316
|
-
name: t.name,
|
|
317
|
-
description: t.description,
|
|
318
|
-
input_schema: _sanitizeInputSchema(t.inputSchema, t.name),
|
|
319
|
-
};
|
|
320
|
-
if (t.deferLoading === true || t.defer_loading === true) out.defer_loading = true;
|
|
321
|
-
return out;
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
function nativeAnthropicTools(opts) {
|
|
325
|
-
return Array.isArray(opts?.nativeTools)
|
|
326
|
-
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
327
|
-
: [];
|
|
328
|
-
}
|
|
329
211
|
// Map the orchestrator-level opts.toolChoice into Anthropic's tool_choice.
|
|
330
212
|
// Only 'none' is activated: it lets the hard-cap final turn keep the tool
|
|
331
213
|
// DEFINITIONS in-request (so the tools->system->messages prefix — and its
|
|
@@ -339,58 +221,11 @@ function nativeAnthropicTools(opts) {
|
|
|
339
221
|
// previously-harmless no-op into a hard 400 on exactly that turn. Attached
|
|
340
222
|
// only when the request actually carries tools (see _doSend). Mirrors
|
|
341
223
|
// anthropic-oauth.mjs.
|
|
342
|
-
function toAnthropicToolChoice(toolChoice) {
|
|
343
|
-
return toolChoice === 'none' ? { type: 'none' } : undefined;
|
|
344
|
-
}
|
|
345
|
-
function discoveredAnthropicToolNames(messages, opts, provider) {
|
|
346
|
-
const anthropicNative = new Set(['anthropic', 'anthropic-oauth']);
|
|
347
|
-
const discovered = new Set(
|
|
348
|
-
Array.isArray(opts?.session?.deferredDiscoveredTools)
|
|
349
|
-
? opts.session.deferredDiscoveredTools.map((name) => String(name || '').trim()).filter(Boolean)
|
|
350
|
-
: [],
|
|
351
|
-
);
|
|
352
|
-
for (const message of Array.isArray(messages) ? messages : []) {
|
|
353
|
-
const native = message?.nativeToolSearch;
|
|
354
|
-
const source = String(native?.provider || '').toLowerCase();
|
|
355
|
-
if (source && source !== provider
|
|
356
|
-
&& !(anthropicNative.has(source) && anthropicNative.has(provider))) continue;
|
|
357
|
-
for (const name of Array.isArray(native?.toolReferences) ? native.toolReferences : []) {
|
|
358
|
-
const key = String(name || '').trim();
|
|
359
|
-
if (key) discovered.add(key);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return discovered;
|
|
363
|
-
}
|
|
364
224
|
function deferredAnthropicTools(activeTools, messages, opts) {
|
|
365
|
-
|
|
366
|
-
// Guard against an all-deferred tools array — the API rejects it with
|
|
367
|
-
// `400: At least one tool must have defer_loading=false` (iteration-cap
|
|
368
|
-
// final turn sends tools: []). See anthropic-oauth.mjs counterpart.
|
|
369
|
-
if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
|
|
370
|
-
const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
371
|
-
const discovered = discoveredAnthropicToolNames(messages, opts, 'anthropic');
|
|
372
|
-
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
373
|
-
return catalog
|
|
374
|
-
.filter((tool) => tool?.name && discovered.has(String(tool.name)) && !active.has(String(tool.name)))
|
|
375
|
-
.map((tool) => ({ ...tool, deferLoading: true }));
|
|
225
|
+
return sharedDeferredAnthropicTools(activeTools, messages, opts, 'anthropic');
|
|
376
226
|
}
|
|
377
227
|
function requestAnthropicTools(tools, messages, opts) {
|
|
378
|
-
|
|
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
|
-
];
|
|
228
|
+
return sharedRequestAnthropicTools(tools, messages, opts, 'anthropic');
|
|
394
229
|
}
|
|
395
230
|
function toAnthropicMessages(messages) {
|
|
396
231
|
// Marker-free lowering. cache_control is applied AFTER sanitization by
|
|
@@ -492,92 +327,6 @@ export function _toAnthropicMessagesForTest(messages) {
|
|
|
492
327
|
// messageTtl === null disables the tail. BP3 (tier3) now rides a system block,
|
|
493
328
|
// so it is no longer marked here.
|
|
494
329
|
// ANTHROPIC_MSG_SLOTS=0 is honoured upstream by passing messageTtl = null.
|
|
495
|
-
function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1 } = {}) {
|
|
496
|
-
if (!Array.isArray(sanitizedMessages) || sanitizedMessages.length === 0) {
|
|
497
|
-
return sanitizedMessages;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const firstText = (content) => {
|
|
501
|
-
if (typeof content === 'string') return content;
|
|
502
|
-
if (Array.isArray(content)) {
|
|
503
|
-
const first = content.find((b) => b?.type === 'text');
|
|
504
|
-
return first && typeof first.text === 'string' ? first.text : '';
|
|
505
|
-
}
|
|
506
|
-
return '';
|
|
507
|
-
};
|
|
508
|
-
const isSystemReminder = (content) => firstText(content).startsWith('<system-reminder>');
|
|
509
|
-
|
|
510
|
-
const markLast = (msg, ttl) => {
|
|
511
|
-
if (!msg) return;
|
|
512
|
-
msg.content = appendCacheControl(msg.content, ttl);
|
|
513
|
-
};
|
|
514
|
-
const ttlRank = (ttl) => ttl?.ttl === '1h' ? 2 : 1;
|
|
515
|
-
const canMarkMessageIdx = (idx) => {
|
|
516
|
-
// System-reminder messages (volatileTail / roleSpecific BP4) vary
|
|
517
|
-
// per-call, so never pin them with a 1h marker. The 1h system blocks
|
|
518
|
-
// (BP1/BP2/BP3) already satisfy Anthropic's "1h before 5m" ordering.
|
|
519
|
-
if (idx < 0) return false;
|
|
520
|
-
const msg = sanitizedMessages[idx];
|
|
521
|
-
if (ttlRank(messageTtl) > ttlRank(CACHE_TTL_VOLATILE)
|
|
522
|
-
&& isSystemReminder(msg?.content)) {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
return true;
|
|
526
|
-
};
|
|
527
|
-
const hasUserText = (msg) => {
|
|
528
|
-
if (msg?.role !== 'user') return false;
|
|
529
|
-
if (isSystemReminder(msg.content)) return false;
|
|
530
|
-
if (typeof msg.content === 'string') return msg.content.trim().length > 0;
|
|
531
|
-
if (!Array.isArray(msg.content)) return false;
|
|
532
|
-
return msg.content.some(b => b?.type === 'text' && typeof b.text === 'string' && b.text.trim().length > 0);
|
|
533
|
-
};
|
|
534
|
-
const previousUserTextAnchorIdx = () => {
|
|
535
|
-
const tailIdx = sanitizedMessages.length - 1;
|
|
536
|
-
for (let i = tailIdx - 1; i >= 0; i--) {
|
|
537
|
-
if (hasUserText(sanitizedMessages[i])) return i;
|
|
538
|
-
}
|
|
539
|
-
return -1;
|
|
540
|
-
};
|
|
541
|
-
const latestToolResultTailIdx = () => {
|
|
542
|
-
for (let i = sanitizedMessages.length - 1; i >= 0; i--) {
|
|
543
|
-
const msg = sanitizedMessages[i];
|
|
544
|
-
if (msg?.role !== 'user' || !Array.isArray(msg.content) || msg.content.length === 0) continue;
|
|
545
|
-
const lastBlock = msg.content[msg.content.length - 1];
|
|
546
|
-
if (lastBlock?.type === 'tool_result') return i;
|
|
547
|
-
}
|
|
548
|
-
return -1;
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
const firstRequestUserPromptIdx = () => {
|
|
552
|
-
// Iteration-1 fallback: on the very first request a session has only the
|
|
553
|
-
// current user prompt — no tool_result tail and no earlier user turn, so
|
|
554
|
-
// both anchors above return -1 and NO message breakpoint is placed. That
|
|
555
|
-
// left the whole tools+system prefix (~4.2k) uncached on iter1: nothing
|
|
556
|
-
// was written, so iter2 re-sent it as a fresh full write instead of a
|
|
557
|
-
// read hit. Anchor the current prompt's tail so the stable prefix is
|
|
558
|
-
// cache-written on the first ask and read back on the next. Only used
|
|
559
|
-
// when neither real anchor exists, so later turns still prefer the
|
|
560
|
-
// tool_result / previous-user-text anchors (never the volatile new
|
|
561
|
-
// prompt). Synthetic <system-reminder> turns are excluded by hasUserText.
|
|
562
|
-
if (latestToolResultTailIdx() !== -1 || previousUserTextAnchorIdx() !== -1) return -1;
|
|
563
|
-
const tailIdx = sanitizedMessages.length - 1;
|
|
564
|
-
return hasUserText(sanitizedMessages[tailIdx]) ? tailIdx : -1;
|
|
565
|
-
};
|
|
566
|
-
if (messageTtl !== null) {
|
|
567
|
-
const slots = Math.max(0, Math.min(4, Number(messageSlots) || 0));
|
|
568
|
-
const marked = new Set();
|
|
569
|
-
const candidates = [latestToolResultTailIdx(), previousUserTextAnchorIdx(), firstRequestUserPromptIdx()];
|
|
570
|
-
for (const idx of candidates) {
|
|
571
|
-
if (slots <= 0) break;
|
|
572
|
-
if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx)) continue;
|
|
573
|
-
markLast(sanitizedMessages[idx], messageTtl);
|
|
574
|
-
marked.add(idx);
|
|
575
|
-
if (marked.size >= slots) break;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return sanitizedMessages;
|
|
580
|
-
}
|
|
581
330
|
export class AnthropicProvider {
|
|
582
331
|
// Anthropic reports usage.input_tokens EXCLUDING cache_read/cache_creation
|
|
583
332
|
// (those are separate fields), so the live context-window footprint must
|
|
@@ -799,6 +548,9 @@ export class AnthropicProvider {
|
|
|
799
548
|
rawUsage: usageRaw,
|
|
800
549
|
provider: this.name,
|
|
801
550
|
requestKind: opts.requestKind || null,
|
|
551
|
+
// Anthropic usage is additive at this inner transport
|
|
552
|
+
// boundary, even when OpenCode Go supplies the provider id.
|
|
553
|
+
inputTokensInclusive: false,
|
|
802
554
|
});
|
|
803
555
|
}
|
|
804
556
|
|
|
@@ -204,12 +204,29 @@ export function _resolveGeminiCacheUsage({ usageMetadata, cachedContent, provide
|
|
|
204
204
|
}
|
|
205
205
|
return 0;
|
|
206
206
|
};
|
|
207
|
-
const
|
|
207
|
+
const promptTokens = firstFinite(
|
|
208
208
|
usageMetadata?.promptTokenCount,
|
|
209
209
|
usageMetadata?.prompt_token_count,
|
|
210
|
+
);
|
|
211
|
+
const totalTokens = firstFinite(
|
|
210
212
|
usageMetadata?.totalTokenCount,
|
|
211
213
|
usageMetadata?.total_token_count,
|
|
212
214
|
);
|
|
215
|
+
// Gemini totalTokenCount includes candidate and thought output tokens.
|
|
216
|
+
// When promptTokenCount is omitted, derive the prompt portion so callers
|
|
217
|
+
// that separately record output tokens do not count them twice.
|
|
218
|
+
const outputTokens = firstFinite(
|
|
219
|
+
usageMetadata?.candidatesTokenCount,
|
|
220
|
+
usageMetadata?.candidates_token_count,
|
|
221
|
+
) + firstFinite(
|
|
222
|
+
usageMetadata?.thoughtsTokenCount,
|
|
223
|
+
usageMetadata?.thoughts_token_count,
|
|
224
|
+
);
|
|
225
|
+
const hasExplicitPromptTokens = Object.prototype.hasOwnProperty.call(usageMetadata || {}, 'promptTokenCount')
|
|
226
|
+
|| Object.prototype.hasOwnProperty.call(usageMetadata || {}, 'prompt_token_count');
|
|
227
|
+
const inputTokens = hasExplicitPromptTokens
|
|
228
|
+
? promptTokens
|
|
229
|
+
: Math.max(0, totalTokens - outputTokens);
|
|
213
230
|
const reportedCachedTokens = firstFinite(
|
|
214
231
|
// generateContent UsageMetadata field.
|
|
215
232
|
usageMetadata?.cachedContentTokenCount,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
2
|
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
3
|
-
import { makeModelCache } from './model-cache.mjs';
|
|
4
3
|
import { withRetry } from './retry-classifier.mjs';
|
|
5
4
|
import { traceAgentUsage, appendAgentTrace } from '../agent-trace.mjs';
|
|
6
5
|
import {
|
|
@@ -50,107 +49,26 @@ import {
|
|
|
50
49
|
_geminiCredentialFingerprint,
|
|
51
50
|
_invalidateGeminiCachesForCredentialFingerprint,
|
|
52
51
|
} from './gemini-cache.mjs';
|
|
52
|
+
import {
|
|
53
|
+
GEMINI_MODELS as MODELS,
|
|
54
|
+
DEFAULT_GEMINI_MODEL as DEFAULT_MODEL,
|
|
55
|
+
geminiModelCache as _modelCache,
|
|
56
|
+
fetchGeminiModelPages,
|
|
57
|
+
resolveLatestGeminiModel,
|
|
58
|
+
ensureLatestGeminiModel,
|
|
59
|
+
fetchAndCacheGeminiModels,
|
|
60
|
+
} from './lib/gemini-model-catalog.mjs';
|
|
53
61
|
|
|
54
62
|
// Legacy import path: tests + gemini-stream import these from gemini.mjs.
|
|
55
63
|
// Re-export the extracted symbols so existing importers resolve unchanged.
|
|
56
64
|
export { createGeminiTextLeakGuard };
|
|
57
65
|
export { parseToolCalls, emitGeminiToolCalls, collectGeminiGroundingSources, parseGeminiTextPartMetadata };
|
|
58
|
-
|
|
59
|
-
const MODELS = [
|
|
60
|
-
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
61
|
-
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
62
|
-
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
63
|
-
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'gemini', contextWindow: 1048576 },
|
|
64
|
-
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'gemini', contextWindow: 1048576 },
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
const DEFAULT_MODEL = MODELS[0].id;
|
|
68
|
-
|
|
69
|
-
// --- Model catalog cache (24h disk TTL) ---
|
|
70
|
-
// Gemini's /models has no `created` timestamp, so latest-resolution is
|
|
71
|
-
// VERSION-based (parse gemini-X.Y) rather than release-date based.
|
|
72
|
-
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
73
|
-
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
74
|
-
// discarded instead of misread.
|
|
75
|
-
const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 2;
|
|
66
|
+
export { fetchGeminiModelPages, resolveLatestGeminiModel, ensureLatestGeminiModel };
|
|
76
67
|
|
|
77
68
|
// De-dupes concurrent force-refreshes so they share one HTTP round-trip,
|
|
78
69
|
// mirroring anthropic-oauth's _modelRefreshInFlight.
|
|
79
70
|
let _modelRefreshInFlight = null;
|
|
80
71
|
|
|
81
|
-
export async function fetchGeminiModelPages(apiKey, fetchFn = fetch) {
|
|
82
|
-
const items = [];
|
|
83
|
-
let pageToken = '';
|
|
84
|
-
do {
|
|
85
|
-
const params = new URLSearchParams({ key: apiKey, pageSize: '1000' });
|
|
86
|
-
if (pageToken) params.set('pageToken', pageToken);
|
|
87
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models?${params}`;
|
|
88
|
-
const res = await fetchFn(url, {
|
|
89
|
-
signal: AbortSignal.timeout(60_000),
|
|
90
|
-
dispatcher: getLlmDispatcher(),
|
|
91
|
-
});
|
|
92
|
-
if (!res.ok) throw new Error(`gemini list_models ${res.status}`);
|
|
93
|
-
const data = await res.json();
|
|
94
|
-
if (Array.isArray(data?.models)) items.push(...data.models);
|
|
95
|
-
pageToken = typeof data?.nextPageToken === 'string' ? data.nextPageToken : '';
|
|
96
|
-
} while (pageToken);
|
|
97
|
-
return items;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const _modelCache = makeModelCache({
|
|
101
|
-
fileName: 'gemini-models.json',
|
|
102
|
-
ttlMs: MODEL_CACHE_TTL_MS,
|
|
103
|
-
version: GEMINI_MODEL_CACHE_SCHEMA_VERSION,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Mirror of anthropic-oauth.mjs _compareVersion: compare two gemini ids by the
|
|
107
|
-
// X.Y version embedded in the id (gemini-3.5-flash -> [3, 5]). Falls back to a
|
|
108
|
-
// lexicographic tiebreak so ordering is total.
|
|
109
|
-
function _compareVersion(a, b) {
|
|
110
|
-
const na = (a.match(/gemini-(\d+)(?:\.(\d+))?/) || []).slice(1).map(Number);
|
|
111
|
-
const nb = (b.match(/gemini-(\d+)(?:\.(\d+))?/) || []).slice(1).map(Number);
|
|
112
|
-
for (let i = 0; i < Math.max(na.length, nb.length); i++) {
|
|
113
|
-
if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
|
|
114
|
-
}
|
|
115
|
-
return a.localeCompare(b);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Per family, mark the highest-version model as latest:true.
|
|
119
|
-
function _markLatestGemini(models) {
|
|
120
|
-
const byFamily = new Map();
|
|
121
|
-
for (const m of models) {
|
|
122
|
-
if (!m?.id) continue;
|
|
123
|
-
const cur = byFamily.get(m.family);
|
|
124
|
-
if (!cur || _compareVersion(m.id, cur.id) > 0) {
|
|
125
|
-
byFamily.set(m.family, m);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
for (const m of byFamily.values()) m.latest = true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Newest chat model by VERSION in the 'gemini-flash' family, read from the
|
|
132
|
-
// on-disk catalog cache. Returns null until cached; callers warm via
|
|
133
|
-
// ensureLatestGeminiModel when null.
|
|
134
|
-
export function resolveLatestGeminiModel() {
|
|
135
|
-
const cached = _modelCache.loadSync();
|
|
136
|
-
if (!Array.isArray(cached)) return null;
|
|
137
|
-
let best = null;
|
|
138
|
-
for (const m of cached) {
|
|
139
|
-
if (!m?.id || m.family !== 'gemini-flash') continue;
|
|
140
|
-
if (!best || _compareVersion(m.id, best.id) > 0) best = m;
|
|
141
|
-
}
|
|
142
|
-
return best?.id || null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function ensureLatestGeminiModel(provider) {
|
|
146
|
-
let m = resolveLatestGeminiModel();
|
|
147
|
-
if (m) return m;
|
|
148
|
-
await provider._refreshModelCache();
|
|
149
|
-
m = resolveLatestGeminiModel();
|
|
150
|
-
if (m) return m;
|
|
151
|
-
throw new Error('[gemini] model catalog unavailable after warmup — cannot resolve default model');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
72
|
// --- Cache accounting/trace: extracted to gemini-cache.mjs ---
|
|
155
73
|
// --- Stream consumption/guards: extracted to gemini-stream.mjs ---
|
|
156
74
|
// --- Schema/content/tool-call mapping: extracted to gemini-schema.mjs ---
|
|
@@ -918,45 +836,12 @@ export class GeminiProvider {
|
|
|
918
836
|
// TTL check) and _refreshModelCache() (bypassing it). Throws on failure so
|
|
919
837
|
// each caller applies its own fallback/logging.
|
|
920
838
|
async _fetchAndCacheModels(apiKey) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
// (robotics, computer-use) that the chat picker should never show.
|
|
924
|
-
const normalized = items
|
|
925
|
-
.filter(m => (m?.name || '').includes('gemini'))
|
|
926
|
-
.filter(m => !Array.isArray(m?.supportedGenerationMethods)
|
|
927
|
-
|| m.supportedGenerationMethods.includes('generateContent'))
|
|
928
|
-
.filter(m => !/embedding|aqa|imagen|robotics|computer-use/.test(m?.name || ''))
|
|
929
|
-
.map(m => {
|
|
930
|
-
const id = (m.name || '').replace(/^models\//, '');
|
|
931
|
-
const family = /flash-lite/.test(id) ? 'gemini-flash-lite'
|
|
932
|
-
: /flash/.test(id) ? 'gemini-flash'
|
|
933
|
-
: /pro/.test(id) ? 'gemini-pro'
|
|
934
|
-
: 'gemini';
|
|
935
|
-
return {
|
|
936
|
-
id,
|
|
937
|
-
display: m.displayName || id,
|
|
938
|
-
family,
|
|
939
|
-
provider: 'gemini',
|
|
940
|
-
// No fabricated fallbacks: when the API omits limits leave
|
|
941
|
-
// them null so enrichModels can fill from the catalog and
|
|
942
|
-
// the picker shows no context label instead of a fake 1M.
|
|
943
|
-
contextWindow: m.inputTokenLimit || null,
|
|
944
|
-
outputTokens: m.outputTokenLimit || null,
|
|
945
|
-
tier: 'version',
|
|
946
|
-
latest: false,
|
|
947
|
-
description: m.description || '',
|
|
948
|
-
};
|
|
949
|
-
});
|
|
950
|
-
_markLatestGemini(normalized);
|
|
951
|
-
// LiteLLM catalog overlays pricing and updated metadata.
|
|
952
|
-
const { enrichModels } = await import('./model-catalog.mjs');
|
|
953
|
-
const { sanitizeModelList } = await import('./model-list-sanitize.mjs');
|
|
954
|
-
const enriched = sanitizeModelList(await enrichModels(normalized, {
|
|
839
|
+
return fetchAndCacheGeminiModels({
|
|
840
|
+
apiKey,
|
|
955
841
|
fetchFn: this._fetch,
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
return enriched;
|
|
842
|
+
modelCache: this._modelCache,
|
|
843
|
+
catalogForceRefresh: this.config.catalogForceRefresh,
|
|
844
|
+
});
|
|
960
845
|
}
|
|
961
846
|
|
|
962
847
|
// Force a catalog refresh (ignores the 24h disk TTL). De-duped via
|