@lazyingart/agintiflow 0.20.187 → 0.20.188

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
@@ -85,11 +85,12 @@ aginti init --template aaps
85
85
  aginti init --template supervision
86
86
  ```
87
87
 
88
- On first interactive use, AgInTiFlow opens an auth wizard if no main model key is found. Pick DeepSeek, OpenAI, Qwen, or Venice, paste the key, and it saves to the ignored project-local `.aginti/.env` file with restricted permissions. You can rerun setup any time:
88
+ On first interactive use, AgInTiFlow opens an auth wizard if no main model key is found. Pick DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, paste the key, and it saves to the ignored project-local `.aginti/.env` file with restricted permissions. You can rerun setup any time:
89
89
 
90
90
  ```bash
91
91
  aginti auth
92
92
  aginti auth deepseek
93
+ aginti auth openrouter
93
94
  aginti auth venice
94
95
  aginti login grsai
95
96
  ```
@@ -99,6 +100,7 @@ Provider signup and key pages:
99
100
  | Provider | Register / key page | API base URL used by AgInTiFlow |
100
101
  | --- | --- | --- |
101
102
  | DeepSeek | [https://platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys) | `https://api.deepseek.com` |
103
+ | OpenRouter | [https://openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) | `https://openrouter.ai/api/v1` |
102
104
  | Venice | [https://venice.ai/settings/api](https://venice.ai/settings/api) | `https://api.venice.ai/api/v1` |
103
105
  | OpenAI | [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys) | `https://api.openai.com/v1` |
104
106
  | Qwen / DashScope | [https://bailian.console.aliyun.com/](https://bailian.console.aliyun.com/) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` |
@@ -273,6 +275,7 @@ Useful selectors:
273
275
  ```
274
276
 
275
277
  Venice routes can be used for optional uncensored or less restricted creative work. DeepSeek remains the economic default for normal engineering workflows. See [docs/model-selection.md](docs/model-selection.md) and [references/venice-model-reference.md](references/venice-model-reference.md).
278
+ OpenRouter is available as a first-class OpenAI-compatible provider with one key and company-grouped model buckets such as OpenRouter OpenAI, Anthropic, Google, DeepSeek, Qwen, Meta, Mistral, Moonshot, and xAI.
276
279
 
277
280
  ## AAPS And Large Workflows
278
281
 
@@ -348,6 +351,9 @@ Common environment variables:
348
351
  ```bash
349
352
  DEEPSEEK_API_KEY=...
350
353
  OPENAI_API_KEY=...
354
+ OPENROUTER_API_KEY=...
355
+ OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
356
+ OPENROUTER_MODEL=openrouter/auto
351
357
  QWEN_API_KEY=...
352
358
  VENICE_API_KEY=...
353
359
  GRSAI_API_KEY=...
@@ -365,6 +371,7 @@ Project-local keys:
365
371
  ```bash
366
372
  aginti init
367
373
  printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
374
+ printf '%s' "$OPENROUTER_API_KEY" | aginti keys set openrouter --stdin
368
375
  printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
369
376
  ```
370
377
 
@@ -25,6 +25,8 @@ aginti --list-routes
25
25
 
26
26
  # one-shot overrides
27
27
  aginti --route-model deepseek-v4-flash --main-model deepseek-v4-pro "fix this project"
28
+ aginti --provider openrouter --model openrouter/auto --routing manual "try a gateway route"
29
+ aginti --provider openrouter --model anthropic/claude-sonnet-4.6 --routing manual "draft a design review"
28
30
  aginti --provider venice --model venice-uncensored-1-2 --routing manual "draft a note"
29
31
  aginti --spare-provider openai --spare-model gpt-5.4 --spare-reasoning medium "review this design"
30
32
  aginti --allow-wrappers --wrapper codex --wrapper-model gpt-5.5 "patch this bug"
@@ -63,6 +65,13 @@ Provider families:
63
65
  | DeepSeek | `deepseek-v4-flash`, `deepseek-v4-pro` |
64
66
  | Venice | `venice-uncensored-1-2`, `venice-uncensored`, `gemma-4-uncensored` |
65
67
  | OpenAI | `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.3-codex-spark`, `gpt-5.2`; each has low/medium/high/xhigh reasoning |
68
+ | OpenRouter | `openrouter/auto`, `openrouter/pareto-code`, `openrouter/free` |
69
+ | OpenRouter OpenAI | `openai/gpt-5.5`, `openai/gpt-5.4`, `openai/gpt-5.4-mini`, `openai/gpt-4o` |
70
+ | OpenRouter Anthropic | Claude routes such as `anthropic/claude-sonnet-4.6`, `anthropic/claude-opus-4.7`, `anthropic/claude-opus-4.8-fast` |
71
+ | OpenRouter Google | Gemini/Gemma routes such as `google/gemini-3.5-flash`, `google/gemini-3.1-flash-lite`, `google/gemma-4-31b-it` |
72
+ | OpenRouter DeepSeek | `deepseek/deepseek-v4-flash`, `deepseek/deepseek-v4-pro`, `deepseek/deepseek-r1-0528` |
73
+ | OpenRouter Qwen | Qwen routes such as `qwen/qwen3.7-max`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-plus` |
74
+ | OpenRouter Meta/Mistral/Moonshot/xAI | `meta-llama/llama-4-maverick`, `mistralai/mistral-medium-3-5`, `moonshotai/kimi-k2.6`, `x-ai/grok-4.3` |
66
75
  | Venice GPT | GPT-family Venice routes such as `openai-gpt-55`, `openai-gpt-54`, `openai-gpt-54-mini`, `openai-gpt-53-codex`, `openai-gpt-52` |
67
76
  | Venice Gemma | Gemma instruct routes such as `google-gemma-4-31b-it`, `google-gemma-4-26b-a4b-it`, `google-gemma-3-27b-it` |
68
77
  | Venice Claude | Claude Sonnet/Opus routes such as `claude-sonnet-4-6`, `claude-opus-4-7`, `claude-opus-4-6` |
@@ -96,6 +105,16 @@ It keeps smart routing enabled. If the Venice key is missing, run `/auth venice`
96
105
  | --- | --- | --- |
97
106
  | `deepseek` | DeepSeek | Default route/main because V4 Flash and V4 Pro are cheap and strong. |
98
107
  | `openai` | OpenAI | Spare/frontier checks, Codex-family work, and explicit manual routes. |
108
+ | `openrouter` | OpenRouter | OpenAI-compatible gateway with auto/free/code routers. |
109
+ | `openrouter-openai` | OpenRouter | OpenAI-family models through OpenRouter. |
110
+ | `openrouter-anthropic` | OpenRouter | Claude-family models through OpenRouter. |
111
+ | `openrouter-google` | OpenRouter | Gemini/Gemma models through OpenRouter. |
112
+ | `openrouter-deepseek` | OpenRouter | DeepSeek models through OpenRouter. |
113
+ | `openrouter-qwen` | OpenRouter | Qwen models through OpenRouter. |
114
+ | `openrouter-meta` | OpenRouter | Meta Llama models through OpenRouter. |
115
+ | `openrouter-mistral` | OpenRouter | Mistral models through OpenRouter. |
116
+ | `openrouter-moonshot` | OpenRouter | Moonshot/Kimi models through OpenRouter. |
117
+ | `openrouter-xai` | OpenRouter | xAI Grok models through OpenRouter. |
99
118
  | `qwen` | Qwen | Chinese and general-purpose OpenAI-compatible tasks. |
100
119
  | `venice` | Venice | Venice-native text shortcuts: Venice 1.2, Venice 1.1, and Gemma 4 Uncensored. |
101
120
  | `venice-gpt` | Venice | GPT-family models through Venice. |
@@ -118,8 +137,10 @@ Keys are set per project and stored only in ignored `.aginti/.env`:
118
137
 
119
138
  ```bash
120
139
  aginti auth
140
+ aginti auth openrouter
121
141
  aginti auth venice
122
142
  aginti keys status
143
+ printf '%s' "$OPENROUTER_API_KEY" | aginti keys set openrouter --stdin
123
144
  printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
124
145
  ```
125
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.187",
3
+ "version": "0.20.188",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -820,6 +820,7 @@ const ariaLabelNodes = [...document.querySelectorAll("[data-i18n-aria-label]")];
820
820
 
821
821
  const defaults = {
822
822
  openai: "gpt-5.4-mini",
823
+ openrouter: "openrouter/auto",
823
824
  deepseek: "deepseek-v4-flash",
824
825
  qwen: "qwen-plus",
825
826
  venice: "venice-uncensored-1-2",
@@ -983,6 +984,7 @@ function renderKeyStatus(status = lastKeyStatus) {
983
984
  if (!status) return;
984
985
  const providers = [
985
986
  ["OpenAI", status.openai],
987
+ ["OpenRouter", status.openrouter],
986
988
  ["DeepSeek", status.deepseek],
987
989
  ["Qwen", status.qwen],
988
990
  ["Venice", status.venice],
@@ -1009,7 +1011,7 @@ function renderKeyStatus(status = lastKeyStatus) {
1009
1011
  })
1010
1012
  );
1011
1013
  }
1012
- if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek || status.qwen || status.venice);
1014
+ if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.openrouter || status.deepseek || status.qwen || status.venice);
1013
1015
  }
1014
1016
 
1015
1017
  function renderProjectStatus(info = projectInfo) {
@@ -1611,6 +1613,7 @@ function fieldValue(field) {
1611
1613
  const providerLabels = {
1612
1614
  deepseek: "DeepSeek",
1613
1615
  openai: "OpenAI",
1616
+ openrouter: "OpenRouter",
1614
1617
  qwen: "Qwen",
1615
1618
  venice: "Venice",
1616
1619
  mock: "Mock local",
@@ -1634,14 +1637,14 @@ function setChoiceOptions(select, options, selectedValue = "", fallbackValue = "
1634
1637
  }
1635
1638
 
1636
1639
  function configuredTextProviders({ includeMock = true } = {}) {
1637
- const base = ["deepseek", "openai", "qwen", "venice"];
1640
+ const base = ["deepseek", "openai", "openrouter", "qwen", "venice"];
1638
1641
  if (includeMock) base.push("mock");
1639
1642
  if (Object.keys(modelCatalog || {}).length === 0) return base;
1640
1643
  return base.filter((provider) => provider === "mock" || Array.isArray(modelCatalog[provider]));
1641
1644
  }
1642
1645
 
1643
1646
  function refreshProviderDropdowns() {
1644
- setChoiceOptions(setupProviderField, ["deepseek", "openai", "qwen", "venice", "grsai"], setupProviderField?.value || "deepseek", "deepseek");
1647
+ setChoiceOptions(setupProviderField, ["deepseek", "openai", "openrouter", "qwen", "venice", "grsai"], setupProviderField?.value || "deepseek", "deepseek");
1645
1648
  setChoiceOptions(providerField, configuredTextProviders({ includeMock: true }), providerField?.value || "deepseek", "deepseek");
1646
1649
  setChoiceOptions(routeProviderField, configuredTextProviders({ includeMock: true }), routeProviderField?.value || "deepseek", "deepseek");
1647
1650
  setChoiceOptions(mainProviderField, configuredTextProviders({ includeMock: false }), mainProviderField?.value || "deepseek", "deepseek");
@@ -3874,7 +3877,13 @@ deleteSessionButton.addEventListener("click", () => {
3874
3877
  });
3875
3878
 
3876
3879
  providerField.addEventListener("change", () => {
3877
- if (providerField.value === "mock" || providerField.value === "venice" || providerField.value === "openai" || providerField.value === "qwen") {
3880
+ if (
3881
+ providerField.value === "mock" ||
3882
+ providerField.value === "venice" ||
3883
+ providerField.value === "openai" ||
3884
+ providerField.value === "openrouter" ||
3885
+ providerField.value === "qwen"
3886
+ ) {
3878
3887
  routingModeField.value = "manual";
3879
3888
  }
3880
3889
 
@@ -4208,6 +4217,7 @@ async function loadConfig() {
4208
4217
  taskProfiles = data.taskProfiles || [];
4209
4218
  projectInfo = data.project || null;
4210
4219
  defaults.openai = data.defaults?.openai?.model || defaults.openai;
4220
+ defaults.openrouter = data.defaults?.openrouter?.model || defaults.openrouter;
4211
4221
  defaults.qwen = data.defaults?.qwen?.model || defaults.qwen;
4212
4222
  defaults.venice = data.defaults?.venice?.model || defaults.venice;
4213
4223
  defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
package/public/index.html CHANGED
@@ -65,10 +65,10 @@
65
65
  <div>
66
66
  <strong data-i18n="setupTitle">Provider setup</strong>
67
67
  <p class="subtle" data-i18n="setupHelp">
68
- DeepSeek/OpenAI/Qwen/Venice keys are missing. Use mock mode, export an env var, or save a project-local key.
68
+ DeepSeek/OpenAI/OpenRouter/Qwen/Venice keys are missing. Use mock mode, export an env var, or save a project-local key.
69
69
  </p>
70
70
  <p class="subtle" data-i18n="setupEnvHelp">
71
- Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, VENICE_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
71
+ Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, QWEN_API_KEY, VENICE_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
72
72
  </p>
73
73
  <p class="subtle key-links">
74
74
  <span data-i18n="setupKeyLinksLabel">Get keys:</span>
@@ -76,6 +76,8 @@
76
76
  <span aria-hidden="true">·</span>
77
77
  <a href="https://platform.openai.com/api-keys" target="_blank" rel="noreferrer" data-i18n="openaiKeyLink">OpenAI API keys</a>
78
78
  <span aria-hidden="true">·</span>
79
+ <a href="https://openrouter.ai/settings/keys" target="_blank" rel="noreferrer">OpenRouter API keys</a>
80
+ <span aria-hidden="true">·</span>
79
81
  <a href="https://venice.ai" target="_blank" rel="noreferrer">Venice API</a>
80
82
  </p>
81
83
  </div>
@@ -85,6 +87,7 @@
85
87
  <select id="setup-provider">
86
88
  <option value="deepseek">DeepSeek</option>
87
89
  <option value="openai">OpenAI</option>
90
+ <option value="openrouter">OpenRouter</option>
88
91
  <option value="qwen">Qwen</option>
89
92
  <option value="venice">Venice</option>
90
93
  <option value="grsai">GRS AI image</option>
@@ -132,6 +135,7 @@
132
135
  <select id="provider" name="provider">
133
136
  <option value="deepseek">DeepSeek</option>
134
137
  <option value="openai">OpenAI</option>
138
+ <option value="openrouter">OpenRouter</option>
135
139
  <option value="qwen">Qwen</option>
136
140
  <option value="venice">Venice</option>
137
141
  <option value="mock" data-i18n="mockProviderOption">Mock local</option>
@@ -414,6 +418,7 @@
414
418
  <select id="routeProvider" name="routeProvider">
415
419
  <option value="deepseek">DeepSeek</option>
416
420
  <option value="openai">OpenAI</option>
421
+ <option value="openrouter">OpenRouter</option>
417
422
  <option value="qwen">Qwen</option>
418
423
  <option value="venice">Venice</option>
419
424
  <option value="mock">Mock</option>
@@ -427,6 +432,7 @@
427
432
  <select id="mainProvider" name="mainProvider">
428
433
  <option value="deepseek">DeepSeek</option>
429
434
  <option value="openai">OpenAI</option>
435
+ <option value="openrouter">OpenRouter</option>
430
436
  <option value="qwen">Qwen</option>
431
437
  <option value="venice">Venice</option>
432
438
  </select>
@@ -438,6 +444,7 @@
438
444
  <span>Spare</span>
439
445
  <select id="spareProvider" name="spareProvider">
440
446
  <option value="openai">OpenAI</option>
447
+ <option value="openrouter">OpenRouter</option>
441
448
  <option value="deepseek">DeepSeek</option>
442
449
  <option value="qwen">Qwen</option>
443
450
  <option value="venice">Venice</option>
@@ -10,6 +10,17 @@ This note records the working publication route for AgInTiFlow so future release
10
10
 
11
11
  Latest verified release:
12
12
 
13
+ - Version: `0.20.187`
14
+ - Commit: `9339b2e`
15
+ - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26701125864`
16
+ - Workflow result: success
17
+ - npm registry check: `npm view @lazyingart/agintiflow version dist-tags.latest --registry=https://registry.npmjs.org` returned `0.20.187`.
18
+ - Installed verification: `npm install -g @lazyingart/agintiflow@0.20.187` then `aginti --version` returned `0.20.187`.
19
+ - Webapp verification: `aginti webapp restart --port 3210` then `curl -fsS http://127.0.0.1:3210/health` returned `version":"0.20.187"` from the global npm package path.
20
+ - Workspace explorer verification: `POST /api/workspace/snapshot` returned no `.aginti/`, `.aginti-work/`, `.sessions/`, or `.env` entries for the project root.
21
+
22
+ Previous verified release:
23
+
13
24
  - Version: `0.20.186`
14
25
  - Commit: `f32a748`
15
26
  - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26700782132`
@@ -10,7 +10,16 @@ import { maskProviderKey, providerKeyPreview, providerKeyStatus, setProviderKey
10
10
 
11
11
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
12
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auth-"));
13
- const envKeys = ["DEEPSEEK_API_KEY", "OPENAI_API_KEY", "LLM_API_KEY", "QWEN_API_KEY", "VENICE_API_KEY", "GRSAI", "GRSAI_API_KEY"];
13
+ const envKeys = [
14
+ "DEEPSEEK_API_KEY",
15
+ "OPENAI_API_KEY",
16
+ "OPENROUTER_API_KEY",
17
+ "LLM_API_KEY",
18
+ "QWEN_API_KEY",
19
+ "VENICE_API_KEY",
20
+ "GRSAI",
21
+ "GRSAI_API_KEY",
22
+ ];
14
23
  const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
15
24
  for (const key of envKeys) delete process.env[key];
16
25
 
@@ -55,57 +64,85 @@ async function runCli(args, stdin = "") {
55
64
 
56
65
  try {
57
66
  assert(normalizeAuthProvider("auxiliary") === "grsai", "auxiliary alias did not normalize to grsai");
67
+ assert(normalizeAuthProvider("or") === "openrouter", "OpenRouter alias did not normalize");
68
+ assert(normalizeAuthProvider("open-router") === "openrouter", "open-router alias did not normalize");
58
69
  assert(normalizeAuthProvider("qwen") === "qwen", "qwen provider did not normalize");
59
70
  assert(normalizeAuthProvider("venice") === "venice", "venice provider did not normalize");
60
71
  assert(authProviderKeyUrl("deepseek") === "https://platform.deepseek.com/api_keys", "DeepSeek key URL is missing");
72
+ assert(authProviderKeyUrl("openrouter") === "https://openrouter.ai/settings/keys", "OpenRouter key URL is missing");
61
73
  assert(authProviderKeyHelp("openai").includes("https://platform.openai.com/api-keys"), "OpenAI key help is missing");
62
74
  assert(maskProviderKey("short") === "s…t (5 chars)", "short key mask was not compact");
63
75
  assert(maskProviderKey("test-openai-key") === "test…-key (15 chars)", "long key mask did not preserve prefix/suffix");
64
76
  const qwenDefaults = getProviderDefaults("qwen");
65
77
  assert(qwenDefaults.provider === "qwen" && qwenDefaults.model, "qwen provider defaults are not available");
78
+ const openrouterDefaults = getProviderDefaults("openrouter");
79
+ assert(
80
+ openrouterDefaults.provider === "openrouter" &&
81
+ openrouterDefaults.baseURL === "https://openrouter.ai/api/v1" &&
82
+ openrouterDefaults.model === "openrouter/auto",
83
+ "openrouter provider defaults are not available"
84
+ );
66
85
  const veniceDefaults = getProviderDefaults("venice");
67
86
  assert(veniceDefaults.provider === "venice" && veniceDefaults.model === "venice-uncensored-1-2", "venice provider defaults are not available");
68
87
 
69
88
  process.env.DEEPSEEK_API_KEY = "ambient-deepseek-key";
89
+ process.env.OPENROUTER_API_KEY = "ambient-openrouter-key";
70
90
  process.env.VENICE_API_KEY = "ambient-venice-key";
71
91
  process.env.GRSAI_API_KEY = "ambient-grsai-key";
72
92
  let status = providerKeyStatus(tempRoot);
73
- assert(status.deepseek && status.venice && status.grsai, "ambient provider keys were not detected");
93
+ assert(status.deepseek && status.openrouter && status.venice && status.grsai, "ambient provider keys were not detected");
74
94
  assert(status.localEnv, "ambient provider keys were not persisted into local .aginti/.env");
75
95
  const localEnv = await fs.readFile(path.join(tempRoot, ".aginti", ".env"), "utf8");
76
96
  assert(localEnv.includes("DEEPSEEK_API_KEY="), "ambient DeepSeek key was not saved locally");
97
+ assert(localEnv.includes("OPENROUTER_API_KEY="), "ambient OpenRouter key was not saved locally");
77
98
  assert(localEnv.includes("VENICE_API_KEY="), "ambient Venice key was not saved locally");
78
99
  assert(localEnv.includes("GRSAI_API_KEY="), "ambient GRS AI key was not saved locally");
79
100
  delete process.env.DEEPSEEK_API_KEY;
101
+ delete process.env.OPENROUTER_API_KEY;
80
102
  delete process.env.VENICE_API_KEY;
81
103
  delete process.env.GRSAI_API_KEY;
82
104
  status = providerKeyStatus(tempRoot);
83
- assert(status.deepseek && status.venice && status.grsai, "persisted provider keys were not reloaded after ambient env was cleared");
105
+ assert(status.deepseek && status.openrouter && status.venice && status.grsai, "persisted provider keys were not reloaded after ambient env was cleared");
84
106
 
85
107
  await setProviderKey(tempRoot, "qwen", "test-qwen-key");
108
+ await setProviderKey(tempRoot, "openrouter", "test-openrouter-key");
86
109
  await setProviderKey(tempRoot, "venice", "test-venice-key");
87
110
  status = providerKeyStatus(tempRoot);
88
111
  assert(status.qwen, "qwen key status was not detected");
112
+ assert(status.openrouter, "openrouter key status was not detected");
89
113
  assert(status.venice, "venice key status was not detected");
90
114
  assert(status.envVars.qwen.includes("QWEN_API_KEY"), "qwen env var name was not reported");
115
+ assert(status.envVars.openrouter.includes("OPENROUTER_API_KEY"), "openrouter env var name was not reported");
91
116
  assert(status.envVars.venice.includes("VENICE_API_KEY"), "venice env var name was not reported");
92
117
  const qwenPreview = providerKeyPreview(tempRoot, "qwen");
93
118
  assert(qwenPreview.available && qwenPreview.preview === "test…-key (13 chars)", "qwen key preview was not masked correctly");
119
+ const openrouterPreview = providerKeyPreview(tempRoot, "openrouter");
120
+ assert(
121
+ openrouterPreview.available && openrouterPreview.preview === "test…-key (19 chars)",
122
+ "openrouter key preview was not masked correctly"
123
+ );
94
124
  const venicePreview = providerKeyPreview(tempRoot, "venice");
95
125
  assert(venicePreview.available && venicePreview.preview === "test…-key (15 chars)", "venice key preview was not masked correctly");
96
126
 
97
127
  await runCli(["keys", "set", "openai", "--stdin"], "test-openai-key");
98
128
  await runCli(["keys", "set", "grsai", "--stdin"], "test-grsai-key");
99
129
  status = providerKeyStatus(tempRoot);
100
- assert(status.openai && status.grsai && status.qwen && status.venice, "stored auth keys were not detected");
130
+ assert(status.openai && status.openrouter && status.grsai && status.qwen && status.venice, "stored auth keys were not detected");
101
131
  const openaiPreview = providerKeyPreview(tempRoot, "openai");
102
132
  assert(openaiPreview.preview === "test…-key (15 chars)", "openai key preview was not masked correctly");
103
133
  assert(openaiPreview.preview !== "test-openai-key", "openai key preview leaked raw key");
104
134
 
105
135
  const keysOutput = await runCli(["keys", "status"]);
136
+ assert(keysOutput.includes("openrouter=available"), "keys status did not include openrouter");
106
137
  assert(keysOutput.includes("qwen=available"), "keys status did not include qwen");
107
138
  assert(keysOutput.includes("venice=available"), "keys status did not include venice");
108
- assert(!keysOutput.includes("test-openai-key") && !keysOutput.includes("test-qwen-key") && !keysOutput.includes("test-venice-key"), "keys status leaked a raw key");
139
+ assert(
140
+ !keysOutput.includes("test-openai-key") &&
141
+ !keysOutput.includes("test-openrouter-key") &&
142
+ !keysOutput.includes("test-qwen-key") &&
143
+ !keysOutput.includes("test-venice-key"),
144
+ "keys status leaked a raw key"
145
+ );
109
146
 
110
147
  console.log(
111
148
  JSON.stringify(
@@ -118,9 +155,11 @@ try {
118
155
  "provider-key-mask",
119
156
  "provider-key-preview",
120
157
  "qwen-defaults",
158
+ "openrouter-defaults",
121
159
  "venice-defaults",
122
160
  "ambient-key-autopersist",
123
161
  "qwen-key-status",
162
+ "openrouter-key-status",
124
163
  "venice-key-status",
125
164
  "cli-key-status-redacted",
126
165
  ],
@@ -207,6 +207,14 @@ assert(modelsForProviderGroup("venice-claude").some((item) => item.id === "claud
207
207
  assert(modelsForProviderGroup("venice-qwen").some((item) => item.id === "qwen3-coder-480b-a35b-instruct-turbo"), "venice-qwen bucket missing Qwen Coder");
208
208
  assert(AUXILIARY_MODEL_CATALOG["venice-image"].some((item) => item.id === "gpt-image-2"), "Venice image catalog missing GPT Image 2");
209
209
  assert(AUXILIARY_MODEL_CATALOG["venice-image"].some((item) => item.id === "wan-2-7-pro-edit"), "Venice image catalog missing Wan edit");
210
+ assert(MODEL_PROVIDER_GROUPS["openrouter"].provider === "openrouter", "openrouter group missing");
211
+ assert(MODEL_PROVIDER_GROUPS["openrouter-openai"].provider === "openrouter", "openrouter-openai group missing");
212
+ assert(modelsForProviderGroup("openrouter").some((item) => item.id === "openrouter/auto"), "openrouter group missing auto router");
213
+ assert(modelsForProviderGroup("openrouter-openai").some((item) => item.id === "openai/gpt-5.4-mini"), "openrouter OpenAI bucket missing GPT-5.4 Mini");
214
+ assert(modelsForProviderGroup("openrouter-anthropic").some((item) => item.id === "anthropic/claude-sonnet-4.6"), "openrouter Anthropic bucket missing Claude Sonnet");
215
+ assert(modelsForProviderGroup("openrouter-google").some((item) => item.id === "google/gemini-3.5-flash"), "openrouter Google bucket missing Gemini");
216
+ assert(modelsForProviderGroup("openrouter-deepseek").some((item) => item.id === "deepseek/deepseek-v4-pro"), "openrouter DeepSeek bucket missing V4 Pro");
217
+ assert(modelsForProviderGroup("openrouter-qwen").some((item) => item.id === "qwen/qwen3.7-max"), "openrouter Qwen bucket missing Qwen Max");
210
218
  const routeChoices = modelRoleChoices("route").map((item) => `${item.provider}/${item.model}`);
211
219
  const mainChoices = modelRoleChoices("main").map((item) => `${item.provider}/${item.model}`);
212
220
  const spareChoices = modelRoleChoices("spare").map((item) => `${item.provider}/${item.model}`);
@@ -231,6 +239,12 @@ for (const expected of [
231
239
  "openai/gpt-5.4-mini",
232
240
  "openai/gpt-5.3-codex",
233
241
  "openai/gpt-5.3-codex-spark",
242
+ "openrouter/openrouter/auto",
243
+ "openrouter/openai/gpt-5.4-mini",
244
+ "openrouter/anthropic/claude-sonnet-4.6",
245
+ "openrouter/google/gemini-3.5-flash",
246
+ "openrouter/deepseek/deepseek-v4-pro",
247
+ "openrouter/qwen/qwen3.7-max",
234
248
  "qwen/qwen-plus",
235
249
  "mock/mock-agent",
236
250
  ]) {
@@ -123,6 +123,18 @@ try {
123
123
 
124
124
  const config = await fetchJson("/api/config");
125
125
  if (!config.keyStatus?.mock) throw new Error("mock provider is not advertised by /api/config");
126
+ if (!Object.prototype.hasOwnProperty.call(config.keyStatus || {}, "openrouter")) {
127
+ throw new Error("OpenRouter key status is not advertised by /api/config");
128
+ }
129
+ if (config.defaults?.openrouter?.baseURL !== "https://openrouter.ai/api/v1") {
130
+ throw new Error(`OpenRouter defaults are missing from /api/config: ${JSON.stringify(config.defaults?.openrouter)}`);
131
+ }
132
+ if (!config.modelCatalog?.openrouter?.some((model) => model.id === "openrouter/auto")) {
133
+ throw new Error("OpenRouter model catalog is missing from /api/config");
134
+ }
135
+ if (!config.modelGroups?.["openrouter-openai"]) {
136
+ throw new Error("OpenRouter company model groups are missing from /api/config");
137
+ }
126
138
  if (!config.workspace?.enabled) throw new Error("workspace file tools are not advertised by /api/config");
127
139
  if (config.preferences?.preferredWrapper !== "codex") throw new Error("Codex is not the default preferred wrapper");
128
140
  if (config.project?.root !== runtimeDir) throw new Error("web project root did not default to launch directory");
@@ -297,7 +297,7 @@ try {
297
297
  await page.click("#open-settings");
298
298
  await page.waitForSelector("#settings-modal[open]");
299
299
  const routeProviders = await optionValues(page, "#routeProvider");
300
- for (const provider of ["deepseek", "openai", "qwen", "venice", "mock"]) {
300
+ for (const provider of ["deepseek", "openai", "openrouter", "qwen", "venice", "mock"]) {
301
301
  if (!routeProviders.includes(provider)) throw new Error(`settings route provider dropdown is missing ${provider}`);
302
302
  }
303
303
  const wrapperOptions = await optionValues(page, "#preferredWrapper");
@@ -36,6 +36,13 @@ export const MAIN_AUTH_PROVIDERS = [
36
36
  description: "OpenAI-compatible fallback",
37
37
  keyUrl: "https://platform.openai.com/api-keys",
38
38
  },
39
+ {
40
+ id: "openrouter",
41
+ label: "OpenRouter",
42
+ keyName: "OPENROUTER_API_KEY",
43
+ description: "OpenAI-compatible multi-provider gateway",
44
+ keyUrl: "https://openrouter.ai/settings/keys",
45
+ },
39
46
  {
40
47
  id: "qwen",
41
48
  label: "Qwen",
@@ -69,6 +76,10 @@ const AUTH_ALIASES = {
69
76
  deepseek: "deepseek",
70
77
  ds: "deepseek",
71
78
  openai: "openai",
79
+ openrouter: "openrouter",
80
+ "open-router": "openrouter",
81
+ or: "openrouter",
82
+ router: "openrouter",
72
83
  qwen: "qwen",
73
84
  venice: "venice",
74
85
  v: "venice",
@@ -76,7 +87,7 @@ const AUTH_ALIASES = {
76
87
 
77
88
  export function normalizeAuthProvider(provider = "", fallback = "deepseek") {
78
89
  const normalized = AUTH_ALIASES[String(provider || "").trim().toLowerCase()] || String(provider || "").trim().toLowerCase();
79
- return ["deepseek", "openai", "qwen", "venice", "grsai"].includes(normalized) ? normalized : fallback;
90
+ return ["deepseek", "openai", "openrouter", "qwen", "venice", "grsai"].includes(normalized) ? normalized : fallback;
80
91
  }
81
92
 
82
93
  function providerLabel(provider = "") {
@@ -396,11 +407,11 @@ export async function chooseAuthProvider({
396
407
 
397
408
  export function shouldPromptForDeepSeek(args = {}, projectRoot = process.cwd()) {
398
409
  const provider = String(args.provider || "").toLowerCase();
399
- if (provider === "mock" || provider === "openai" || provider === "qwen" || provider === "venice") return false;
410
+ if (provider === "mock" || provider === "openai" || provider === "openrouter" || provider === "qwen" || provider === "venice") return false;
400
411
  if (process.env.AGINTIFLOW_NO_AUTH_PROMPT === "1") return false;
401
412
  if (!input.isTTY || !output.isTTY) return false;
402
413
  const status = providerKeyStatus(projectRoot);
403
- return !status.deepseek && !status.openai && !status.qwen && !status.venice;
414
+ return !status.deepseek && !status.openai && !status.openrouter && !status.qwen && !status.venice;
404
415
  }
405
416
 
406
417
  export async function promptAndSaveDeepSeekKey(projectRoot = process.cwd(), options = {}) {
@@ -422,7 +433,7 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
422
433
  const status = providerKeyStatus(projectRoot);
423
434
  const initialProvider = normalizeAuthProvider(options.provider || options.initialProvider || "deepseek", "deepseek");
424
435
  const directProvider =
425
- options.provider && ["deepseek", "openai", "qwen", "venice", "grsai"].includes(normalizeAuthProvider(options.provider, ""))
436
+ options.provider && ["deepseek", "openai", "openrouter", "qwen", "venice", "grsai"].includes(normalizeAuthProvider(options.provider, ""))
426
437
  ? normalizeAuthProvider(options.provider)
427
438
  : "";
428
439
  const mainProvider =
@@ -164,6 +164,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
164
164
  capability("docker", Boolean(dockerStatus?.dockerAvailable), dockerStatus || {}),
165
165
  capability("deepseek-key", keyStatus.deepseek, { envVars: keyStatus.envVars.deepseek }),
166
166
  capability("openai-key", keyStatus.openai, { envVars: keyStatus.envVars.openai }),
167
+ capability("openrouter-key", keyStatus.openrouter, { envVars: keyStatus.envVars.openrouter }),
167
168
  capability("qwen-key", keyStatus.qwen, { envVars: keyStatus.envVars.qwen }),
168
169
  capability("venice-key", keyStatus.venice, { envVars: keyStatus.envVars.venice }),
169
170
  capability("grsai-key", keyStatus.grsai, {
@@ -238,6 +239,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
238
239
  keys: {
239
240
  deepseek: keyStatus.deepseek,
240
241
  openai: keyStatus.openai,
242
+ openrouter: keyStatus.openrouter,
241
243
  qwen: keyStatus.qwen,
242
244
  venice: keyStatus.venice,
243
245
  grsai: keyStatus.grsai,
@@ -321,7 +323,7 @@ export function printCapabilityReport(report) {
321
323
  console.log(
322
324
  `keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
323
325
  report.keys.openai ? "available" : "missing"
324
- } qwen=${report.keys.qwen ? "available" : "missing"} venice=${
326
+ } openrouter=${report.keys.openrouter ? "available" : "missing"} qwen=${report.keys.qwen ? "available" : "missing"} venice=${
325
327
  report.keys.venice ? "available" : "missing"
326
328
  } grsai=${
327
329
  report.keys.grsai ? "available" : "missing"
package/src/cli.js CHANGED
@@ -762,7 +762,7 @@ function exitOnUnknownOptions(parsed) {
762
762
 
763
763
  function printUsage() {
764
764
  console.log(
765
- 'Usage: aginti [chat] OR aginti init [--template minimal|disciplined|coding|research|writing|design|aaps|supervision] OR aginti web [--port 3210] OR aginti docker [status|setup|install-host] OR aginti update OR aginti image [--json] [--dry-run] [--format png|webp|svg] "prompt" OR aginti models OR aginti aaps [status|init|files|validate|compile|check|run] OR aginti mcp [status|config|inspect|tools|resources|read|prompts|prompt|call|restart] OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [-s safe|normal|danger] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--scs|--scs auto|--no-scs] [--dynamic-steps auto|on|off] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--mcp|--no-mcp] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
765
+ 'Usage: aginti [chat] OR aginti init [--template minimal|disciplined|coding|research|writing|design|aaps|supervision] OR aginti web [--port 3210] OR aginti docker [status|setup|install-host] OR aginti update OR aginti image [--json] [--dry-run] [--format png|webp|svg] "prompt" OR aginti models OR aginti aaps [status|init|files|validate|compile|check|run] OR aginti mcp [status|config|inspect|tools|resources|read|prompts|prompt|call|restart] OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|openrouter|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [-s safe|normal|danger] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--scs|--scs auto|--no-scs] [--dynamic-steps auto|on|off] [--routing smart|fast|complex|manual] [--provider deepseek|openai|openrouter|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--mcp|--no-mcp] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
766
766
  );
767
767
  console.log("Permission shortcuts: -s safe asks before writes/setup; -s normal allows current-project writes and Docker setup; -s danger enables trusted host/full-access mode.");
768
768
  console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
@@ -1157,6 +1157,7 @@ async function restartWebAppAfterUpdate({ commandCwd = process.cwd(), language =
1157
1157
  function providerLabel(provider) {
1158
1158
  const normalized = String(provider || "").toLowerCase();
1159
1159
  if (normalized === "openai") return "OpenAI";
1160
+ if (normalized === "openrouter" || normalized === "or" || normalized === "open-router") return "OpenRouter";
1160
1161
  if (normalized === "qwen") return "Qwen";
1161
1162
  if (normalized === "venice") return "Venice";
1162
1163
  if (normalized === "grsai" || normalized === "auxiliary") return "GRSAI";
@@ -1394,7 +1395,7 @@ async function readStdin() {
1394
1395
  async function ensureDeepSeekKeyForOneShot(args) {
1395
1396
  if (!shouldPromptForDeepSeek(args, process.cwd())) return true;
1396
1397
  console.log("No main model API key is configured for this project.");
1397
- console.log("Choose DeepSeek, OpenAI, Qwen, or Venice, then paste a key to save in `.aginti/.env` with 0600 permissions.");
1398
+ console.log("Choose DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, then paste a key to save in `.aginti/.env` with 0600 permissions.");
1398
1399
  const result = await runAuthWizard(process.cwd(), { provider: args.provider || "" });
1399
1400
  printAuthWizardResult(result);
1400
1401
  if (result.saved.some((item) => item.provider !== "grsai")) {
@@ -1413,9 +1414,9 @@ async function handleKeyCommand(argv) {
1413
1414
  status.openai ? "available" : "missing"
1414
1415
  } qwen=${status.qwen ? "available" : "missing"} venice=${
1415
1416
  status.venice ? "available" : "missing"
1416
- } grsai=${status.grsai ? "available" : "missing"} mock=available localEnv=${status.localEnv}`
1417
+ } openrouter=${status.openrouter ? "available" : "missing"} grsai=${status.grsai ? "available" : "missing"} mock=available localEnv=${status.localEnv}`
1417
1418
  );
1418
- console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; Qwen=QWEN_API_KEY; Venice=VENICE_API_KEY; image=GRSAI or GRSAI_API_KEY");
1419
+ console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; OpenRouter=OPENROUTER_API_KEY; Qwen=QWEN_API_KEY; Venice=VENICE_API_KEY; image=GRSAI or GRSAI_API_KEY");
1419
1420
  return;
1420
1421
  }
1421
1422
 
@@ -1431,7 +1432,7 @@ async function handleKeyCommand(argv) {
1431
1432
  return;
1432
1433
  }
1433
1434
 
1434
- console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|qwen|venice|grsai [--stdin]");
1435
+ console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|openrouter|qwen|venice|grsai [--stdin]");
1435
1436
  process.exit(1);
1436
1437
  }
1437
1438
 
package/src/config.js CHANGED
@@ -45,11 +45,13 @@ export function resolveRuntimeConfig(args, overrides = {}) {
45
45
  ? "deepseek"
46
46
  : process.env.OPENAI_API_KEY
47
47
  ? "openai"
48
- : process.env.QWEN_API_KEY
49
- ? "qwen"
50
- : process.env.VENICE_API_KEY
51
- ? "venice"
52
- : "deepseek");
48
+ : process.env.OPENROUTER_API_KEY
49
+ ? "openrouter"
50
+ : process.env.QWEN_API_KEY
51
+ ? "qwen"
52
+ : process.env.VENICE_API_KEY
53
+ ? "venice"
54
+ : "deepseek");
53
55
  const routingMode = normalizeRoutingMode(overrides.routingMode || args.routingMode || process.env.AGENT_ROUTING_MODE || "smart");
54
56
  const taskProfile = normalizeTaskProfile(overrides.taskProfile || args.taskProfile || process.env.AGINTI_TASK_PROFILE || "auto");
55
57
  const language = resolveLanguage(overrides.language || args.language || process.env.AGINTI_LANGUAGE || "");
package/src/guardrails.js CHANGED
@@ -285,7 +285,7 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
285
285
  const brief = String(args.writingBrief || args.brief || args.prompt || "").trim();
286
286
  if (!brief) return { allowed: false, reason: "Writing specialist requires writingBrief.", category: "writing-specialist" };
287
287
  const provider = String(args.provider || process.env.AGINTI_WRITING_PROVIDER || "").trim();
288
- if (provider && !["deepseek", "openai", "qwen", "venice", "mock"].includes(provider)) {
288
+ if (provider && !["deepseek", "openai", "openrouter", "qwen", "venice", "mock"].includes(provider)) {
289
289
  return { allowed: false, reason: `Unknown writing specialist provider: ${provider}`, category: "writing-specialist" };
290
290
  }
291
291
  const payloadBytes = Buffer.byteLength(
@@ -321,7 +321,7 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
321
321
  return { allowed: false, reason: "JSON specialist requires schema or schemaJson.", category: "json-specialist" };
322
322
  }
323
323
  const provider = String(args.provider || process.env.AGINTI_JSON_PROVIDER || "").trim();
324
- if (provider && !["deepseek", "openai", "qwen", "venice", "mock"].includes(provider)) {
324
+ if (provider && !["deepseek", "openai", "openrouter", "qwen", "venice", "mock"].includes(provider)) {
325
325
  return { allowed: false, reason: `Unknown JSON specialist provider: ${provider}`, category: "json-specialist" };
326
326
  }
327
327
  const payloadBytes = Buffer.byteLength(
@@ -360,7 +360,7 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
360
360
  return { allowed: false, reason: "JSON specialist batch concurrency is limited to 16.", category: "json-specialist" };
361
361
  }
362
362
  const provider = String(args.provider || args.defaults?.provider || process.env.AGINTI_JSON_PROVIDER || "").trim();
363
- if (provider && !["deepseek", "openai", "qwen", "venice", "mock"].includes(provider)) {
363
+ if (provider && !["deepseek", "openai", "openrouter", "qwen", "venice", "mock"].includes(provider)) {
364
364
  return { allowed: false, reason: `Unknown JSON specialist provider: ${provider}`, category: "json-specialist" };
365
365
  }
366
366
  const payloadBytes = Buffer.byteLength(JSON.stringify({ defaults: args.defaults || {}, items }), "utf8");