@lazyingart/agintiflow 0.18.0 → 0.19.0

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,70 @@
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
+ /route deepseek/deepseek-v4-flash
37
+ /model deepseek/deepseek-v4-pro
38
+ /spare openai/gpt-5.4 medium
39
+ /wrapper codex gpt-5.5 medium
40
+ /auxilliary model grsai/nano-banana-2
41
+ ```
42
+
43
+ ## Provider Buckets
44
+
45
+ | Bucket | Provider | Typical use |
46
+ | --- | --- | --- |
47
+ | `deepseek` | DeepSeek | Default route/main because V4 Flash and V4 Pro are cheap and strong. |
48
+ | `openai` | OpenAI | Spare/frontier checks, Codex-family work, and explicit manual routes. |
49
+ | `qwen` | Qwen | Chinese and general-purpose OpenAI-compatible tasks. |
50
+ | `venice-uncensored` | Venice | Venice-native uncensored text models. |
51
+ | `venice-gpt` | Venice | GPT-family models through Venice. |
52
+ | `venice-claude` | Venice | Claude-family models through Venice. |
53
+ | `venice-gemma` | Venice | Gemma-family models through Venice. |
54
+ | `venice-qwen` | Venice | Qwen-family models through Venice. |
55
+ | `venice-image` | Venice | Image generation/editing such as Nano Banana, GPT Image, Wan, Qwen Image. |
56
+ | `grsai` | GRS AI | Auxiliary image generation only. |
57
+
58
+ ## Keys
59
+
60
+ Keys are set per project and stored only in ignored `.aginti/.env`:
61
+
62
+ ```bash
63
+ aginti auth
64
+ aginti auth venice
65
+ aginti keys status
66
+ printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
67
+ ```
68
+
69
+ 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.
70
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
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,40 @@ 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
+ /route deepseek/deepseek-v4-flash
46
+ /model deepseek/deepseek-v4-pro
47
+ /spare openai/gpt-5.4 medium
48
+ /wrapper codex gpt-5.5 medium
49
+ /auxilliary model grsai/nano-banana-2
50
+ ```
24
51
 
25
52
  ## OpenAI Model Reference
26
53
 
@@ -0,0 +1,88 @@
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
+ const roles = getModelRoleDefaults();
51
+ assert(roles.route.provider === "deepseek", "route provider default should be deepseek");
52
+ assert(roles.route.model === "deepseek-v4-flash", "route model default should be deepseek-v4-flash");
53
+ assert(roles.main.model === "deepseek-v4-pro", "main model default should be deepseek-v4-pro");
54
+ assert(roles.spare.provider === "openai" && roles.spare.model === "gpt-5.4", "spare model default should be OpenAI GPT-5.4");
55
+ assert(roles.wrapper.provider === "codex" && roles.wrapper.model === "gpt-5.5", "wrapper default should be Codex GPT-5.5");
56
+ assert(roles.auxiliary.provider === "grsai" && roles.auxiliary.model === "nano-banana-2", "auxiliary default should be GRS AI Nano Banana");
57
+
58
+ const complexRoute = selectModelRoute({
59
+ routingMode: "complex",
60
+ provider: "deepseek",
61
+ mainModel: "deepseek-v4-pro",
62
+ });
63
+ assert(complexRoute.model === "deepseek-v4-pro", "complex route did not use main model override");
64
+
65
+ const fastRoute = selectModelRoute({
66
+ routingMode: "fast",
67
+ provider: "deepseek",
68
+ routeModel: "deepseek-v4-flash",
69
+ });
70
+ assert(fastRoute.model === "deepseek-v4-flash", "fast route did not use route model override");
71
+
72
+ assert(MODEL_PROVIDER_GROUPS["venice-gpt"].provider === "venice", "venice-gpt group missing");
73
+ assert(modelsForProviderGroup("venice-gemma").some((item) => item.id === "gemma-4-uncensored"), "venice-gemma bucket missing Gemma");
74
+ assert(AUXILIARY_MODEL_CATALOG["venice-image"].some((item) => item.id === "gpt-image-2"), "Venice image catalog missing GPT Image 2");
75
+
76
+ const output = await runCli(["models"]);
77
+ assert(output.includes("/route") && output.includes("/spare") && output.includes("venice-gpt"), "aginti models output missing role details");
78
+
79
+ console.log(
80
+ JSON.stringify(
81
+ {
82
+ ok: true,
83
+ checks: ["role-defaults", "route-overrides", "provider-groups", "auxiliary-catalog", "cli-models-command"],
84
+ },
85
+ null,
86
+ 2
87
+ )
88
+ );
@@ -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",
@@ -252,10 +252,16 @@ function resultUrls(payload) {
252
252
 
253
253
  function normalizeImageProvider(value = "") {
254
254
  const normalized = String(value || "").trim().toLowerCase();
255
- if (["venice", "venice-ai", "veniceai"].includes(normalized)) return "venice";
255
+ if (["venice", "venice-image", "venice-ai", "veniceai"].includes(normalized)) return "venice";
256
256
  return "grsai";
257
257
  }
258
258
 
259
+ function defaultImageProvider(config = {}) {
260
+ return normalizeImageProvider(
261
+ config.auxiliaryProvider || process.env.AGINTI_AUX_PROVIDER || process.env.VENICE_IMAGE_PROVIDER || process.env.GRSAI_IMAGE_PROVIDER || ""
262
+ );
263
+ }
264
+
259
265
  function veniceBaseUrl(value = "") {
260
266
  return String(value || process.env.VENICE_API_BASE || process.env.VENICE_BASE_URL || DEFAULT_VENICE_BASE)
261
267
  .trim()
@@ -268,7 +274,9 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
268
274
  throw new Error("Missing VENICE_API_KEY. Run `aginti login venice` or `aginti keys set venice --stdin`.");
269
275
  }
270
276
 
271
- const model = String(args.model || process.env.VENICE_IMAGE_MODEL || DEFAULT_VENICE_IMAGE_MODEL).trim() || DEFAULT_VENICE_IMAGE_MODEL;
277
+ const model =
278
+ String(args.model || process.env.AGINTI_AUX_MODEL || process.env.VENICE_IMAGE_MODEL || DEFAULT_VENICE_IMAGE_MODEL).trim() ||
279
+ DEFAULT_VENICE_IMAGE_MODEL;
272
280
  const format = String(args.format || "png").trim().toLowerCase() === "webp" ? "webp" : "png";
273
281
  const sizing = veniceSizingForModel(model, args);
274
282
  const payload = {
@@ -356,12 +364,13 @@ export async function generateImage(args = {}, config = {}) {
356
364
  if (converted.url) references.push(converted);
357
365
  }
358
366
 
359
- const provider = normalizeImageProvider(args.provider || "");
367
+ const provider = args.provider ? normalizeImageProvider(args.provider) : defaultImageProvider(config);
360
368
  const host = provider === "venice" ? veniceBaseUrl(args.host) : String(args.host || DEFAULT_GRS_HOST).trim().replace(/\/+$/, "") || DEFAULT_GRS_HOST;
361
369
  const model =
362
370
  provider === "venice"
363
- ? String(args.model || process.env.VENICE_IMAGE_MODEL || DEFAULT_VENICE_IMAGE_MODEL).trim() || DEFAULT_VENICE_IMAGE_MODEL
364
- : String(args.model || DEFAULT_IMAGE_MODEL).trim() || DEFAULT_IMAGE_MODEL;
371
+ ? String(args.model || config.auxiliaryModel || process.env.AGINTI_AUX_MODEL || process.env.VENICE_IMAGE_MODEL || DEFAULT_VENICE_IMAGE_MODEL).trim() ||
372
+ DEFAULT_VENICE_IMAGE_MODEL
373
+ : String(args.model || config.auxiliaryModel || process.env.AGINTI_AUX_MODEL || DEFAULT_IMAGE_MODEL).trim() || DEFAULT_IMAGE_MODEL;
365
374
  const outputStem = safeStem(args.outputStem || "image");
366
375
  const payload = {
367
376
  model,