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
|
@@ -31,6 +31,7 @@ import { OpenAICompatProvider } from './openai-compat.mjs';
|
|
|
31
31
|
import { createTimeoutSignal } from '../stall-policy.mjs';
|
|
32
32
|
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
33
33
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
34
|
+
import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
|
|
34
35
|
|
|
35
36
|
// --- Constants ---
|
|
36
37
|
// xAI's shared OAuth client. The consent screen renders this as "Grok Build".
|
|
@@ -584,120 +585,6 @@ export async function ensureLatestGrokModel(provider) {
|
|
|
584
585
|
|
|
585
586
|
let _modelRefreshInFlight = null;
|
|
586
587
|
|
|
587
|
-
// Grok's gRPC tool registry rejects a root anyOf/oneOf when even one branch is
|
|
588
|
-
// not an object. Keep this adapter local to OAuth Grok requests: other
|
|
589
|
-
// providers may support the original union schema. Tool definitions are never
|
|
590
|
-
// mutated, so callers can safely reuse them for later requests.
|
|
591
|
-
function schemasDeepEqual(left, right) {
|
|
592
|
-
if (Object.is(left, right)) return true;
|
|
593
|
-
if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
|
|
594
|
-
if (Array.isArray(left) || Array.isArray(right)) {
|
|
595
|
-
return Array.isArray(left) && Array.isArray(right)
|
|
596
|
-
&& left.length === right.length
|
|
597
|
-
&& left.every((value, index) => schemasDeepEqual(value, right[index]));
|
|
598
|
-
}
|
|
599
|
-
const leftKeys = Object.keys(left).sort();
|
|
600
|
-
const rightKeys = Object.keys(right).sort();
|
|
601
|
-
return leftKeys.length === rightKeys.length
|
|
602
|
-
&& leftKeys.every((key, index) => key === rightKeys[index]
|
|
603
|
-
&& schemasDeepEqual(left[key], right[key]));
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function pureAnyOfAlternatives(schema) {
|
|
607
|
-
return schema && typeof schema === 'object' && !Array.isArray(schema)
|
|
608
|
-
&& Object.keys(schema).length === 1
|
|
609
|
-
&& Array.isArray(schema.anyOf)
|
|
610
|
-
? schema.anyOf
|
|
611
|
-
: [schema];
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function mergeGrokObjectBranchProperties(objectBranches) {
|
|
615
|
-
const properties = {};
|
|
616
|
-
for (const branch of objectBranches) {
|
|
617
|
-
for (const [name, schema] of Object.entries(branch.properties || {})) {
|
|
618
|
-
if (!Object.prototype.hasOwnProperty.call(properties, name)) {
|
|
619
|
-
properties[name] = schema;
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
if (schemasDeepEqual(properties[name], schema)) continue;
|
|
623
|
-
const alternatives = [
|
|
624
|
-
...pureAnyOfAlternatives(properties[name]),
|
|
625
|
-
...pureAnyOfAlternatives(schema),
|
|
626
|
-
];
|
|
627
|
-
const deduped = alternatives.reduce(
|
|
628
|
-
(unique, alternative) => unique.some(item => schemasDeepEqual(item, alternative))
|
|
629
|
-
? unique
|
|
630
|
-
: [...unique, alternative],
|
|
631
|
-
[],
|
|
632
|
-
);
|
|
633
|
-
properties[name] = deduped.length === 1 ? deduped[0] : { anyOf: deduped };
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return properties;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function normalizeGrokToolSchema(schema) {
|
|
640
|
-
if (!schema || typeof schema !== 'object' || Array.isArray(schema)
|
|
641
|
-
|| (!Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf))) {
|
|
642
|
-
return schema;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const { anyOf, oneOf, ...root } = schema;
|
|
646
|
-
const branches = [...(Array.isArray(anyOf) ? anyOf : []), ...(Array.isArray(oneOf) ? oneOf : [])];
|
|
647
|
-
const objectBranches = branches.filter((branch) => branch && typeof branch === 'object' && !Array.isArray(branch)
|
|
648
|
-
&& (branch.type === 'object'
|
|
649
|
-
|| (Array.isArray(branch.type) && branch.type.includes('object'))
|
|
650
|
-
|| (branch.properties && typeof branch.properties === 'object')));
|
|
651
|
-
|
|
652
|
-
// A function's arguments must be an object even if its original union has
|
|
653
|
-
// no explicit object alternative. The permissive fallback still retains
|
|
654
|
-
// root-level metadata such as description and additionalProperties.
|
|
655
|
-
if (!objectBranches.length) {
|
|
656
|
-
return {
|
|
657
|
-
...root,
|
|
658
|
-
type: 'object',
|
|
659
|
-
...(!Object.prototype.hasOwnProperty.call(root, 'additionalProperties')
|
|
660
|
-
? { additionalProperties: true }
|
|
661
|
-
: {}),
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const properties = objectBranches.some(branch => branch.properties) || root.properties
|
|
666
|
-
? {
|
|
667
|
-
...mergeGrokObjectBranchProperties(objectBranches),
|
|
668
|
-
...(root.properties || {}),
|
|
669
|
-
}
|
|
670
|
-
: undefined;
|
|
671
|
-
const branchRequiredInEvery = (Array.isArray(objectBranches[0].required) ? objectBranches[0].required : [])
|
|
672
|
-
.filter(key => objectBranches.every(branch => Array.isArray(branch.required) && branch.required.includes(key)));
|
|
673
|
-
const required = [...new Set([
|
|
674
|
-
...(Array.isArray(root.required) ? root.required : []),
|
|
675
|
-
...branchRequiredInEvery,
|
|
676
|
-
])];
|
|
677
|
-
const { properties: _rootProperties, required: _rootRequired, ...rootWithoutPropertiesOrRequired } = root;
|
|
678
|
-
const mergedObjectBranches = Object.assign({}, ...objectBranches);
|
|
679
|
-
const {
|
|
680
|
-
properties: _branchProperties,
|
|
681
|
-
required: _branchRequired,
|
|
682
|
-
...mergedObjectBranchesWithoutPropertiesOrRequired
|
|
683
|
-
} = mergedObjectBranches;
|
|
684
|
-
return {
|
|
685
|
-
...mergedObjectBranchesWithoutPropertiesOrRequired,
|
|
686
|
-
...rootWithoutPropertiesOrRequired,
|
|
687
|
-
type: 'object',
|
|
688
|
-
...(properties ? { properties } : {}),
|
|
689
|
-
...(required.length ? { required } : {}),
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function normalizeGrokToolSchemas(tools) {
|
|
694
|
-
if (!Array.isArray(tools)) return tools;
|
|
695
|
-
return tools.map((tool) => {
|
|
696
|
-
const inputSchema = normalizeGrokToolSchema(tool?.inputSchema);
|
|
697
|
-
return inputSchema === tool?.inputSchema ? tool : { ...tool, inputSchema };
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
588
|
export class GrokOAuthProvider {
|
|
702
589
|
// OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
|
|
703
590
|
static inputExcludesCache = false;
|
|
@@ -840,7 +727,10 @@ export class GrokOAuthProvider {
|
|
|
840
727
|
process.stderr.write('[grok-oauth] 401, force-refreshing token...\n');
|
|
841
728
|
const fresh = await this.ensureAuth({ forceRefresh: true });
|
|
842
729
|
const retryInner = this._ensureInner(fresh.access_token, useModel);
|
|
843
|
-
|
|
730
|
+
const retryOpts = err?.__warmup?.usage
|
|
731
|
+
? { ...(sendOpts || {}), _carriedWarmup: err.__warmup }
|
|
732
|
+
: sendOpts;
|
|
733
|
+
return await retryInner._doSend(messages, useModel, grokTools, retryOpts);
|
|
844
734
|
}
|
|
845
735
|
throw err;
|
|
846
736
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { providerNativeToolPrefixCount } from '../../../../../session-runtime/provider-request-tools.mjs';
|
|
2
|
+
|
|
3
|
+
export const ANTHROPIC_CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' };
|
|
4
|
+
export const ANTHROPIC_CACHE_TTL_VOLATILE = { type: 'ephemeral' };
|
|
5
|
+
|
|
6
|
+
export function appendAnthropicCacheControl(content, ttl = ANTHROPIC_CACHE_TTL_VOLATILE) {
|
|
7
|
+
const withCacheControl = (block) => {
|
|
8
|
+
if (!block || typeof block !== 'object' || block.cache_control) return block;
|
|
9
|
+
return { ...block, cache_control: ttl };
|
|
10
|
+
};
|
|
11
|
+
if (Array.isArray(content)) {
|
|
12
|
+
if (content.length === 0) return content;
|
|
13
|
+
const next = [...content];
|
|
14
|
+
next[next.length - 1] = withCacheControl(next[next.length - 1]);
|
|
15
|
+
return next;
|
|
16
|
+
}
|
|
17
|
+
if (typeof content === 'string') {
|
|
18
|
+
return [withCacheControl({ type: 'text', text: content })];
|
|
19
|
+
}
|
|
20
|
+
return content;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveAnthropicCacheTtls(opts) {
|
|
24
|
+
const strategy = opts?.cacheStrategy || {};
|
|
25
|
+
const pick = (layer, fallback) => {
|
|
26
|
+
const value = strategy[layer];
|
|
27
|
+
if (value === '1h') return ANTHROPIC_CACHE_TTL_STABLE;
|
|
28
|
+
if (value === '5m') return ANTHROPIC_CACHE_TTL_VOLATILE;
|
|
29
|
+
if (value === 'none') return null;
|
|
30
|
+
return fallback;
|
|
31
|
+
};
|
|
32
|
+
const resolved = {
|
|
33
|
+
tools: pick('tools', null),
|
|
34
|
+
system: pick('system', ANTHROPIC_CACHE_TTL_STABLE),
|
|
35
|
+
tier3: pick('tier3', ANTHROPIC_CACHE_TTL_STABLE),
|
|
36
|
+
messages: pick('messages', ANTHROPIC_CACHE_TTL_STABLE),
|
|
37
|
+
};
|
|
38
|
+
const ttlRank = (ttl) => (ttl === ANTHROPIC_CACHE_TTL_STABLE ? 2 : 1);
|
|
39
|
+
let minRank = Infinity;
|
|
40
|
+
for (const layer of ['system', 'tier3', 'messages']) {
|
|
41
|
+
if (!resolved[layer]) continue;
|
|
42
|
+
const rank = ttlRank(resolved[layer]);
|
|
43
|
+
if (rank > minRank) resolved[layer] = ANTHROPIC_CACHE_TTL_VOLATILE;
|
|
44
|
+
else minRank = rank;
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clampAnthropicThinkingBudget(value, maxTokens) {
|
|
50
|
+
const desired = Math.floor(Number(value));
|
|
51
|
+
const max = Math.floor(Number(maxTokens));
|
|
52
|
+
if (!Number.isFinite(desired) || desired <= 0 || !Number.isFinite(max)) return null;
|
|
53
|
+
const ceiling = max - 1024;
|
|
54
|
+
if (ceiling < 1024) return null;
|
|
55
|
+
return Math.max(1024, Math.min(desired, ceiling));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sanitizeAnthropicInputSchema(schema, toolName, logTag) {
|
|
59
|
+
if (!schema || typeof schema !== 'object') {
|
|
60
|
+
return { type: 'object', properties: {} };
|
|
61
|
+
}
|
|
62
|
+
const compound = schema.oneOf || schema.anyOf || schema.allOf;
|
|
63
|
+
if (!compound) return structuredClone(schema);
|
|
64
|
+
const mergedProps = { ...(schema.properties && typeof schema.properties === 'object' ? schema.properties : {}) };
|
|
65
|
+
const branchDescs = [];
|
|
66
|
+
for (const branch of Array.isArray(compound) ? compound : []) {
|
|
67
|
+
if (branch && typeof branch === 'object' && branch.properties) {
|
|
68
|
+
Object.assign(mergedProps, branch.properties);
|
|
69
|
+
}
|
|
70
|
+
if (branch && typeof branch === 'object') {
|
|
71
|
+
const parts = [];
|
|
72
|
+
if (branch.description) parts.push(branch.description);
|
|
73
|
+
else if (branch.type) parts.push(`type:${branch.type}`);
|
|
74
|
+
if (parts.length) branchDescs.push(parts.join(' '));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const compoundKey = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'allOf';
|
|
78
|
+
let description = schema.description || '';
|
|
79
|
+
if (branchDescs.length) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
let used = 0;
|
|
82
|
+
for (let i = 0; i < branchDescs.length; i++) {
|
|
83
|
+
const value = `(variant ${i + 1}: ${branchDescs[i]})`;
|
|
84
|
+
if (used + value.length + (parts.length ? 1 : 0) > 500) break;
|
|
85
|
+
parts.push(value);
|
|
86
|
+
used += value.length + (parts.length > 1 ? 1 : 0);
|
|
87
|
+
}
|
|
88
|
+
const addition = parts.join(' ');
|
|
89
|
+
if (addition) description = description ? `${description} ${addition}` : addition;
|
|
90
|
+
}
|
|
91
|
+
if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
92
|
+
process.stderr.write(
|
|
93
|
+
`[${logTag}-sanitizer] tool="${toolName ?? ''}" compound="${compoundKey}" branches=${Array.isArray(compound) ? compound.length : 0} mergedProps=${Object.keys(mergedProps).length}\n`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
type: 'object',
|
|
98
|
+
...(description ? { description } : {}),
|
|
99
|
+
properties: mergedProps,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function toAnthropicTools(tools, logTag) {
|
|
104
|
+
return tools.map((tool) => {
|
|
105
|
+
const out = {
|
|
106
|
+
name: tool.name,
|
|
107
|
+
description: tool.description,
|
|
108
|
+
input_schema: sanitizeAnthropicInputSchema(tool.inputSchema, tool.name, logTag),
|
|
109
|
+
};
|
|
110
|
+
if (tool.deferLoading === true || tool.defer_loading === true) out.defer_loading = true;
|
|
111
|
+
return out;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function toAnthropicToolChoice(toolChoice) {
|
|
116
|
+
return toolChoice === 'none' ? { type: 'none' } : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function deferredAnthropicTools(activeTools, messages, opts, provider) {
|
|
120
|
+
if (opts?.session?.deferredNativeTools !== true) return [];
|
|
121
|
+
if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
|
|
122
|
+
const active = new Set(activeTools.map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
123
|
+
const anthropicNative = new Set(['anthropic', 'anthropic-oauth']);
|
|
124
|
+
const discovered = new Set(
|
|
125
|
+
Array.isArray(opts?.session?.deferredDiscoveredTools)
|
|
126
|
+
? opts.session.deferredDiscoveredTools.map((name) => String(name || '').trim()).filter(Boolean)
|
|
127
|
+
: [],
|
|
128
|
+
);
|
|
129
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
130
|
+
const native = message?.nativeToolSearch;
|
|
131
|
+
const source = String(native?.provider || '').toLowerCase();
|
|
132
|
+
if (source && source !== provider
|
|
133
|
+
&& !(anthropicNative.has(source) && anthropicNative.has(provider))) continue;
|
|
134
|
+
for (const name of Array.isArray(native?.toolReferences) ? native.toolReferences : []) {
|
|
135
|
+
const key = String(name || '').trim();
|
|
136
|
+
if (key) discovered.add(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
140
|
+
return catalog
|
|
141
|
+
.filter((tool) => tool?.name && discovered.has(String(tool.name)) && !active.has(String(tool.name)))
|
|
142
|
+
.map((tool) => ({ ...tool, deferLoading: true }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function requestAnthropicTools(tools, messages, opts, provider) {
|
|
146
|
+
const activeTools = Array.isArray(tools) ? tools : [];
|
|
147
|
+
if (opts?.providerToolSnapshotAuthoritative === true) {
|
|
148
|
+
const nativePrefixCount = providerNativeToolPrefixCount(
|
|
149
|
+
activeTools,
|
|
150
|
+
opts.providerNativeToolPrefixCount,
|
|
151
|
+
);
|
|
152
|
+
return [
|
|
153
|
+
...activeTools.slice(0, nativePrefixCount),
|
|
154
|
+
...toAnthropicTools(activeTools.slice(nativePrefixCount), provider),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
const deferredTools = deferredAnthropicTools(activeTools, messages, opts, provider);
|
|
158
|
+
const nativeTools = Array.isArray(opts?.nativeTools)
|
|
159
|
+
? opts.nativeTools.filter((tool) => tool && typeof tool === 'object')
|
|
160
|
+
: [];
|
|
161
|
+
return [
|
|
162
|
+
...nativeTools,
|
|
163
|
+
...toAnthropicTools([...activeTools, ...deferredTools], provider),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function applyAnthropicCacheMarkers(sanitizedMessages, {
|
|
168
|
+
messageTtl = ANTHROPIC_CACHE_TTL_VOLATILE,
|
|
169
|
+
messageSlots = 1,
|
|
170
|
+
} = {}) {
|
|
171
|
+
if (!Array.isArray(sanitizedMessages) || sanitizedMessages.length === 0) {
|
|
172
|
+
return sanitizedMessages;
|
|
173
|
+
}
|
|
174
|
+
const firstText = (content) => {
|
|
175
|
+
if (typeof content === 'string') return content;
|
|
176
|
+
if (Array.isArray(content)) {
|
|
177
|
+
const first = content.find((block) => block?.type === 'text');
|
|
178
|
+
return first && typeof first.text === 'string' ? first.text : '';
|
|
179
|
+
}
|
|
180
|
+
return '';
|
|
181
|
+
};
|
|
182
|
+
const isSystemReminder = (content) => firstText(content).startsWith('<system-reminder>');
|
|
183
|
+
const hasUserText = (message) => {
|
|
184
|
+
if (message?.role !== 'user' || isSystemReminder(message.content)) return false;
|
|
185
|
+
if (typeof message.content === 'string') return message.content.trim().length > 0;
|
|
186
|
+
if (!Array.isArray(message.content)) return false;
|
|
187
|
+
return message.content.some((block) => block?.type === 'text'
|
|
188
|
+
&& typeof block.text === 'string' && block.text.trim().length > 0);
|
|
189
|
+
};
|
|
190
|
+
const previousUserTextAnchorIdx = () => {
|
|
191
|
+
for (let i = sanitizedMessages.length - 2; i >= 0; i--) {
|
|
192
|
+
if (hasUserText(sanitizedMessages[i])) return i;
|
|
193
|
+
}
|
|
194
|
+
return -1;
|
|
195
|
+
};
|
|
196
|
+
const latestToolResultTailIdx = () => {
|
|
197
|
+
for (let i = sanitizedMessages.length - 1; i >= 0; i--) {
|
|
198
|
+
const message = sanitizedMessages[i];
|
|
199
|
+
if (message?.role !== 'user' || !Array.isArray(message.content) || message.content.length === 0) continue;
|
|
200
|
+
if (message.content[message.content.length - 1]?.type === 'tool_result') return i;
|
|
201
|
+
}
|
|
202
|
+
return -1;
|
|
203
|
+
};
|
|
204
|
+
const firstRequestUserPromptIdx = () => {
|
|
205
|
+
if (latestToolResultTailIdx() !== -1 || previousUserTextAnchorIdx() !== -1) return -1;
|
|
206
|
+
const tailIdx = sanitizedMessages.length - 1;
|
|
207
|
+
return hasUserText(sanitizedMessages[tailIdx]) ? tailIdx : -1;
|
|
208
|
+
};
|
|
209
|
+
if (messageTtl !== null) {
|
|
210
|
+
const slots = Math.max(0, Math.min(4, Number(messageSlots) || 0));
|
|
211
|
+
const marked = new Set();
|
|
212
|
+
const candidates = [latestToolResultTailIdx(), previousUserTextAnchorIdx(), firstRequestUserPromptIdx()];
|
|
213
|
+
for (const idx of candidates) {
|
|
214
|
+
if (slots <= 0) break;
|
|
215
|
+
if (idx < 0 || marked.has(idx)) continue;
|
|
216
|
+
const message = sanitizedMessages[idx];
|
|
217
|
+
if (messageTtl?.ttl === '1h' && isSystemReminder(message?.content)) continue;
|
|
218
|
+
message.content = appendAnthropicCacheControl(message.content, messageTtl);
|
|
219
|
+
marked.add(idx);
|
|
220
|
+
if (marked.size >= slots) break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return sanitizedMessages;
|
|
224
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getLlmDispatcher } from '../../../../shared/llm/http-agent.mjs';
|
|
2
|
+
import { makeModelCache } from '../model-cache.mjs';
|
|
3
|
+
|
|
4
|
+
export const GEMINI_MODELS = [
|
|
5
|
+
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
6
|
+
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
7
|
+
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
8
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'gemini', contextWindow: 1048576 },
|
|
9
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'gemini', contextWindow: 1048576 },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_GEMINI_MODEL = GEMINI_MODELS[0].id;
|
|
13
|
+
|
|
14
|
+
export const geminiModelCache = makeModelCache({
|
|
15
|
+
fileName: 'gemini-models.json',
|
|
16
|
+
ttlMs: 24 * 60 * 60_000,
|
|
17
|
+
version: 2,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export async function fetchGeminiModelPages(apiKey, fetchFn = fetch) {
|
|
21
|
+
const items = [];
|
|
22
|
+
let pageToken = '';
|
|
23
|
+
do {
|
|
24
|
+
const params = new URLSearchParams({ key: apiKey, pageSize: '1000' });
|
|
25
|
+
if (pageToken) params.set('pageToken', pageToken);
|
|
26
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models?${params}`;
|
|
27
|
+
const res = await fetchFn(url, {
|
|
28
|
+
signal: AbortSignal.timeout(60_000),
|
|
29
|
+
dispatcher: getLlmDispatcher(),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) throw new Error(`gemini list_models ${res.status}`);
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
if (Array.isArray(data?.models)) items.push(...data.models);
|
|
34
|
+
pageToken = typeof data?.nextPageToken === 'string' ? data.nextPageToken : '';
|
|
35
|
+
} while (pageToken);
|
|
36
|
+
return items;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function compareVersion(a, b) {
|
|
40
|
+
const na = (a.match(/gemini-(\d+)(?:\.(\d+))?/) || []).slice(1).map(Number);
|
|
41
|
+
const nb = (b.match(/gemini-(\d+)(?:\.(\d+))?/) || []).slice(1).map(Number);
|
|
42
|
+
for (let i = 0; i < Math.max(na.length, nb.length); i++) {
|
|
43
|
+
if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
|
|
44
|
+
}
|
|
45
|
+
return a.localeCompare(b);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function markLatest(models) {
|
|
49
|
+
const byFamily = new Map();
|
|
50
|
+
for (const model of models) {
|
|
51
|
+
if (!model?.id) continue;
|
|
52
|
+
const current = byFamily.get(model.family);
|
|
53
|
+
if (!current || compareVersion(model.id, current.id) > 0) {
|
|
54
|
+
byFamily.set(model.family, model);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const model of byFamily.values()) model.latest = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveLatestGeminiModel() {
|
|
61
|
+
const cached = geminiModelCache.loadSync();
|
|
62
|
+
if (!Array.isArray(cached)) return null;
|
|
63
|
+
let best = null;
|
|
64
|
+
for (const model of cached) {
|
|
65
|
+
if (!model?.id || model.family !== 'gemini-flash') continue;
|
|
66
|
+
if (!best || compareVersion(model.id, best.id) > 0) best = model;
|
|
67
|
+
}
|
|
68
|
+
return best?.id || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function ensureLatestGeminiModel(provider) {
|
|
72
|
+
let model = resolveLatestGeminiModel();
|
|
73
|
+
if (model) return model;
|
|
74
|
+
await provider._refreshModelCache();
|
|
75
|
+
model = resolveLatestGeminiModel();
|
|
76
|
+
if (model) return model;
|
|
77
|
+
throw new Error('[gemini] model catalog unavailable after warmup — cannot resolve default model');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function fetchAndCacheGeminiModels({
|
|
81
|
+
apiKey,
|
|
82
|
+
fetchFn,
|
|
83
|
+
modelCache,
|
|
84
|
+
catalogForceRefresh,
|
|
85
|
+
}) {
|
|
86
|
+
const items = await fetchGeminiModelPages(apiKey, fetchFn);
|
|
87
|
+
const normalized = items
|
|
88
|
+
.filter(model => (model?.name || '').includes('gemini'))
|
|
89
|
+
.filter(model => !Array.isArray(model?.supportedGenerationMethods)
|
|
90
|
+
|| model.supportedGenerationMethods.includes('generateContent'))
|
|
91
|
+
.filter(model => !/embedding|aqa|imagen|robotics|computer-use/.test(model?.name || ''))
|
|
92
|
+
.map(model => {
|
|
93
|
+
const id = (model.name || '').replace(/^models\//, '');
|
|
94
|
+
const family = /flash-lite/.test(id) ? 'gemini-flash-lite'
|
|
95
|
+
: /flash/.test(id) ? 'gemini-flash'
|
|
96
|
+
: /pro/.test(id) ? 'gemini-pro'
|
|
97
|
+
: 'gemini';
|
|
98
|
+
return {
|
|
99
|
+
id,
|
|
100
|
+
display: model.displayName || id,
|
|
101
|
+
family,
|
|
102
|
+
provider: 'gemini',
|
|
103
|
+
contextWindow: model.inputTokenLimit || null,
|
|
104
|
+
outputTokens: model.outputTokenLimit || null,
|
|
105
|
+
tier: 'version',
|
|
106
|
+
latest: false,
|
|
107
|
+
description: model.description || '',
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
markLatest(normalized);
|
|
111
|
+
const { enrichModels } = await import('../model-catalog.mjs');
|
|
112
|
+
const { sanitizeModelList } = await import('../model-list-sanitize.mjs');
|
|
113
|
+
const enriched = sanitizeModelList(await enrichModels(normalized, {
|
|
114
|
+
fetchFn,
|
|
115
|
+
force: catalogForceRefresh === true,
|
|
116
|
+
}), { provider: 'gemini' });
|
|
117
|
+
modelCache.save(enriched);
|
|
118
|
+
return enriched;
|
|
119
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Grok's gRPC tool registry rejects a root anyOf/oneOf when even one branch
|
|
2
|
+
// is not an object. Tool definitions are never mutated.
|
|
3
|
+
|
|
4
|
+
function schemasDeepEqual(left, right) {
|
|
5
|
+
if (Object.is(left, right)) return true;
|
|
6
|
+
if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
|
|
7
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
8
|
+
return Array.isArray(left) && Array.isArray(right)
|
|
9
|
+
&& left.length === right.length
|
|
10
|
+
&& left.every((value, index) => schemasDeepEqual(value, right[index]));
|
|
11
|
+
}
|
|
12
|
+
const leftKeys = Object.keys(left).sort();
|
|
13
|
+
const rightKeys = Object.keys(right).sort();
|
|
14
|
+
return leftKeys.length === rightKeys.length
|
|
15
|
+
&& leftKeys.every((key, index) => key === rightKeys[index]
|
|
16
|
+
&& schemasDeepEqual(left[key], right[key]));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pureAnyOfAlternatives(schema) {
|
|
20
|
+
return schema && typeof schema === 'object' && !Array.isArray(schema)
|
|
21
|
+
&& Object.keys(schema).length === 1
|
|
22
|
+
&& Array.isArray(schema.anyOf)
|
|
23
|
+
? schema.anyOf
|
|
24
|
+
: [schema];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mergeObjectBranchProperties(objectBranches) {
|
|
28
|
+
const properties = {};
|
|
29
|
+
for (const branch of objectBranches) {
|
|
30
|
+
for (const [name, schema] of Object.entries(branch.properties || {})) {
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(properties, name)) {
|
|
32
|
+
properties[name] = schema;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (schemasDeepEqual(properties[name], schema)) continue;
|
|
36
|
+
const alternatives = [
|
|
37
|
+
...pureAnyOfAlternatives(properties[name]),
|
|
38
|
+
...pureAnyOfAlternatives(schema),
|
|
39
|
+
];
|
|
40
|
+
const deduped = alternatives.reduce(
|
|
41
|
+
(unique, alternative) => unique.some(item => schemasDeepEqual(item, alternative))
|
|
42
|
+
? unique
|
|
43
|
+
: [...unique, alternative],
|
|
44
|
+
[],
|
|
45
|
+
);
|
|
46
|
+
properties[name] = deduped.length === 1 ? deduped[0] : { anyOf: deduped };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return properties;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeGrokToolSchema(schema) {
|
|
53
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)
|
|
54
|
+
|| (!Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf))) {
|
|
55
|
+
return schema;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { anyOf, oneOf, ...root } = schema;
|
|
59
|
+
const branches = [...(Array.isArray(anyOf) ? anyOf : []), ...(Array.isArray(oneOf) ? oneOf : [])];
|
|
60
|
+
const objectBranches = branches.filter((branch) => branch && typeof branch === 'object' && !Array.isArray(branch)
|
|
61
|
+
&& (branch.type === 'object'
|
|
62
|
+
|| (Array.isArray(branch.type) && branch.type.includes('object'))
|
|
63
|
+
|| (branch.properties && typeof branch.properties === 'object')));
|
|
64
|
+
|
|
65
|
+
if (!objectBranches.length) {
|
|
66
|
+
return {
|
|
67
|
+
...root,
|
|
68
|
+
type: 'object',
|
|
69
|
+
...(!Object.prototype.hasOwnProperty.call(root, 'additionalProperties')
|
|
70
|
+
? { additionalProperties: true }
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const properties = objectBranches.some(branch => branch.properties) || root.properties
|
|
76
|
+
? {
|
|
77
|
+
...mergeObjectBranchProperties(objectBranches),
|
|
78
|
+
...(root.properties || {}),
|
|
79
|
+
}
|
|
80
|
+
: undefined;
|
|
81
|
+
const branchRequiredInEvery = (Array.isArray(objectBranches[0].required) ? objectBranches[0].required : [])
|
|
82
|
+
.filter(key => objectBranches.every(branch => Array.isArray(branch.required) && branch.required.includes(key)));
|
|
83
|
+
const required = [...new Set([
|
|
84
|
+
...(Array.isArray(root.required) ? root.required : []),
|
|
85
|
+
...branchRequiredInEvery,
|
|
86
|
+
])];
|
|
87
|
+
const { properties: _rootProperties, required: _rootRequired, ...rootWithoutPropertiesOrRequired } = root;
|
|
88
|
+
const mergedObjectBranches = Object.assign({}, ...objectBranches);
|
|
89
|
+
const {
|
|
90
|
+
properties: _branchProperties,
|
|
91
|
+
required: _branchRequired,
|
|
92
|
+
...mergedObjectBranchesWithoutPropertiesOrRequired
|
|
93
|
+
} = mergedObjectBranches;
|
|
94
|
+
return {
|
|
95
|
+
...mergedObjectBranchesWithoutPropertiesOrRequired,
|
|
96
|
+
...rootWithoutPropertiesOrRequired,
|
|
97
|
+
type: 'object',
|
|
98
|
+
...(properties ? { properties } : {}),
|
|
99
|
+
...(required.length ? { required } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeGrokToolSchemas(tools) {
|
|
104
|
+
if (!Array.isArray(tools)) return tools;
|
|
105
|
+
return tools.map((tool) => {
|
|
106
|
+
const inputSchema = normalizeGrokToolSchema(tool?.inputSchema);
|
|
107
|
+
return inputSchema === tool?.inputSchema ? tool : { ...tool, inputSchema };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function truncatedCompatStreamError(label, detail) {
|
|
2
|
+
return Object.assign(
|
|
3
|
+
new Error(`${label} SSE stream truncated${detail ? `: ${detail}` : ''}`),
|
|
4
|
+
{ name: 'TruncatedStreamError', code: 'TRUNCATED_STREAM', truncatedStream: true },
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Invalid-tool-args marker: completed-but-malformed tool_call arguments JSON must NOT throw
|
|
9
|
+
// (kills the turn) NOR be silently swallowed to `{}`. Instead the parse
|
|
10
|
+
// failure is carried as data on the tool call's `arguments` slot so the
|
|
11
|
+
// dispatch loop can turn it into an is_error tool_result and let the model
|
|
12
|
+
// re-issue the call with valid JSON in the SAME turn (follow-up retry).
|
|
13
|
+
// { __invalidToolArgs: true, __rawArguments: <raw string>, __parseError: <msg> }
|
|
14
|
+
export function makeInvalidToolArgsMarker(rawArguments, parseError) {
|
|
15
|
+
return {
|
|
16
|
+
__invalidToolArgs: true,
|
|
17
|
+
__rawArguments: typeof rawArguments === 'string' ? rawArguments : String(rawArguments ?? ''),
|
|
18
|
+
__parseError: typeof parseError === 'string' ? parseError : String(parseError ?? 'parse error'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function isInvalidToolArgsMarker(value) {
|
|
22
|
+
return !!value && typeof value === 'object' && value.__invalidToolArgs === true;
|
|
23
|
+
}
|
|
24
|
+
/** Model-facing tool_result text for a tool call whose arguments failed to
|
|
25
|
+
* parse; instructs the model to retry with valid JSON in the same turn. */
|
|
26
|
+
export function formatInvalidToolArgsResult(call) {
|
|
27
|
+
const name = call?.name || 'tool';
|
|
28
|
+
const detail = call?.arguments?.__parseError || 'arguments were not valid JSON';
|
|
29
|
+
return `The arguments provided to \`${name}\` are invalid JSON and could not be parsed: ${detail}. Re-issue this tool call with valid JSON arguments.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Completed tool_call.arguments must be valid JSON; empty/missing → {}.
|
|
33
|
+
* @param {any} raw - raw arguments value (string or object)
|
|
34
|
+
* @param {string} label - provider label for error messages
|
|
35
|
+
* @param {{id?:string,name?:string,index?:number,finishReason?:string}} [meta] - optional tool-call identity for diagnostics.
|
|
36
|
+
* When `meta.finishReason` is set, a completion/finish signal was observed for
|
|
37
|
+
* the call: a JSON.parse failure is then deterministic bad JSON (permanent),
|
|
38
|
+
* not a mid-stream truncation (retryable). */
|
|
39
|
+
export function parseCompletedToolCallArgumentsJson(raw, label, meta) {
|
|
40
|
+
const text = typeof raw === 'string' ? raw : (raw == null ? '' : String(raw));
|
|
41
|
+
const src = text === '' ? '{}' : text;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(src);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const preview = text.length <= 64
|
|
46
|
+
? text
|
|
47
|
+
: text.slice(0, 32) + '...' + text.slice(-32);
|
|
48
|
+
const detailParts = [`invalid tool_call arguments JSON: len=${text.length} preview=${JSON.stringify(preview)}`];
|
|
49
|
+
if (meta) {
|
|
50
|
+
const m = {};
|
|
51
|
+
if (meta.id) m.id = meta.id;
|
|
52
|
+
if (meta.name) m.name = meta.name;
|
|
53
|
+
if (meta.index != null) m.index = meta.index;
|
|
54
|
+
if (meta.finishReason) m.finishReason = meta.finishReason;
|
|
55
|
+
detailParts.push(`tool=${JSON.stringify(m)}`);
|
|
56
|
+
}
|
|
57
|
+
// Invariant: a completion/finish signal was observed for this tool call
|
|
58
|
+
// (finish_reason present, or a per-call/response "done" event fired), so
|
|
59
|
+
// the arguments are NOT mid-stream-truncated — they are complete but
|
|
60
|
+
// malformed. Return an invalid-args MARKER (not a throw) so the
|
|
61
|
+
// dispatch loop feeds the parse error back to the model as a
|
|
62
|
+
// tool_result and the model self-corrects in the same turn. Only an
|
|
63
|
+
// unfinished stream (no finishReason) stays the retryable truncation
|
|
64
|
+
// case — that transient behavior is deliberately preserved.
|
|
65
|
+
if (meta?.finishReason) {
|
|
66
|
+
return makeInvalidToolArgsMarker(text, err instanceof Error ? err.message : String(err));
|
|
67
|
+
}
|
|
68
|
+
throw truncatedCompatStreamError(label, detailParts.join(' '));
|
|
69
|
+
}
|
|
70
|
+
}
|