mixdog 0.9.94 → 0.9.96
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +109 -0
- package/package.json +26 -13
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +20 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +13 -2
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +29 -14
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +29 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +13 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +82 -22
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +40 -36
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -33
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +168 -83
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +34 -18
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +89 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +6 -39
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -6
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +124 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +7 -16
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +20 -43
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +76 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +38 -13
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +138 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +177 -16
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +21 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +217 -25
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +60 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +3 -3
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/index.mjs +5 -38
- package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +8 -0
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/tool-defs.mjs +7 -11
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +133 -4
- package/src/runtime/shared/background-tasks.mjs +6 -3
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +60 -31
- package/src/runtime/shared/config.mjs +130 -44
- package/src/runtime/shared/resource-admission.mjs +146 -58
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +2 -7
- package/src/runtime/shared/turn-snapshot.mjs +411 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +552 -0
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/runtime-core.mjs +57 -19
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +11 -2
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +139 -23
- package/src/session-runtime/settings-api.mjs +5 -17
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +25 -28
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +631 -0
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +231 -9
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +1069 -0
- package/src/standalone/engine-daemon-protocol.mjs +32 -0
- package/src/standalone/engine-daemon-service.mjs +1108 -0
- package/src/standalone/engine-daemon-transport.mjs +794 -0
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/session-protocol.mjs +188 -0
- package/src/tui/App.jsx +92 -64
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +10 -2
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +20 -20
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +24 -20
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +59 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +16 -9
- package/src/tui/app/settings-picker.mjs +60 -65
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +57 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +39 -17
- package/src/tui/app/use-transcript-scroll.mjs +39 -10
- package/src/tui/app/use-transcript-window.mjs +62 -14
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +81 -38
- package/src/tui/components/Spinner.jsx +89 -95
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/dist/index.mjs +1694 -10633
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +110 -3
- package/src/tui/engine/session-api-ext.mjs +24 -5
- package/src/tui/engine/session-api.mjs +165 -55
- package/src/tui/engine/session-flow.mjs +50 -5
- package/src/tui/engine/tool-card-results.mjs +10 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +11 -64
- package/src/tui/engine-local-session.mjs +1124 -0
- package/src/tui/engine.mjs +16 -1065
- package/src/tui/index.jsx +41 -4
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +83 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
collectGeminiGroundingSources,
|
|
24
24
|
parseGeminiTextPartMetadata,
|
|
25
25
|
} from './gemini-schema.mjs';
|
|
26
|
+
import { parseProviderJsonBatch } from './stream-json-pool.mjs';
|
|
26
27
|
|
|
27
28
|
export const GEMINI_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
|
|
28
29
|
'MIXDOG_GEMINI_FIRST_BYTE_TIMEOUT_MS',
|
|
@@ -131,6 +132,19 @@ function geminiChunkHasFunctionCall(chunk) {
|
|
|
131
132
|
return parts.some((p) => p && p.functionCall);
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
export function geminiChunkProgressKind(chunk) {
|
|
136
|
+
const parts = chunk?.candidates?.[0]?.content?.parts;
|
|
137
|
+
if (!Array.isArray(parts)) return 'transport';
|
|
138
|
+
if (parts.some((part) => part?.functionCall)) return 'tool';
|
|
139
|
+
if (parts.some((part) => part?.thought === true && typeof part.text === 'string' && part.text)) {
|
|
140
|
+
return 'reasoning';
|
|
141
|
+
}
|
|
142
|
+
if (parts.some((part) => part?.thought !== true && typeof part?.text === 'string' && part.text)) {
|
|
143
|
+
return 'text';
|
|
144
|
+
}
|
|
145
|
+
return 'transport';
|
|
146
|
+
}
|
|
147
|
+
|
|
134
148
|
/**
|
|
135
149
|
* Aggregate streamed GenerateContentResponse chunks into one response object
|
|
136
150
|
* (same shape as a non-streaming generateContent JSON body).
|
|
@@ -256,7 +270,7 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
256
270
|
};
|
|
257
271
|
leakedCalls.push(call);
|
|
258
272
|
try { onToolCall?.(call); } catch {}
|
|
259
|
-
try { onStreamDelta?.(); } catch {}
|
|
273
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
260
274
|
};
|
|
261
275
|
|
|
262
276
|
const pumpLeakBuffer = (final) => {
|
|
@@ -407,6 +421,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
407
421
|
if (done) break;
|
|
408
422
|
resetIdleTimer();
|
|
409
423
|
buffer += decoder.decode(value, { stream: true });
|
|
424
|
+
const payloads = [];
|
|
410
425
|
let lineEnd;
|
|
411
426
|
while ((lineEnd = buffer.indexOf('\n')) >= 0) {
|
|
412
427
|
let line = buffer.slice(0, lineEnd);
|
|
@@ -415,18 +430,24 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
415
430
|
if (!line.startsWith('data: ')) continue;
|
|
416
431
|
const data = line.slice(6).trim();
|
|
417
432
|
if (!data || data === '[DONE]') continue;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
433
|
+
payloads.push(data);
|
|
434
|
+
}
|
|
435
|
+
let parsedChunks;
|
|
436
|
+
try {
|
|
437
|
+
// Preserve the established delivery boundary: once read()
|
|
438
|
+
// returned the bytes, parse and relay them before the next
|
|
439
|
+
// iteration observes cancellation.
|
|
440
|
+
parsedChunks = await parseProviderJsonBatch(payloads);
|
|
441
|
+
} catch (cause) {
|
|
442
|
+
throw geminiStreamCorruptionError(`${label} corrupt SSE JSON`, cause);
|
|
443
|
+
}
|
|
444
|
+
for (const parsed of parsedChunks) {
|
|
424
445
|
if (!sawStreamChunk) {
|
|
425
446
|
sawStreamChunk = true;
|
|
426
447
|
clearFirstByteTimer();
|
|
427
448
|
}
|
|
428
449
|
allChunks.push(parsed);
|
|
429
|
-
try { onStreamDelta?.(); } catch {}
|
|
450
|
+
try { onStreamDelta?.(geminiChunkProgressKind(parsed)); } catch {}
|
|
430
451
|
if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
|
|
431
452
|
if (onTextDelta || textLeakGuard) {
|
|
432
453
|
const t = geminiChunkText(parsed);
|
|
@@ -440,13 +461,13 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
440
461
|
const data = line.slice(6).trim();
|
|
441
462
|
if (data && data !== '[DONE]') {
|
|
442
463
|
try {
|
|
443
|
-
const parsed =
|
|
464
|
+
const [parsed] = await parseProviderJsonBatch([data]);
|
|
444
465
|
if (!sawStreamChunk) {
|
|
445
466
|
sawStreamChunk = true;
|
|
446
467
|
clearFirstByteTimer();
|
|
447
468
|
}
|
|
448
469
|
allChunks.push(parsed);
|
|
449
|
-
try { onStreamDelta?.(); } catch {}
|
|
470
|
+
try { onStreamDelta?.(geminiChunkProgressKind(parsed)); } catch {}
|
|
450
471
|
if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
|
|
451
472
|
if (onTextDelta || textLeakGuard) {
|
|
452
473
|
const t = geminiChunkText(parsed);
|
|
@@ -677,7 +698,7 @@ export async function consumeGeminiSdkStream(streamResult, {
|
|
|
677
698
|
}
|
|
678
699
|
resetIdleTimer();
|
|
679
700
|
if (step.value) collectedChunks.push(step.value);
|
|
680
|
-
try { onStreamDelta?.(); } catch {}
|
|
701
|
+
try { onStreamDelta?.(geminiChunkProgressKind(step.value)); } catch {}
|
|
681
702
|
if (!sawFunctionCall && geminiChunkHasFunctionCall(step.value)) sawFunctionCall = true;
|
|
682
703
|
if (onTextDelta || textLeakGuard) {
|
|
683
704
|
const t = geminiChunkText(step.value);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
2
|
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
3
|
-
import { withRetry } from './retry-classifier.mjs';
|
|
3
|
+
import { canFallbackNonStreaming, withRetry } from './retry-classifier.mjs';
|
|
4
4
|
import { traceAgentUsage, appendAgentTrace } from '../agent-trace.mjs';
|
|
5
5
|
import {
|
|
6
6
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
@@ -158,6 +158,53 @@ export class GeminiProvider {
|
|
|
158
158
|
return this.config?.apiKey || process.env.GEMINI_API_KEY || '';
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* cc parity (claude.ts:2504): re-issue a dead stream ONCE as a
|
|
163
|
+
* non-streaming generateContent call instead of failing the turn.
|
|
164
|
+
* Narrower than cc by design — canFallbackNonStreaming() clears only a
|
|
165
|
+
* stream that exposed nothing, so rendered text is never duplicated and a
|
|
166
|
+
* dispatched tool can never run twice. Returns the aggregated response, or
|
|
167
|
+
* null when the failure is ineligible or the fallback itself fails.
|
|
168
|
+
*/
|
|
169
|
+
async _recoverGeminiNonStreaming({ streamErr, signal, opts, model, generate }) {
|
|
170
|
+
if (!canFallbackNonStreaming(streamErr, { signal })) return null;
|
|
171
|
+
let aggregated;
|
|
172
|
+
try {
|
|
173
|
+
try { opts?.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
174
|
+
aggregated = await generate(signal || undefined);
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (!aggregated || !Array.isArray(aggregated.candidates) || aggregated.candidates.length === 0) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`[gemini] stream failed (${streamErr?.code || streamErr?.message || 'unknown'}); `
|
|
184
|
+
+ `recovered via non-streaming generateContent\n`,
|
|
185
|
+
);
|
|
186
|
+
} catch { /* best-effort */ }
|
|
187
|
+
try {
|
|
188
|
+
appendAgentTrace({
|
|
189
|
+
sessionId: opts?.sessionId || opts?.session?.id || null,
|
|
190
|
+
iteration: Number.isFinite(Number(opts?.iteration)) ? Number(opts.iteration) : null,
|
|
191
|
+
kind: 'transport_fallback',
|
|
192
|
+
provider: 'gemini',
|
|
193
|
+
model,
|
|
194
|
+
transport: 'non-streaming',
|
|
195
|
+
payload: {
|
|
196
|
+
from: 'stream',
|
|
197
|
+
to: 'non-streaming',
|
|
198
|
+
reason: streamErr?.retryClassifier || streamErr?.code || streamErr?.message || 'stream_failed',
|
|
199
|
+
error_code: streamErr?.code || null,
|
|
200
|
+
error_http_status: Number(streamErr?.httpStatus || streamErr?.status || 0) || null,
|
|
201
|
+
error_classifier: streamErr?.retryClassifier || streamErr?.midstreamClassifier || null,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
} catch { /* best-effort */ }
|
|
205
|
+
return aggregated;
|
|
206
|
+
}
|
|
207
|
+
|
|
161
208
|
// Explicit cachedContents API. The implicit cache layer on Gemini 3.x
|
|
162
209
|
// does not surface cachedContentTokenCount in usageMetadata, so the only
|
|
163
210
|
// way to obtain measurable + billable cache savings is to register the
|
|
@@ -655,7 +702,30 @@ export class GeminiProvider {
|
|
|
655
702
|
}
|
|
656
703
|
return await this._doSend(messages, model, tools, opts, { skipExplicitCache: true });
|
|
657
704
|
}
|
|
658
|
-
|
|
705
|
+
const recovered = await this._recoverGeminiNonStreaming({
|
|
706
|
+
streamErr: err,
|
|
707
|
+
signal: totalSignal,
|
|
708
|
+
opts,
|
|
709
|
+
model: useModel,
|
|
710
|
+
generate: async (fallbackSignal) => {
|
|
711
|
+
const nonStreamUrl = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(useModel)}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
712
|
+
const res = await this._fetch(nonStreamUrl, {
|
|
713
|
+
method: 'POST',
|
|
714
|
+
headers: { 'Content-Type': 'application/json' },
|
|
715
|
+
body: JSON.stringify(body),
|
|
716
|
+
signal: fallbackSignal,
|
|
717
|
+
dispatcher: getLlmDispatcher(),
|
|
718
|
+
});
|
|
719
|
+
if (!res.ok) {
|
|
720
|
+
const text = await res.text().catch(() => '');
|
|
721
|
+
throw geminiRestError(res, text, 'Gemini REST generateContent');
|
|
722
|
+
}
|
|
723
|
+
return await res.json();
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
if (!recovered) throw err;
|
|
727
|
+
textLeakGuard = null;
|
|
728
|
+
response = recovered;
|
|
659
729
|
} finally {
|
|
660
730
|
restPassthrough.cleanup();
|
|
661
731
|
}
|
|
@@ -749,6 +819,23 @@ export class GeminiProvider {
|
|
|
749
819
|
},
|
|
750
820
|
},
|
|
751
821
|
);
|
|
822
|
+
} catch (err) {
|
|
823
|
+
const recovered = await this._recoverGeminiNonStreaming({
|
|
824
|
+
streamErr: err,
|
|
825
|
+
signal: totalSignal,
|
|
826
|
+
opts,
|
|
827
|
+
model: useModel,
|
|
828
|
+
generate: async (fallbackSignal) => {
|
|
829
|
+
const result = await genModel.generateContent(
|
|
830
|
+
{ contents },
|
|
831
|
+
{ signal: fallbackSignal },
|
|
832
|
+
);
|
|
833
|
+
return result?.response ?? null;
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
if (!recovered) throw err;
|
|
837
|
+
textLeakGuard = null;
|
|
838
|
+
response = recovered;
|
|
752
839
|
} finally {
|
|
753
840
|
sdkPassthrough.cleanup();
|
|
754
841
|
}
|
|
@@ -23,7 +23,7 @@ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs
|
|
|
23
23
|
import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
|
|
24
24
|
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
25
25
|
import { makeModelCache } from './model-cache.mjs';
|
|
26
|
-
import { OpenAICompatProvider } from './openai-compat.mjs';
|
|
26
|
+
import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
|
|
27
27
|
import { createTimeoutSignal } from '../stall-policy.mjs';
|
|
28
28
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
29
29
|
import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
|
|
@@ -205,6 +205,17 @@ export class GrokOAuthProvider {
|
|
|
205
205
|
constructor(config) {
|
|
206
206
|
this.config = config || {};
|
|
207
207
|
this.tokens = loadTokens();
|
|
208
|
+
// The credential/header-bound inner provider is created lazily, but its
|
|
209
|
+
// heavy SDK + undici graph and proxy socket are not request-specific.
|
|
210
|
+
// Pay both during the daemon's process-wide provider warmup so first
|
|
211
|
+
// Grok send only builds the cheap inner client.
|
|
212
|
+
try { preloadOpenAICompatRuntime(); } catch { /* authoritative send surfaces load failures */ }
|
|
213
|
+
if (this.config?.preconnect !== false) {
|
|
214
|
+
const warm = typeof this.config?.preconnectFn === 'function'
|
|
215
|
+
? this.config.preconnectFn
|
|
216
|
+
: preconnect;
|
|
217
|
+
try { warm(PROXY_BASE_URL); } catch { /* best-effort */ }
|
|
218
|
+
}
|
|
208
219
|
}
|
|
209
220
|
|
|
210
221
|
async ensureAuth({ forceRefresh = false } = {}) {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* || toolCallsComplete > 0 || toolCallsDispatched > 0
|
|
51
51
|
* sideEffectDispatched = toolCallsDispatched > 0
|
|
52
52
|
* replayUnsafe = the ONE architecture-specific deny MixDog adds on top
|
|
53
|
-
* of the
|
|
53
|
+
* of the standard retry rules, because it streams UI text
|
|
54
54
|
* and dispatches tools eagerly:
|
|
55
55
|
* visibleOutput || sideEffectDispatched
|
|
56
56
|
* || dispatchAmbiguous || unsafe marker
|
|
@@ -35,8 +35,8 @@ function _codexInstallationId(sendOpts) {
|
|
|
35
35
|
|| `mixdog-${_hashText(`${process.env.USERPROFILE || process.env.HOME || ''}:${process.cwd()}`, 32)}`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// The identity block
|
|
39
|
-
//
|
|
38
|
+
// The identity block is rebuilt per request: never cached on the pooled
|
|
39
|
+
// socket, or a later turn would
|
|
40
40
|
// replay the first turn's identity.
|
|
41
41
|
function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = false } = {}) {
|
|
42
42
|
const sessionId = _cleanMetaString(sendOpts?.codexSessionId || sendOpts?.session?.codexSessionId || poolKey || cacheKey)
|
|
@@ -49,8 +49,8 @@ function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = fa
|
|
|
49
49
|
: _sessionStartedAtUnixMs(sessionId);
|
|
50
50
|
const requestKind = _codexRequestKind(sendOpts, sessionId);
|
|
51
51
|
const wireParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY === '1';
|
|
52
|
-
//
|
|
53
|
-
//
|
|
52
|
+
// The reference client opens the WS with a prewarm (empty turn_id) BEFORE
|
|
53
|
+
// the real turn. Under wire parity the handshake IS that prewarm, so its
|
|
54
54
|
// turn_id empties and its request_kind becomes 'prewarm' instead of
|
|
55
55
|
// presenting the handshake as a live turn. Parity off is unchanged.
|
|
56
56
|
const isPrewarm = requestKind === 'prewarm' || handshake === true;
|
|
@@ -66,7 +66,7 @@ function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = fa
|
|
|
66
66
|
turn_id: turnId,
|
|
67
67
|
window_id: windowId,
|
|
68
68
|
request_kind: effectiveRequestKind,
|
|
69
|
-
// Richer
|
|
69
|
+
// Richer turn-metadata. A/B
|
|
70
70
|
// 2026-07-04 showed no effect, so it stays behind a knob for future
|
|
71
71
|
// probes; wire parity implies it.
|
|
72
72
|
...((process.env.MIXDOG_OAI_TURN_METADATA_RICH === '1' || wireParity) ? {
|
|
@@ -98,8 +98,8 @@ export function _metadataTrace(metadata) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Handshake projection of the same identity.
|
|
102
|
-
// request
|
|
101
|
+
// Handshake projection of the same identity. These ride on every
|
|
102
|
+
// request; A/B 2026-07-04
|
|
103
103
|
// showed the turn-metadata blob alone lifts prefix-cache hits, so it is ON by
|
|
104
104
|
// default. MIXDOG_OAI_TURN_METADATA overrides:
|
|
105
105
|
// unset|1|turn-metadata : window-id + turn-metadata + installation-id
|
|
@@ -141,7 +141,7 @@ export function _withCodexWsClientMetadata(frame, entry, enabled, context = {})
|
|
|
141
141
|
'x-codex-ws-stream-request-start-ms': String(Date.now()),
|
|
142
142
|
};
|
|
143
143
|
if (entry && typeof entry === 'object') {
|
|
144
|
-
//
|
|
144
|
+
// x-codex-turn-state is scoped to ONE turn while
|
|
145
145
|
// pooled sockets span turns: attribute a captured token to the FIRST
|
|
146
146
|
// turn that observes it, then drop it once turn_id moves on. An empty
|
|
147
147
|
// turn_id (parity prewarm) is a valid owner, so the check is against
|
|
@@ -59,6 +59,22 @@ export function xaiResponsesCacheRouting(opts, params, rawTools, model) {
|
|
|
59
59
|
const scope = String(opts?.xaiResponsesCacheScope || process.env.MIXDOG_XAI_RESPONSES_CACHE_SCOPE || 'prefix')
|
|
60
60
|
.trim()
|
|
61
61
|
.toLowerCase();
|
|
62
|
+
// 'none' omits prompt_cache_key entirely, matching xAI's own reference
|
|
63
|
+
// sampler (grok-build sends prompt_cache_key: None) and zed's xAI provider
|
|
64
|
+
// (supports_prompt_cache_key() == false): the service falls back to
|
|
65
|
+
// automatic prompt-prefix caching with no client-supplied lane. Callers
|
|
66
|
+
// must treat a null key as "do not send the field".
|
|
67
|
+
if (scope === 'none' || scope === 'off' || scope === 'omit') {
|
|
68
|
+
const sessionId = String(opts?.sessionId || opts?.session?.id || '').trim();
|
|
69
|
+
const prefixHash = traceHash(xaiPrefixSeed({ opts, params, rawTools, model }));
|
|
70
|
+
return {
|
|
71
|
+
key: null,
|
|
72
|
+
mode: 'none',
|
|
73
|
+
seedHash: null,
|
|
74
|
+
prefixHash,
|
|
75
|
+
ownerSessionHash: sessionId ? traceHash(sessionId) : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
62
78
|
if (scope !== 'prefix') {
|
|
63
79
|
return xaiCacheRouting(opts, params, rawTools, model);
|
|
64
80
|
}
|
|
@@ -183,7 +199,9 @@ export function useXaiResponsesWebSocketWarmup(opts, config, { previousResponseI
|
|
|
183
199
|
if (['0', 'false', 'off', 'none', 'disabled'].includes(mode)) return false;
|
|
184
200
|
if (['1', 'true', 'on', 'always', 'force'].includes(mode)) return true;
|
|
185
201
|
}
|
|
186
|
-
|
|
202
|
+
// A generate:false response can still carry provider usage/quota. Keep it
|
|
203
|
+
// strictly opt-in; transport-only HTTP/WS preconnect remains cost-free.
|
|
204
|
+
return false;
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
// Codex-aligned default: no compat cache lane/serialization. Keep the override
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
3
|
-
import { withRetry } from './retry-classifier.mjs';
|
|
3
|
+
import { canFallbackNonStreaming, withRetry } from './retry-classifier.mjs';
|
|
4
4
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
5
5
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
6
6
|
import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
|
|
@@ -69,6 +69,16 @@ function loadOpenAI() {
|
|
|
69
69
|
return _OpenAI;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Grok OAuth creates its credential-bound inner provider only when send() has
|
|
73
|
+
// the final token and request headers. Let the process-wide provider warmup pay
|
|
74
|
+
// the OpenAI SDK + undici pool load before that first request reaches the hot
|
|
75
|
+
// path, without constructing a fake credential-bound client.
|
|
76
|
+
export function preloadOpenAICompatRuntime() {
|
|
77
|
+
const OpenAI = loadOpenAI();
|
|
78
|
+
getLlmDispatcher();
|
|
79
|
+
return OpenAI;
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
function attachCompletedWarmup(err, warmup) {
|
|
73
83
|
if (!err || !warmup?.usage) return err;
|
|
74
84
|
try {
|
|
@@ -238,7 +248,8 @@ export class OpenAICompatProvider {
|
|
|
238
248
|
// extraHeaders behave exactly as before.
|
|
239
249
|
this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(config.extraHeaders || {}) };
|
|
240
250
|
this.defaultModel = preset?.defaultModel || 'default';
|
|
241
|
-
|
|
251
|
+
const OpenAI = preloadOpenAICompatRuntime();
|
|
252
|
+
this.client = new OpenAI({
|
|
242
253
|
baseURL,
|
|
243
254
|
apiKey,
|
|
244
255
|
defaultHeaders: this.defaultHeaders,
|
|
@@ -253,6 +264,12 @@ export class OpenAICompatProvider {
|
|
|
253
264
|
// a harmless no-op then.
|
|
254
265
|
fetchOptions: { dispatcher: getLlmDispatcher() },
|
|
255
266
|
});
|
|
267
|
+
// Provider registry initialization normally runs during daemon warmup.
|
|
268
|
+
// Start the origin handshake there; the per-send call below remains as
|
|
269
|
+
// the TTL-gated rewarm after long idle gaps.
|
|
270
|
+
if (this.config?.preconnect !== false) {
|
|
271
|
+
try { this._preconnectFn(this.baseURL); } catch { /* best-effort */ }
|
|
272
|
+
}
|
|
256
273
|
}
|
|
257
274
|
get _preconnectFn() {
|
|
258
275
|
return typeof this.config?.preconnectFn === 'function'
|
|
@@ -420,44 +437,52 @@ export class OpenAICompatProvider {
|
|
|
420
437
|
params.stream_options = { include_usage: true };
|
|
421
438
|
let assembled;
|
|
422
439
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
440
|
+
try {
|
|
441
|
+
assembled = await withRetry(
|
|
442
|
+
async ({ signal: attemptSignal }) => {
|
|
443
|
+
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
444
|
+
const stream = await withRetry(
|
|
445
|
+
({ signal: openSignal }) => this.client.chat.completions.create(params, { signal: openSignal }),
|
|
446
|
+
{
|
|
447
|
+
signal: attemptSignal,
|
|
448
|
+
// Single attempt: this inner wrapper exists only to
|
|
449
|
+
// apply the first-byte per-attempt timeout. Retry is
|
|
450
|
+
// owned by the outer withRetry — nesting retry loops
|
|
451
|
+
// here multiplied tail latency (5x5).
|
|
452
|
+
maxAttempts: 1,
|
|
453
|
+
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
454
|
+
perAttemptLabel: `${this.name} first byte`,
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
|
|
458
|
+
return consumeCompatChatCompletionStream(stream, {
|
|
429
459
|
signal: attemptSignal,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
460
|
+
label: this.name,
|
|
461
|
+
onStreamDelta: opts.onStreamDelta,
|
|
462
|
+
onToolCall: opts.onToolCall,
|
|
463
|
+
onTextDelta: opts.onTextDelta,
|
|
464
|
+
parseToolCalls,
|
|
465
|
+
// Known tool names for the leaked-tool-call guard:
|
|
466
|
+
// recovered leaked calls only synthesize when they name
|
|
467
|
+
// a tool actually offered to this request.
|
|
468
|
+
knownToolNames: knownToolNamesFromOpenAITools(params.tools),
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
signal: totalSignal.signal,
|
|
473
|
+
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
474
|
+
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
475
|
+
process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
437
476
|
},
|
|
438
|
-
);
|
|
439
|
-
try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
|
|
440
|
-
return consumeCompatChatCompletionStream(stream, {
|
|
441
|
-
signal: attemptSignal,
|
|
442
|
-
label: this.name,
|
|
443
|
-
onStreamDelta: opts.onStreamDelta,
|
|
444
|
-
onToolCall: opts.onToolCall,
|
|
445
|
-
onTextDelta: opts.onTextDelta,
|
|
446
|
-
parseToolCalls,
|
|
447
|
-
// Known tool names for the leaked-tool-call guard:
|
|
448
|
-
// recovered leaked calls only synthesize when they name
|
|
449
|
-
// a tool actually offered to this request.
|
|
450
|
-
knownToolNames: knownToolNamesFromOpenAITools(params.tools),
|
|
451
|
-
});
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
signal: totalSignal.signal,
|
|
455
|
-
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
456
|
-
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
457
|
-
process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
458
477
|
},
|
|
459
|
-
|
|
460
|
-
)
|
|
478
|
+
);
|
|
479
|
+
} catch (streamErr) {
|
|
480
|
+
const recovered = await this._recoverCompatNonStreaming({
|
|
481
|
+
streamErr, params, opts, signal: totalSignal.signal, useModel,
|
|
482
|
+
});
|
|
483
|
+
if (!recovered) throw streamErr;
|
|
484
|
+
assembled = recovered;
|
|
485
|
+
}
|
|
461
486
|
} finally {
|
|
462
487
|
totalSignal.cleanup();
|
|
463
488
|
}
|
|
@@ -567,6 +592,83 @@ export class OpenAICompatProvider {
|
|
|
567
592
|
})() : undefined,
|
|
568
593
|
};
|
|
569
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* cc parity (claude.ts:2504): when a stream dies, re-issue the SAME request
|
|
597
|
+
* with `stream:false` rather than failing the turn. Narrower than cc on
|
|
598
|
+
* purpose — canFallbackNonStreaming() clears only a stream that exposed
|
|
599
|
+
* NOTHING, so an eagerly dispatched tool can never run twice and rendered
|
|
600
|
+
* text is never duplicated (that case belongs to the loop's retraction
|
|
601
|
+
* replay). Returns the same assembled shape the stream consumer produces,
|
|
602
|
+
* or null when the failure is ineligible or the fallback itself fails.
|
|
603
|
+
*/
|
|
604
|
+
async _recoverCompatNonStreaming({ streamErr, params, opts, signal, useModel }) {
|
|
605
|
+
if (!canFallbackNonStreaming(streamErr, { signal })) return null;
|
|
606
|
+
const nonStreamParams = { ...params, stream: false };
|
|
607
|
+
delete nonStreamParams.stream_options;
|
|
608
|
+
let response;
|
|
609
|
+
try {
|
|
610
|
+
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
611
|
+
response = await withRetry(
|
|
612
|
+
({ signal: attemptSignal }) => this.client.chat.completions.create(
|
|
613
|
+
nonStreamParams,
|
|
614
|
+
{ signal: attemptSignal },
|
|
615
|
+
),
|
|
616
|
+
{
|
|
617
|
+
signal,
|
|
618
|
+
maxAttempts: 1,
|
|
619
|
+
// A non-streaming call returns only when generation is done,
|
|
620
|
+
// so the first-byte bound would false-abort it.
|
|
621
|
+
perAttemptTimeoutMs: PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
622
|
+
perAttemptLabel: `${this.name} non-streaming fallback`,
|
|
623
|
+
},
|
|
624
|
+
);
|
|
625
|
+
} catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const choice = response?.choices?.[0] || null;
|
|
629
|
+
if (!choice) return null;
|
|
630
|
+
const message = choice.message || {};
|
|
631
|
+
let toolCalls;
|
|
632
|
+
try {
|
|
633
|
+
toolCalls = parseToolCalls(choice, this.name);
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
process.stderr.write(
|
|
639
|
+
`[${this.name}] stream failed (${streamErr?.code || streamErr?.message || 'unknown'}); `
|
|
640
|
+
+ `recovered via non-streaming request\n`,
|
|
641
|
+
);
|
|
642
|
+
} catch { /* best-effort */ }
|
|
643
|
+
try {
|
|
644
|
+
appendAgentTrace({
|
|
645
|
+
sessionId: opts?.sessionId || opts?.session?.id || null,
|
|
646
|
+
iteration: Number.isFinite(Number(opts?.iteration)) ? Number(opts.iteration) : null,
|
|
647
|
+
kind: 'transport_fallback',
|
|
648
|
+
provider: this.name,
|
|
649
|
+
model: useModel,
|
|
650
|
+
transport: 'non-streaming',
|
|
651
|
+
payload: {
|
|
652
|
+
from: 'stream',
|
|
653
|
+
to: 'non-streaming',
|
|
654
|
+
reason: streamErr?.retryClassifier || streamErr?.code || streamErr?.message || 'stream_failed',
|
|
655
|
+
error_code: streamErr?.code || null,
|
|
656
|
+
error_http_status: Number(streamErr?.httpStatus || streamErr?.status || 0) || null,
|
|
657
|
+
error_classifier: streamErr?.retryClassifier || streamErr?.midstreamClassifier || null,
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
} catch { /* best-effort */ }
|
|
661
|
+
return {
|
|
662
|
+
response,
|
|
663
|
+
model: response.model || useModel,
|
|
664
|
+
content: typeof message.content === 'string' ? message.content : '',
|
|
665
|
+
toolCalls,
|
|
666
|
+
stopReason: choice.finish_reason || null,
|
|
667
|
+
reasoningContent: typeof message.reasoning_content === 'string' ? message.reasoning_content : null,
|
|
668
|
+
rawUsage: response.usage || null,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
570
672
|
async _doSendXaiResponses(messages, useModel, tools, opts) {
|
|
571
673
|
const signal = opts.signal || null;
|
|
572
674
|
if (signal?.aborted) {
|
|
@@ -589,8 +691,11 @@ export class OpenAICompatProvider {
|
|
|
589
691
|
// existing request bodies.
|
|
590
692
|
store: false,
|
|
591
693
|
include: ['reasoning.encrypted_content'],
|
|
592
|
-
prompt_cache_key: cacheRouting.key,
|
|
593
694
|
};
|
|
695
|
+
// A null routing key means "omit the field" (cache scope 'none'):
|
|
696
|
+
// xAI's own client sends no prompt_cache_key and relies on the
|
|
697
|
+
// service's automatic prefix caching.
|
|
698
|
+
if (cacheRouting.key) params.prompt_cache_key = cacheRouting.key;
|
|
594
699
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
595
700
|
const nativeTools = nativeResponsesTools(opts);
|
|
596
701
|
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [], { provider: 'xai' })];
|
|
@@ -797,8 +902,8 @@ export class OpenAICompatProvider {
|
|
|
797
902
|
input,
|
|
798
903
|
store: false,
|
|
799
904
|
include: ['reasoning.encrypted_content'],
|
|
800
|
-
prompt_cache_key: cacheRouting.key,
|
|
801
905
|
};
|
|
906
|
+
if (cacheRouting.key) params.prompt_cache_key = cacheRouting.key;
|
|
802
907
|
const instructions = xaiSystemInstructions(messages);
|
|
803
908
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
804
909
|
// xAI rejects instructions together with previous_response_id; the
|