mixdog 0.9.50 → 0.9.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
} from '../stall-policy.mjs';
|
|
18
18
|
import { scanLeakedToolCalls } from './anthropic-leaked-toolcall.mjs';
|
|
19
19
|
import { traceHash, stableTraceStringify } from './trace-utils.mjs';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
parseToolCalls,
|
|
22
|
+
emitGeminiToolCalls,
|
|
23
|
+
collectGeminiGroundingSources,
|
|
24
|
+
parseGeminiTextPartMetadata,
|
|
25
|
+
} from './gemini.mjs';
|
|
21
26
|
|
|
22
27
|
export const GEMINI_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
|
|
23
28
|
'MIXDOG_GEMINI_FIRST_BYTE_TIMEOUT_MS',
|
|
@@ -47,10 +52,17 @@ export function geminiTruncatedStreamError(message) {
|
|
|
47
52
|
// stall after visible output was silently retried. Visible-text stalls also
|
|
48
53
|
// gain streamStalled + partialContent so the loop's partial-final path can
|
|
49
54
|
// keep the streamed output instead of dropping the turn.
|
|
50
|
-
export function stampGeminiStreamFailure(err, {
|
|
55
|
+
export function stampGeminiStreamFailure(err, {
|
|
56
|
+
relayedText = '',
|
|
57
|
+
textLeakGuard = null,
|
|
58
|
+
sawFunctionCall = false,
|
|
59
|
+
chunks = [],
|
|
60
|
+
} = {}) {
|
|
51
61
|
if (!err || typeof err !== 'object') return err;
|
|
52
62
|
const leaked = (textLeakGuard?.getLeakedToolCalls?.() || []).length > 0;
|
|
53
|
-
const
|
|
63
|
+
const finalizedText = textLeakGuard?.getVisibleText?.();
|
|
64
|
+
const visibleText = typeof finalizedText === 'string' ? finalizedText : relayedText;
|
|
65
|
+
const visible = visibleText.length > 0;
|
|
54
66
|
// A native functionCall chunk (not a text-leaked one) is also an in-flight
|
|
55
67
|
// tool use — replaying would double-dispatch, and partial-final must NOT
|
|
56
68
|
// treat it as a clean no-tool summary.
|
|
@@ -58,13 +70,16 @@ export function stampGeminiStreamFailure(err, { relayedText = '', textLeakGuard
|
|
|
58
70
|
try {
|
|
59
71
|
if (visible) { err.liveTextEmitted = true; err.unsafeToRetry = true; }
|
|
60
72
|
if (pendingTool) { err.emittedToolCall = true; err.unsafeToRetry = true; }
|
|
73
|
+
const partialParts = aggregateGeminiStreamChunks(chunks)?.candidates?.[0]?.content?.parts || [];
|
|
74
|
+
const providerMetadata = parseGeminiTextPartMetadata(partialParts);
|
|
75
|
+
if (providerMetadata) err.providerMetadata = providerMetadata;
|
|
61
76
|
// TRUNCATED_STREAM EOF after visible output must also carry the
|
|
62
77
|
// partial-final stamps (streamStalled/partialContent), aligning with the
|
|
63
78
|
// compat streams — otherwise live output is dropped instead of kept.
|
|
64
79
|
if (visible && !leaked
|
|
65
80
|
&& (err.code === 'EGEMINITIMEOUT' || err.code === 'TRUNCATED_STREAM' || err.truncatedStream === true)) {
|
|
66
81
|
err.streamStalled = true;
|
|
67
|
-
if (typeof err.partialContent !== 'string') err.partialContent =
|
|
82
|
+
if (typeof err.partialContent !== 'string') err.partialContent = visibleText;
|
|
68
83
|
if (err.pendingToolUse === undefined) err.pendingToolUse = pendingTool;
|
|
69
84
|
}
|
|
70
85
|
} catch { /* best-effort */ }
|
|
@@ -113,6 +128,7 @@ export function aggregateGeminiStreamChunks(responses) {
|
|
|
113
128
|
for (const part of candidate.content.parts) {
|
|
114
129
|
const newPart = {};
|
|
115
130
|
if (part.text) newPart.text = part.text;
|
|
131
|
+
if (part.thought === true) newPart.thought = true;
|
|
116
132
|
if (part.functionCall) newPart.functionCall = part.functionCall;
|
|
117
133
|
if (part.thoughtSignature) newPart.thoughtSignature = part.thoughtSignature;
|
|
118
134
|
if (part.thought_signature) newPart.thought_signature = part.thought_signature;
|
|
@@ -130,11 +146,11 @@ export function aggregateGeminiStreamChunks(responses) {
|
|
|
130
146
|
return aggregatedResponse;
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
export function assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label }) {
|
|
149
|
+
export function assertGeminiStreamCompleted({ sawStreamChunk, finishReason, promptBlockReason, label }) {
|
|
134
150
|
if (!sawStreamChunk) {
|
|
135
151
|
throw geminiTruncatedStreamError(`${label} truncated: empty stream`);
|
|
136
152
|
}
|
|
137
|
-
if (!finishReason) {
|
|
153
|
+
if (!finishReason && !promptBlockReason) {
|
|
138
154
|
throw geminiTruncatedStreamError(`${label} truncated: no finishReason`);
|
|
139
155
|
}
|
|
140
156
|
}
|
|
@@ -148,7 +164,8 @@ export function geminiChunkText(chunk) {
|
|
|
148
164
|
if (!Array.isArray(parts)) return '';
|
|
149
165
|
let text = '';
|
|
150
166
|
for (const p of parts) {
|
|
151
|
-
|
|
167
|
+
// Thought summaries are reasoning, not user-visible answer deltas.
|
|
168
|
+
if (p && p.thought !== true && typeof p.text === 'string') text += p.text;
|
|
152
169
|
}
|
|
153
170
|
return text;
|
|
154
171
|
}
|
|
@@ -171,6 +188,7 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
171
188
|
const _enabled = _knownTools.size > 0;
|
|
172
189
|
const _isKnownTool = (name) => _knownTools.has(name);
|
|
173
190
|
let leakBuffer = '';
|
|
191
|
+
let visibleText = '';
|
|
174
192
|
const leakedCalls = [];
|
|
175
193
|
const dispatchedFingerprints = new Set();
|
|
176
194
|
|
|
@@ -207,7 +225,10 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
207
225
|
const { emit, calls, rest } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final });
|
|
208
226
|
leakBuffer = rest;
|
|
209
227
|
if (emit && onTextDelta) {
|
|
228
|
+
visibleText += emit;
|
|
210
229
|
try { onTextDelta(emit); } catch {}
|
|
230
|
+
} else if (emit) {
|
|
231
|
+
visibleText += emit;
|
|
211
232
|
}
|
|
212
233
|
for (const c of calls) dispatchLeakedCall(c);
|
|
213
234
|
};
|
|
@@ -217,6 +238,7 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
217
238
|
feedText(text) {
|
|
218
239
|
if (!text) return;
|
|
219
240
|
if (!_enabled) {
|
|
241
|
+
visibleText += text;
|
|
220
242
|
try { onTextDelta?.(text); } catch {}
|
|
221
243
|
return;
|
|
222
244
|
}
|
|
@@ -247,6 +269,9 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
247
269
|
getLeakedToolCalls() {
|
|
248
270
|
return leakedCalls.length ? [...leakedCalls] : [];
|
|
249
271
|
},
|
|
272
|
+
getVisibleText() {
|
|
273
|
+
return visibleText;
|
|
274
|
+
},
|
|
250
275
|
};
|
|
251
276
|
}
|
|
252
277
|
|
|
@@ -262,6 +287,12 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
262
287
|
let idleReject = null;
|
|
263
288
|
let relayedText = '';
|
|
264
289
|
let sawFunctionCall = false;
|
|
290
|
+
let leakGuardFinalized = false;
|
|
291
|
+
const finalizeLeakGuard = () => {
|
|
292
|
+
if (leakGuardFinalized) return;
|
|
293
|
+
leakGuardFinalized = true;
|
|
294
|
+
try { textLeakGuard?.finalize(); } catch {}
|
|
295
|
+
};
|
|
265
296
|
|
|
266
297
|
let firstByteTimer = setTimeout(() => {
|
|
267
298
|
try { reader.cancel('first byte timeout'); } catch {}
|
|
@@ -381,25 +412,27 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
381
412
|
}
|
|
382
413
|
}
|
|
383
414
|
} catch (err) {
|
|
384
|
-
|
|
415
|
+
finalizeLeakGuard();
|
|
416
|
+
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall, chunks: allChunks });
|
|
385
417
|
} finally {
|
|
386
418
|
clearFirstByteTimer();
|
|
387
419
|
if (idleTimer) clearTimeout(idleTimer);
|
|
388
420
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
389
421
|
try { reader.releaseLock(); } catch {}
|
|
390
|
-
|
|
422
|
+
finalizeLeakGuard();
|
|
391
423
|
}
|
|
392
424
|
|
|
393
425
|
const aggregated = aggregateGeminiStreamChunks(allChunks);
|
|
394
426
|
const finishReason = aggregated.candidates?.[0]?.finishReason || null;
|
|
427
|
+
const promptBlockReason = aggregated.promptFeedback?.blockReason || null;
|
|
395
428
|
// Truncation (no finishReason) after visible output must carry the same
|
|
396
429
|
// safety stamps as an in-loop failure — assert throws OUTSIDE the catch
|
|
397
430
|
// above, so stamp here too (review High: transient TRUNCATED_STREAM would
|
|
398
431
|
// otherwise replay/double-render live text via withRetry).
|
|
399
432
|
try {
|
|
400
|
-
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
|
|
433
|
+
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, promptBlockReason, label });
|
|
401
434
|
} catch (err) {
|
|
402
|
-
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
|
|
435
|
+
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall, chunks: allChunks });
|
|
403
436
|
}
|
|
404
437
|
return aggregated;
|
|
405
438
|
}
|
|
@@ -413,6 +446,12 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
|
|
|
413
446
|
let inFlightReject = null;
|
|
414
447
|
let relayedText = '';
|
|
415
448
|
let sawFunctionCall = false;
|
|
449
|
+
let leakGuardFinalized = false;
|
|
450
|
+
const finalizeLeakGuard = () => {
|
|
451
|
+
if (leakGuardFinalized) return;
|
|
452
|
+
leakGuardFinalized = true;
|
|
453
|
+
try { textLeakGuard?.finalize(); } catch {}
|
|
454
|
+
};
|
|
416
455
|
|
|
417
456
|
const armFirstByteTimer = () => {
|
|
418
457
|
if (firstByteTimer) clearTimeout(firstByteTimer);
|
|
@@ -536,18 +575,20 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
|
|
|
536
575
|
}
|
|
537
576
|
} catch (err) {
|
|
538
577
|
clearFirstByteTimer();
|
|
578
|
+
let failure = err;
|
|
539
579
|
if (signal?.aborted) {
|
|
540
580
|
const reason = signal.reason;
|
|
541
|
-
|
|
581
|
+
failure = reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
542
582
|
}
|
|
543
|
-
|
|
583
|
+
finalizeLeakGuard();
|
|
584
|
+
throw stampGeminiStreamFailure(failure, { relayedText, textLeakGuard, sawFunctionCall, chunks: collectedChunks });
|
|
544
585
|
} finally {
|
|
545
586
|
clearFirstByteTimer();
|
|
546
587
|
if (idleTimer) clearTimeout(idleTimer);
|
|
547
588
|
if (signal && onSignalAbort) {
|
|
548
589
|
try { signal.removeEventListener('abort', onSignalAbort); } catch {}
|
|
549
590
|
}
|
|
550
|
-
|
|
591
|
+
finalizeLeakGuard();
|
|
551
592
|
}
|
|
552
593
|
|
|
553
594
|
// Aggregate the raw wire chunks locally instead of awaiting the SDK's
|
|
@@ -566,21 +607,29 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
|
|
|
566
607
|
try {
|
|
567
608
|
response = await streamResult.response;
|
|
568
609
|
} catch (err) {
|
|
610
|
+
let failure = err;
|
|
569
611
|
if (signal?.aborted) {
|
|
570
612
|
const reason = signal.reason;
|
|
571
|
-
|
|
613
|
+
failure = reason instanceof Error ? reason : new Error(`${label} aborted`);
|
|
572
614
|
}
|
|
573
|
-
|
|
615
|
+
finalizeLeakGuard();
|
|
616
|
+
throw stampGeminiStreamFailure(failure, {
|
|
617
|
+
relayedText,
|
|
618
|
+
textLeakGuard,
|
|
619
|
+
sawFunctionCall,
|
|
620
|
+
chunks: collectedChunks,
|
|
621
|
+
});
|
|
574
622
|
}
|
|
575
623
|
raw = response?.candidates ? response : (response?.response || response);
|
|
576
624
|
}
|
|
577
625
|
const finishReason = raw?.candidates?.[0]?.finishReason || null;
|
|
626
|
+
const promptBlockReason = raw?.promptFeedback?.blockReason || null;
|
|
578
627
|
// Same stamping as the REST consumer: truncation after visible output
|
|
579
628
|
// must not classify transient (review High).
|
|
580
629
|
try {
|
|
581
|
-
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
|
|
630
|
+
assertGeminiStreamCompleted({ sawStreamChunk, finishReason, promptBlockReason, label });
|
|
582
631
|
} catch (err) {
|
|
583
|
-
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
|
|
632
|
+
throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall, chunks: collectedChunks });
|
|
584
633
|
}
|
|
585
634
|
return raw;
|
|
586
635
|
}
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
parseToolCalls,
|
|
30
30
|
emitGeminiToolCalls,
|
|
31
31
|
collectGeminiGroundingSources,
|
|
32
|
+
parseGeminiThinkingParts,
|
|
33
|
+
parseGeminiTextPartMetadata,
|
|
32
34
|
} from './gemini-schema.mjs';
|
|
33
35
|
import {
|
|
34
36
|
_estimateGeminiCacheTokens,
|
|
@@ -45,12 +47,14 @@ import {
|
|
|
45
47
|
writeGeminiCacheTrace,
|
|
46
48
|
geminiGlobalCacheCreates,
|
|
47
49
|
GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS,
|
|
50
|
+
_geminiCredentialFingerprint,
|
|
51
|
+
_invalidateGeminiCachesForCredentialFingerprint,
|
|
48
52
|
} from './gemini-cache.mjs';
|
|
49
53
|
|
|
50
54
|
// Legacy import path: tests + gemini-stream import these from gemini.mjs.
|
|
51
55
|
// Re-export the extracted symbols so existing importers resolve unchanged.
|
|
52
56
|
export { createGeminiTextLeakGuard };
|
|
53
|
-
export { parseToolCalls, emitGeminiToolCalls, collectGeminiGroundingSources };
|
|
57
|
+
export { parseToolCalls, emitGeminiToolCalls, collectGeminiGroundingSources, parseGeminiTextPartMetadata };
|
|
54
58
|
|
|
55
59
|
const MODELS = [
|
|
56
60
|
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
|
|
@@ -74,6 +78,25 @@ const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 2;
|
|
|
74
78
|
// mirroring anthropic-oauth's _modelRefreshInFlight.
|
|
75
79
|
let _modelRefreshInFlight = null;
|
|
76
80
|
|
|
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
|
+
|
|
77
100
|
const _modelCache = makeModelCache({
|
|
78
101
|
fileName: 'gemini-models.json',
|
|
79
102
|
ttlMs: MODEL_CACHE_TTL_MS,
|
|
@@ -139,14 +162,24 @@ export class GeminiProvider {
|
|
|
139
162
|
name = 'gemini';
|
|
140
163
|
genAI;
|
|
141
164
|
config;
|
|
165
|
+
_fetch;
|
|
166
|
+
_preconnect;
|
|
167
|
+
_createGenAI;
|
|
168
|
+
_modelCache;
|
|
142
169
|
|
|
143
|
-
constructor(config) {
|
|
170
|
+
constructor(config = {}) {
|
|
144
171
|
this.config = config;
|
|
172
|
+
this._fetch = typeof config.fetchFn === 'function' ? config.fetchFn : fetch;
|
|
173
|
+
this._preconnect = typeof config.preconnectFn === 'function' ? config.preconnectFn : preconnect;
|
|
174
|
+
this._createGenAI = typeof config.createGenAI === 'function'
|
|
175
|
+
? config.createGenAI
|
|
176
|
+
: (apiKey) => new GoogleGenerativeAI(apiKey);
|
|
177
|
+
this._modelCache = config.modelCache || _modelCache;
|
|
145
178
|
const apiKey = config.apiKey || process.env.GEMINI_API_KEY || '';
|
|
146
|
-
this.genAI =
|
|
179
|
+
this.genAI = config.genAI || this._createGenAI(apiKey);
|
|
147
180
|
// Warm a kept-alive socket to the Gemini REST API so the first cache/
|
|
148
181
|
// generateContent request skips the cold TLS handshake. Best-effort.
|
|
149
|
-
|
|
182
|
+
this._preconnect('https://generativelanguage.googleapis.com');
|
|
150
183
|
}
|
|
151
184
|
|
|
152
185
|
reloadApiKey() {
|
|
@@ -159,9 +192,11 @@ export class GeminiProvider {
|
|
|
159
192
|
// key via _getApiKey() → this.config.apiKey) don't keep using
|
|
160
193
|
// the stale key after a rotation; genAI alone is not enough.
|
|
161
194
|
this.config = { ...(this.config || {}), apiKey: newKey };
|
|
162
|
-
this.genAI =
|
|
195
|
+
this.genAI = this._createGenAI(newKey);
|
|
163
196
|
}
|
|
197
|
+
return newKey || '';
|
|
164
198
|
} catch { /* best effort */ }
|
|
199
|
+
return '';
|
|
165
200
|
}
|
|
166
201
|
|
|
167
202
|
_getApiKey() {
|
|
@@ -176,7 +211,7 @@ export class GeminiProvider {
|
|
|
176
211
|
// one cache slot warm without re-creation overhead; storage cost (~$0.5/M
|
|
177
212
|
// tokens/hour) is dwarfed by the 75% input-price discount on hits beyond
|
|
178
213
|
// a few iterations.
|
|
179
|
-
async _ensureGeminiCache({ apiKey, model, systemInstruction, geminiTools, contents, opts }) {
|
|
214
|
+
async _ensureGeminiCache({ apiKey, model, systemInstruction, geminiTools, toolConfig, contents, opts }) {
|
|
180
215
|
if (Array.isArray(opts?.nativeTools) && opts.nativeTools.length) return null;
|
|
181
216
|
// Kill-switch: MIXDOG_GEMINI_EXPLICIT_CACHE=0 skips cachedContents
|
|
182
217
|
// entirely and relies on Gemini's implicit prefix caching (2.5+/3.x
|
|
@@ -184,6 +219,7 @@ export class GeminiProvider {
|
|
|
184
219
|
const explicitMode = String(process.env.MIXDOG_GEMINI_EXPLICIT_CACHE || '').trim().toLowerCase();
|
|
185
220
|
if (['0', 'false', 'off', 'no'].includes(explicitMode)) return null;
|
|
186
221
|
const state = opts.providerState?.gemini || null;
|
|
222
|
+
const credentialFingerprint = _geminiCredentialFingerprint(apiKey);
|
|
187
223
|
const now = Date.now();
|
|
188
224
|
const currentIter = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : 1;
|
|
189
225
|
const refreshEveryN = Number(process.env.MIXDOG_GEMINI_CACHE_REFRESH_EVERY) > 0
|
|
@@ -213,17 +249,21 @@ export class GeminiProvider {
|
|
|
213
249
|
model,
|
|
214
250
|
systemInstruction,
|
|
215
251
|
geminiTools,
|
|
252
|
+
toolConfig,
|
|
216
253
|
contents,
|
|
217
254
|
prefixCount: statePrefixContentCount,
|
|
218
255
|
})
|
|
219
256
|
: null;
|
|
220
257
|
const modelMatches = !!state?.cacheName && state?.cacheModel === model;
|
|
258
|
+
const credentialMatches = !!state?.cacheName
|
|
259
|
+
&& state?.cacheCredentialFingerprint === credentialFingerprint;
|
|
221
260
|
const prefixMatches = !!state?.cacheName
|
|
222
261
|
&& statePrefixContentCount != null
|
|
223
262
|
&& statePrefixContentCount <= (Array.isArray(contents) ? contents.length : 0)
|
|
224
263
|
&& !!state?.cachePrefixHash
|
|
225
264
|
&& state.cachePrefixHash === currentStatePrefixHash;
|
|
226
|
-
const canAttachState = !!state?.cacheName && cacheLiveMs > 0
|
|
265
|
+
const canAttachState = !!state?.cacheName && cacheLiveMs > 0
|
|
266
|
+
&& modelMatches && credentialMatches && prefixMatches;
|
|
227
267
|
const canReuseState = canAttachState && cacheLiveMs > reuseHeadroomMs && itersSinceCreate < refreshEveryN;
|
|
228
268
|
try {
|
|
229
269
|
appendAgentTrace({
|
|
@@ -239,6 +279,7 @@ export class GeminiProvider {
|
|
|
239
279
|
statePrefixHash: state?.cachePrefixHash || null,
|
|
240
280
|
currentStatePrefixHash,
|
|
241
281
|
modelMatches,
|
|
282
|
+
credentialMatches,
|
|
242
283
|
prefixMatches,
|
|
243
284
|
canAttachState,
|
|
244
285
|
cacheLiveMs,
|
|
@@ -279,11 +320,12 @@ export class GeminiProvider {
|
|
|
279
320
|
model,
|
|
280
321
|
systemInstruction,
|
|
281
322
|
geminiTools,
|
|
323
|
+
toolConfig,
|
|
282
324
|
contents,
|
|
283
325
|
prefixCount: cachePrefixContentCount,
|
|
284
326
|
});
|
|
285
327
|
const globalCacheKey = _geminiGlobalCacheKey({
|
|
286
|
-
|
|
328
|
+
credentialFingerprint,
|
|
287
329
|
model,
|
|
288
330
|
cachePrefixHash,
|
|
289
331
|
cachePrefixContentCount,
|
|
@@ -340,6 +382,7 @@ export class GeminiProvider {
|
|
|
340
382
|
if (Array.isArray(geminiTools) && geminiTools.length) {
|
|
341
383
|
body.tools = geminiTools;
|
|
342
384
|
}
|
|
385
|
+
if (toolConfig) body.toolConfig = toolConfig;
|
|
343
386
|
// Capture conversation prefix (everything except the latest user/
|
|
344
387
|
// tool input that the generateContent call will carry) inside the
|
|
345
388
|
// cache. cachedContents only accepts role='user' or 'model';
|
|
@@ -353,7 +396,7 @@ export class GeminiProvider {
|
|
|
353
396
|
// only the 20s ceiling. Without merging opts.signal a session that is
|
|
354
397
|
// aborted (stall-watchdog / closeSession) mid-cache-create leaves this
|
|
355
398
|
// preflight request running until its own timeout fires.
|
|
356
|
-
const res = await
|
|
399
|
+
const res = await this._fetch(url, {
|
|
357
400
|
method: 'POST',
|
|
358
401
|
headers: { 'Content-Type': 'application/json' },
|
|
359
402
|
body: JSON.stringify(body),
|
|
@@ -436,7 +479,7 @@ export class GeminiProvider {
|
|
|
436
479
|
setTimeout(() => {
|
|
437
480
|
if (_geminiGlobalCacheNameIsLive(priorCacheName)) return;
|
|
438
481
|
const delUrl = `https://generativelanguage.googleapis.com/v1beta/${priorCacheName}?key=${encodeURIComponent(apiKey)}`;
|
|
439
|
-
|
|
482
|
+
this._fetch(delUrl, { method: 'DELETE', signal: AbortSignal.timeout(10_000), dispatcher: getLlmDispatcher() })
|
|
440
483
|
.catch(() => { /* TTL expiry will reclaim it */ });
|
|
441
484
|
}, GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS).unref?.();
|
|
442
485
|
}
|
|
@@ -449,6 +492,7 @@ export class GeminiProvider {
|
|
|
449
492
|
cacheTokenSize,
|
|
450
493
|
cachePrefixContentCount,
|
|
451
494
|
cachePrefixHash,
|
|
495
|
+
cacheCredentialFingerprint: credentialFingerprint,
|
|
452
496
|
};
|
|
453
497
|
_setGeminiGlobalCache(globalCacheKey, entry);
|
|
454
498
|
return entry;
|
|
@@ -473,16 +517,25 @@ export class GeminiProvider {
|
|
|
473
517
|
async send(messages, model, tools, sendOpts) {
|
|
474
518
|
// Re-warm a kept-alive socket before the turn (TTL-gated no-op while
|
|
475
519
|
// hot) so a post-idle request skips the cold TLS handshake.
|
|
476
|
-
|
|
520
|
+
this._preconnect('https://generativelanguage.googleapis.com');
|
|
477
521
|
try {
|
|
478
522
|
return await this._doSend(messages, model, tools, sendOpts);
|
|
479
523
|
} catch (err) {
|
|
480
|
-
if (err
|
|
524
|
+
if (err?.status === 401 || err?.status === 403
|
|
525
|
+
|| (err?.message && (err.message.includes('401') || err.message.includes('403')))) {
|
|
481
526
|
if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
|
|
482
527
|
throw err;
|
|
483
528
|
}
|
|
484
529
|
process.stderr.write(`[provider] Auth error, re-reading provider authentication...\n`);
|
|
485
|
-
this.
|
|
530
|
+
const oldCredentialFingerprint = _geminiCredentialFingerprint(this._getApiKey());
|
|
531
|
+
const newKey = this.reloadApiKey();
|
|
532
|
+
_invalidateGeminiCachesForCredentialFingerprint(oldCredentialFingerprint);
|
|
533
|
+
const geminiState = sendOpts?.providerState?.gemini;
|
|
534
|
+
if (geminiState?.cacheName) {
|
|
535
|
+
const { gemini: _dropGemini, ...rest } = sendOpts.providerState;
|
|
536
|
+
sendOpts.providerState = rest;
|
|
537
|
+
}
|
|
538
|
+
if (!newKey) throw err;
|
|
486
539
|
return await this._doSend(messages, model, tools, sendOpts);
|
|
487
540
|
}
|
|
488
541
|
throw err;
|
|
@@ -527,20 +580,16 @@ export class GeminiProvider {
|
|
|
527
580
|
let textLeakGuard = null;
|
|
528
581
|
|
|
529
582
|
// Explicit cachedContents (system + tools + prior-turn transcript).
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
// cachedContent without tools yields an empty completion (function
|
|
535
|
-
// calling silently disabled). The contents payload captures the
|
|
536
|
-
// accumulated prefix; we refresh the cache every N iterations so
|
|
537
|
-
// recent turns also enter the cached prefix instead of being billed
|
|
538
|
-
// at full input rates.
|
|
583
|
+
// Cache system/tools/toolConfig together. Google rejects repeating
|
|
584
|
+
// those fields on generateContent when cachedContent is attached.
|
|
585
|
+
// The contents payload captures the accumulated prefix; refresh every
|
|
586
|
+
// N iterations so recent turns also enter the cached prefix.
|
|
539
587
|
const cachedContent = await this._ensureGeminiCache({
|
|
540
588
|
apiKey: this._getApiKey(),
|
|
541
589
|
model: useModel,
|
|
542
590
|
systemInstruction,
|
|
543
591
|
geminiTools,
|
|
592
|
+
toolConfig,
|
|
544
593
|
contents,
|
|
545
594
|
opts,
|
|
546
595
|
});
|
|
@@ -567,7 +616,8 @@ export class GeminiProvider {
|
|
|
567
616
|
contents: deltaContents.length ? deltaContents : contents.slice(-1),
|
|
568
617
|
cachedContent,
|
|
569
618
|
};
|
|
570
|
-
|
|
619
|
+
// cachedContent owns tools + toolConfig. The API rejects a
|
|
620
|
+
// generateContent request that repeats either field.
|
|
571
621
|
// Option A (mirror anthropic-oauth): no absolute wall-clock cap on a
|
|
572
622
|
// live streaming turn. A stream that keeps emitting SSE deltas must
|
|
573
623
|
// not be killed by a fixed total-lifetime timer — that false-aborts
|
|
@@ -589,7 +639,7 @@ export class GeminiProvider {
|
|
|
589
639
|
);
|
|
590
640
|
let res;
|
|
591
641
|
try {
|
|
592
|
-
res = await
|
|
642
|
+
res = await this._fetch(genUrl, {
|
|
593
643
|
method: 'POST',
|
|
594
644
|
headers: { 'Content-Type': 'application/json' },
|
|
595
645
|
body: JSON.stringify(body),
|
|
@@ -726,8 +776,10 @@ export class GeminiProvider {
|
|
|
726
776
|
cachedContent,
|
|
727
777
|
});
|
|
728
778
|
const candidate = response.candidates?.[0] || null;
|
|
729
|
-
const
|
|
779
|
+
const responseParts = candidate?.content?.parts ?? [];
|
|
780
|
+
const textParts = responseParts.filter(p => p?.thought !== true && 'text' in p);
|
|
730
781
|
const rawContent = textParts.map(p => 'text' in p ? p.text : '').join('');
|
|
782
|
+
const providerMetadata = parseGeminiTextPartMetadata(responseParts);
|
|
731
783
|
const content = textLeakGuard?.enabled
|
|
732
784
|
? textLeakGuard.scrubAssistantText(rawContent)
|
|
733
785
|
: rawContent;
|
|
@@ -752,18 +804,13 @@ export class GeminiProvider {
|
|
|
752
804
|
// nudge, or surface to the user. Missing finishReason (still
|
|
753
805
|
// streaming / unknown) is left alone — existing success paths for
|
|
754
806
|
// genuinely complete responses keep working.
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
'PROHIBITED_CONTENT',
|
|
763
|
-
'SPII',
|
|
764
|
-
'MALFORMED_FUNCTION_CALL',
|
|
765
|
-
]);
|
|
766
|
-
if (finishReason && incompleteFinishReasons.has(finishReason)) {
|
|
807
|
+
const promptBlockReason = response.promptFeedback?.blockReason || null;
|
|
808
|
+
const finishReason = candidate?.finishReason || (promptBlockReason ? `PROMPT_${promptBlockReason}` : null);
|
|
809
|
+
const normalizedFinishReason = String(finishReason || '').replace(/^FINISH_REASON_/, '');
|
|
810
|
+
// STOP is the only successful terminal reason. Treat newly-added
|
|
811
|
+
// safety/image/tool/malformed reasons as incomplete by default instead
|
|
812
|
+
// of silently accepting partial or empty output.
|
|
813
|
+
if (finishReason && normalizedFinishReason !== 'STOP') {
|
|
767
814
|
const err = Object.assign(
|
|
768
815
|
new Error(`Gemini response incomplete: finishReason=${finishReason}`),
|
|
769
816
|
{
|
|
@@ -773,6 +820,7 @@ export class GeminiProvider {
|
|
|
773
820
|
finishReason,
|
|
774
821
|
partialContent: content,
|
|
775
822
|
partialToolCalls: toolCalls,
|
|
823
|
+
providerMetadata,
|
|
776
824
|
model: useModel,
|
|
777
825
|
rawUsage: response.usageMetadata || null,
|
|
778
826
|
},
|
|
@@ -844,6 +892,7 @@ export class GeminiProvider {
|
|
|
844
892
|
model: useModel,
|
|
845
893
|
toolCalls,
|
|
846
894
|
citations: citations.length ? citations : undefined,
|
|
895
|
+
providerMetadata,
|
|
847
896
|
providerState: opts.providerState,
|
|
848
897
|
// Use the same normalized usage object traceAgentUsage recorded,
|
|
849
898
|
// including snake_case SDK aliases and cache-create fallback.
|
|
@@ -852,7 +901,7 @@ export class GeminiProvider {
|
|
|
852
901
|
}
|
|
853
902
|
|
|
854
903
|
async listModels() {
|
|
855
|
-
const cached = _modelCache.loadSync();
|
|
904
|
+
const cached = this._modelCache.loadSync();
|
|
856
905
|
if (cached) return cached;
|
|
857
906
|
// Dynamic lookup via Gemini v1beta /models. Requires API key.
|
|
858
907
|
const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
|
|
@@ -869,16 +918,13 @@ export class GeminiProvider {
|
|
|
869
918
|
// TTL check) and _refreshModelCache() (bypassing it). Throws on failure so
|
|
870
919
|
// each caller applies its own fallback/logging.
|
|
871
920
|
async _fetchAndCacheModels(apiKey) {
|
|
872
|
-
const
|
|
873
|
-
const listSignal = AbortSignal.timeout(60_000);
|
|
874
|
-
const res = await fetch(url, { signal: listSignal, dispatcher: getLlmDispatcher() });
|
|
875
|
-
if (!res.ok) throw new Error(`gemini list_models ${res.status}`);
|
|
876
|
-
const data = await res.json();
|
|
877
|
-
const items = Array.isArray(data?.models) ? data.models : [];
|
|
921
|
+
const items = await fetchGeminiModelPages(apiKey, this._fetch);
|
|
878
922
|
// Filter to Gemini family; skip embedding/imagen and non-coding SKUs
|
|
879
923
|
// (robotics, computer-use) that the chat picker should never show.
|
|
880
924
|
const normalized = items
|
|
881
925
|
.filter(m => (m?.name || '').includes('gemini'))
|
|
926
|
+
.filter(m => !Array.isArray(m?.supportedGenerationMethods)
|
|
927
|
+
|| m.supportedGenerationMethods.includes('generateContent'))
|
|
882
928
|
.filter(m => !/embedding|aqa|imagen|robotics|computer-use/.test(m?.name || ''))
|
|
883
929
|
.map(m => {
|
|
884
930
|
const id = (m.name || '').replace(/^models\//, '');
|
|
@@ -905,8 +951,11 @@ export class GeminiProvider {
|
|
|
905
951
|
// LiteLLM catalog overlays pricing and updated metadata.
|
|
906
952
|
const { enrichModels } = await import('./model-catalog.mjs');
|
|
907
953
|
const { sanitizeModelList } = await import('./model-list-sanitize.mjs');
|
|
908
|
-
const enriched = sanitizeModelList(await enrichModels(normalized
|
|
909
|
-
|
|
954
|
+
const enriched = sanitizeModelList(await enrichModels(normalized, {
|
|
955
|
+
fetchFn: this._fetch,
|
|
956
|
+
force: this.config.catalogForceRefresh === true,
|
|
957
|
+
}), { provider: 'gemini' });
|
|
958
|
+
this._modelCache.save(enriched);
|
|
910
959
|
return enriched;
|
|
911
960
|
}
|
|
912
961
|
|
|
@@ -713,9 +713,6 @@ export class GrokOAuthProvider {
|
|
|
713
713
|
constructor(config) {
|
|
714
714
|
this.config = config || {};
|
|
715
715
|
this.tokens = loadTokens();
|
|
716
|
-
// Warm a kept-alive socket to the xAI inference API so the first
|
|
717
|
-
// request skips the cold TLS handshake. Best-effort; never throws.
|
|
718
|
-
preconnect(INFERENCE_BASE_URL);
|
|
719
716
|
}
|
|
720
717
|
|
|
721
718
|
async ensureAuth({ forceRefresh = false } = {}) {
|
|
@@ -805,7 +802,12 @@ export class GrokOAuthProvider {
|
|
|
805
802
|
async send(messages, model, tools, sendOpts) {
|
|
806
803
|
// Re-warm a kept-alive socket before the turn (TTL-gated no-op while
|
|
807
804
|
// hot) so a post-idle request skips the cold TLS handshake.
|
|
808
|
-
preconnect
|
|
805
|
+
if (this.config?.preconnect !== false) {
|
|
806
|
+
const warm = typeof this.config?.preconnectFn === 'function'
|
|
807
|
+
? this.config.preconnectFn
|
|
808
|
+
: preconnect;
|
|
809
|
+
warm(INFERENCE_BASE_URL);
|
|
810
|
+
}
|
|
809
811
|
const useModel = normalizeGrokModelId(
|
|
810
812
|
model || await ensureLatestGrokModel(this),
|
|
811
813
|
);
|
|
@@ -829,6 +831,12 @@ export class GrokOAuthProvider {
|
|
|
829
831
|
// fix it, so it must surface unretried.
|
|
830
832
|
populateHttpStatusFromMessage(err);
|
|
831
833
|
if (Number(err?.httpStatus || err?.status) === 401) {
|
|
834
|
+
// A stream-level 401 after text/tool dispatch cannot be safely
|
|
835
|
+
// replayed: the client has already observed output or may have
|
|
836
|
+
// executed a side-effecting tool.
|
|
837
|
+
if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
|
|
838
|
+
throw err;
|
|
839
|
+
}
|
|
832
840
|
process.stderr.write('[grok-oauth] 401, force-refreshing token...\n');
|
|
833
841
|
const fresh = await this.ensureAuth({ forceRefresh: true });
|
|
834
842
|
const retryInner = this._ensureInner(fresh.access_token, useModel);
|