openmausbot 0.1.77 → 0.1.79
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/dist/assets/{index-DeK-z9ld.js → index-C2Je5jdF.js} +1 -1
- package/dist/assets/index-D_TFZLlo.js +305 -0
- package/dist/assets/index-Pbb6Ao0s.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +11 -5
- package/dist-server/drivers/agents-proxy.js +786 -29
- package/dist-server/enterprise/server/index.js +0 -1
- package/dist-server/index.js +17598 -7843
- package/dist-server/local-computer.js +0 -1
- package/dist-server/mcp-server.js +8 -1
- package/dist-server/openmausbot.js +9195 -759
- package/dist-server/pair-cli.js +9195 -759
- package/dist-server/proxy-paths.js +0 -1
- package/dist-server/server/auto-approve.js +16 -0
- package/dist-server/server/bot-overview.js +6 -2
- package/dist-server/server/bot-package.js +17 -3
- package/dist-server/server/box.js +98 -41
- package/dist-server/server/browser-engine.js +1 -1
- package/dist-server/server/browser-runtime.js +11 -3
- package/dist-server/server/browser-tool-shape.js +72 -0
- package/dist-server/server/chief-of-staff.js +2 -2
- package/dist-server/server/claude-accounts.js +1 -0
- package/dist-server/server/cli-setup.js +1 -1
- package/dist-server/server/composio.js +18 -0
- package/dist-server/server/config.js +16 -4
- package/dist-server/server/container-computer.js +0 -12
- package/dist-server/server/drivers/acp/core.js +4 -19
- package/dist-server/server/drivers/agents-proxy.js +74 -25
- package/dist-server/server/drivers/boxagent.js +48 -4
- package/dist-server/server/drivers/chat-mcp-tools.js +368 -0
- package/dist-server/server/drivers/chat-tool-approval.js +42 -0
- package/dist-server/server/drivers/claude.js +87 -32
- package/dist-server/server/drivers/codex.js +69 -31
- package/dist-server/server/drivers/grok.js +4 -0
- package/dist-server/server/drivers/minimax.js +11 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +92 -0
- package/dist-server/server/drivers/openai-chat.js +332 -88
- package/dist-server/server/drivers/openai-compat.js +4 -0
- package/dist-server/server/drivers/pi.js +1 -9
- package/dist-server/server/index.js +271 -136
- package/dist-server/server/package-export.js +16 -14
- package/dist-server/server/peer-approval.js +1 -1
- package/dist-server/server/profile-requests.js +26 -4
- package/dist-server/server/proxy-paths.js +0 -1
- package/dist-server/server/room-handoffs.js +85 -10
- package/dist-server/server/routine-requests.js +86 -3
- package/dist-server/server/routines.js +31 -13
- package/dist-server/server/setup-mode.js +11 -13
- package/dist-server/server/skill-learn.js +4 -4
- package/dist-server/server/skills.js +21 -0
- package/dist-server/server/store.js +90 -1
- package/dist-server/server/system-prompt.js +7 -5
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-setup-requests.js +83 -31
- package/dist-server/server/tts/chatterbox.js +83 -0
- package/dist-server/server/tts/index.js +45 -12
- package/dist-server/server/workspace.js +12 -2
- package/dist-server/shared/ask-question.js +28 -0
- package/dist-server/shared/computer-contention.js +17 -0
- package/dist-server/shared/cron-label.js +39 -0
- package/dist-server/shared/routine-schedule.js +89 -0
- package/dist-server/shared/team-backup.js +1 -1
- package/dist-server/vps-container-mcp.js +11 -5
- package/enterprise/server/index.js +0 -1
- package/package.json +1 -1
- package/dist/assets/index-XdHJ-9CF.css +0 -1
- package/dist/assets/index-lhqE1umn.js +0 -305
- package/dist-server/computer-proxy.js +0 -1196
- package/dist-server/server/computer-proxy.js +0 -1070
- package/dist-server/server/remote-computer.js +0 -170
|
@@ -12,15 +12,13 @@
|
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { stripWorkspaceCredentialEnv } from "../config.js";
|
|
15
|
-
import { computerProxyEnv } from "../container-computer.js";
|
|
16
15
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
17
|
-
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
18
16
|
import { isHarnessOwnedMcpEnvName } from "../mcp-registry.js";
|
|
19
17
|
import { newEventId, newId } from "../contracts.js";
|
|
20
18
|
import { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
21
19
|
import { codexLocalProviderArgs } from "./local-inject.js";
|
|
22
20
|
import { augmentedPath, splitCliString } from "../env-path.js";
|
|
23
|
-
import { classifyError, computeBackoff, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
21
|
+
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
24
22
|
import { appendNative } from "./native.js";
|
|
25
23
|
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
26
24
|
import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
|
|
@@ -452,6 +450,9 @@ export const CodexDriver = {
|
|
|
452
450
|
// One driver instance serves many threads. Interrupt state belongs to
|
|
453
451
|
// this turn so activity elsewhere cannot cancel or revive its retry.
|
|
454
452
|
let stopRequested = false;
|
|
453
|
+
// Wakes a retry backoff the moment Stop arrives, so the turn settles
|
|
454
|
+
// now rather than after the full wait.
|
|
455
|
+
const stopSignal = new AbortController();
|
|
455
456
|
const { threadId } = turn;
|
|
456
457
|
// Direct adapter callers predating the per-bot selector retain the
|
|
457
458
|
// instance's legacy fullAuto setting. Harness turns always send an
|
|
@@ -479,23 +480,7 @@ export const CodexDriver = {
|
|
|
479
480
|
if (turn.integrations?.agents) {
|
|
480
481
|
mountMcpServer(appServerArgs, env, "agents", turn.integrations.agents);
|
|
481
482
|
}
|
|
482
|
-
if (turn.integrations?.
|
|
483
|
-
const proxyEnv = computerProxyEnv(turn.integrations.computer);
|
|
484
|
-
mountMcpServer(appServerArgs, env, "computer", {
|
|
485
|
-
command: process.execPath,
|
|
486
|
-
args: [SPAWNED_PROXIES.computer],
|
|
487
|
-
env: {
|
|
488
|
-
ELECTRON_RUN_AS_NODE: "1",
|
|
489
|
-
OGB_BOX_ID: proxyEnv.OGB_BOX_ID ?? "",
|
|
490
|
-
OGB_BOX_TOKEN: proxyEnv.OGB_BOX_TOKEN ?? "",
|
|
491
|
-
// who-is-driving endpoint, so a person taking the wheel in the
|
|
492
|
-
// panel pauses this bot's hands mid-turn
|
|
493
|
-
OMB_CONTROL_URL: proxyEnv.OMB_CONTROL_URL ?? "",
|
|
494
|
-
OMB_CONTROL_TOKEN: proxyEnv.OMB_CONTROL_TOKEN ?? "",
|
|
495
|
-
},
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
else if (turn.integrations?.localComputer) {
|
|
483
|
+
if (turn.integrations?.localComputer) {
|
|
499
484
|
// The host daemon and isolated Local VM both arrive as a direct Cua
|
|
500
485
|
// Driver stdio MCP server. Codex sees the same computer tool surface.
|
|
501
486
|
mountMcpServer(appServerArgs, env, "computer", turn.integrations.localComputer);
|
|
@@ -594,6 +579,7 @@ export const CodexDriver = {
|
|
|
594
579
|
let completeStoppedTurn;
|
|
595
580
|
const stop = async () => {
|
|
596
581
|
stopRequested = true;
|
|
582
|
+
stopSignal.abort();
|
|
597
583
|
const stopped = await terminate();
|
|
598
584
|
if (stopped)
|
|
599
585
|
completeStoppedTurn?.();
|
|
@@ -926,6 +912,7 @@ export const CodexDriver = {
|
|
|
926
912
|
catch {
|
|
927
913
|
continue;
|
|
928
914
|
}
|
|
915
|
+
stderrSinceOutput = "";
|
|
929
916
|
const loggedMessage = codexNativeIncomingLogMessage(msg, sensitiveResponseIds);
|
|
930
917
|
appendNative(threadId, { dir: "in", source: "codex.app-server", msg: loggedMessage });
|
|
931
918
|
if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
@@ -947,10 +934,20 @@ export const CodexDriver = {
|
|
|
947
934
|
}
|
|
948
935
|
});
|
|
949
936
|
let stderr = "";
|
|
937
|
+
// Stderr that arrived after the last parsed protocol message. The
|
|
938
|
+
// full buffer accumulates for the whole process lifetime, so its
|
|
939
|
+
// tail can name a long-past event (a websocket 426 logged at turn
|
|
940
|
+
// start, echoed half an hour later when something else kills the
|
|
941
|
+
// process). Only this slice can explain an exit; older bytes are
|
|
942
|
+
// context, not cause.
|
|
943
|
+
let stderrSinceOutput = "";
|
|
950
944
|
child.stderr.on("data", (c) => {
|
|
951
945
|
stderr += c;
|
|
946
|
+
stderrSinceOutput += c;
|
|
952
947
|
if (stderr.length > 8192)
|
|
953
948
|
stderr = stderr.slice(-8192);
|
|
949
|
+
if (stderrSinceOutput.length > 2048)
|
|
950
|
+
stderrSinceOutput = stderrSinceOutput.slice(-2048);
|
|
954
951
|
});
|
|
955
952
|
child.on("error", (e) => {
|
|
956
953
|
if (abandoned)
|
|
@@ -958,7 +955,7 @@ export const CodexDriver = {
|
|
|
958
955
|
emit({ ...base(threadId, turnId), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
|
|
959
956
|
void settle(false, "spawn_error");
|
|
960
957
|
});
|
|
961
|
-
child.on("close", (code) => {
|
|
958
|
+
child.on("close", (code, signal) => {
|
|
962
959
|
if (abandoned)
|
|
963
960
|
return;
|
|
964
961
|
if (state.settled) {
|
|
@@ -967,14 +964,56 @@ export const CodexDriver = {
|
|
|
967
964
|
void stop();
|
|
968
965
|
return;
|
|
969
966
|
}
|
|
970
|
-
|
|
967
|
+
// The child died before the turn completed. Attribute the exit
|
|
968
|
+
// honestly: name the signal when it was killed, and only quote
|
|
969
|
+
// stderr that arrived after the last protocol message. A stale
|
|
970
|
+
// tail here once misattributed a whole day of killed turns to a
|
|
971
|
+
// websocket 426 logged at turn start.
|
|
972
|
+
const recentStderr = stderrSinceOutput.trim();
|
|
973
|
+
const hadStreamedOutput = codexTurnId !== null || state.sawStreamDelta;
|
|
974
|
+
const verdict = classifyError({ exitCode: code, stderr: recentStderr || stderr });
|
|
975
|
+
// Safe re-dispatch: relaunch only when the app-server never
|
|
976
|
+
// acknowledged turn/start — no native turn began, nothing was
|
|
977
|
+
// streamed, so replaying the input cannot duplicate work. After
|
|
978
|
+
// any acknowledgement (or any buffered pre-ack event) the turn
|
|
979
|
+
// settles instead: a replay could re-run tools the user saw.
|
|
980
|
+
if (!stopRequested && codexTurnId === null && earlyNotifications.length === 0 &&
|
|
981
|
+
verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1) {
|
|
982
|
+
const delayMs = computeBackoff(attempt);
|
|
983
|
+
attempt++;
|
|
971
984
|
emit({
|
|
972
985
|
...base(threadId, turnId),
|
|
973
|
-
type: "
|
|
974
|
-
|
|
986
|
+
type: "turn.retrying",
|
|
987
|
+
attempt,
|
|
988
|
+
delayMs,
|
|
989
|
+
reason: verdict.reason,
|
|
975
990
|
});
|
|
976
|
-
|
|
991
|
+
// Retire this attempt before anything async runs, so a late
|
|
992
|
+
// rpc timer rejection in the handshake catch cannot relaunch
|
|
993
|
+
// a second time on top of this one.
|
|
994
|
+
abandoned = true;
|
|
995
|
+
void (async () => {
|
|
996
|
+
const alreadyDead = child.exitCode !== null || child.signalCode !== null;
|
|
997
|
+
if (!alreadyDead && !(await terminate())) {
|
|
998
|
+
void settle(false, "shutdown_timeout");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
await interruptibleDelay(Math.max(1, Math.round(delayMs * retryScale)), stopSignal.signal).promise;
|
|
1002
|
+
if (!stopRequested) {
|
|
1003
|
+
void launchAttempt(attempt).catch(() => { });
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
await settle(false, "interrupted");
|
|
1007
|
+
}
|
|
1008
|
+
})().catch(() => { });
|
|
1009
|
+
return;
|
|
977
1010
|
}
|
|
1011
|
+
emit({
|
|
1012
|
+
...base(threadId, turnId),
|
|
1013
|
+
type: "runtime.error",
|
|
1014
|
+
message: `codex exited ${code}${signal ? ` (signal ${signal})` : ""} before turn/completed${recentStderr ? `: ${recentStderr.slice(-300)}` : hadStreamedOutput && stderr.trim() ? "; no stderr after the last app-server output" : ""}`,
|
|
1015
|
+
});
|
|
1016
|
+
void settle(false, "exit_before_result");
|
|
978
1017
|
});
|
|
979
1018
|
active.set(threadId, { stop, turnId, asks });
|
|
980
1019
|
// Relaunching the app-server is still the same logical turn. Keep the
|
|
@@ -1122,7 +1161,7 @@ export const CodexDriver = {
|
|
|
1122
1161
|
const message = e instanceof Error ? e.message : String(e);
|
|
1123
1162
|
const needsAuth = /(?:\b401\b|unauthorized|missing bearer|authentication required)/i.test(message);
|
|
1124
1163
|
const verdict = classifyError(failure);
|
|
1125
|
-
if (!state.settled && !needsAuth && verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1 && state.sawStreamDelta === false) {
|
|
1164
|
+
if (!state.settled && !abandoned && !needsAuth && verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1 && state.sawStreamDelta === false) {
|
|
1126
1165
|
const delayMs = computeBackoff(attempt);
|
|
1127
1166
|
attempt++;
|
|
1128
1167
|
emit({
|
|
@@ -1139,10 +1178,7 @@ export const CodexDriver = {
|
|
|
1139
1178
|
void settle(false, "shutdown_timeout");
|
|
1140
1179
|
return;
|
|
1141
1180
|
}
|
|
1142
|
-
await
|
|
1143
|
-
const timer = setTimeout(resolve, Math.max(1, Math.round(delayMs * retryScale)));
|
|
1144
|
-
timer.unref?.();
|
|
1145
|
-
});
|
|
1181
|
+
await interruptibleDelay(Math.max(1, Math.round(delayMs * retryScale)), stopSignal.signal).promise;
|
|
1146
1182
|
if (!stopRequested) {
|
|
1147
1183
|
void launchAttempt(attempt).catch(() => { });
|
|
1148
1184
|
}
|
|
@@ -1151,7 +1187,9 @@ export const CodexDriver = {
|
|
|
1151
1187
|
}
|
|
1152
1188
|
return;
|
|
1153
1189
|
}
|
|
1154
|
-
|
|
1190
|
+
// abandoned marks an attempt retired by a retry; its late rpc
|
|
1191
|
+
// timeouts must neither report a spurious error nor relaunch again
|
|
1192
|
+
if (!state.settled && !abandoned) {
|
|
1155
1193
|
emit({
|
|
1156
1194
|
...base(threadId, turnId),
|
|
1157
1195
|
type: "runtime.error",
|
|
@@ -11,7 +11,10 @@ const MODELS = {
|
|
|
11
11
|
};
|
|
12
12
|
function decodeConfig(raw) {
|
|
13
13
|
const config = (raw ?? {});
|
|
14
|
+
if (config.tools !== undefined && typeof config.tools !== "boolean")
|
|
15
|
+
throw new Error("tools must be a boolean");
|
|
14
16
|
return {
|
|
17
|
+
...(config.tools !== undefined ? { tools: config.tools } : {}),
|
|
15
18
|
url: typeof config.url === "string" ? config.url : DEFAULT_URL,
|
|
16
19
|
apiKeyEnv: typeof config.apiKeyEnv === "string" ? config.apiKeyEnv : "XAI_API_KEY",
|
|
17
20
|
};
|
|
@@ -30,6 +33,7 @@ export const GrokDriver = {
|
|
|
30
33
|
driverKind: DRIVER_KIND,
|
|
31
34
|
apiKey,
|
|
32
35
|
apiUrl: config.url,
|
|
36
|
+
tools: config.tools,
|
|
33
37
|
models: () => MODELS,
|
|
34
38
|
requestBody: (model, messages, stream) => ({ model, messages, stream }),
|
|
35
39
|
httpErrorLabel: "xAI",
|
|
@@ -11,10 +11,12 @@ const DEFAULT_URL = "https://api.minimax.io/v1";
|
|
|
11
11
|
const CN_URL = "https://api.minimaxi.com/v1";
|
|
12
12
|
const MODELS = {
|
|
13
13
|
default: "MiniMax-M3",
|
|
14
|
+
// `custom: true` because the driver is access "custom": the picker opens
|
|
15
|
+
// custom-access engines in the pane that lists only custom-flagged models.
|
|
14
16
|
options: [
|
|
15
|
-
{ id: "MiniMax-M3", label: "MiniMax M3", contextWindow: 1_000_000 },
|
|
16
|
-
{ id: "MiniMax-M2.7", label: "MiniMax M2.7", contextWindow: 204_800 },
|
|
17
|
-
{ id: "MiniMax-M2.7-highspeed", label: "MiniMax M2.7 Highspeed", contextWindow: 204_800 },
|
|
17
|
+
{ id: "MiniMax-M3", label: "MiniMax M3", contextWindow: 1_000_000, custom: true },
|
|
18
|
+
{ id: "MiniMax-M2.7", label: "MiniMax M2.7", contextWindow: 204_800, custom: true },
|
|
19
|
+
{ id: "MiniMax-M2.7-highspeed", label: "MiniMax M2.7 Highspeed", contextWindow: 204_800, custom: true },
|
|
18
20
|
],
|
|
19
21
|
};
|
|
20
22
|
const localConfigSchema = z.object({
|
|
@@ -45,9 +47,13 @@ export function loadLocalMiniMaxConfig(home = homedir()) {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
export function decodeMinimaxConfig(raw) {
|
|
50
|
+
const tools = (raw ?? {}).tools;
|
|
51
|
+
if (tools !== undefined && typeof tools !== "boolean")
|
|
52
|
+
throw new Error("tools must be a boolean");
|
|
48
53
|
const parsed = driverConfigSchema.safeParse(raw ?? {});
|
|
49
54
|
const config = parsed.success ? parsed.data : {};
|
|
50
55
|
return {
|
|
56
|
+
...(tools !== undefined ? { tools } : {}),
|
|
51
57
|
url: normalizedApiUrl(config.url?.trim() || process.env.MINIMAX_BASE_URL?.trim() || DEFAULT_URL),
|
|
52
58
|
};
|
|
53
59
|
}
|
|
@@ -83,6 +89,7 @@ export const MinimaxDriver = {
|
|
|
83
89
|
driverKind: DRIVER_KIND,
|
|
84
90
|
apiKey,
|
|
85
91
|
apiUrl,
|
|
92
|
+
tools: input.config.tools,
|
|
86
93
|
models: () => models,
|
|
87
94
|
requestBody: (model, messages, stream) => ({
|
|
88
95
|
model,
|
|
@@ -95,6 +102,7 @@ export const MinimaxDriver = {
|
|
|
95
102
|
missingKeyError: `no MiniMax key — set ${API_KEY_ENV} or run mmx auth login --api-key …`,
|
|
96
103
|
unavailableReason: `no MiniMax API key — run mmx auth login --api-key … or set ${API_KEY_ENV}`,
|
|
97
104
|
timeoutMs: 180_000,
|
|
105
|
+
reasoning: true,
|
|
98
106
|
billing: "metered",
|
|
99
107
|
includeUsageInCompleted: true,
|
|
100
108
|
noBodyError: "MiniMax returned no response body",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const MAX_CHAT_TOOL_CALLS = 32;
|
|
2
|
+
const MAX_ARGUMENT_CHARS = 256_000;
|
|
3
|
+
export class ChatProtocolError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export function object(value) {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Opaque provider reasoning blocks must keep their original order/signatures.
|
|
10
|
+
* OpenRouter streams the sequence as arrays of blocks, not tool-call deltas. */
|
|
11
|
+
export class ChatReasoningDetails {
|
|
12
|
+
blocks = [];
|
|
13
|
+
chars = 0;
|
|
14
|
+
add(value) {
|
|
15
|
+
if (value === undefined || value === null)
|
|
16
|
+
return;
|
|
17
|
+
if (!Array.isArray(value) || value.some((block) => !object(block))) {
|
|
18
|
+
throw new ChatProtocolError("provider returned invalid reasoning details");
|
|
19
|
+
}
|
|
20
|
+
this.chars += JSON.stringify(value).length;
|
|
21
|
+
if (this.chars > 256_000)
|
|
22
|
+
throw new ChatProtocolError("provider reasoning details exceeded the size limit");
|
|
23
|
+
this.blocks.push(...value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** One accumulator per completion: indexes identify deltas; IDs correlate results. */
|
|
27
|
+
export class ChatToolCalls {
|
|
28
|
+
calls = new Map();
|
|
29
|
+
argumentChars = 0;
|
|
30
|
+
add(value, streaming) {
|
|
31
|
+
if (value === undefined || value === null)
|
|
32
|
+
return;
|
|
33
|
+
if (!Array.isArray(value))
|
|
34
|
+
throw new ChatProtocolError("provider returned invalid tool_calls");
|
|
35
|
+
for (const [position, raw] of value.entries()) {
|
|
36
|
+
const delta = object(raw);
|
|
37
|
+
const index = streaming ? delta?.index : position;
|
|
38
|
+
if (!delta || !Number.isInteger(index) || Number(index) < 0 || Number(index) >= MAX_CHAT_TOOL_CALLS) {
|
|
39
|
+
throw new ChatProtocolError("provider returned an invalid tool-call index");
|
|
40
|
+
}
|
|
41
|
+
const call = this.calls.get(Number(index)) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
|
|
42
|
+
if (delta.type !== undefined && delta.type !== "function")
|
|
43
|
+
throw new ChatProtocolError("unsupported tool-call type");
|
|
44
|
+
if (delta.id !== undefined) {
|
|
45
|
+
if (typeof delta.id !== "string" || !delta.id || delta.id.length > 256 || (call.id && call.id !== delta.id)) {
|
|
46
|
+
throw new ChatProtocolError("provider changed or omitted a tool-call ID");
|
|
47
|
+
}
|
|
48
|
+
call.id = delta.id;
|
|
49
|
+
}
|
|
50
|
+
if (delta.function !== undefined) {
|
|
51
|
+
const fn = object(delta.function);
|
|
52
|
+
if (!fn)
|
|
53
|
+
throw new ChatProtocolError("provider returned an invalid tool-call function");
|
|
54
|
+
for (const field of ["name", "arguments"]) {
|
|
55
|
+
if (fn[field] === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
if (typeof fn[field] !== "string")
|
|
58
|
+
throw new ChatProtocolError("provider returned invalid tool-call arguments or name");
|
|
59
|
+
call.function[field] += fn[field];
|
|
60
|
+
if (field === "arguments")
|
|
61
|
+
this.argumentChars += fn[field].length;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (call.function.name.length > 64 || this.argumentChars > MAX_ARGUMENT_CHARS) {
|
|
65
|
+
throw new ChatProtocolError("provider tool calls exceeded the size limit");
|
|
66
|
+
}
|
|
67
|
+
this.calls.set(Number(index), call);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finish(reason, malformedFrame) {
|
|
71
|
+
const calls = [...this.calls.entries()].sort(([a], [b]) => a - b).map(([, call]) => call);
|
|
72
|
+
if (malformedFrame)
|
|
73
|
+
throw new ChatProtocolError("provider stream contained a malformed completion frame");
|
|
74
|
+
if (reason === "function_call")
|
|
75
|
+
throw new ChatProtocolError("legacy function_call is unsupported; use structured tool_calls");
|
|
76
|
+
if (reason === "tool_calls" && !calls.length)
|
|
77
|
+
throw new ChatProtocolError("provider ended for tool calls without a complete call");
|
|
78
|
+
if (!calls.length)
|
|
79
|
+
return calls;
|
|
80
|
+
if (reason !== null && reason !== "tool_calls" && reason !== "stop") {
|
|
81
|
+
throw new ChatProtocolError("provider tool calls were truncated or malformed");
|
|
82
|
+
}
|
|
83
|
+
const ids = new Set();
|
|
84
|
+
for (const call of calls) {
|
|
85
|
+
if (!call.id || ids.has(call.id) || !/^[A-Za-z0-9_-]{1,64}$/.test(call.function.name)) {
|
|
86
|
+
throw new ChatProtocolError("provider returned an incomplete or duplicate tool call");
|
|
87
|
+
}
|
|
88
|
+
ids.add(call.id);
|
|
89
|
+
}
|
|
90
|
+
return calls;
|
|
91
|
+
}
|
|
92
|
+
}
|