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.
Files changed (111) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench-run.mjs +2 -2
  3. package/scripts/compact-pressure-test.mjs +104 -0
  4. package/scripts/desktop-session-bridge-test.mjs +704 -0
  5. package/scripts/freevar-smoke.mjs +7 -4
  6. package/scripts/lifecycle-api-test.mjs +65 -4
  7. package/scripts/max-output-recovery-test.mjs +31 -0
  8. package/scripts/memory-core-input-test.mjs +10 -0
  9. package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
  10. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  11. package/scripts/parent-abort-link-test.mjs +24 -0
  12. package/scripts/process-lifecycle-test.mjs +80 -22
  13. package/scripts/provider-contract-test.mjs +257 -0
  14. package/scripts/provider-toolcall-test.mjs +172 -10
  15. package/scripts/session-orphan-sweep-test.mjs +27 -1
  16. package/src/lib/keychain-cjs.cjs +36 -23
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
  18. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  19. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
  22. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
  24. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
  27. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  28. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  29. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  30. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
  41. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
  42. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  43. package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
  44. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
  46. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  47. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  48. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  50. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  53. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
  55. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  62. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
  63. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  64. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
  65. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  66. package/src/runtime/channels/backends/discord.mjs +6 -6
  67. package/src/runtime/channels/lib/config.mjs +15 -2
  68. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  69. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  70. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  72. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  73. package/src/runtime/memory/index.mjs +24 -198
  74. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  75. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  76. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  77. package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
  78. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  79. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  80. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  81. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  82. package/src/runtime/shared/config.mjs +58 -13
  83. package/src/runtime/shared/llm/cost.mjs +14 -4
  84. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  85. package/src/runtime/shared/process-lifecycle.mjs +92 -19
  86. package/src/runtime/shared/process-shutdown.mjs +6 -0
  87. package/src/runtime/shared/resource-admission.mjs +7 -2
  88. package/src/session-runtime/channel-config-api.mjs +7 -7
  89. package/src/session-runtime/config-lifecycle.mjs +20 -17
  90. package/src/session-runtime/cwd-plugins.mjs +9 -7
  91. package/src/session-runtime/env.mjs +1 -2
  92. package/src/session-runtime/hitch-profile.mjs +45 -0
  93. package/src/session-runtime/lifecycle-api.mjs +36 -9
  94. package/src/session-runtime/mcp-glue.mjs +6 -11
  95. package/src/session-runtime/provider-init-key.mjs +17 -0
  96. package/src/session-runtime/runtime-core.mjs +44 -103
  97. package/src/session-runtime/runtime-paths.mjs +20 -0
  98. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  99. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  100. package/src/session-runtime/tool-catalog.mjs +15 -89
  101. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  102. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  103. package/src/standalone/agent-tool.mjs +12 -162
  104. package/src/standalone/channel-admin.mjs +29 -0
  105. package/src/tui/App.jsx +11 -5
  106. package/src/tui/dist/index.mjs +202 -65
  107. package/src/tui/engine/session-api-ext.mjs +37 -4
  108. package/src/tui/index.jsx +1 -0
  109. package/src/tui/lib/voice-setup.mjs +5 -5
  110. package/scripts/_devtools-stub.mjs +0 -1
  111. 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
- return await retryInner._doSend(messages, useModel, grokTools, sendOpts);
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,6 @@
1
+ export function envPositiveInt(name, fallback) {
2
+ const raw = process.env[name];
3
+ if (raw == null || raw === '') return fallback;
4
+ const value = Number(raw);
5
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
6
+ }
@@ -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
+ }