@lazyingart/agintiflow 0.20.4 → 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.
@@ -48,14 +48,17 @@ Interactive commands:
48
48
 
49
49
  In the interactive CLI, `/provider`, `/route`, `/model`, `/spare`, and `/auxiliary model` without arguments open selectors. Use Up/Down/Left/Right to move through choices, Enter to confirm, and Esc to cancel. Slash-command hints use the same arrow selection behavior: type a prefix such as `/mo`, use arrows to choose `/model` or `/models`, then press Enter or Tab.
50
50
 
51
- `/venice` is a toggle. When off, it enables:
51
+ `/venice` opens a two-step selector for the Venice route and main models. The current text choices are:
52
52
 
53
53
  ```text
54
- /route venice/venice-uncensored-1-2
55
- /main venice/venice-uncensored-1-2
54
+ venice/venice-uncensored-1-2
55
+ venice/venice-uncensored
56
+ venice/gemma-4-uncensored
56
57
  ```
57
58
 
58
- When already on, `/venice` switches back to the DeepSeek defaults:
59
+ For scripts or non-interactive terminals, `/venice` uses Venice 1.2 for both roles. You can also set both roles directly with `/venice 1.2`, `/venice 1.1`, or `/venice gemma`. Use two values to set route and main separately, for example `/venice 1.2 gemma`.
60
+
61
+ Use `/venice off` to switch back to the DeepSeek defaults:
59
62
 
60
63
  ```text
61
64
  /route deepseek/deepseek-v4-flash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.4",
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` is a toggle: it keeps smart routing enabled and points both route and main roles at `venice/venice-uncensored-1-2`; running it again or using `/venice off` restores `deepseek/deepseek-v4-flash` for route and `deepseek/deepseek-v4-pro` for main.
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 |
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 |
@@ -19,7 +19,7 @@ Store `VENICE_API_KEY` only in ignored local files such as `.aginti/.env` or a s
19
19
 
20
20
  | Bucket | Primary model | Other useful models | Use |
21
21
  |---|---|---|---|
22
- | `venice-uncensored` | `venice-uncensored-1-2` | `venice-uncensored`, `venice-uncensored-role-play` | Private/uncensored Venice-native text route |
22
+ | `venice-uncensored` | `venice-uncensored-1-2` | `e2ee-venice-uncensored-24b-p`, `venice-uncensored-role-play`, `venice-uncensored` legacy | Private/uncensored Venice-native text route |
23
23
  | `venice-gemma` | `gemma-4-uncensored` | `google-gemma-4-31b-it`, `google-gemma-4-26b-a4b-it` | Low-cost open-model writing and experimentation |
24
24
  | `venice-qwen` | `qwen3-6-27b` | `qwen-3-6-plus`, `qwen3-coder-480b-a35b-instruct-turbo`, `qwen3-vl-235b-a22b` | Chinese/open coding and general tasks |
25
25
  | `venice-gpt` | `openai-gpt-55` | `openai-gpt-54`, `openai-gpt-54-mini`, `openai-gpt-53-codex`, `openai-gpt-53-codex-spark` | OpenAI-family routes through Venice |
@@ -44,12 +44,15 @@ Store `VENICE_API_KEY` only in ignored local files such as `.aginti/.env` or a s
44
44
  | `google-gemma-4-26b-a4b-it` | 256K | Google Gemma 4 26B A4B Instruct |
45
45
  | `venice-uncensored-1-2` | 128K | Venice Uncensored 1.2 |
46
46
  | `openai-gpt-54-mini` | 400K | GPT-5.4 Mini |
47
+ | `e2ee-venice-uncensored-24b-p` | 32K | Venice Uncensored 1.1 E2EE |
47
48
  | `venice-uncensored-role-play` | 128K | Venice Role Play Uncensored |
48
49
  | `gemini-3-1-pro-preview` | 1.0M | Gemini 3.1 Pro Preview |
49
50
  | `claude-sonnet-4-6` | 1.0M | Claude Sonnet 4.6 |
50
51
  | `openai-gpt-53-codex` | 400K | GPT-5.3 Codex |
51
52
  | `openai-gpt-52` | 256K | GPT-5.2 |
52
- | `venice-uncensored` | 32K | Venice Uncensored legacy |
53
+ | `venice-uncensored` | 32K | Venice Uncensored legacy / deprecated |
54
+
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.
53
56
 
54
57
  ## Image And Edit Models
55
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
 
@@ -109,11 +109,24 @@ assert(fastRoute.model === "deepseek-v4-flash", "fast route did not use route mo
109
109
 
110
110
  assert(MODEL_PROVIDER_GROUPS["venice-gpt"].provider === "venice", "venice-gpt group missing");
111
111
  assert(modelsForProviderGroup("venice-gemma").some((item) => item.id === "gemma-4-uncensored"), "venice-gemma bucket missing Gemma");
112
+ assert(modelsForProviderGroup("venice-uncensored").some((item) => item.id === "e2ee-venice-uncensored-24b-p"), "venice-uncensored bucket missing Venice 1.1");
112
113
  assert(AUXILIARY_MODEL_CATALOG["venice-image"].some((item) => item.id === "gpt-image-2"), "Venice image catalog missing GPT Image 2");
113
114
  const parsedTextToolCalls = parseTextToolCalls('[TOOL_CALLS]list_files[ARGS]call_123[ARGS]{"path":".","maxDepth":1}');
114
115
  assert(parsedTextToolCalls.length === 1, "Venice text tool-call parser did not detect encoded tool call");
115
116
  assert(parsedTextToolCalls[0].function.name === "list_files", "Venice text tool-call parser returned wrong tool name");
116
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");
117
130
 
118
131
  const output = await runCli(["models"]);
119
132
  assert(output.includes("/route") && output.includes("/spare") && output.includes("venice-gpt"), "aginti models output missing role details");
@@ -122,6 +135,9 @@ const interactiveOutput = await runInteractive("/venice\n");
122
135
  assert(interactiveOutput.includes("venice=on"), "/venice did not enable Venice roles");
123
136
  assert(interactiveOutput.includes("route=venice/venice-uncensored-1-2"), "/venice did not set Venice route role");
124
137
  assert(interactiveOutput.includes("main=venice/venice-uncensored-1-2"), "/venice did not set Venice main role");
138
+ const interactiveGemmaOutput = await runInteractive("/venice 1.1 gemma\n");
139
+ assert(interactiveGemmaOutput.includes("route=venice/venice-uncensored"), "/venice 1.1 did not set Venice 1.1 route role");
140
+ assert(interactiveGemmaOutput.includes("main=venice/gemma-4-uncensored"), "/venice gemma did not set Gemma 4 main role");
125
141
  const interactiveOffOutput = await runInteractive("/venice off\n");
126
142
  assert(interactiveOffOutput.includes("venice=off"), "/venice off did not restore DeepSeek roles");
127
143
  assert(interactiveOffOutput.includes("route=deepseek/deepseek-v4-flash"), "/venice off did not restore DeepSeek route role");
@@ -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;
@@ -593,7 +593,7 @@ function printHelp() {
593
593
  " /instructions Show AGINTI.md project instructions status.",
594
594
  " /memory Alias for /instructions.",
595
595
  " /models Show route/main/spare/wrapper/auxiliary model roles.",
596
- " /venice [on|off] Toggle Venice Uncensored 1.2 for route and main roles.",
596
+ " /venice [off|model] Pick Venice route/main models, or restore DeepSeek defaults.",
597
597
  " /route [mode|provider/model]",
598
598
  " Open route selector, or set routing/fast route model.",
599
599
  " /model [provider/model] Open main-model selector, or set the active/main model.",
@@ -1640,23 +1640,61 @@ function useDeepSeekDefaults(state) {
1640
1640
  state.mainModel = "deepseek-v4-pro";
1641
1641
  }
1642
1642
 
1643
- function useVeniceDefaults(state) {
1643
+ function veniceTextModelChoices() {
1644
+ return [
1645
+ {
1646
+ provider: "venice",
1647
+ model: "venice-uncensored-1-2",
1648
+ label: "Venice 1.2",
1649
+ description: "default Venice text model; 128K context",
1650
+ },
1651
+ {
1652
+ provider: "venice",
1653
+ model: "venice-uncensored",
1654
+ label: "Venice 1.1",
1655
+ description: "legacy Venice text model; 32K context",
1656
+ },
1657
+ {
1658
+ provider: "venice",
1659
+ model: "gemma-4-uncensored",
1660
+ label: "Gemma 4",
1661
+ description: "Gemma-family Venice text model; 256K context",
1662
+ },
1663
+ ];
1664
+ }
1665
+
1666
+ function resolveVeniceTextModel(value = "") {
1667
+ const normalized = String(value).trim().toLowerCase();
1668
+ if (!normalized || normalized === "on" || normalized === "venice" || normalized === "1.2" || normalized === "venice-1.2") {
1669
+ return "venice-uncensored-1-2";
1670
+ }
1671
+ if (
1672
+ normalized === "1.1" ||
1673
+ normalized === "venice-1.1"
1674
+ ) {
1675
+ return "venice-uncensored";
1676
+ }
1677
+ if (normalized === "legacy" || normalized === "venice-uncensored") {
1678
+ return "venice-uncensored";
1679
+ }
1680
+ if (normalized === "e2ee" || normalized === "e2ee-venice-uncensored-24b-p") {
1681
+ return "e2ee-venice-uncensored-24b-p";
1682
+ }
1683
+ if (normalized === "gemma" || normalized === "gemma4" || normalized === "gemma-4" || normalized === "gemma-4-uncensored") {
1684
+ return "gemma-4-uncensored";
1685
+ }
1686
+ const exact = veniceTextModelChoices().find((item) => item.model.toLowerCase() === normalized);
1687
+ return exact?.model || "";
1688
+ }
1689
+
1690
+ function useVeniceModels(state, routeModel = "venice-uncensored-1-2", mainModel = routeModel) {
1644
1691
  state.routingMode = "smart";
1645
1692
  state.provider = "deepseek";
1646
1693
  state.model = "";
1647
1694
  state.routeProvider = "venice";
1648
- state.routeModel = "venice-uncensored-1-2";
1695
+ state.routeModel = routeModel;
1649
1696
  state.mainProvider = "venice";
1650
- state.mainModel = "venice-uncensored-1-2";
1651
- }
1652
-
1653
- function isVeniceDefaultActive(state) {
1654
- return (
1655
- state.routeProvider === "venice" &&
1656
- state.routeModel === "venice-uncensored-1-2" &&
1657
- state.mainProvider === "venice" &&
1658
- state.mainModel === "venice-uncensored-1-2"
1659
- );
1697
+ state.mainModel = mainModel;
1660
1698
  }
1661
1699
 
1662
1700
  function printModelRoles(state) {
@@ -1737,12 +1775,30 @@ function modelRoleChoices(role = "main") {
1737
1775
  {
1738
1776
  provider: "venice",
1739
1777
  model: "venice-uncensored-1-2",
1740
- label: "Venice Uncensored 1.2",
1778
+ label: "Venice 1.2",
1741
1779
  description: "Venice text route; use /auth venice if missing",
1742
1780
  route: true,
1743
1781
  main: true,
1744
1782
  spare: true,
1745
1783
  },
1784
+ {
1785
+ provider: "venice",
1786
+ model: "venice-uncensored",
1787
+ label: "Venice 1.1",
1788
+ description: "legacy Venice text route; use /auth venice if missing",
1789
+ route: true,
1790
+ main: true,
1791
+ spare: true,
1792
+ },
1793
+ {
1794
+ provider: "venice",
1795
+ model: "gemma-4-uncensored",
1796
+ label: "Gemma 4",
1797
+ description: "Gemma-family Venice route; use /auth venice if missing",
1798
+ route: true,
1799
+ main: true,
1800
+ spare: true,
1801
+ },
1746
1802
  {
1747
1803
  provider: "openai",
1748
1804
  model: "gpt-5.5",
@@ -1849,7 +1905,7 @@ function providerChoices() {
1849
1905
  provider: "venice",
1850
1906
  model: "venice-uncensored-1-2",
1851
1907
  label: "Venice",
1852
- description: "manual Venice route; /venice toggles route+main roles",
1908
+ description: "manual Venice route; /venice picks route+main roles",
1853
1909
  },
1854
1910
  {
1855
1911
  provider: "qwen",
@@ -1876,14 +1932,19 @@ function clearSelector(lineCount) {
1876
1932
  }
1877
1933
  }
1878
1934
 
1935
+ function clearSelectorSequence(lineCount) {
1936
+ return Array.from({ length: Math.max(lineCount, 0) }, () => "\x1b[1A\r\x1b[2K").join("");
1937
+ }
1938
+
1879
1939
  function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0 }) {
1880
- if (lineCount > 0) clearSelector(lineCount);
1881
1940
  const width = Math.min(Math.max(terminalWidth() - 2, 60), 110);
1882
1941
  const bodyWidth = width - 4;
1942
+ const safeTitle = compactLine(title, bodyWidth);
1943
+ const safeSubtitle = compactLine(subtitle, bodyWidth);
1883
1944
  const rows = [
1884
1945
  `╭${"─".repeat(width - 2)}╮`,
1885
- `│ ${padVisible(title, bodyWidth)} │`,
1886
- `│ ${padVisible(subtitle, bodyWidth)} │`,
1946
+ `│ ${padVisible(safeTitle, bodyWidth)} │`,
1947
+ `│ ${padVisible(safeSubtitle, bodyWidth)} │`,
1887
1948
  `├${"─".repeat(width - 2)}┤`,
1888
1949
  ...options.map((option, index) => {
1889
1950
  const marker = index === selectedIndex ? ">" : " ";
@@ -1892,7 +1953,7 @@ function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0
1892
1953
  }),
1893
1954
  `╰${"─".repeat(width - 2)}╯`,
1894
1955
  ];
1895
- output.write(`${rows.join("\n")}\n`);
1956
+ output.write(`${lineCount > 0 ? clearSelectorSequence(lineCount) : ""}${rows.join("\n")}\n`);
1896
1957
  return rows.length;
1897
1958
  }
1898
1959
 
@@ -2012,6 +2073,38 @@ async function pickModelRole(role, state) {
2012
2073
  return true;
2013
2074
  }
2014
2075
 
2076
+ async function pickVeniceRouteAndMain(state) {
2077
+ const options = veniceTextModelChoices();
2078
+ const routeIndex = Math.max(
2079
+ options.findIndex((item) => item.provider === state.routeProvider && item.model === state.routeModel),
2080
+ 0
2081
+ );
2082
+ const route = await selectModelChoice({
2083
+ title: "Select Venice route model",
2084
+ subtitle: "Route handles planning and short turns. Up/Down selects, Enter confirms, Esc cancels.",
2085
+ options,
2086
+ initialIndex: routeIndex,
2087
+ });
2088
+ if (!route) return false;
2089
+
2090
+ const mainIndex = Math.max(
2091
+ options.findIndex((item) => item.provider === state.mainProvider && item.model === state.mainModel),
2092
+ options.findIndex((item) => item.model === route.model),
2093
+ 0
2094
+ );
2095
+ const main = await selectModelChoice({
2096
+ title: "Select Venice main model",
2097
+ subtitle: "Main handles complex work. Up/Down selects, Enter confirms, Esc cancels.",
2098
+ options,
2099
+ initialIndex: mainIndex,
2100
+ });
2101
+ if (!main) return false;
2102
+
2103
+ useVeniceModels(state, route.model, main.model);
2104
+ printSystemLine(`venice=on routing=smart route=venice/${state.routeModel} main=venice/${state.mainModel}`);
2105
+ return true;
2106
+ }
2107
+
2015
2108
  async function pickProvider(state) {
2016
2109
  const options = providerChoices();
2017
2110
  const currentProvider = state.provider || "deepseek";
@@ -2266,19 +2359,45 @@ async function handleCommand(line, state, packageDir) {
2266
2359
  return true;
2267
2360
  }
2268
2361
  if (command === "venice") {
2269
- const action = value || (isVeniceDefaultActive(state) ? "off" : "on");
2362
+ const canUseSelector = input.isTTY && output.isTTY && typeof input.setRawMode === "function";
2363
+ const action = value || (canUseSelector ? "select" : "on");
2270
2364
  if (action === "off" || action === "deepseek" || action === "default") {
2271
2365
  useDeepSeekDefaults(state);
2272
2366
  printSystemLine("venice=off routing=smart route=deepseek/deepseek-v4-flash main=deepseek/deepseek-v4-pro");
2273
2367
  return true;
2274
2368
  }
2275
- if (action !== "on" && action !== "venice") {
2276
- printAgentMessage("Usage: /venice [on|off]. No argument toggles between Venice and DeepSeek defaults.");
2369
+ if (action === "select" && canUseSelector) {
2370
+ const changed = await pickVeniceRouteAndMain(state);
2371
+ if (!changed) {
2372
+ printSystemLine(`route=${state.routeProvider || "deepseek"}/${state.routeModel || "deepseek-v4-flash"} main=${state.mainProvider || "deepseek"}/${state.mainModel || "deepseek-v4-pro"}`);
2373
+ return true;
2374
+ }
2375
+ const keys = providerKeyStatus(process.cwd());
2376
+ if (!keys.venice) {
2377
+ printAgentMessage("Venice model roles are selected, but no Venice key is configured. Run `/auth venice` to save one.");
2378
+ }
2277
2379
  return true;
2278
2380
  }
2279
- useVeniceDefaults(state);
2381
+
2382
+ const parts = action.split(/\s+/).filter(Boolean);
2383
+ const routeModel = resolveVeniceTextModel(parts[0] || "on");
2384
+ const mainModel = resolveVeniceTextModel(parts.slice(1).join(" ") || parts[0] || "on");
2385
+ if (!routeModel || !mainModel) {
2386
+ printAgentMessage(
2387
+ [
2388
+ "Usage: /venice [off|1.2|1.1|gemma] [main-model]",
2389
+ "Examples:",
2390
+ " /venice",
2391
+ " /venice gemma",
2392
+ " /venice 1.2 gemma",
2393
+ " /venice off",
2394
+ ].join("\n")
2395
+ );
2396
+ return true;
2397
+ }
2398
+ useVeniceModels(state, routeModel, mainModel);
2280
2399
  const keys = providerKeyStatus(process.cwd());
2281
- printSystemLine("venice=on routing=smart route=venice/venice-uncensored-1-2 main=venice/venice-uncensored-1-2");
2400
+ printSystemLine(`venice=on routing=smart route=venice/${state.routeModel} main=venice/${state.mainModel}`);
2282
2401
  if (!keys.venice) {
2283
2402
  printAgentMessage("Venice model roles are selected, but no Venice key is configured. Run `/auth venice` to save one.");
2284
2403
  }
@@ -2525,6 +2644,7 @@ async function runPrompt(prompt, state, packageDir) {
2525
2644
  printSystemLine(`status=running workingOn=${state.activeGoal}`);
2526
2645
  }
2527
2646
  let result;
2647
+ let runError = null;
2528
2648
  let queuedAfterFinish = [];
2529
2649
  try {
2530
2650
  result = await runAgent({
@@ -2552,6 +2672,8 @@ async function runPrompt(prompt, state, packageDir) {
2552
2672
  onEvent: (type, data = {}) => {
2553
2673
  if (type === "plan.created") {
2554
2674
  printStatusEvent(state, "planned");
2675
+ } else if (type === "model.requested") {
2676
+ printStatusEvent(state, "model_wait", `${data.provider || "model"}/${data.model || ""}`);
2555
2677
  } else if (type === "tool.started") {
2556
2678
  printStatusEvent(state, "tool", data.toolName || "unknown");
2557
2679
  } else if (type === "tool.completed") {
@@ -2574,10 +2696,18 @@ async function runPrompt(prompt, state, packageDir) {
2574
2696
  }
2575
2697
  },
2576
2698
  });
2699
+ } catch (error) {
2700
+ runError = error;
2577
2701
  } finally {
2578
2702
  detachInterrupts();
2579
2703
  queuedAfterFinish = await liveInput.stop();
2580
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
+ }
2581
2711
  state.sessionId = result.sessionId || state.sessionId;
2582
2712
  state.status = result.stopped ? "stopped" : "idle";
2583
2713
  state.activeGoal = "";
@@ -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 pattern = /\[TOOL_CALLS\]([A-Za-z0-9_-]+)\[ARGS\]([A-Za-z0-9_.:-]+)\[ARGS\]([\s\S]*?)(?=\[TOOL_CALLS\]|$)/g;
82
- for (const match of text.matchAll(pattern)) {
83
- const name = match[1]?.trim();
84
- const id = match[2]?.trim() || `text-tool-${calls.length + 1}`;
85
- const rawArgs = match[3]?.trim() || "{}";
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) return response;
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 = String(message.content || "").split("[TOOL_CALLS]")[0].trim();
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 response = await client.chat.completions.create(
923
- {
924
- model: config.model,
925
- temperature: 0,
926
- tool_choice: toolChoiceForProvider(config, messages),
927
- parallel_tool_calls: false,
928
- messages: prepareMessages(config, messages),
929
- tools,
930
- },
931
- requestOptions(config)
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
  }
@@ -215,12 +215,19 @@ export const PROVIDER_MODEL_CATALOG = {
215
215
  context: "128K",
216
216
  description: "Current Venice uncensored text route.",
217
217
  },
218
+ {
219
+ id: "e2ee-venice-uncensored-24b-p",
220
+ label: "Venice Uncensored 1.1",
221
+ bucket: "venice-uncensored",
222
+ context: "32K",
223
+ description: "E2EE Venice uncensored 1.1 text route.",
224
+ },
218
225
  {
219
226
  id: "venice-uncensored",
220
- label: "Venice Uncensored",
227
+ label: "Venice Uncensored Legacy",
221
228
  bucket: "venice-uncensored",
222
229
  context: "32K",
223
- description: "Legacy Venice uncensored text route.",
230
+ description: "Deprecated legacy Venice uncensored text route.",
224
231
  },
225
232
  {
226
233
  id: "venice-uncensored-role-play",