@lazyingart/agintiflow 0.18.0 → 0.19.1

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/README.md CHANGED
@@ -77,6 +77,8 @@ Inside chat, type normal requests such as `write a small Python CLI app with tes
77
77
 
78
78
  For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSeek v4 pro by default and exposes `apply_patch` as a deterministic workspace tool. It supports exact replacements, Codex-style patch envelopes, and unified diffs, with preflight checks, path guardrails, hashes, and compact per-file diffs. See [docs/patch-tools.md](docs/patch-tools.md).
79
79
 
80
+ Model choice is role-based: `/route` defaults to DeepSeek V4 Flash, `/model` or `/main` defaults to DeepSeek V4 Pro, `/spare` defaults to OpenAI GPT-5.4 medium, `/wrapper` defaults to Codex GPT-5.5 medium when enabled, and `/auxilliary` defaults to GRS AI/Nano Banana for image tools. See [docs/model-selection.md](docs/model-selection.md) and run `aginti models`.
81
+
80
82
  For larger repositories, use `--profile large-codebase` or choose **Large codebase engineering** in the web UI. The web default stays **Auto**, and Auto now escalates codebase/system/debugging prompts to the same engineering loop when needed. Complex work routes to DeepSeek v4 pro, starts with `inspect_project`, then uses search/read/patch/check loops inspired by Codex, Copilot SDK, Claude Code, Gemini CLI, Qwen, and Claw Code. See [docs/large-codebase-engineering.md](docs/large-codebase-engineering.md).
81
83
 
82
84
  AgInTiFlow can also spend cheap DeepSeek calls on parallel scout notes before the main executor starts a complicated task. It first writes a bounded project map to `.aginti/codebase-map.json`, then runs scouts for architecture, implementation, review, research, context mapping, tests, git workflow, integration, symbol tracing, and dependency risks. A coordinator Swarm Board is injected for the main agent and saved as `artifacts/scout-blackboard.json` in the session. The executor still does the real file/shell/browser work itself. Disable with `--no-parallel-scouts` or set `--scout-count 1..10`.
@@ -0,0 +1,79 @@
1
+ # Model Selection
2
+
3
+ AgInTiFlow treats model choice as a role-based control plane. A provider supplies models; a role decides how the agent uses one.
4
+
5
+ ## Default Roles
6
+
7
+ | Role | CLI command | Default | Purpose |
8
+ | --- | --- | --- | --- |
9
+ | Route | `/route` | `deepseek/deepseek-v4-flash` | Fast planner, triage, short tasks, and routing decisions. |
10
+ | Main | `/model` or `/main` | `deepseek/deepseek-v4-pro` | Complex executor for coding, debugging, writing, and long tasks. |
11
+ | Spare | `/spare` | `openai/gpt-5.4` with `medium` reasoning | Optional fallback or cross-check model. |
12
+ | Wrapper | `/wrapper` | `codex gpt-5.5 medium` | External coding assistant when wrapper tools are enabled. |
13
+ | Auxiliary | `/auxilliary` | `grsai/nano-banana-2` | Image/media tools; Venice image models are optional. |
14
+
15
+ Smart routing still works as before: normal work goes to the route model, and complex work goes to the main model. Manual provider/model selection remains available for one-off runs.
16
+
17
+ ## CLI Commands
18
+
19
+ ```bash
20
+ aginti models
21
+ aginti --list-models
22
+ aginti --list-routes
23
+
24
+ # one-shot overrides
25
+ aginti --route-model deepseek-v4-flash --main-model deepseek-v4-pro "fix this project"
26
+ aginti --provider venice --model venice-uncensored-1-2 --routing manual "draft a note"
27
+ aginti --spare-provider openai --spare-model gpt-5.4 --spare-reasoning medium "review this design"
28
+ aginti --allow-wrappers --wrapper codex --wrapper-model gpt-5.5 "patch this bug"
29
+ aginti --image --aux-provider venice --aux-model gpt-image-2 "generate a logo"
30
+ ```
31
+
32
+ Interactive commands:
33
+
34
+ ```text
35
+ /models
36
+ /venice
37
+ /route deepseek/deepseek-v4-flash
38
+ /model deepseek/deepseek-v4-pro
39
+ /spare openai/gpt-5.4 medium
40
+ /wrapper codex gpt-5.5 medium
41
+ /auxilliary model grsai/nano-banana-2
42
+ ```
43
+
44
+ `/venice` is a shortcut for:
45
+
46
+ ```text
47
+ /route venice/venice-uncensored-1-2
48
+ /main venice/venice-uncensored-1-2
49
+ ```
50
+
51
+ It keeps smart routing enabled, so normal and complex tasks still use the same route/main policy, but both roles resolve to Venice Uncensored 1.2. If the Venice key is missing, run `/auth venice`.
52
+
53
+ ## Provider Buckets
54
+
55
+ | Bucket | Provider | Typical use |
56
+ | --- | --- | --- |
57
+ | `deepseek` | DeepSeek | Default route/main because V4 Flash and V4 Pro are cheap and strong. |
58
+ | `openai` | OpenAI | Spare/frontier checks, Codex-family work, and explicit manual routes. |
59
+ | `qwen` | Qwen | Chinese and general-purpose OpenAI-compatible tasks. |
60
+ | `venice-uncensored` | Venice | Venice-native uncensored text models. |
61
+ | `venice-gpt` | Venice | GPT-family models through Venice. |
62
+ | `venice-claude` | Venice | Claude-family models through Venice. |
63
+ | `venice-gemma` | Venice | Gemma-family models through Venice. |
64
+ | `venice-qwen` | Venice | Qwen-family models through Venice. |
65
+ | `venice-image` | Venice | Image generation/editing such as Nano Banana, GPT Image, Wan, Qwen Image. |
66
+ | `grsai` | GRS AI | Auxiliary image generation only. |
67
+
68
+ ## Keys
69
+
70
+ Keys are set per project and stored only in ignored `.aginti/.env`:
71
+
72
+ ```bash
73
+ aginti auth
74
+ aginti auth venice
75
+ aginti keys status
76
+ printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
77
+ ```
78
+
79
+ The web UI mirrors the same roles in its **Model roles** panel. Use the top provider/model fields for manual runs; use the role fields to change how smart routing chooses route, main, spare, wrapper, and auxiliary models.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
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",
@@ -52,6 +52,7 @@
52
52
  "scripts/smoke-coding-tools.js",
53
53
  "scripts/smoke-capabilities.js",
54
54
  "scripts/smoke-inbox.js",
55
+ "scripts/smoke-model-roles.js",
55
56
  "scripts/smoke-platform.js",
56
57
  "scripts/smoke-skills.js",
57
58
  "scripts/smoke-tmux-tools.js",
@@ -78,11 +79,12 @@
78
79
  "smoke:skills": "node scripts/smoke-skills.js",
79
80
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
80
81
  "smoke:inbox": "node scripts/smoke-inbox.js",
82
+ "smoke:model-roles": "node scripts/smoke-model-roles.js",
81
83
  "smoke:platform": "node scripts/smoke-platform.js",
82
84
  "smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
83
85
  "smoke:web-api": "node scripts/smoke-web-api.js",
84
86
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
85
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:capabilities && npm run smoke:platform && npm run smoke:skills && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
87
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:skills && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
86
88
  "pack:dry-run": "npm pack --dry-run",
87
89
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
88
90
  },
package/public/app.js CHANGED
@@ -651,7 +651,19 @@ const providerField = document.querySelector("#provider");
651
651
  const modelField = document.querySelector("#model");
652
652
  const modelOptionsEl = document.querySelector("#model-options");
653
653
  const modelCatalogEl = document.querySelector("#model-catalog");
654
+ const modelRoleGridEl = document.querySelector("#model-role-grid");
654
655
  const modelRoutePillEl = document.querySelector("#model-route-pill");
656
+ const routeProviderField = document.querySelector("#routeProvider");
657
+ const routeModelField = document.querySelector("#routeModel");
658
+ const mainProviderField = document.querySelector("#mainProvider");
659
+ const mainModelField = document.querySelector("#mainModel");
660
+ const spareProviderField = document.querySelector("#spareProvider");
661
+ const spareModelField = document.querySelector("#spareModel");
662
+ const spareReasoningField = document.querySelector("#spareReasoning");
663
+ const wrapperModelField = document.querySelector("#wrapperModel");
664
+ const wrapperReasoningField = document.querySelector("#wrapperReasoning");
665
+ const auxiliaryProviderField = document.querySelector("#auxiliaryProvider");
666
+ const auxiliaryModelField = document.querySelector("#auxiliaryModel");
655
667
  const routingHintEl = document.querySelector("#routing-hint");
656
668
  const modelRouteStatusEl = document.querySelector("#model-route-status");
657
669
  const projectStatusEl = document.querySelector("#project-status");
@@ -728,6 +740,9 @@ const defaults = {
728
740
  let currentLanguage = "en";
729
741
  let routingPresets = {};
730
742
  let modelCatalog = {};
743
+ let modelRoles = {};
744
+ let modelGroups = {};
745
+ let auxiliaryModelCatalog = {};
731
746
  let taskProfiles = [];
732
747
  let projectInfo = null;
733
748
  let currentSessionId = "";
@@ -970,6 +985,50 @@ function providerModelOptions(provider = providerField.value) {
970
985
  return modelCatalog[provider] || [];
971
986
  }
972
987
 
988
+ function renderModelRoles() {
989
+ if (!modelRoleGridEl) return;
990
+ const roles = {
991
+ route: {
992
+ ...(modelRoles.route || {}),
993
+ provider: routeProviderField?.value || modelRoles.route?.provider || "deepseek",
994
+ model: routeModelField?.value || modelRoles.route?.model || "deepseek-v4-flash",
995
+ },
996
+ main: {
997
+ ...(modelRoles.main || {}),
998
+ provider: mainProviderField?.value || modelRoles.main?.provider || "deepseek",
999
+ model: mainModelField?.value || modelRoles.main?.model || "deepseek-v4-pro",
1000
+ },
1001
+ spare: {
1002
+ ...(modelRoles.spare || {}),
1003
+ provider: spareProviderField?.value || modelRoles.spare?.provider || "openai",
1004
+ model: spareModelField?.value || modelRoles.spare?.model || "gpt-5.4",
1005
+ reasoning: spareReasoningField?.value || modelRoles.spare?.reasoning || "medium",
1006
+ },
1007
+ wrapper: {
1008
+ ...(modelRoles.wrapper || {}),
1009
+ provider: preferredWrapperField?.value || modelRoles.wrapper?.provider || "codex",
1010
+ model: wrapperModelField?.value || modelRoles.wrapper?.model || "gpt-5.5",
1011
+ reasoning: wrapperReasoningField?.value || modelRoles.wrapper?.reasoning || "medium",
1012
+ },
1013
+ auxiliary: {
1014
+ ...(modelRoles.auxiliary || {}),
1015
+ provider: auxiliaryProviderField?.value || modelRoles.auxiliary?.provider || "grsai",
1016
+ model: auxiliaryModelField?.value || modelRoles.auxiliary?.model || "nano-banana-2",
1017
+ },
1018
+ };
1019
+ modelRoleGridEl.innerHTML = Object.values(roles)
1020
+ .map((role) => {
1021
+ const reasoning = role.reasoning ? ` · ${role.reasoning}` : "";
1022
+ return `
1023
+ <div class="model-role-card">
1024
+ <strong>${escapeHtml(role.label || role.id)}</strong>
1025
+ <span>${escapeHtml(`${role.provider}/${role.model}${reasoning}`)}</span>
1026
+ </div>
1027
+ `;
1028
+ })
1029
+ .join("");
1030
+ }
1031
+
973
1032
  function renderModelOptions() {
974
1033
  const provider = providerField.value || "deepseek";
975
1034
  const options = providerModelOptions(provider);
@@ -981,11 +1040,12 @@ function renderModelOptions() {
981
1040
  if (modelRoutePillEl) {
982
1041
  const mode = routingModeField.value || "smart";
983
1042
  const primary = modelField.value.trim() || defaults[provider] || "";
984
- const secondary = provider === "deepseek" ? defaults.deepseek : routingPresets.complex?.model || "deepseek-v4-pro";
1043
+ const routeLabel = `${routeProviderField?.value || "deepseek"}/${routeModelField?.value || "deepseek-v4-flash"}`;
1044
+ const mainLabel = `${mainProviderField?.value || "deepseek"}/${mainModelField?.value || "deepseek-v4-pro"}`;
985
1045
  modelRoutePillEl.textContent =
986
1046
  mode === "manual"
987
1047
  ? `${provider} · ${primary}`
988
- : `smart · primary ${primary || "auto"} · secondary ${secondary || "auto"}`;
1048
+ : `${mode} · route ${routeLabel} · main ${mainLabel}`;
989
1049
  }
990
1050
  if (!modelCatalogEl) return;
991
1051
  if (options.length === 0) {
@@ -1002,6 +1062,7 @@ function renderModelOptions() {
1002
1062
  `
1003
1063
  )
1004
1064
  .join("");
1065
+ renderModelRoles();
1005
1066
  }
1006
1067
 
1007
1068
  function updateRoutingHint() {
@@ -1015,7 +1076,16 @@ function updateRoutingHint() {
1015
1076
  routingHintEl.textContent = t(hintKey);
1016
1077
 
1017
1078
  if (mode !== "manual" && providerField.value === "deepseek") {
1018
- const preset = mode === "complex" ? routingPresets.complex : routingPresets.fast;
1079
+ const preset =
1080
+ mode === "complex"
1081
+ ? {
1082
+ provider: mainProviderField?.value || routingPresets.complex?.provider,
1083
+ model: mainModelField?.value || routingPresets.complex?.model,
1084
+ }
1085
+ : {
1086
+ provider: routeProviderField?.value || routingPresets.fast?.provider,
1087
+ model: routeModelField?.value || routingPresets.fast?.model,
1088
+ };
1019
1089
  if (preset) {
1020
1090
  providerField.value = preset.provider === "deepseek" ? "deepseek" : providerField.value;
1021
1091
  modelField.value = preset.model || modelField.value;
@@ -1083,6 +1153,17 @@ function formPayload() {
1083
1153
  routingMode: routingModeField.value,
1084
1154
  provider: providerField.value,
1085
1155
  model: modelField.value.trim(),
1156
+ routeProvider: routeProviderField?.value || "deepseek",
1157
+ routeModel: routeModelField?.value.trim() || "deepseek-v4-flash",
1158
+ mainProvider: mainProviderField?.value || "deepseek",
1159
+ mainModel: mainModelField?.value.trim() || "deepseek-v4-pro",
1160
+ spareProvider: spareProviderField?.value || "openai",
1161
+ spareModel: spareModelField?.value.trim() || "gpt-5.4",
1162
+ spareReasoning: spareReasoningField?.value || "medium",
1163
+ wrapperModel: wrapperModelField?.value.trim() || "gpt-5.5",
1164
+ wrapperReasoning: wrapperReasoningField?.value || "medium",
1165
+ auxiliaryProvider: auxiliaryProviderField?.value || "grsai",
1166
+ auxiliaryModel: auxiliaryModelField?.value.trim() || "nano-banana-2",
1086
1167
  startUrl: document.querySelector("#startUrl").value.trim(),
1087
1168
  allowedDomains: document.querySelector("#allowedDomains").value.trim(),
1088
1169
  commandCwd: document.querySelector("#commandCwd").value.trim(),
@@ -2366,6 +2447,18 @@ modelCatalogEl?.addEventListener("click", (event) => {
2366
2447
  });
2367
2448
 
2368
2449
  modelField.addEventListener("input", updateRoutingHint);
2450
+ [routeProviderField, routeModelField, mainProviderField, mainModelField, spareProviderField, spareModelField, spareReasoningField, wrapperModelField, wrapperReasoningField, auxiliaryProviderField, auxiliaryModelField]
2451
+ .filter(Boolean)
2452
+ .forEach((field) => {
2453
+ field.addEventListener("input", () => {
2454
+ renderModelRoles();
2455
+ schedulePreferenceSave();
2456
+ });
2457
+ field.addEventListener("change", () => {
2458
+ renderModelRoles();
2459
+ schedulePreferenceSave();
2460
+ });
2461
+ });
2369
2462
  sandboxModeField.addEventListener("change", updatePackageWarning);
2370
2463
  packageInstallPolicyField.addEventListener("change", updatePackageWarning);
2371
2464
  allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
@@ -2590,6 +2683,9 @@ async function loadConfig() {
2590
2683
  const prefs = data.preferences || {};
2591
2684
  routingPresets = data.routing?.presets || {};
2592
2685
  modelCatalog = data.modelCatalog || {};
2686
+ modelRoles = data.modelRoles || {};
2687
+ modelGroups = data.modelGroups || {};
2688
+ auxiliaryModelCatalog = data.auxiliaryModelCatalog || {};
2593
2689
  taskProfiles = data.taskProfiles || [];
2594
2690
  projectInfo = data.project || null;
2595
2691
  defaults.openai = data.defaults?.openai?.model || defaults.openai;
@@ -2603,6 +2699,17 @@ async function loadConfig() {
2603
2699
  routingModeField.value = prefs.routingMode || "smart";
2604
2700
  providerField.value = prefs.provider || "deepseek";
2605
2701
  modelField.value = prefs.model || defaults[providerField.value] || "deepseek-v4-flash";
2702
+ if (routeProviderField) routeProviderField.value = prefs.routeProvider || modelRoles.route?.provider || "deepseek";
2703
+ if (routeModelField) routeModelField.value = prefs.routeModel || modelRoles.route?.model || "deepseek-v4-flash";
2704
+ if (mainProviderField) mainProviderField.value = prefs.mainProvider || modelRoles.main?.provider || "deepseek";
2705
+ if (mainModelField) mainModelField.value = prefs.mainModel || modelRoles.main?.model || "deepseek-v4-pro";
2706
+ if (spareProviderField) spareProviderField.value = prefs.spareProvider || modelRoles.spare?.provider || "openai";
2707
+ if (spareModelField) spareModelField.value = prefs.spareModel || modelRoles.spare?.model || "gpt-5.4";
2708
+ if (spareReasoningField) spareReasoningField.value = prefs.spareReasoning || modelRoles.spare?.reasoning || "medium";
2709
+ if (wrapperModelField) wrapperModelField.value = prefs.wrapperModel || modelRoles.wrapper?.model || "gpt-5.5";
2710
+ if (wrapperReasoningField) wrapperReasoningField.value = prefs.wrapperReasoning || modelRoles.wrapper?.reasoning || "medium";
2711
+ if (auxiliaryProviderField) auxiliaryProviderField.value = prefs.auxiliaryProvider || modelRoles.auxiliary?.provider || "grsai";
2712
+ if (auxiliaryModelField) auxiliaryModelField.value = prefs.auxiliaryModel || modelRoles.auxiliary?.model || "nano-banana-2";
2606
2713
  renderTaskProfiles(prefs.taskProfile || "auto");
2607
2714
  document.querySelector("#startUrl").value = prefs.startUrl || "";
2608
2715
  document.querySelector("#allowedDomains").value = prefs.allowedDomains || "";
package/public/index.html CHANGED
@@ -118,9 +118,74 @@
118
118
  <span>Route</span>
119
119
  <strong id="model-route-pill">DeepSeek flash/pro</strong>
120
120
  </div>
121
+ <div id="model-role-grid" class="model-role-grid"></div>
121
122
  <div id="model-catalog" class="model-catalog"></div>
122
123
  </section>
123
124
 
125
+ <section class="model-role-editor" aria-label="Model role settings">
126
+ <div class="model-role-editor-head">
127
+ <strong>Model roles</strong>
128
+ <span>Route, main, spare, wrapper, auxiliary</span>
129
+ </div>
130
+ <div class="role-editor-grid">
131
+ <label>
132
+ <span>Route</span>
133
+ <select id="routeProvider" name="routeProvider">
134
+ <option value="deepseek">DeepSeek</option>
135
+ <option value="openai">OpenAI</option>
136
+ <option value="qwen">Qwen</option>
137
+ <option value="venice">Venice</option>
138
+ <option value="mock">Mock</option>
139
+ </select>
140
+ <input id="routeModel" name="routeModel" type="text" placeholder="deepseek-v4-flash" />
141
+ </label>
142
+ <label>
143
+ <span>Main</span>
144
+ <select id="mainProvider" name="mainProvider">
145
+ <option value="deepseek">DeepSeek</option>
146
+ <option value="openai">OpenAI</option>
147
+ <option value="qwen">Qwen</option>
148
+ <option value="venice">Venice</option>
149
+ </select>
150
+ <input id="mainModel" name="mainModel" type="text" placeholder="deepseek-v4-pro" />
151
+ </label>
152
+ <label>
153
+ <span>Spare</span>
154
+ <select id="spareProvider" name="spareProvider">
155
+ <option value="openai">OpenAI</option>
156
+ <option value="deepseek">DeepSeek</option>
157
+ <option value="qwen">Qwen</option>
158
+ <option value="venice">Venice</option>
159
+ </select>
160
+ <input id="spareModel" name="spareModel" type="text" placeholder="gpt-5.4" />
161
+ <select id="spareReasoning" name="spareReasoning">
162
+ <option value="low">low</option>
163
+ <option value="medium">medium</option>
164
+ <option value="high">high</option>
165
+ <option value="xhigh">xhigh</option>
166
+ </select>
167
+ </label>
168
+ <label>
169
+ <span>Wrapper</span>
170
+ <input id="wrapperModel" name="wrapperModel" type="text" placeholder="gpt-5.5" />
171
+ <select id="wrapperReasoning" name="wrapperReasoning">
172
+ <option value="low">low</option>
173
+ <option value="medium">medium</option>
174
+ <option value="high">high</option>
175
+ <option value="xhigh">xhigh</option>
176
+ </select>
177
+ </label>
178
+ <label>
179
+ <span>Auxiliary</span>
180
+ <select id="auxiliaryProvider" name="auxiliaryProvider">
181
+ <option value="grsai">GRS AI</option>
182
+ <option value="venice">Venice image</option>
183
+ </select>
184
+ <input id="auxiliaryModel" name="auxiliaryModel" type="text" placeholder="nano-banana-2" />
185
+ </label>
186
+ </div>
187
+ </section>
188
+
124
189
  <label>
125
190
  <span data-i18n="taskProfileLabel">Task profile</span>
126
191
  <select id="taskProfile" name="taskProfile">
package/public/styles.css CHANGED
@@ -157,6 +157,82 @@ h1 {
157
157
  gap: 8px;
158
158
  }
159
159
 
160
+ .model-role-grid {
161
+ display: grid;
162
+ gap: 8px;
163
+ grid-template-columns: repeat(2, minmax(0, 1fr));
164
+ }
165
+
166
+ .model-role-card {
167
+ min-width: 0;
168
+ padding: 10px;
169
+ border: 1px solid rgba(15, 118, 110, 0.18);
170
+ border-radius: 14px;
171
+ background: rgba(255, 255, 255, 0.72);
172
+ }
173
+
174
+ .model-role-card strong,
175
+ .model-role-card span {
176
+ display: block;
177
+ overflow: hidden;
178
+ text-overflow: ellipsis;
179
+ white-space: nowrap;
180
+ }
181
+
182
+ .model-role-card strong {
183
+ color: #0f766e;
184
+ font-size: 0.84rem;
185
+ }
186
+
187
+ .model-role-card span {
188
+ color: var(--muted);
189
+ font-size: 0.76rem;
190
+ }
191
+
192
+ .model-role-editor {
193
+ display: grid;
194
+ gap: 10px;
195
+ margin-top: -2px;
196
+ padding: 12px;
197
+ border: 1px solid rgba(29, 78, 216, 0.14);
198
+ border-radius: 16px;
199
+ background:
200
+ linear-gradient(135deg, rgba(219, 234, 254, 0.62), rgba(255, 251, 235, 0.82)),
201
+ rgba(255, 255, 255, 0.82);
202
+ }
203
+
204
+ .model-role-editor-head {
205
+ display: flex;
206
+ justify-content: space-between;
207
+ gap: 10px;
208
+ color: var(--muted);
209
+ font-size: 0.78rem;
210
+ }
211
+
212
+ .model-role-editor-head strong {
213
+ color: #1d4ed8;
214
+ text-transform: uppercase;
215
+ letter-spacing: 0.08em;
216
+ }
217
+
218
+ .role-editor-grid {
219
+ display: grid;
220
+ gap: 10px;
221
+ }
222
+
223
+ .role-editor-grid label {
224
+ display: grid;
225
+ gap: 6px;
226
+ }
227
+
228
+ .role-editor-grid label > span {
229
+ color: var(--muted);
230
+ font-size: 0.74rem;
231
+ font-weight: 800;
232
+ letter-spacing: 0.07em;
233
+ text-transform: uppercase;
234
+ }
235
+
160
236
  .model-chip {
161
237
  display: grid;
162
238
  gap: 2px;
@@ -14,13 +14,43 @@ DeepSeek remains the default smart-route backend:
14
14
 
15
15
  If the user selects OpenAI, Qwen, or Venice as the provider, AgInTiFlow treats that as the primary manual provider even when the UI still shows a smart policy. This makes provider selection predictable while preserving DeepSeek as the default.
16
16
 
17
- ## Provider Sections
17
+ ## Role-Based Model Sections
18
18
 
19
- The web UI should show three ideas clearly:
19
+ The UI should be role-first rather than provider-first:
20
20
 
21
- - **Route:** smart, fast, complex, or manual.
22
- - **Primary:** the provider/model currently used for the next run.
23
- - **Secondary:** the fallback/escalation route, usually DeepSeek V4 Pro or a wrapper such as Codex.
21
+ - **Route model (`/route`):** fast planner and triage model. Default `deepseek/deepseek-v4-flash`.
22
+ - **Main model (`/model` or `/main`):** complex executor. Default `deepseek/deepseek-v4-pro`.
23
+ - **Spare model (`/spare`):** fallback/cross-check model. Default `openai/gpt-5.4` with `medium` reasoning.
24
+ - **Wrapper (`/wrapper`):** external coding assistant. Default Codex with `gpt-5.5` medium reasoning, disabled unless wrapper tools are enabled.
25
+ - **Auxiliary (`/auxilliary`):** media/image tools. Default `grsai/nano-banana-2`; Venice image models are optional.
26
+
27
+ This keeps the mental model stable: providers are supply, roles are policy.
28
+
29
+ ## Current Commands
30
+
31
+ ```bash
32
+ aginti models
33
+ aginti --list-models
34
+ aginti --list-routes
35
+ aginti --route-model deepseek-v4-flash --main-model deepseek-v4-pro "fix this project"
36
+ aginti --spare-provider openai --spare-model gpt-5.4 --spare-reasoning medium "review this patch"
37
+ aginti --allow-wrappers --wrapper codex --wrapper-model gpt-5.5 "use Codex as a helper"
38
+ aginti --image --aux-provider venice --aux-model gpt-image-2 "generate an image"
39
+ ```
40
+
41
+ Interactive equivalents:
42
+
43
+ ```text
44
+ /models
45
+ /venice
46
+ /route deepseek/deepseek-v4-flash
47
+ /model deepseek/deepseek-v4-pro
48
+ /spare openai/gpt-5.4 medium
49
+ /wrapper codex gpt-5.5 medium
50
+ /auxilliary model grsai/nano-banana-2
51
+ ```
52
+
53
+ `/venice` keeps smart routing enabled and points both route and main roles at `venice/venice-uncensored-1-2`.
24
54
 
25
55
  ## OpenAI Model Reference
26
56
 
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ AUXILIARY_MODEL_CATALOG,
7
+ MODEL_PROVIDER_GROUPS,
8
+ getModelRoleDefaults,
9
+ modelsForProviderGroup,
10
+ selectModelRoute,
11
+ } from "../src/model-routing.js";
12
+
13
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+
15
+ function assert(condition, message) {
16
+ if (!condition) throw new Error(message);
17
+ }
18
+
19
+ function runCli(args) {
20
+ return new Promise((resolve, reject) => {
21
+ const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), ...args], {
22
+ cwd: repoRoot,
23
+ stdio: ["ignore", "pipe", "pipe"],
24
+ env: { ...process.env },
25
+ });
26
+ let stdout = "";
27
+ let stderr = "";
28
+ const timer = setTimeout(() => {
29
+ child.kill("SIGTERM");
30
+ reject(new Error("model role CLI smoke timed out"));
31
+ }, 12000);
32
+ child.stdout.on("data", (chunk) => {
33
+ stdout += String(chunk);
34
+ });
35
+ child.stderr.on("data", (chunk) => {
36
+ stderr += String(chunk);
37
+ });
38
+ child.on("error", (error) => {
39
+ clearTimeout(timer);
40
+ reject(error);
41
+ });
42
+ child.on("close", (code) => {
43
+ clearTimeout(timer);
44
+ if (code === 0) resolve(stdout);
45
+ else reject(new Error(`model role CLI smoke failed ${code}\n${stdout}\n${stderr}`));
46
+ });
47
+ });
48
+ }
49
+
50
+ function runInteractive(input) {
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "chat"], {
53
+ cwd: repoRoot,
54
+ stdio: ["pipe", "pipe", "pipe"],
55
+ env: {
56
+ ...process.env,
57
+ DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY || "test-deepseek-key-not-real",
58
+ VENICE_API_KEY: process.env.VENICE_API_KEY || "test-venice-key-not-real",
59
+ AGINTIFLOW_NO_COLOR: "1",
60
+ },
61
+ });
62
+ let stdout = "";
63
+ let stderr = "";
64
+ const timer = setTimeout(() => {
65
+ child.kill("SIGTERM");
66
+ reject(new Error("model role interactive smoke timed out"));
67
+ }, 12000);
68
+ child.stdout.on("data", (chunk) => {
69
+ stdout += String(chunk);
70
+ });
71
+ child.stderr.on("data", (chunk) => {
72
+ stderr += String(chunk);
73
+ });
74
+ child.on("error", (error) => {
75
+ clearTimeout(timer);
76
+ reject(error);
77
+ });
78
+ child.on("close", (code) => {
79
+ clearTimeout(timer);
80
+ if (code === 0) resolve(stdout);
81
+ else reject(new Error(`model role interactive smoke failed ${code}\n${stdout}\n${stderr}`));
82
+ });
83
+ child.stdin.end(input);
84
+ });
85
+ }
86
+
87
+ const roles = getModelRoleDefaults();
88
+ assert(roles.route.provider === "deepseek", "route provider default should be deepseek");
89
+ assert(roles.route.model === "deepseek-v4-flash", "route model default should be deepseek-v4-flash");
90
+ assert(roles.main.model === "deepseek-v4-pro", "main model default should be deepseek-v4-pro");
91
+ assert(roles.spare.provider === "openai" && roles.spare.model === "gpt-5.4", "spare model default should be OpenAI GPT-5.4");
92
+ assert(roles.wrapper.provider === "codex" && roles.wrapper.model === "gpt-5.5", "wrapper default should be Codex GPT-5.5");
93
+ assert(roles.auxiliary.provider === "grsai" && roles.auxiliary.model === "nano-banana-2", "auxiliary default should be GRS AI Nano Banana");
94
+
95
+ const complexRoute = selectModelRoute({
96
+ routingMode: "complex",
97
+ provider: "deepseek",
98
+ mainModel: "deepseek-v4-pro",
99
+ });
100
+ assert(complexRoute.model === "deepseek-v4-pro", "complex route did not use main model override");
101
+
102
+ const fastRoute = selectModelRoute({
103
+ routingMode: "fast",
104
+ provider: "deepseek",
105
+ routeModel: "deepseek-v4-flash",
106
+ });
107
+ assert(fastRoute.model === "deepseek-v4-flash", "fast route did not use route model override");
108
+
109
+ assert(MODEL_PROVIDER_GROUPS["venice-gpt"].provider === "venice", "venice-gpt group missing");
110
+ assert(modelsForProviderGroup("venice-gemma").some((item) => item.id === "gemma-4-uncensored"), "venice-gemma bucket missing Gemma");
111
+ assert(AUXILIARY_MODEL_CATALOG["venice-image"].some((item) => item.id === "gpt-image-2"), "Venice image catalog missing GPT Image 2");
112
+
113
+ const output = await runCli(["models"]);
114
+ assert(output.includes("/route") && output.includes("/spare") && output.includes("venice-gpt"), "aginti models output missing role details");
115
+
116
+ const interactiveOutput = await runInteractive("/venice\n/status\n/exit\n");
117
+ assert(interactiveOutput.includes("route=venice/venice-uncensored-1-2"), "/venice did not set Venice route role");
118
+ assert(interactiveOutput.includes("main=venice/venice-uncensored-1-2"), "/venice did not set Venice main role");
119
+
120
+ console.log(
121
+ JSON.stringify(
122
+ {
123
+ ok: true,
124
+ checks: ["role-defaults", "route-overrides", "provider-groups", "auxiliary-catalog", "cli-models-command", "venice-shortcut"],
125
+ },
126
+ null,
127
+ 2
128
+ )
129
+ );
@@ -87,6 +87,12 @@ try {
87
87
  if (!config.modelCatalog?.venice?.some((model) => model.id === "venice-uncensored-1-2")) {
88
88
  throw new Error("venice model catalog is not advertised by /api/config");
89
89
  }
90
+ if (config.modelRoles?.route?.model !== "deepseek-v4-flash" || config.modelRoles?.main?.model !== "deepseek-v4-pro") {
91
+ throw new Error("model role defaults are not advertised by /api/config");
92
+ }
93
+ if (!config.modelGroups?.["venice-gpt"] || !config.auxiliaryModelCatalog?.["venice-image"]) {
94
+ throw new Error("model provider groups are not advertised by /api/config");
95
+ }
90
96
 
91
97
  const keyStatus = await fetchJson("/api/keys/status");
92
98
  if (typeof keyStatus.keyStatus?.deepseek !== "boolean") throw new Error("key status endpoint is invalid");
@@ -149,6 +155,12 @@ try {
149
155
  provider: "mock",
150
156
  routingMode: "manual",
151
157
  model: "mock-agent",
158
+ routeModel: "deepseek-v4-flash",
159
+ mainModel: "deepseek-v4-pro",
160
+ spareProvider: "openai",
161
+ spareModel: "gpt-5.4",
162
+ auxiliaryProvider: "grsai",
163
+ auxiliaryModel: "nano-banana-2",
152
164
  goal: "Report the current working directory with a safe command.",
153
165
  commandCwd: runtimeDir,
154
166
  sandboxMode: "host",