mixdog 0.9.1 → 0.9.3
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 +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
3
|
-
PROVIDER_SSE_IDLE_TIMEOUT_MS,
|
|
4
3
|
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
4
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
5
|
+
streamStalledError,
|
|
5
6
|
createTimeoutSignal,
|
|
6
7
|
providerTimeoutError,
|
|
7
8
|
} from '../stall-policy.mjs';
|
|
8
9
|
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
9
10
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
11
|
+
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
12
|
+
import { randomBytes } from 'crypto';
|
|
13
|
+
|
|
14
|
+
// Synthesize a native-shaped OpenAI tool call from a recovered leaked call.
|
|
15
|
+
// Matches the `call_...` id scheme the native Responses/Chat paths use so the
|
|
16
|
+
// dispatch loop and any downstream tool_result reference line up.
|
|
17
|
+
function synthLeakedOpenAICall(recovered) {
|
|
18
|
+
let args = recovered?.arguments;
|
|
19
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
20
|
+
return {
|
|
21
|
+
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
22
|
+
name: recovered.name,
|
|
23
|
+
arguments: args,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
10
26
|
|
|
11
27
|
function truncatedCompatStreamError(label, detail) {
|
|
12
28
|
return Object.assign(
|
|
@@ -15,8 +31,7 @@ function truncatedCompatStreamError(label, detail) {
|
|
|
15
31
|
);
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
// Invalid-tool-args marker
|
|
19
|
-
// opencode): completed-but-malformed tool_call arguments JSON must NOT throw
|
|
34
|
+
// Invalid-tool-args marker: completed-but-malformed tool_call arguments JSON must NOT throw
|
|
20
35
|
// (kills the turn) NOR be silently swallowed to `{}`. Instead the parse
|
|
21
36
|
// failure is carried as data on the tool call's `arguments` slot so the
|
|
22
37
|
// dispatch loop can turn it into an is_error tool_result and let the model
|
|
@@ -33,8 +48,7 @@ export function isInvalidToolArgsMarker(value) {
|
|
|
33
48
|
return !!value && typeof value === 'object' && value.__invalidToolArgs === true;
|
|
34
49
|
}
|
|
35
50
|
/** Model-facing tool_result text for a tool call whose arguments failed to
|
|
36
|
-
* parse
|
|
37
|
-
* `failed to parse function arguments` — instructs an in-turn retry. */
|
|
51
|
+
* parse; instructs the model to retry with valid JSON in the same turn. */
|
|
38
52
|
export function formatInvalidToolArgsResult(call) {
|
|
39
53
|
const name = call?.name || 'tool';
|
|
40
54
|
const detail = call?.arguments?.__parseError || 'arguments were not valid JSON';
|
|
@@ -69,9 +83,9 @@ export function parseCompletedToolCallArgumentsJson(raw, label, meta) {
|
|
|
69
83
|
// Invariant: a completion/finish signal was observed for this tool call
|
|
70
84
|
// (finish_reason present, or a per-call/response "done" event fired), so
|
|
71
85
|
// the arguments are NOT mid-stream-truncated — they are complete but
|
|
72
|
-
// malformed.
|
|
73
|
-
//
|
|
74
|
-
//
|
|
86
|
+
// malformed. Return an invalid-args MARKER (not a throw) so the
|
|
87
|
+
// dispatch loop feeds the parse error back to the model as a
|
|
88
|
+
// tool_result and the model self-corrects in the same turn. Only an
|
|
75
89
|
// unfinished stream (no finishReason) stays the retryable truncation
|
|
76
90
|
// case — that transient behavior is deliberately preserved.
|
|
77
91
|
if (meta?.finishReason) {
|
|
@@ -87,17 +101,29 @@ function firstByteCompatStreamError(label) {
|
|
|
87
101
|
return err;
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, idleLabel } = {}) {
|
|
104
|
+
async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, idleLabel, emittedToolCall } = {}) {
|
|
91
105
|
let idleTimer = null;
|
|
92
106
|
let idleReject = null;
|
|
93
107
|
let idleTimedOut = false;
|
|
108
|
+
// Double-dispatch guard (reviewer High): if a tool call was already emitted
|
|
109
|
+
// this stream, a stall must be unsafe-to-retry so withRetry() won't replay
|
|
110
|
+
// the turn and re-run the side-effecting tool. `emittedToolCall` may be a
|
|
111
|
+
// boolean or a getter evaluated at abort time (state mutates mid-stream).
|
|
112
|
+
const didEmitToolCall = () => {
|
|
113
|
+
try { return typeof emittedToolCall === 'function' ? !!emittedToolCall() : !!emittedToolCall; }
|
|
114
|
+
catch { return false; }
|
|
115
|
+
};
|
|
94
116
|
const armIdle = () => {
|
|
95
117
|
if (!idleEnabled || !(idleMs > 0)) return;
|
|
96
118
|
if (idleTimer) clearTimeout(idleTimer);
|
|
97
119
|
idleTimer = setTimeout(() => {
|
|
98
120
|
idleTimedOut = true;
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
// SEMANTIC idle abort: this timer is (re)armed only around waiting
|
|
122
|
+
// for the NEXT stream event, so keepalive/comment frames the SDK
|
|
123
|
+
// filters out cannot keep it alive. Throw the named terminal
|
|
124
|
+
// StreamStalledError so the retry-classifier treats it as a stream
|
|
125
|
+
// failure (owner gets notified) rather than a user cancel.
|
|
126
|
+
const e = streamStalledError(idleLabel || 'compat SSE', idleMs, { emittedToolCall: didEmitToolCall() });
|
|
101
127
|
if (idleReject) {
|
|
102
128
|
const r = idleReject;
|
|
103
129
|
idleReject = null;
|
|
@@ -143,7 +169,7 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
|
|
|
143
169
|
return result;
|
|
144
170
|
} catch (err) {
|
|
145
171
|
if (idleTimer) clearTimeout(idleTimer);
|
|
146
|
-
if (idleTimedOut) throw
|
|
172
|
+
if (idleTimedOut) throw streamStalledError(idleLabel || 'compat SSE', idleMs, { emittedToolCall: didEmitToolCall() });
|
|
147
173
|
throw err;
|
|
148
174
|
}
|
|
149
175
|
}
|
|
@@ -213,6 +239,15 @@ function emitCompatToolCallOnce(state, call, onToolCall) {
|
|
|
213
239
|
const key = `id:${call.id}`;
|
|
214
240
|
if (!state.emittedToolCallKeys) state.emittedToolCallKeys = new Set();
|
|
215
241
|
if (state.emittedToolCallKeys.has(key)) return false;
|
|
242
|
+
// Fix 2: cross-path name+args dedupe. A synthesized text-leaked call and an
|
|
243
|
+
// identical native tool_call must fire onToolCall exactly once. state._toolDedupe
|
|
244
|
+
// is created per stream; when absent (older callers) behavior is unchanged.
|
|
245
|
+
if (state._toolDedupe && !state._toolDedupe.shouldDispatch(call.name, call.arguments)) {
|
|
246
|
+
// Still mark the id as emitted so later id-frames for the same native
|
|
247
|
+
// call don't retry, but do NOT invoke onToolCall (already dispatched).
|
|
248
|
+
state.emittedToolCallKeys.add(key);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
216
251
|
state.emittedToolCallKeys.add(key);
|
|
217
252
|
state.emittedToolCall = true;
|
|
218
253
|
const { _pendingItemId, ...cleanCall } = call;
|
|
@@ -257,11 +292,14 @@ function isMaxOutputIncompleteReason(reason) {
|
|
|
257
292
|
return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
|
|
258
293
|
}
|
|
259
294
|
|
|
260
|
-
export async function consumeCompatChatCompletionStream(stream, { signal, label, onStreamDelta, onToolCall, onTextDelta, parseToolCalls } = {}) {
|
|
295
|
+
export async function consumeCompatChatCompletionStream(stream, { signal, label, onStreamDelta, onToolCall, onTextDelta, parseToolCalls, knownToolNames } = {}) {
|
|
261
296
|
const iterator = stream[Symbol.asyncIterator]();
|
|
262
297
|
const firstByteTimeout = createTimeoutSignal(signal, PROVIDER_FIRST_BYTE_TIMEOUT_MS, `${label} first byte`);
|
|
263
298
|
const idleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
|
|
264
|
-
|
|
299
|
+
// Per-event (last-event-relative) SEMANTIC idle: nextAsyncWithWatchdog arms
|
|
300
|
+
// the timer only while awaiting the NEXT stream event, so a stream that
|
|
301
|
+
// emits some deltas then goes silent trips it within the window.
|
|
302
|
+
const idleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
265
303
|
let sawFirstEvent = false;
|
|
266
304
|
let content = '';
|
|
267
305
|
let reasoningContent = '';
|
|
@@ -276,6 +314,46 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
276
314
|
let rawUsage = null;
|
|
277
315
|
const toolAcc = new Map();
|
|
278
316
|
const toolBucketState = { _orderSeq: 0, _nextAnonId: 0, _lastAnonKey: null };
|
|
317
|
+
// Fix 2: one dedupe per stream, shared by the synthetic leaked-call
|
|
318
|
+
// dispatch and every native emit so an identical (name,args) fires once.
|
|
319
|
+
const _toolDedupe = createToolCallDedupe();
|
|
320
|
+
// Leaked tool-call guard: the model sometimes emits a tool call as plain
|
|
321
|
+
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
322
|
+
// `<|channel|>...to=functions.NAME...<|call|>`) inside `delta.content`
|
|
323
|
+
// instead of a native `tool_calls` delta. Route content through the guard
|
|
324
|
+
// so leaked calls are suppressed from visible text, synthesized, and
|
|
325
|
+
// dispatched like native calls. Additive: the native tool_calls path is
|
|
326
|
+
// untouched. Harmony detection is opt-in here (gpt-oss compat backends).
|
|
327
|
+
const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
|
|
328
|
+
const dispatchLeakedCall = (recovered) => {
|
|
329
|
+
const call = synthLeakedOpenAICall(recovered);
|
|
330
|
+
const emitState = { emittedToolCallKeys: new Set(), _toolDedupe };
|
|
331
|
+
emitCompatToolCallOnce(emitState, call, onToolCall);
|
|
332
|
+
return call;
|
|
333
|
+
};
|
|
334
|
+
const leakedCalls = [];
|
|
335
|
+
const relayText = (delta) => {
|
|
336
|
+
const { text, calls } = leakGuard.push(delta);
|
|
337
|
+
if (text) {
|
|
338
|
+
content += text;
|
|
339
|
+
if (onTextDelta) {
|
|
340
|
+
emittedText = true;
|
|
341
|
+
try { onTextDelta(text); } catch {}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const c of calls) leakedCalls.push(dispatchLeakedCall(c));
|
|
345
|
+
};
|
|
346
|
+
const flushLeak = () => {
|
|
347
|
+
const { text, calls } = leakGuard.flush();
|
|
348
|
+
if (text) {
|
|
349
|
+
content += text;
|
|
350
|
+
if (onTextDelta) {
|
|
351
|
+
emittedText = true;
|
|
352
|
+
try { onTextDelta(text); } catch {}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const c of calls) leakedCalls.push(dispatchLeakedCall(c));
|
|
356
|
+
};
|
|
279
357
|
try {
|
|
280
358
|
while (true) {
|
|
281
359
|
const { value: chunk, done } = await nextAsyncWithWatchdog(iterator, {
|
|
@@ -285,6 +363,9 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
285
363
|
idleMs,
|
|
286
364
|
idleEnabled: sawFirstEvent && idleEnabled,
|
|
287
365
|
idleLabel: `${label} SSE idle`,
|
|
366
|
+
// A stall after a tool call has already been dispatched (native
|
|
367
|
+
// or recovered-leaked) must be unsafe-to-retry (no double-run).
|
|
368
|
+
emittedToolCall: () => leakedCalls.length > 0 || toolAcc.size > 0,
|
|
288
369
|
});
|
|
289
370
|
if (done) break;
|
|
290
371
|
if (!sawFirstEvent) {
|
|
@@ -296,12 +377,18 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
296
377
|
if (chunk?.model) model = chunk.model;
|
|
297
378
|
const choice = chunk?.choices?.[0];
|
|
298
379
|
if (choice?.delta?.content) {
|
|
299
|
-
|
|
300
|
-
//
|
|
380
|
+
// Live text relay (gateway): explicit assistant text delta,
|
|
381
|
+
// routed through the leaked-tool-call guard (which appends to
|
|
382
|
+
// `content`, forwards visible text, and recovers leaked calls).
|
|
301
383
|
// reasoning_content + tool_calls deltas stay off this path.
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
384
|
+
if (leakGuard.enabled) {
|
|
385
|
+
relayText(choice.delta.content);
|
|
386
|
+
} else {
|
|
387
|
+
content += choice.delta.content;
|
|
388
|
+
if (onTextDelta) {
|
|
389
|
+
emittedText = true;
|
|
390
|
+
try { onTextDelta(choice.delta.content); } catch {}
|
|
391
|
+
}
|
|
305
392
|
}
|
|
306
393
|
}
|
|
307
394
|
if (typeof choice?.delta?.reasoning_content === 'string') {
|
|
@@ -311,9 +398,23 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
311
398
|
if (choice?.finish_reason) stopReason = choice.finish_reason;
|
|
312
399
|
if (chunk?.usage) rawUsage = chunk.usage;
|
|
313
400
|
}
|
|
401
|
+
// Flush any partial-sentinel tail held back mid-stream so legitimate
|
|
402
|
+
// trailing text is never lost.
|
|
403
|
+
if (leakGuard.enabled) flushLeak();
|
|
314
404
|
} catch (err) {
|
|
315
405
|
// Any mid-stream failure after live text was relayed is non-retryable.
|
|
316
406
|
if (emittedText) throw markErrorLiveTextEmitted(err);
|
|
407
|
+
// Partial-final recovery: on a mid-stream stall, attach the
|
|
408
|
+
// streamed partial state so the loop can accept a wedged FINAL no-tool
|
|
409
|
+
// summary as partial-final success. pendingToolUse gates out any
|
|
410
|
+
// in-flight/emitted tool call.
|
|
411
|
+
if (err?.streamStalled === true) {
|
|
412
|
+
try {
|
|
413
|
+
err.partialContent = content;
|
|
414
|
+
err.pendingToolUse = toolAcc.size > 0 || leakedCalls.length > 0;
|
|
415
|
+
err.partialModel = model || undefined;
|
|
416
|
+
} catch { /* best-effort */ }
|
|
417
|
+
}
|
|
317
418
|
throw err;
|
|
318
419
|
} finally {
|
|
319
420
|
firstByteTimeout.cleanup();
|
|
@@ -356,9 +457,18 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
356
457
|
throw err;
|
|
357
458
|
}
|
|
358
459
|
if (Array.isArray(toolCalls) && toolCalls.length) {
|
|
359
|
-
const emitState = { emittedToolCallKeys: new Set() };
|
|
460
|
+
const emitState = { emittedToolCallKeys: new Set(), _toolDedupe };
|
|
360
461
|
for (const call of toolCalls) emitCompatToolCallOnce(emitState, call, onToolCall);
|
|
361
462
|
}
|
|
463
|
+
// Fold recovered leaked calls into the returned toolCalls so the dispatch
|
|
464
|
+
// loop treats them exactly like native ones. They were already emitted via
|
|
465
|
+
// onToolCall in relayText/flushLeak, so no re-dispatch here. Dedupe the
|
|
466
|
+
// final array by name+args (Fix 2, array side): a synthetic leaked call and
|
|
467
|
+
// an identical native tool_call must not both remain, else the loop runs
|
|
468
|
+
// the side-effecting tool twice.
|
|
469
|
+
if (leakedCalls.length) {
|
|
470
|
+
toolCalls = dedupeToolCallList([...(Array.isArray(toolCalls) ? toolCalls : []), ...leakedCalls]);
|
|
471
|
+
}
|
|
362
472
|
return {
|
|
363
473
|
response,
|
|
364
474
|
model,
|
|
@@ -370,18 +480,21 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
370
480
|
};
|
|
371
481
|
}
|
|
372
482
|
|
|
373
|
-
function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesToolCalls, responseOutputText, onStreamDelta, onToolCall, onTextDelta }) {
|
|
483
|
+
function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesToolCalls, responseOutputText, onStreamDelta, onToolCall, onTextDelta, relayLeakText }) {
|
|
374
484
|
if (!event || typeof event.type !== 'string') return;
|
|
375
485
|
const pushToolSearchCall = (item) => {
|
|
376
486
|
if (!item || item.type !== 'tool_search_call') return;
|
|
377
487
|
const callId = item.call_id || item.id || '';
|
|
378
488
|
if (!callId || state.toolCalls.some((call) => call.id === callId)) return;
|
|
489
|
+
const _tsArgs = item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
|
|
490
|
+
? item.arguments
|
|
491
|
+
: parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: callId, name: 'tool_search', finishReason: 'done' });
|
|
379
492
|
const call = {
|
|
380
493
|
id: callId,
|
|
381
494
|
name: 'tool_search',
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
495
|
+
// Schema is a plain object ({query,select,limit}); an array must
|
|
496
|
+
// never pass through as args.
|
|
497
|
+
arguments: (_tsArgs && typeof _tsArgs === 'object' && !Array.isArray(_tsArgs)) ? _tsArgs : {},
|
|
385
498
|
nativeType: 'tool_search_call',
|
|
386
499
|
};
|
|
387
500
|
state.toolCalls.push(call);
|
|
@@ -399,12 +512,17 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
399
512
|
if (event.response?.id) state.responseId = event.response.id;
|
|
400
513
|
break;
|
|
401
514
|
case 'response.output_text.delta':
|
|
402
|
-
state.content += event.delta || '';
|
|
403
515
|
state.sawOutput = true;
|
|
404
516
|
try { onStreamDelta?.(); } catch {}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
517
|
+
// Route assistant text through the leaked-tool-call guard (appends
|
|
518
|
+
// to state.content, forwards visible text, recovers leaked calls).
|
|
519
|
+
if (relayLeakText) relayLeakText(event.delta || '');
|
|
520
|
+
else {
|
|
521
|
+
state.content += event.delta || '';
|
|
522
|
+
if (event.delta && onTextDelta) {
|
|
523
|
+
state.emittedText = true;
|
|
524
|
+
try { onTextDelta(event.delta); } catch {}
|
|
525
|
+
}
|
|
408
526
|
}
|
|
409
527
|
break;
|
|
410
528
|
case 'response.output_item.added':
|
|
@@ -413,13 +531,29 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
413
531
|
name: event.item.name || '',
|
|
414
532
|
callId: event.item.call_id || '',
|
|
415
533
|
});
|
|
534
|
+
state.toolInFlight = true;
|
|
535
|
+
} else if (event.item?.type === 'custom_tool_call') {
|
|
536
|
+
state.toolInFlight = true;
|
|
537
|
+
} else if (event.item?.type === 'tool_search_call') {
|
|
538
|
+
// Mark tool_search in-flight at item-added time, same as
|
|
539
|
+
// function_call/custom_tool_call above, so the stall-recovery
|
|
540
|
+
// pendingToolUse gate never drops a mid-flight tool_search
|
|
541
|
+
// before response.output_item.done pushes it.
|
|
542
|
+
state.toolInFlight = true;
|
|
416
543
|
}
|
|
417
544
|
try { onStreamDelta?.(); } catch {}
|
|
418
545
|
break;
|
|
419
546
|
case 'response.function_call_arguments.delta':
|
|
547
|
+
// A tool call's args are streaming — mark tool work in-flight so a
|
|
548
|
+
// mid-args stall is NEVER accepted as a text-only partial-final.
|
|
549
|
+
state.toolInFlight = true;
|
|
420
550
|
try { onStreamDelta?.(); } catch {}
|
|
421
551
|
break;
|
|
422
552
|
case 'response.custom_tool_call_input.delta':
|
|
553
|
+
// Custom-tool input streams before output_item.done records the call
|
|
554
|
+
// in pendingCalls; flag it so a mid-input stall gates out partial-
|
|
555
|
+
// final success (otherwise a tool-bearing turn looks text-only).
|
|
556
|
+
state.toolInFlight = true;
|
|
423
557
|
try { onStreamDelta?.(); } catch {}
|
|
424
558
|
break;
|
|
425
559
|
case 'response.function_call_arguments.done': {
|
|
@@ -555,11 +689,13 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
555
689
|
onTextDelta,
|
|
556
690
|
parseResponsesToolCalls,
|
|
557
691
|
responseOutputText,
|
|
692
|
+
knownToolNames,
|
|
558
693
|
} = {}) {
|
|
559
694
|
const iterator = stream[Symbol.asyncIterator]();
|
|
560
695
|
const firstByteTimeout = createTimeoutSignal(signal, PROVIDER_FIRST_BYTE_TIMEOUT_MS, `${label} first byte`);
|
|
561
696
|
const idleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
|
|
562
|
-
|
|
697
|
+
// Per-event (last-event-relative) SEMANTIC idle — see the Chat path note.
|
|
698
|
+
const idleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
563
699
|
const state = {
|
|
564
700
|
content: '',
|
|
565
701
|
model: '',
|
|
@@ -572,13 +708,51 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
572
708
|
completed: false,
|
|
573
709
|
completedResponse: null,
|
|
574
710
|
sawOutput: false,
|
|
711
|
+
// Fix 2: cross-path name+args dedupe shared by synthetic leaked-call
|
|
712
|
+
// dispatch and every native emit in this Responses stream.
|
|
713
|
+
_toolDedupe: createToolCallDedupe(),
|
|
575
714
|
// Gateway live-text relay invariant: set once a non-empty text chunk
|
|
576
715
|
// has been forwarded. A later failure is non-retryable (rendered text
|
|
577
716
|
// cannot be withdrawn; a retry would concatenate attempts).
|
|
578
717
|
emittedText: false,
|
|
579
718
|
};
|
|
580
719
|
let sawFirstEvent = false;
|
|
581
|
-
|
|
720
|
+
// Leaked tool-call guard for the Responses text stream. Same recovery as
|
|
721
|
+
// the Chat path: leaked XML/harmony tool syntax in `output_text.delta` is
|
|
722
|
+
// suppressed from visible text, synthesized, and dispatched like native.
|
|
723
|
+
const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
|
|
724
|
+
const leakedCalls = [];
|
|
725
|
+
const dispatchLeakedCall = (recovered) => {
|
|
726
|
+
const call = synthLeakedOpenAICall(recovered);
|
|
727
|
+
emitCompatToolCallOnce(state, call, onToolCall);
|
|
728
|
+
leakedCalls.push(call);
|
|
729
|
+
};
|
|
730
|
+
const relayLeakText = leakGuard.enabled
|
|
731
|
+
? (delta) => {
|
|
732
|
+
const { text, calls } = leakGuard.push(delta);
|
|
733
|
+
if (text) {
|
|
734
|
+
state.content += text;
|
|
735
|
+
if (onTextDelta) {
|
|
736
|
+
state.emittedText = true;
|
|
737
|
+
try { onTextDelta(text); } catch {}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
741
|
+
}
|
|
742
|
+
: null;
|
|
743
|
+
const flushLeak = () => {
|
|
744
|
+
if (!leakGuard.enabled) return;
|
|
745
|
+
const { text, calls } = leakGuard.flush();
|
|
746
|
+
if (text) {
|
|
747
|
+
state.content += text;
|
|
748
|
+
if (onTextDelta) {
|
|
749
|
+
state.emittedText = true;
|
|
750
|
+
try { onTextDelta(text); } catch {}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
754
|
+
};
|
|
755
|
+
const deps = { label, parseResponsesToolCalls, responseOutputText, onStreamDelta, onToolCall, onTextDelta, relayLeakText };
|
|
582
756
|
try {
|
|
583
757
|
while (true) {
|
|
584
758
|
const { value: event, done } = await nextAsyncWithWatchdog(iterator, {
|
|
@@ -586,6 +760,9 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
586
760
|
idleMs,
|
|
587
761
|
idleEnabled: sawFirstEvent && idleEnabled,
|
|
588
762
|
idleLabel: `${label} SSE idle`,
|
|
763
|
+
// Unsafe-to-retry once any tool call (native or recovered-leaked)
|
|
764
|
+
// has been emitted this stream — avoid a double side-effect.
|
|
765
|
+
emittedToolCall: () => state.emittedToolCall || leakedCalls.length > 0,
|
|
589
766
|
});
|
|
590
767
|
if (done) break;
|
|
591
768
|
if (!sawFirstEvent) {
|
|
@@ -594,7 +771,21 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
594
771
|
}
|
|
595
772
|
handleCompatResponsesStreamEvent(event, state, deps);
|
|
596
773
|
}
|
|
774
|
+
flushLeak();
|
|
597
775
|
} catch (err) {
|
|
776
|
+
// Partial-final recovery: attach streamed partial state so a
|
|
777
|
+
// wedged FINAL no-tool summary can be accepted as partial-final success.
|
|
778
|
+
if (err?.streamStalled === true) {
|
|
779
|
+
try {
|
|
780
|
+
err.partialContent = state.content || '';
|
|
781
|
+
err.pendingToolUse = state.emittedToolCall === true
|
|
782
|
+
|| leakedCalls.length > 0
|
|
783
|
+
|| (state.pendingCalls && state.pendingCalls.size > 0)
|
|
784
|
+
|| (Array.isArray(state.toolCalls) && state.toolCalls.length > 0)
|
|
785
|
+
|| state.toolInFlight === true;
|
|
786
|
+
err.partialModel = state.model || undefined;
|
|
787
|
+
} catch { /* best-effort */ }
|
|
788
|
+
}
|
|
598
789
|
throw markUnsafeRetryIfToolEmitted(err, state);
|
|
599
790
|
} finally {
|
|
600
791
|
firstByteTimeout.cleanup();
|
|
@@ -618,9 +809,14 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
618
809
|
output_text: state.content,
|
|
619
810
|
output: [],
|
|
620
811
|
};
|
|
621
|
-
|
|
812
|
+
let toolCalls = state.toolCalls.length
|
|
622
813
|
? state.toolCalls.map(({ _pendingItemId, ...t }) => t)
|
|
623
814
|
: parseResponsesToolCalls(response, label);
|
|
815
|
+
// Fold recovered leaked calls in (already emitted via onToolCall above).
|
|
816
|
+
// Dedupe by name+args so an identical native+synthetic pair can't run twice.
|
|
817
|
+
if (leakedCalls.length) {
|
|
818
|
+
toolCalls = dedupeToolCallList([...(Array.isArray(toolCalls) ? toolCalls : []), ...leakedCalls]);
|
|
819
|
+
}
|
|
624
820
|
return {
|
|
625
821
|
response,
|
|
626
822
|
content: state.content || responseOutputText(response),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { loadConfig } from '../config.mjs';
|
|
4
4
|
import { shouldFallbackTransport, withRetry } from './retry-classifier.mjs';
|
|
5
|
+
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
5
6
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
6
7
|
import {
|
|
7
8
|
consumeCompatChatCompletionStream,
|
|
@@ -32,6 +33,17 @@ import {
|
|
|
32
33
|
customToolCallFromResponseItem,
|
|
33
34
|
} from './custom-tool-wire.mjs';
|
|
34
35
|
import { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
|
|
36
|
+
|
|
37
|
+
const requireOpenAI = createRequire(import.meta.url);
|
|
38
|
+
let _OpenAI = null;
|
|
39
|
+
|
|
40
|
+
function loadOpenAI() {
|
|
41
|
+
if (!_OpenAI) {
|
|
42
|
+
const mod = requireOpenAI('openai');
|
|
43
|
+
_OpenAI = mod.default || mod.OpenAI || mod;
|
|
44
|
+
}
|
|
45
|
+
return _OpenAI;
|
|
46
|
+
}
|
|
35
47
|
export { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
|
|
36
48
|
const PRESETS = OPENAI_COMPAT_PRESETS;
|
|
37
49
|
const MODEL_LIST_TIMEOUT_MS = resolveTimeoutMs(
|
|
@@ -970,6 +982,25 @@ function nativeResponsesTools(opts) {
|
|
|
970
982
|
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
971
983
|
: [];
|
|
972
984
|
}
|
|
985
|
+
// Known tool-name sets for the leaked-tool-call guard, derived from the exact
|
|
986
|
+
// request body so a recovered leaked call is only synthesized when it names a
|
|
987
|
+
// tool the model was actually offered. Chat tools nest the name under
|
|
988
|
+
// `function.name`; Responses tools carry a top-level `name`.
|
|
989
|
+
function knownToolNamesFromOpenAITools(tools) {
|
|
990
|
+
return new Set(
|
|
991
|
+
(Array.isArray(tools) ? tools : [])
|
|
992
|
+
.map((t) => (typeof t?.function?.name === 'string' ? t.function.name
|
|
993
|
+
: typeof t?.name === 'string' ? t.name : null))
|
|
994
|
+
.filter(Boolean),
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
function knownToolNamesFromResponsesTools(tools) {
|
|
998
|
+
return new Set(
|
|
999
|
+
(Array.isArray(tools) ? tools : [])
|
|
1000
|
+
.map((t) => (typeof t?.name === 'string' ? t.name : null))
|
|
1001
|
+
.filter(Boolean),
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
973
1004
|
export function parseToolCalls(choice, label) {
|
|
974
1005
|
const calls = choice.message?.tool_calls;
|
|
975
1006
|
if (!calls?.length)
|
|
@@ -1001,12 +1032,15 @@ export function parseResponsesToolCalls(response, label) {
|
|
|
1001
1032
|
const call = customToolCallFromResponseItem(item);
|
|
1002
1033
|
if (call) out.push(call);
|
|
1003
1034
|
} else if (item?.type === 'tool_search_call') {
|
|
1035
|
+
const _tsArgs = item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
|
|
1036
|
+
? item.arguments
|
|
1037
|
+
: parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: item.call_id || item.id, name: 'tool_search', finishReason });
|
|
1004
1038
|
out.push({
|
|
1005
1039
|
id: item.call_id || item.id,
|
|
1006
1040
|
name: 'tool_search',
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1041
|
+
// Schema is a plain object ({query,select,limit}); an array
|
|
1042
|
+
// (parsed JSON or passthrough) must never pass through as args.
|
|
1043
|
+
arguments: (_tsArgs && typeof _tsArgs === 'object' && !Array.isArray(_tsArgs)) ? _tsArgs : {},
|
|
1010
1044
|
nativeType: 'tool_search_call',
|
|
1011
1045
|
});
|
|
1012
1046
|
}
|
|
@@ -1198,10 +1232,20 @@ export class OpenAICompatProvider {
|
|
|
1198
1232
|
// extraHeaders behave exactly as before.
|
|
1199
1233
|
this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(config.extraHeaders || {}) };
|
|
1200
1234
|
this.defaultModel = preset?.defaultModel || 'default';
|
|
1201
|
-
this.client = new
|
|
1235
|
+
this.client = new (loadOpenAI())({
|
|
1202
1236
|
baseURL,
|
|
1203
1237
|
apiKey,
|
|
1204
1238
|
defaultHeaders: this.defaultHeaders,
|
|
1239
|
+
// The SDK's own retry loop (default 2) would nest underneath our
|
|
1240
|
+
// withRetry wrapper and multiply tail latency on a transient
|
|
1241
|
+
// backend. We own retry/backoff via withRetry, so disable the SDK's.
|
|
1242
|
+
maxRetries: 0,
|
|
1243
|
+
// Force the shared long-keepalive undici dispatcher to be installed
|
|
1244
|
+
// globally (setGlobalDispatcher) so the SDK's global fetch rides a
|
|
1245
|
+
// warm socket pool instead of Node's short-keepalive default. The
|
|
1246
|
+
// return value is undefined once installed globally; the option is
|
|
1247
|
+
// a harmless no-op then.
|
|
1248
|
+
fetchOptions: { dispatcher: getLlmDispatcher() },
|
|
1205
1249
|
});
|
|
1206
1250
|
}
|
|
1207
1251
|
reloadApiKey() {
|
|
@@ -1216,10 +1260,12 @@ export class OpenAICompatProvider {
|
|
|
1216
1260
|
this.baseURL = baseURL;
|
|
1217
1261
|
this.apiKey = newKey;
|
|
1218
1262
|
this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(this.config.extraHeaders || {}) };
|
|
1219
|
-
this.client = new
|
|
1263
|
+
this.client = new (loadOpenAI())({
|
|
1220
1264
|
baseURL,
|
|
1221
1265
|
apiKey: newKey,
|
|
1222
1266
|
defaultHeaders: this.defaultHeaders,
|
|
1267
|
+
maxRetries: 0,
|
|
1268
|
+
fetchOptions: { dispatcher: getLlmDispatcher() },
|
|
1223
1269
|
});
|
|
1224
1270
|
}
|
|
1225
1271
|
} catch { /* best effort */ }
|
|
@@ -1239,6 +1285,10 @@ export class OpenAICompatProvider {
|
|
|
1239
1285
|
async _doSend(messages, model, tools, sendOpts) {
|
|
1240
1286
|
const useModel = model || this.defaultModel;
|
|
1241
1287
|
const opts = sendOpts || {};
|
|
1288
|
+
// Re-warm a kept-alive socket to the provider origin before the turn so
|
|
1289
|
+
// the request hot path lands on a live socket instead of paying a cold
|
|
1290
|
+
// TLS handshake after an idle gap. Fire-and-forget; never awaited.
|
|
1291
|
+
preconnect(this.baseURL);
|
|
1242
1292
|
if (this.name === 'xai' && useXaiResponsesApi(opts, this.config)) {
|
|
1243
1293
|
if (useXaiResponsesWebSocket(opts, this.config)) {
|
|
1244
1294
|
try {
|
|
@@ -1339,6 +1389,11 @@ export class OpenAICompatProvider {
|
|
|
1339
1389
|
({ signal: openSignal }) => this.client.chat.completions.create(params, { signal: openSignal }),
|
|
1340
1390
|
{
|
|
1341
1391
|
signal: attemptSignal,
|
|
1392
|
+
// Single attempt: this inner wrapper exists only to
|
|
1393
|
+
// apply the first-byte per-attempt timeout. Retry is
|
|
1394
|
+
// owned by the outer withRetry — nesting retry loops
|
|
1395
|
+
// here multiplied tail latency (5x5).
|
|
1396
|
+
maxAttempts: 1,
|
|
1342
1397
|
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1343
1398
|
perAttemptLabel: `${this.name} first byte`,
|
|
1344
1399
|
},
|
|
@@ -1351,6 +1406,10 @@ export class OpenAICompatProvider {
|
|
|
1351
1406
|
onToolCall: opts.onToolCall,
|
|
1352
1407
|
onTextDelta: opts.onTextDelta,
|
|
1353
1408
|
parseToolCalls,
|
|
1409
|
+
// Known tool names for the leaked-tool-call guard:
|
|
1410
|
+
// recovered leaked calls only synthesize when they name
|
|
1411
|
+
// a tool actually offered to this request.
|
|
1412
|
+
knownToolNames: knownToolNamesFromOpenAITools(params.tools),
|
|
1354
1413
|
});
|
|
1355
1414
|
},
|
|
1356
1415
|
{
|
|
@@ -1434,6 +1493,13 @@ export class OpenAICompatProvider {
|
|
|
1434
1493
|
model: response.model || useModel,
|
|
1435
1494
|
toolCalls,
|
|
1436
1495
|
stopReason,
|
|
1496
|
+
// P1 audit fix: a text-only completion that hit finish_reason=
|
|
1497
|
+
// 'length' (no tool calls, so not thrown above as
|
|
1498
|
+
// ProviderIncompleteError) previously returned as an ordinary
|
|
1499
|
+
// success with no signal that the content is a mid-sentence
|
|
1500
|
+
// cutoff. Flag it so loop.mjs can surface a one-line warning
|
|
1501
|
+
// instead of silently treating a truncated answer as complete.
|
|
1502
|
+
...(stopReason === 'length' && (assembled.content || '').length > 0 ? { truncated: true } : {}),
|
|
1437
1503
|
...(reasoningContent ? { reasoningContent } : {}),
|
|
1438
1504
|
usage: response.usage ? (() => {
|
|
1439
1505
|
const input = response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0;
|
|
@@ -1516,6 +1582,9 @@ export class OpenAICompatProvider {
|
|
|
1516
1582
|
({ signal: openSignal }) => this.client.responses.create(params, { signal: openSignal }),
|
|
1517
1583
|
{
|
|
1518
1584
|
signal: attemptSignal,
|
|
1585
|
+
// Single attempt: first-byte timeout only; retry
|
|
1586
|
+
// is owned by the outer withRetry (see chat path).
|
|
1587
|
+
maxAttempts: 1,
|
|
1519
1588
|
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
1520
1589
|
perAttemptLabel: 'xai responses first byte',
|
|
1521
1590
|
},
|
|
@@ -1529,6 +1598,7 @@ export class OpenAICompatProvider {
|
|
|
1529
1598
|
onTextDelta: opts.onTextDelta,
|
|
1530
1599
|
parseResponsesToolCalls,
|
|
1531
1600
|
responseOutputText,
|
|
1601
|
+
knownToolNames: knownToolNamesFromResponsesTools(params.tools),
|
|
1532
1602
|
});
|
|
1533
1603
|
},
|
|
1534
1604
|
{
|
|
@@ -1598,6 +1668,10 @@ export class OpenAICompatProvider {
|
|
|
1598
1668
|
model: response.model || useModel,
|
|
1599
1669
|
toolCalls,
|
|
1600
1670
|
stopReason: streamed.stopReason || null,
|
|
1671
|
+
// P1 audit fix: mirror the chat-completions truncated flag for
|
|
1672
|
+
// the xAI Responses HTTP path — a max-output cutoff with real
|
|
1673
|
+
// content must not look identical to a clean stop.
|
|
1674
|
+
...(streamed.stopReason === 'length' && (streamed.content || '').length > 0 ? { truncated: true } : {}),
|
|
1601
1675
|
citations: searchSources.citations.length ? searchSources.citations : undefined,
|
|
1602
1676
|
webSearchCalls: searchSources.webSearchCalls.length ? searchSources.webSearchCalls : undefined,
|
|
1603
1677
|
providerState: {
|
|
@@ -1753,6 +1827,9 @@ export class OpenAICompatProvider {
|
|
|
1753
1827
|
model: result.model || useModel,
|
|
1754
1828
|
toolCalls: result.toolCalls,
|
|
1755
1829
|
stopReason: result.stopReason || null,
|
|
1830
|
+
// P1 audit fix: same truncated signal as the HTTP path (see
|
|
1831
|
+
// _doSendXaiResponses above) for the WebSocket transport.
|
|
1832
|
+
...(result.stopReason === 'length' && (result.content || '').length > 0 ? { truncated: true } : {}),
|
|
1756
1833
|
providerState: {
|
|
1757
1834
|
...(opts.providerState || {}),
|
|
1758
1835
|
xaiResponses: {
|