@lazyingart/agintiflow 0.20.5 → 0.20.6
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/docs/model-selection.md +1 -1
- package/package.json +1 -1
- package/references/model-routing-provider-design.md +2 -2
- package/references/venice-model-reference.md +1 -1
- package/scripts/smoke-model-roles.js +14 -2
- package/src/agent-runner.js +30 -1
- package/src/interactive-cli.js +25 -9
- package/src/model-client.js +155 -19
package/docs/model-selection.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a web-first coding agent and CLI with DeepSeek routing, sandboxed tools, model providers, canvas artifacts, and optional wrappers.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -53,7 +53,7 @@ Interactive equivalents:
|
|
|
53
53
|
/auxiliary model grsai/nano-banana-2
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
`/venice` opens a route/main selector for Venice text models. The selector includes `venice/venice-uncensored-1-2` (Venice 1.2), `venice/
|
|
56
|
+
`/venice` opens a route/main selector for Venice text models. The selector includes `venice/venice-uncensored-1-2` (Venice 1.2), `venice/venice-uncensored` (Venice 1.1), and `venice/gemma-4-uncensored` (Gemma 4). In non-interactive shells, `/venice` keeps script compatibility by selecting `venice/venice-uncensored-1-2` for both roles. `/venice 1.2 gemma` sets route to Venice 1.2 and main to Gemma 4; `/venice off` restores `deepseek/deepseek-v4-flash` for route and `deepseek/deepseek-v4-pro` for main.
|
|
57
57
|
|
|
58
58
|
The web UI should expose model names as dropdowns, not free-text fields. The left panel should stay focused on common daily controls, while model-role editing and less-used switches live in an Advanced settings modal. The terminal-like capability panels belong after the runtime log so the left control panel remains short.
|
|
59
59
|
|
|
@@ -74,7 +74,7 @@ Codex wrapper defaults stay separate from native OpenAI API settings: GPT-5.5 me
|
|
|
74
74
|
|
|
75
75
|
| UI bucket | Concrete default | Notes |
|
|
76
76
|
|---|---|---|
|
|
77
|
-
| `venice-uncensored` | `venice-uncensored-1-2` | Venice-native text; `/venice` also exposes `
|
|
77
|
+
| `venice-uncensored` | `venice-uncensored-1-2` | Venice-native text; `/venice` also exposes `venice-uncensored` as Venice 1.1 |
|
|
78
78
|
| `venice-qwen` | `qwen3-6-27b` | Qwen-family text/code |
|
|
79
79
|
| `venice-gpt` | `openai-gpt-55` | OpenAI-family through Venice |
|
|
80
80
|
| `venice-claude` | `claude-sonnet-4-6` | Claude-family through Venice |
|
|
@@ -52,7 +52,7 @@ Store `VENICE_API_KEY` only in ignored local files such as `.aginti/.env` or a s
|
|
|
52
52
|
| `openai-gpt-52` | 256K | GPT-5.2 |
|
|
53
53
|
| `venice-uncensored` | 32K | Venice Uncensored legacy / deprecated |
|
|
54
54
|
|
|
55
|
-
AgInTiFlow exposes the primary Venice text choices through `/venice`: Venice 1.2 (`venice-uncensored-1-2`), Venice 1.1 (`
|
|
55
|
+
AgInTiFlow exposes the primary Venice text choices through `/venice`: Venice 1.2 (`venice-uncensored-1-2`), Venice 1.1 (`venice-uncensored`), and Gemma 4 (`gemma-4-uncensored`). The command can select route and main independently, so a fast Venice route can be paired with a larger Gemma main model when useful. The E2EE 1.1 ID (`e2ee-venice-uncensored-24b-p`) is documented for reference, but the selector uses the working non-E2EE 1.1 route because the E2EE route currently returns an upstream provider error in live tests.
|
|
56
56
|
|
|
57
57
|
## Image And Edit Models
|
|
58
58
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
modelsForProviderGroup,
|
|
10
10
|
selectModelRoute,
|
|
11
11
|
} from "../src/model-routing.js";
|
|
12
|
-
import { parseTextToolCalls } from "../src/model-client.js";
|
|
12
|
+
import { parseTextToolCalls, usesTextToolProtocol } from "../src/model-client.js";
|
|
13
13
|
|
|
14
14
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
15
|
|
|
@@ -115,6 +115,18 @@ const parsedTextToolCalls = parseTextToolCalls('[TOOL_CALLS]list_files[ARGS]call
|
|
|
115
115
|
assert(parsedTextToolCalls.length === 1, "Venice text tool-call parser did not detect encoded tool call");
|
|
116
116
|
assert(parsedTextToolCalls[0].function.name === "list_files", "Venice text tool-call parser returned wrong tool name");
|
|
117
117
|
assert(parsedTextToolCalls[0].function.arguments.includes('"maxDepth":1'), "Venice text tool-call parser returned wrong arguments");
|
|
118
|
+
const looseTextToolCalls = parseTextToolCalls('[TOOL_CALLS]list_files[ARGS]{"path":"."}[TOOL_CALLS]inspect_project[ARGS]{"path":"."}');
|
|
119
|
+
assert(looseTextToolCalls.length === 2, "Venice loose text tool-call parser did not detect multiple calls");
|
|
120
|
+
assert(looseTextToolCalls[1].function.name === "inspect_project", "Venice loose text tool-call parser returned wrong second tool");
|
|
121
|
+
const nativeMarkerText = parseTextToolCalls('Done. <|tool_call>call:finish{result:<|"|>Done<|"|>}');
|
|
122
|
+
assert(nativeMarkerText.length === 0, "native marker text should not be treated as JSON text tool call");
|
|
123
|
+
const jsonBlockToolCalls = parseTextToolCalls('TOOL_CALLS:\n```json\n[{"name":"list_files","arguments":{"path":"/workspace"}}]\n```');
|
|
124
|
+
assert(jsonBlockToolCalls.length === 1, "Venice JSON text tool-call parser did not detect JSON block calls");
|
|
125
|
+
assert(jsonBlockToolCalls[0].function.arguments.includes("/workspace"), "Venice JSON text tool-call parser returned wrong arguments");
|
|
126
|
+
assert(usesTextToolProtocol({ provider: "venice", model: "gemma-4-uncensored" }), "Venice Gemma should use text tool protocol");
|
|
127
|
+
assert(usesTextToolProtocol({ provider: "venice", model: "e2ee-venice-uncensored-24b-p" }), "Venice 1.1 should use text tool protocol");
|
|
128
|
+
assert(usesTextToolProtocol({ provider: "venice", model: "venice-uncensored" }), "Venice legacy 1.1 should use text tool protocol");
|
|
129
|
+
assert(!usesTextToolProtocol({ provider: "venice", model: "venice-uncensored-1-2" }), "Venice 1.2 should keep native tool calls first");
|
|
118
130
|
|
|
119
131
|
const output = await runCli(["models"]);
|
|
120
132
|
assert(output.includes("/route") && output.includes("/spare") && output.includes("venice-gpt"), "aginti models output missing role details");
|
|
@@ -124,7 +136,7 @@ assert(interactiveOutput.includes("venice=on"), "/venice did not enable Venice r
|
|
|
124
136
|
assert(interactiveOutput.includes("route=venice/venice-uncensored-1-2"), "/venice did not set Venice route role");
|
|
125
137
|
assert(interactiveOutput.includes("main=venice/venice-uncensored-1-2"), "/venice did not set Venice main role");
|
|
126
138
|
const interactiveGemmaOutput = await runInteractive("/venice 1.1 gemma\n");
|
|
127
|
-
assert(interactiveGemmaOutput.includes("route=venice/
|
|
139
|
+
assert(interactiveGemmaOutput.includes("route=venice/venice-uncensored"), "/venice 1.1 did not set Venice 1.1 route role");
|
|
128
140
|
assert(interactiveGemmaOutput.includes("main=venice/gemma-4-uncensored"), "/venice gemma did not set Gemma 4 main role");
|
|
129
141
|
const interactiveOffOutput = await runInteractive("/venice off\n");
|
|
130
142
|
assert(interactiveOffOutput.includes("venice=off"), "/venice off did not restore DeepSeek roles");
|
package/src/agent-runner.js
CHANGED
|
@@ -739,7 +739,7 @@ async function buildSnapshot(browserState, store, step, config) {
|
|
|
739
739
|
|
|
740
740
|
async function injectQueuedUserMessages(store, state, observers) {
|
|
741
741
|
const inbox = await store.drainInbox();
|
|
742
|
-
if (inbox.length === 0) return;
|
|
742
|
+
if (inbox.length === 0) return 0;
|
|
743
743
|
|
|
744
744
|
for (const item of inbox) {
|
|
745
745
|
const content = String(item.content || "").trim();
|
|
@@ -762,6 +762,7 @@ async function injectQueuedUserMessages(store, state, observers) {
|
|
|
762
762
|
priority: item.priority || "normal",
|
|
763
763
|
});
|
|
764
764
|
}
|
|
765
|
+
return inbox.length;
|
|
765
766
|
}
|
|
766
767
|
|
|
767
768
|
async function executeTool(browserState, toolCall, snapshot, config, store, observers, state) {
|
|
@@ -1374,6 +1375,16 @@ export async function runAgent(config) {
|
|
|
1374
1375
|
});
|
|
1375
1376
|
|
|
1376
1377
|
throwIfAborted(config);
|
|
1378
|
+
await store.appendEvent("model.requested", {
|
|
1379
|
+
step,
|
|
1380
|
+
provider: config.provider,
|
|
1381
|
+
model: config.model,
|
|
1382
|
+
});
|
|
1383
|
+
observers.event("model.requested", {
|
|
1384
|
+
step,
|
|
1385
|
+
provider: config.provider,
|
|
1386
|
+
model: config.model,
|
|
1387
|
+
});
|
|
1377
1388
|
const response = await requestNextStep(client, config, state.messages);
|
|
1378
1389
|
const assistantMessage = response.choices[0]?.message;
|
|
1379
1390
|
if (!assistantMessage) {
|
|
@@ -1399,6 +1410,13 @@ export async function runAgent(config) {
|
|
|
1399
1410
|
const toolCalls = assistantMessage.tool_calls || [];
|
|
1400
1411
|
|
|
1401
1412
|
if (toolCalls.length === 0) {
|
|
1413
|
+
const queuedCount = await injectQueuedUserMessages(store, state, observers);
|
|
1414
|
+
if (queuedCount > 0) {
|
|
1415
|
+
state.stepsCompleted = step;
|
|
1416
|
+
state.updatedAt = new Date().toISOString();
|
|
1417
|
+
await store.saveState(state);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1402
1420
|
const fallback = assistantMessage.content?.trim() || "No tool call returned.";
|
|
1403
1421
|
appendChatEntry(state, "assistant", fallback);
|
|
1404
1422
|
await store.appendEvent("session.finished", {
|
|
@@ -1419,6 +1437,7 @@ export async function runAgent(config) {
|
|
|
1419
1437
|
};
|
|
1420
1438
|
}
|
|
1421
1439
|
|
|
1440
|
+
let continueForQueuedInput = false;
|
|
1422
1441
|
for (const toolCall of toolCalls) {
|
|
1423
1442
|
throwIfAborted(config);
|
|
1424
1443
|
const toolResult = await executeTool(browserState, toolCall, snapshot, config, store, observers, state);
|
|
@@ -1462,6 +1481,14 @@ export async function runAgent(config) {
|
|
|
1462
1481
|
}
|
|
1463
1482
|
|
|
1464
1483
|
if (toolResult.done) {
|
|
1484
|
+
const queuedCount = await injectQueuedUserMessages(store, state, observers);
|
|
1485
|
+
if (queuedCount > 0) {
|
|
1486
|
+
state.stepsCompleted = step;
|
|
1487
|
+
state.updatedAt = new Date().toISOString();
|
|
1488
|
+
await store.saveState(state);
|
|
1489
|
+
continueForQueuedInput = true;
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1465
1492
|
state.stepsCompleted = step;
|
|
1466
1493
|
state.updatedAt = new Date().toISOString();
|
|
1467
1494
|
state.meta.lastUrl = browserState.page?.url() || state.meta.lastUrl;
|
|
@@ -1487,6 +1514,8 @@ export async function runAgent(config) {
|
|
|
1487
1514
|
}
|
|
1488
1515
|
}
|
|
1489
1516
|
|
|
1517
|
+
if (continueForQueuedInput) continue;
|
|
1518
|
+
|
|
1490
1519
|
await injectQueuedUserMessages(store, state, observers);
|
|
1491
1520
|
|
|
1492
1521
|
state.stepsCompleted = step;
|
package/src/interactive-cli.js
CHANGED
|
@@ -1650,9 +1650,9 @@ function veniceTextModelChoices() {
|
|
|
1650
1650
|
},
|
|
1651
1651
|
{
|
|
1652
1652
|
provider: "venice",
|
|
1653
|
-
model: "
|
|
1653
|
+
model: "venice-uncensored",
|
|
1654
1654
|
label: "Venice 1.1",
|
|
1655
|
-
description: "
|
|
1655
|
+
description: "legacy Venice text model; 32K context",
|
|
1656
1656
|
},
|
|
1657
1657
|
{
|
|
1658
1658
|
provider: "venice",
|
|
@@ -1670,14 +1670,16 @@ function resolveVeniceTextModel(value = "") {
|
|
|
1670
1670
|
}
|
|
1671
1671
|
if (
|
|
1672
1672
|
normalized === "1.1" ||
|
|
1673
|
-
normalized === "venice-1.1"
|
|
1674
|
-
normalized === "e2ee-venice-uncensored-24b-p"
|
|
1673
|
+
normalized === "venice-1.1"
|
|
1675
1674
|
) {
|
|
1676
|
-
return "
|
|
1675
|
+
return "venice-uncensored";
|
|
1677
1676
|
}
|
|
1678
1677
|
if (normalized === "legacy" || normalized === "venice-uncensored") {
|
|
1679
1678
|
return "venice-uncensored";
|
|
1680
1679
|
}
|
|
1680
|
+
if (normalized === "e2ee" || normalized === "e2ee-venice-uncensored-24b-p") {
|
|
1681
|
+
return "e2ee-venice-uncensored-24b-p";
|
|
1682
|
+
}
|
|
1681
1683
|
if (normalized === "gemma" || normalized === "gemma4" || normalized === "gemma-4" || normalized === "gemma-4-uncensored") {
|
|
1682
1684
|
return "gemma-4-uncensored";
|
|
1683
1685
|
}
|
|
@@ -1781,9 +1783,9 @@ function modelRoleChoices(role = "main") {
|
|
|
1781
1783
|
},
|
|
1782
1784
|
{
|
|
1783
1785
|
provider: "venice",
|
|
1784
|
-
model: "
|
|
1786
|
+
model: "venice-uncensored",
|
|
1785
1787
|
label: "Venice 1.1",
|
|
1786
|
-
description: "
|
|
1788
|
+
description: "legacy Venice text route; use /auth venice if missing",
|
|
1787
1789
|
route: true,
|
|
1788
1790
|
main: true,
|
|
1789
1791
|
spare: true,
|
|
@@ -1930,8 +1932,11 @@ function clearSelector(lineCount) {
|
|
|
1930
1932
|
}
|
|
1931
1933
|
}
|
|
1932
1934
|
|
|
1935
|
+
function clearSelectorSequence(lineCount) {
|
|
1936
|
+
return Array.from({ length: Math.max(lineCount, 0) }, () => "\x1b[1A\r\x1b[2K").join("");
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1933
1939
|
function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0 }) {
|
|
1934
|
-
if (lineCount > 0) clearSelector(lineCount);
|
|
1935
1940
|
const width = Math.min(Math.max(terminalWidth() - 2, 60), 110);
|
|
1936
1941
|
const bodyWidth = width - 4;
|
|
1937
1942
|
const safeTitle = compactLine(title, bodyWidth);
|
|
@@ -1948,7 +1953,7 @@ function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0
|
|
|
1948
1953
|
}),
|
|
1949
1954
|
`╰${"─".repeat(width - 2)}╯`,
|
|
1950
1955
|
];
|
|
1951
|
-
output.write(`${rows.join("\n")}\n`);
|
|
1956
|
+
output.write(`${lineCount > 0 ? clearSelectorSequence(lineCount) : ""}${rows.join("\n")}\n`);
|
|
1952
1957
|
return rows.length;
|
|
1953
1958
|
}
|
|
1954
1959
|
|
|
@@ -2639,6 +2644,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
2639
2644
|
printSystemLine(`status=running workingOn=${state.activeGoal}`);
|
|
2640
2645
|
}
|
|
2641
2646
|
let result;
|
|
2647
|
+
let runError = null;
|
|
2642
2648
|
let queuedAfterFinish = [];
|
|
2643
2649
|
try {
|
|
2644
2650
|
result = await runAgent({
|
|
@@ -2666,6 +2672,8 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
2666
2672
|
onEvent: (type, data = {}) => {
|
|
2667
2673
|
if (type === "plan.created") {
|
|
2668
2674
|
printStatusEvent(state, "planned");
|
|
2675
|
+
} else if (type === "model.requested") {
|
|
2676
|
+
printStatusEvent(state, "model_wait", `${data.provider || "model"}/${data.model || ""}`);
|
|
2669
2677
|
} else if (type === "tool.started") {
|
|
2670
2678
|
printStatusEvent(state, "tool", data.toolName || "unknown");
|
|
2671
2679
|
} else if (type === "tool.completed") {
|
|
@@ -2688,10 +2696,18 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
2688
2696
|
}
|
|
2689
2697
|
},
|
|
2690
2698
|
});
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
runError = error;
|
|
2691
2701
|
} finally {
|
|
2692
2702
|
detachInterrupts();
|
|
2693
2703
|
queuedAfterFinish = await liveInput.stop();
|
|
2694
2704
|
}
|
|
2705
|
+
if (runError) {
|
|
2706
|
+
state.status = isAbortError(runError) ? "stopped" : "failed";
|
|
2707
|
+
state.activeGoal = "";
|
|
2708
|
+
printSystemLine(`status=${state.status} session=${state.sessionId}`);
|
|
2709
|
+
throw runError;
|
|
2710
|
+
}
|
|
2695
2711
|
state.sessionId = result.sessionId || state.sessionId;
|
|
2696
2712
|
state.status = result.stopped ? "stopped" : "idle";
|
|
2697
2713
|
state.activeGoal = "";
|
package/src/model-client.js
CHANGED
|
@@ -73,16 +73,110 @@ function toolChoiceForProvider(config, messages = []) {
|
|
|
73
73
|
return messages.some((message) => message.role === "tool") ? "auto" : "required";
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export function usesTextToolProtocol(config = {}) {
|
|
77
|
+
if (config.provider !== "venice") return false;
|
|
78
|
+
const model = String(config.model || "").toLowerCase();
|
|
79
|
+
return model === "gemma-4-uncensored" || model === "e2ee-venice-uncensored-24b-p" || model === "venice-uncensored";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function shouldRetryWithTextToolProtocol(error, config = {}) {
|
|
83
|
+
if (config.provider !== "venice") return false;
|
|
84
|
+
const message = [
|
|
85
|
+
error?.message,
|
|
86
|
+
error?.error?.message,
|
|
87
|
+
error?.response?.data?.error?.message,
|
|
88
|
+
error?.response?.data?.message,
|
|
89
|
+
]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(" ");
|
|
92
|
+
return /invalid request parameters|tool_choice|parallel_tool_calls|tools/i.test(message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function textToolProtocolPrompt(tools = []) {
|
|
96
|
+
const toolLines = tools.map((tool) => {
|
|
97
|
+
const fn = tool.function || {};
|
|
98
|
+
const properties = fn.parameters?.properties ? Object.keys(fn.parameters.properties).slice(0, 8) : [];
|
|
99
|
+
const required = Array.isArray(fn.parameters?.required) ? fn.parameters.required : [];
|
|
100
|
+
const args = properties.length > 0 ? ` args=${properties.join(",")}${required.length ? ` required=${required.join(",")}` : ""}` : "";
|
|
101
|
+
return `- ${fn.name}: ${String(fn.description || "").slice(0, 180)}${args}`;
|
|
102
|
+
});
|
|
103
|
+
return [
|
|
104
|
+
"This provider/model may not accept native OpenAI function-call parameters.",
|
|
105
|
+
"Use this text tool protocol when you need a tool:",
|
|
106
|
+
'[TOOL_CALLS]tool_name[ARGS]{"arg":"value"}',
|
|
107
|
+
'A strict id form is also accepted: [TOOL_CALLS]tool_name[ARGS]call_short_id[ARGS]{"arg":"value"}',
|
|
108
|
+
'A JSON block form is accepted too: TOOL_CALLS: ```json [{"name":"tool_name","arguments":{"arg":"value"}}] ```',
|
|
109
|
+
"Return only one or more TOOL_CALLS blocks when calling tools; do not wrap them in markdown.",
|
|
110
|
+
"If no tool is needed, answer normally.",
|
|
111
|
+
"Available text tools:",
|
|
112
|
+
...toolLines,
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function messagesWithTextToolProtocol(config, messages, tools) {
|
|
117
|
+
const prepared = prepareMessages(config, messages).map((message) => {
|
|
118
|
+
if (message.role === "tool") {
|
|
119
|
+
return {
|
|
120
|
+
role: "user",
|
|
121
|
+
content: `Tool result for ${message.tool_call_id || "previous tool"}:\n${message.content || ""}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
125
|
+
return {
|
|
126
|
+
role: "assistant",
|
|
127
|
+
content:
|
|
128
|
+
message.content ||
|
|
129
|
+
`Requested tools: ${message.tool_calls
|
|
130
|
+
.map((call) => `${call.function?.name || "tool"}(${call.function?.arguments || "{}"})`)
|
|
131
|
+
.join("; ")}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return message;
|
|
135
|
+
});
|
|
136
|
+
const protocol = { role: "system", content: textToolProtocolPrompt(tools) };
|
|
137
|
+
if (prepared[0]?.role === "system") return [prepared[0], protocol, ...prepared.slice(1)];
|
|
138
|
+
return [protocol, ...prepared];
|
|
139
|
+
}
|
|
140
|
+
|
|
76
141
|
export function parseTextToolCalls(content = "") {
|
|
77
142
|
const text = String(content || "");
|
|
78
|
-
if (!text.includes("[TOOL_CALLS]")) return [];
|
|
143
|
+
if (!text.includes("[TOOL_CALLS]") && !/TOOL_CALLS\s*:/i.test(text)) return [];
|
|
79
144
|
|
|
80
145
|
const calls = [];
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
146
|
+
const jsonBlock = text.match(/TOOL_CALLS\s*:\s*```(?:json)?\s*([\s\S]*?)```/i);
|
|
147
|
+
if (jsonBlock?.[1]) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(jsonBlock[1].trim());
|
|
150
|
+
if (Array.isArray(parsed)) {
|
|
151
|
+
for (const item of parsed) {
|
|
152
|
+
const name = String(item?.name || item?.tool || "").trim();
|
|
153
|
+
if (!name) continue;
|
|
154
|
+
const args = item?.arguments && typeof item.arguments === "object" ? item.arguments : {};
|
|
155
|
+
calls.push({
|
|
156
|
+
id: String(item?.id || `text-tool-${calls.length + 1}`),
|
|
157
|
+
type: "function",
|
|
158
|
+
function: {
|
|
159
|
+
name,
|
|
160
|
+
arguments: JSON.stringify(args),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Fall through to bracket parser below.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const chunk of text.split("[TOOL_CALLS]").slice(1)) {
|
|
171
|
+
const match = chunk.match(/^([A-Za-z0-9_-]+)\[ARGS\]([\s\S]*?)$/);
|
|
172
|
+
const name = match?.[1]?.trim();
|
|
173
|
+
let rawArgs = match?.[2]?.trim() || "{}";
|
|
174
|
+
let id = `text-tool-${calls.length + 1}`;
|
|
175
|
+
const strictParts = rawArgs.split("[ARGS]");
|
|
176
|
+
if (strictParts.length >= 2 && !rawArgs.startsWith("{") && !rawArgs.startsWith("[")) {
|
|
177
|
+
id = strictParts.shift()?.trim() || id;
|
|
178
|
+
rawArgs = strictParts.join("[ARGS]").trim() || "{}";
|
|
179
|
+
}
|
|
86
180
|
if (!name) continue;
|
|
87
181
|
try {
|
|
88
182
|
JSON.parse(rawArgs);
|
|
@@ -101,14 +195,39 @@ export function parseTextToolCalls(content = "") {
|
|
|
101
195
|
return calls;
|
|
102
196
|
}
|
|
103
197
|
|
|
198
|
+
function textBeforeToolCallMarker(content = "") {
|
|
199
|
+
return String(content || "")
|
|
200
|
+
.split("[TOOL_CALLS]")[0]
|
|
201
|
+
.split("TOOL_CALLS:")[0]
|
|
202
|
+
.split("<|tool_call>")[0]
|
|
203
|
+
.trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
104
206
|
function normalizeTextToolCallResponse(response) {
|
|
105
207
|
const message = response?.choices?.[0]?.message;
|
|
106
208
|
if (!message || Array.isArray(message.tool_calls) && message.tool_calls.length > 0) return response;
|
|
107
209
|
|
|
108
210
|
const calls = parseTextToolCalls(message.content || "");
|
|
109
|
-
if (calls.length === 0)
|
|
211
|
+
if (calls.length === 0) {
|
|
212
|
+
const cleanedContent = textBeforeToolCallMarker(message.content || "");
|
|
213
|
+
if (!cleanedContent || cleanedContent === message.content) return response;
|
|
214
|
+
return {
|
|
215
|
+
...response,
|
|
216
|
+
choices: response.choices.map((choice, index) =>
|
|
217
|
+
index === 0
|
|
218
|
+
? {
|
|
219
|
+
...choice,
|
|
220
|
+
message: {
|
|
221
|
+
...message,
|
|
222
|
+
content: cleanedContent,
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
: choice
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
110
229
|
|
|
111
|
-
const content =
|
|
230
|
+
const content = textBeforeToolCallMarker(message.content || "");
|
|
112
231
|
return {
|
|
113
232
|
...response,
|
|
114
233
|
choices: response.choices.map((choice, index) =>
|
|
@@ -919,16 +1038,33 @@ export async function requestNextStep(client, config, messages) {
|
|
|
919
1038
|
]);
|
|
920
1039
|
}
|
|
921
1040
|
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1041
|
+
const textToolProtocol = usesTextToolProtocol(config);
|
|
1042
|
+
const nativePayload = {
|
|
1043
|
+
model: config.model,
|
|
1044
|
+
temperature: 0,
|
|
1045
|
+
tool_choice: toolChoiceForProvider(config, messages),
|
|
1046
|
+
parallel_tool_calls: false,
|
|
1047
|
+
messages: prepareMessages(config, messages),
|
|
1048
|
+
tools,
|
|
1049
|
+
};
|
|
1050
|
+
const textPayload = {
|
|
1051
|
+
model: config.model,
|
|
1052
|
+
temperature: 0,
|
|
1053
|
+
messages: messagesWithTextToolProtocol(config, messages, tools),
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
let response;
|
|
1057
|
+
try {
|
|
1058
|
+
response = await client.chat.completions.create(
|
|
1059
|
+
textToolProtocol ? textPayload : nativePayload,
|
|
1060
|
+
requestOptions(config)
|
|
1061
|
+
);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (!textToolProtocol && shouldRetryWithTextToolProtocol(error, config)) {
|
|
1064
|
+
response = await client.chat.completions.create(textPayload, requestOptions(config));
|
|
1065
|
+
} else {
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
933
1069
|
return normalizeTextToolCallResponse(response);
|
|
934
1070
|
}
|