amicus 3.0.0 → 3.1.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +43 -0
- package/README.md +12 -3
- package/electron/setup-ui.js +27 -3
- package/package.json +1 -1
- package/skills/second-opinion/SKILL.md +2 -0
- package/skills/sidecar/SKILL.md +66 -38
- package/src/cli-handlers-resume-continue.js +31 -4
- package/src/cli-handlers-run.js +15 -4
- package/src/cli.js +13 -0
- package/src/mcp-server.js +99 -12
- package/src/mcp-tools.js +26 -4
- package/src/opencode-client.js +18 -2
- package/src/sidecar/continue.js +10 -3
- package/src/sidecar/fanout-leg.js +26 -1
- package/src/sidecar/fanout-output.js +5 -0
- package/src/sidecar/fanout-validate.js +81 -0
- package/src/sidecar/fanout.js +65 -77
- package/src/sidecar/session-utils.js +6 -0
- package/src/sidecar/setup.js +2 -1
- package/src/utils/alias-resolver.js +6 -35
- package/src/utils/api-key-store.js +1 -9
- package/src/utils/auth-json.js +1 -1
- package/src/utils/config.js +98 -16
- package/src/utils/curated-models.js +33 -4
- package/src/utils/gateway-router.js +115 -0
- package/src/utils/input-validators.js +12 -42
- package/src/utils/model-classification.js +65 -0
- package/src/utils/model-descriptor.js +72 -0
- package/src/utils/model-fetcher.js +35 -9
- package/src/utils/model-input-default.js +32 -0
- package/src/utils/model-validator.js +68 -84
- package/src/utils/provider-registry.js +57 -0
- package/src/utils/quick-picks.js +11 -3
- package/src/utils/result-schema-rebuild.js +98 -0
- package/src/utils/result-schema.js +6 -76
- package/src/utils/route-error.js +137 -0
- package/src/utils/route-launch.js +179 -0
- package/src/utils/start-helpers.js +96 -43
- package/src/utils/validators.js +1 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christian Wagner"
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [3.1.0] - 2026-07-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Direct-first gateway routing** (#61): bare `provider/model` model IDs (e.g. `anthropic/claude-opus-4-5`)
|
|
13
|
+
now route to your **direct** provider key when one is configured, falling back to OpenRouter only when
|
|
14
|
+
it isn't. An explicit `openrouter/...`-prefixed model ID remains a force-OpenRouter override — that
|
|
15
|
+
literal form never changes behavior.
|
|
16
|
+
- New `--gateway auto|direct|openrouter` CLI flag on `start`, `fanout`, and `continue` (`auto` is the
|
|
17
|
+
direct-first default) and a matching `gateway` enum on the MCP `amicus_start` / `amicus_fanout` /
|
|
18
|
+
`amicus_continue` tools.
|
|
19
|
+
- New `routing.prefer` config key (`"direct"` default | `"openrouter"`) sets the global default; the
|
|
20
|
+
per-call `--gateway`/`gateway` param overrides it for that run.
|
|
21
|
+
- Non-interactive CLI (`--json`) and MCP now emit a structured `model_route_error` (`type`, `field`,
|
|
22
|
+
`requested`, `reason`) instead of an ad hoc message when a request can't be routed — identical shape
|
|
23
|
+
on both surfaces.
|
|
24
|
+
- Interactive runs get a picker with alternatives when a direct route misses (e.g. key missing or model
|
|
25
|
+
not on that vendor's live catalog), instead of failing outright.
|
|
26
|
+
- Live Anthropic model fetcher: the model catalog now queries Anthropic's API directly for the current
|
|
27
|
+
model list, the same live-fetch treatment OpenAI and Google already had.
|
|
28
|
+
- Session provenance (resume/continue) preserves the gateway a run originally resolved to, even if keys
|
|
29
|
+
or `routing.prefer` change in between.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Default aliases for direct-capable vendors** (`openai`, `google`, `anthropic`, `deepseek`) now resolve
|
|
34
|
+
to bare canonical model IDs instead of `openrouter/...`-prefixed ones, so they participate in
|
|
35
|
+
direct-first routing out of the box. Gateway-only vendors (`qwen`, `grok`, `glm`, and other
|
|
36
|
+
OpenRouter-exclusive families) are unchanged — they still resolve through OpenRouter, since there's no
|
|
37
|
+
direct key path for them.
|
|
38
|
+
- **Migration:** if you hold both an OpenRouter key and a direct key for one of the four vendors above,
|
|
39
|
+
the next run against that vendor moves you to the direct route and prints a one-time notice; it's
|
|
40
|
+
silent after that. Set `routing.prefer: "openrouter"` in config (or pass `--gateway openrouter` /
|
|
41
|
+
`gateway: "openrouter"` per call) to keep routing everything through OpenRouter as before. Aliases you
|
|
42
|
+
already overrode via `amicus setup --add-alias` are untouched.
|
|
43
|
+
|
|
44
|
+
### Notes
|
|
45
|
+
|
|
46
|
+
- Builds on the #61 gateway-routing foundation (router core, resolution modes, key discovery) merged to
|
|
47
|
+
main ahead of this release; this release wires that router into the live launch path (CLI + MCP), adds
|
|
48
|
+
the control surface (`--gateway` / `gateway` / `routing.prefer`), and switches default guidance to the
|
|
49
|
+
direct-first form.
|
|
50
|
+
|
|
8
51
|
## [3.0.0] - 2026-07-15
|
|
9
52
|
|
|
10
53
|
### ⚠️ Breaking
|
package/README.md
CHANGED
|
@@ -319,7 +319,7 @@ $ amicus status demo123 --json
|
|
|
319
319
|
"taskId": "demo123",
|
|
320
320
|
"status": "complete",
|
|
321
321
|
"elapsed": "5m 0s",
|
|
322
|
-
"version": "3.
|
|
322
|
+
"version": "3.1.0",
|
|
323
323
|
"model": "google/gemini-2.5-flash",
|
|
324
324
|
"phase": "terminal"
|
|
325
325
|
}
|
|
@@ -338,7 +338,16 @@ amicus models # list the catalog
|
|
|
338
338
|
amicus models --search gemini # filter by substring
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
`start`/`fanout` validate your model against the catalog before launching (skip with `--no-validate-model`). You can also always bypass aliases and pass a full `provider/model` or `openrouter/provider/model`
|
|
341
|
+
`start`/`fanout` validate your model against the catalog before launching (skip with `--no-validate-model`). You can also always bypass aliases and pass a full model ID directly — bare `provider/model` (canonical) or `openrouter/provider/model` (explicit override); see Routing below. Catalog internals, alias management, and the full-id passthrough table are in **[docs/usage.md § Models](./docs/usage.md#amicus-models--the-model-catalog)**.
|
|
342
|
+
|
|
343
|
+
### Routing
|
|
344
|
+
|
|
345
|
+
- **Bare `provider/model`** (e.g. `openai/gpt-5.5`, `anthropic/claude-opus-4.8`, `google/gemini-3.5-flash`) is the canonical, policy-routed form — Amicus routes it **direct-first**: your direct provider key when one is configured, falling back to OpenRouter automatically when only an OpenRouter key exists.
|
|
346
|
+
- **`openrouter/provider/model`** is an explicit override that always forces OpenRouter, even when a direct key is present — reach for it deliberately, or for gateway-only vendors with no direct integration (Qwen, Grok, Mistral, GLM, …).
|
|
347
|
+
- **`--gateway auto|direct|openrouter`** (CLI, also on the MCP tools) overrides routing for one call; `auto` is the direct-first default. `routing.prefer` in `config.json` (`"direct"` by default, or `"openrouter"`) sets the global default.
|
|
348
|
+
- **One-time migration notice:** if you hold both an OpenRouter key and a direct key for a vendor, the first launch that resolves to that vendor under **`auto`** routing (the default) prints a one-time notice that routing moved to direct API; set `routing.prefer: "openrouter"` to restore the old all-OpenRouter behavior. The notice never fires when you explicitly pass `--gateway direct` — that's your own choice, not a migration.
|
|
349
|
+
|
|
350
|
+
Full details, the API-key/prefix table, and the migration notice are in **[docs/configuration.md](./docs/configuration.md)**.
|
|
342
351
|
|
|
343
352
|
---
|
|
344
353
|
|
|
@@ -417,7 +426,7 @@ Run `amicus doctor` first — it checks keys, catalog, OpenCode binary, Electron
|
|
|
417
426
|
| "council review this" does nothing | The `second-opinion` skill isn't installed | Check `~/.claude/skills/second-opinion/SKILL.md` exists; re-run `npm install -g amicus` (postinstall installs both skills) |
|
|
418
427
|
| `npm install -g amicus` fails with `EEXIST: … claude-sidecar` | The old upstream `claude-sidecar` package is still installed globally; npm won't overwrite another package's bin shims | `npm uninstall -g claude-sidecar`, then `npm install -g amicus`. Your keys and past sessions are not lost, but v2.0.0 no longer reads the old paths automatically — see [docs/SHIMS.md](./docs/SHIMS.md) for the one-time migration steps (rename `~/.config/sidecar/` and any `.claude/sidecar_sessions/` dirs). |
|
|
419
428
|
| Install fails partway, or `amicus doctor` reports the OpenCode binary "not found" | A **transient** error during the OpenCode engine's own postinstall (a spawn `ENOENT`, or an antivirus file-lock while it lays down its 11 per-platform binaries) can roll back the whole atomic install — retrying usually succeeds | Just re-run `npm install -g amicus`. If it still fails, clear the cache first: `npm cache clean --force && npm install -g amicus`. |
|
|
420
|
-
| `401` / auth error |
|
|
429
|
+
| `401` / auth error | No usable key for the model's vendor — bare `provider/model` ids fall back to `OPENROUTER_API_KEY` automatically, so this means neither the direct key nor an OpenRouter key is configured (or `--gateway direct`/`openrouter` forced a gateway whose key is missing) | Run `amicus setup`, or `amicus key <provider> <key>` to add the missing key; see [Routing](#routing). |
|
|
421
430
|
| `402` / "Payment Required" on first council review / `start` / `fanout` call | Your OpenRouter key is real but has no credit. Key save (`amicus key openrouter <key>` or the setup wizard's key step) only checks that the key **authenticates** — it doesn't check balance, so a zero-credit key saves cleanly and only fails later, on the first real model call. (The `amicus council` subcommand itself is deterministic math and never calls a model.) | Add credit at [openrouter.ai/credits](https://openrouter.ai/credits), **or** switch to a zero-cost council: `amicus setup` → option 2 (Free OpenRouter council) builds one from live `:free`-suffixed models and saves it as `councils.free` — then run `amicus fanout --council free …`. See "Free council (zero-cost)" under [The Council](#the-council) above. |
|
|
422
431
|
| Session not found | No session matches the given ID | Run `amicus list`, or omit `--session-id` to use the most recent. |
|
|
423
432
|
| No conversation history found | Project-path encoding | Check `~/.claude/projects/`; `/` and `_` in the project path are encoded as `-` in the directory name. |
|
package/electron/setup-ui.js
CHANGED
|
@@ -10,6 +10,7 @@ const { getDefaultAliases } = require('../src/utils/config');
|
|
|
10
10
|
const { getBrandName } = require('./toolbar');
|
|
11
11
|
const { resolveQuickPicks } = require('../src/utils/quick-picks');
|
|
12
12
|
const { PROVIDER_FAMILY_NAMES } = require('../src/utils/model-fetcher');
|
|
13
|
+
const { listDirectProviders } = require('../src/utils/provider-registry');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @param {object} [options={}]
|
|
@@ -32,6 +33,7 @@ function buildSetupHTML(options = {}) {
|
|
|
32
33
|
const providerNamesJson = JSON.stringify(PROVIDER_NAMES);
|
|
33
34
|
const defaultAliasesJson = JSON.stringify(getDefaultAliases());
|
|
34
35
|
const familyNamesJson = JSON.stringify(PROVIDER_FAMILY_NAMES);
|
|
36
|
+
const directProvidersJson = JSON.stringify(listDirectProviders());
|
|
35
37
|
return `<!DOCTYPE html>
|
|
36
38
|
<html><head><meta charset="utf-8"><title>Amicus Setup</title>
|
|
37
39
|
<style>${css}</style></head><body>
|
|
@@ -53,11 +55,11 @@ function buildSetupHTML(options = {}) {
|
|
|
53
55
|
</div>
|
|
54
56
|
</div>
|
|
55
57
|
<div class="footer"><div class="footer-brand"><svg width="15" height="15" viewBox="0 0 32 32" fill="none"><path d="M4 8H19"/><path d="M4 11H14L19 8"/><path d="M4 14H13L19 8"/><path d="M4 17H12L19 8"/><path d="M4 20H11L19 8"/><path d="M4 23H10L19 8"/><path class="brand-main" d="M19 8H28"/></svg> ${brandName}</div><div class="footer-nav"><button class="nav-btn" id="back-btn" style="display:none">Back</button><button class="nav-btn primary" id="next-btn" disabled>Next</button><button class="nav-btn primary" id="finish-btn" style="display:none">Finish</button></div></div>
|
|
56
|
-
${buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson, familyNamesJson)}
|
|
58
|
+
${buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson, familyNamesJson, directProvidersJson)}
|
|
57
59
|
</body></html>`;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson, familyNamesJson) {
|
|
62
|
+
function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson, familyNamesJson, directProvidersJson) {
|
|
61
63
|
const keysJs = buildKeysScript();
|
|
62
64
|
const aliasJs = buildAliasScript();
|
|
63
65
|
const councilJs = buildCouncilScript();
|
|
@@ -72,7 +74,9 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
72
74
|
var providerNamesData = ${providerNamesJson};
|
|
73
75
|
var defaultAliases = ${defaultAliasesJson};
|
|
74
76
|
var PROVIDER_FAMILY_NAMES = ${familyNamesJson};
|
|
77
|
+
var directProviders = ${directProvidersJson};
|
|
75
78
|
var routingChoices = {};
|
|
79
|
+
var explicitRouteChoices = {};
|
|
76
80
|
var aliasEdits = {};
|
|
77
81
|
var aliasDisplay = {};
|
|
78
82
|
window.availableModels = null;
|
|
@@ -195,6 +199,21 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
195
199
|
} else { nextBtn.disabled = false; }
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
// #61: an auto-selected (non-explicit) openrouter/<vendor>/<model> route
|
|
203
|
+
// whose vendor also has a direct integration must be stored bare
|
|
204
|
+
// (<vendor>/<model>) so the gateway router can policy-route it direct-first;
|
|
205
|
+
// a stored openrouter/... string is treated as an explicit force-OpenRouter
|
|
206
|
+
// literal and never reconsiders direct-first. Mirrors
|
|
207
|
+
// src/utils/curated-models.js's toCanonicalDefault exactly. Gateway-only
|
|
208
|
+
// vendors (not in directProviders, e.g. qwen/grok/glm/...) pass through
|
|
209
|
+
// unchanged since OpenRouter is their only route anyway.
|
|
210
|
+
function toBareIfDirect(route) {
|
|
211
|
+
if (typeof route !== 'string' || route.indexOf('openrouter/') !== 0) { return route; }
|
|
212
|
+
var rest = route.slice('openrouter/'.length);
|
|
213
|
+
var vendor = rest.split('/')[0];
|
|
214
|
+
return directProviders.indexOf(vendor) !== -1 ? rest : route;
|
|
215
|
+
}
|
|
216
|
+
|
|
198
217
|
// Single source of the route choice for a quick-pick row: explicit pill
|
|
199
218
|
// choice if its key still exists, else first provider with a key, else
|
|
200
219
|
// the row's first route. Returns the full model id or null.
|
|
@@ -209,7 +228,11 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
209
228
|
}
|
|
210
229
|
if (!prov) { prov = provs[0]; }
|
|
211
230
|
}
|
|
212
|
-
|
|
231
|
+
var route = mc.routes[prov] || null;
|
|
232
|
+
// Only canonicalize auto-picks; an explicit "via OpenRouter" pill click
|
|
233
|
+
// is a deliberate choice and is returned unchanged.
|
|
234
|
+
if (route && !explicitRouteChoices[mc.alias]) { route = toBareIfDirect(route); }
|
|
235
|
+
return route;
|
|
213
236
|
}
|
|
214
237
|
|
|
215
238
|
function updateRoutingPills() {
|
|
@@ -367,6 +390,7 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
367
390
|
var provider = pill.getAttribute('data-provider');
|
|
368
391
|
if (!alias || !provider) { return; }
|
|
369
392
|
routingChoices[alias] = provider;
|
|
393
|
+
explicitRouteChoices[alias] = true;
|
|
370
394
|
var toggle = pill.parentElement;
|
|
371
395
|
toggle.querySelectorAll('.route-pill').forEach(function(p) { p.classList.toggle('active', p === pill); });
|
|
372
396
|
updateWritePreviews();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
6
6
|
"keywords": [
|
|
@@ -523,6 +523,8 @@ Use these together with `amicus council stats` (the ledger — authoritative qua
|
|
|
523
523
|
|
|
524
524
|
Always **rank recommendations by fit**, state the trade-off for each option, and surface the estimated cost (an estimate, not a guarantee; unpriced legs disclosed as "cost unknown"). Never present a single option without explanation.
|
|
525
525
|
|
|
526
|
+
**Model naming for council members.** Name bench members by alias (`gemini`, `gpt`, `deepseek`, `opus`, …) or by full `provider/model` id — both work with `--models`/`amicus_fanout`. A bare canonical id (e.g. `anthropic/claude-opus-4.8`) is policy-routed **direct-first**: Amicus uses the user's direct provider key when one is configured, falling back to OpenRouter automatically. `openrouter/provider/model` is an explicit force-OpenRouter override — reach for it only when the user deliberately wants a specific member to run through OpenRouter (e.g. to use a free-tier variant), or for gateway-only vendors with no direct integration. A per-run `--gateway auto|direct|openrouter` (also on `amicus_fanout`/`amicus_start`) overrides routing for the whole wave if the user asks for it; leave it unset (`auto`) by default.
|
|
527
|
+
|
|
526
528
|
---
|
|
527
529
|
|
|
528
530
|
## Output & naming
|
package/skills/sidecar/SKILL.md
CHANGED
|
@@ -75,9 +75,19 @@ Amicus uses the OpenCode SDK to communicate with LLM providers. You need to conf
|
|
|
75
75
|
|
|
76
76
|
**Recommended:** run `amicus setup` — a guided wizard that stores keys in `~/.config/amicus/.env`, picks your default model from the live catalog, and seeds aliases. The options below are manual alternatives.
|
|
77
77
|
|
|
78
|
+
### Model Naming: Bare Canonical Is the Normal Form
|
|
79
|
+
|
|
80
|
+
Read this before picking a model string — it decides both routing and which credentials get used:
|
|
81
|
+
|
|
82
|
+
- **Bare `provider/model`** (e.g. `openai/gpt-5.5`, `anthropic/claude-opus-4.8`, `google/gemini-3.5-flash`) is the **canonical, policy-routed** form — use it by default. Amicus routes it **direct-first**: your direct provider key when one is configured, falling back to OpenRouter automatically when only `OPENROUTER_API_KEY` exists. You never pick the gateway by hand.
|
|
83
|
+
- **`openrouter/provider/model`** (e.g. `openrouter/google/gemini-3.5-flash`) is an **explicit force-OpenRouter override** — it always routes through OpenRouter even when a direct key is present. Reach for it deliberately, or for gateway-only vendors with no direct integration at all (Qwen, Grok, Mistral, GLM, MiniMax, Kimi, Seed, …) — those *require* the `openrouter/` prefix.
|
|
84
|
+
- Short aliases (`gemini`, `gpt`, `opus`, `claude`, `deepseek`, …) resolve to one of the two forms above via your config; `amicus setup` seeds direct-capable vendors to the bare canonical form automatically.
|
|
85
|
+
- **Routing controls:** `routing.prefer` in `~/.config/amicus/config.json` sets the global default — `"direct"` (the default) or `"openrouter"` to always prefer OpenRouter. Per call, `--gateway auto|direct|openrouter` overrides it (`auto` = direct-first, same as the default).
|
|
86
|
+
- **One-time migration notice:** if you hold both an OpenRouter key and a direct key for a vendor, the first launch that resolves to that vendor prints a one-time notice that routing moved to direct API (previously OpenRouter); set `routing.prefer: "openrouter"` (or pass `--gateway openrouter`) to restore the old all-OpenRouter behavior.
|
|
87
|
+
|
|
78
88
|
### Option A: OpenRouter (Recommended for Multi-Model Access)
|
|
79
89
|
|
|
80
|
-
OpenRouter provides unified access to many models (Gemini, GPT
|
|
90
|
+
OpenRouter provides unified access to many models (Gemini, GPT, Claude, o3, etc.) with a single API key. If you'd rather not hand-edit JSON, run `amicus setup` instead — it stores the key for you.
|
|
81
91
|
|
|
82
92
|
**Step 1: Get an OpenRouter API key**
|
|
83
93
|
- Sign up at https://openrouter.ai
|
|
@@ -103,12 +113,11 @@ EOF
|
|
|
103
113
|
amicus start --model gemini --prompt "Say hello" --no-ui
|
|
104
114
|
```
|
|
105
115
|
|
|
106
|
-
**
|
|
107
|
-
When using OpenRouter, prefix the model with `openrouter/`:
|
|
116
|
+
**With OpenRouter as your only key**, aliases and bare canonical ids both work — the router falls back to OpenRouter automatically since no direct key exists for the vendor:
|
|
108
117
|
```bash
|
|
109
|
-
amicus start --model gemini --prompt "..."
|
|
110
|
-
amicus start --model
|
|
111
|
-
amicus start --model
|
|
118
|
+
amicus start --model gemini --prompt "..." # alias
|
|
119
|
+
amicus start --model google/gemini-3.5-flash --prompt "..." # bare canonical, falls back to OpenRouter
|
|
120
|
+
amicus start --model openrouter/google/gemini-3.5-flash --prompt "..." # explicit override — same result here
|
|
112
121
|
```
|
|
113
122
|
|
|
114
123
|
### Option B: Direct API Keys (Provider-Specific)
|
|
@@ -136,8 +145,8 @@ Add these to your shell profile (`~/.bashrc`, `~/.zshrc`) for persistence.
|
|
|
136
145
|
- Run `amicus setup` to store keys in amicus's config (recommended)
|
|
137
146
|
- Move your exports to `~/.zshenv` (sourced by all zsh shell types)
|
|
138
147
|
|
|
139
|
-
**Model names with direct
|
|
140
|
-
|
|
148
|
+
**Model names with a direct key configured:**
|
|
149
|
+
Use the bare canonical `provider/model` form — direct-first routing picks up these keys automatically, no `openrouter/` prefix needed:
|
|
141
150
|
```bash
|
|
142
151
|
amicus start --model google/<model-name> --prompt "..."
|
|
143
152
|
amicus start --model openai/<model-name> --prompt "..."
|
|
@@ -146,18 +155,15 @@ amicus start --model anthropic/<model-name> --prompt "..."
|
|
|
146
155
|
|
|
147
156
|
### Model Naming Summary
|
|
148
157
|
|
|
149
|
-
|
|
|
150
|
-
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
| Direct OpenAI API | `openai/model` | `openai/<model-name>` |
|
|
154
|
-
| Direct Anthropic API | `anthropic/model` | `anthropic/<model-name>` |
|
|
158
|
+
| Form | Meaning | Credentials used |
|
|
159
|
+
|------|---------|-------------------|
|
|
160
|
+
| `provider/model` (bare, canonical) | Policy-routed, direct-first | Direct provider key if configured, else `OPENROUTER_API_KEY` |
|
|
161
|
+
| `openrouter/provider/model` | Explicit force-OpenRouter override (required for gateway-only vendors) | `OPENROUTER_API_KEY`, always |
|
|
155
162
|
|
|
156
|
-
**Important:**
|
|
157
|
-
- `
|
|
158
|
-
- `
|
|
159
|
-
- `
|
|
160
|
-
- `anthropic/...` → Uses `ANTHROPIC_API_KEY` environment variable
|
|
163
|
+
**Important:** the bare form is not tied to one credential — the router decides at launch time:
|
|
164
|
+
- `google/...`, `openai/...`, `anthropic/...`, `deepseek/...` (bare) → that vendor's direct key if present, else `OPENROUTER_API_KEY`
|
|
165
|
+
- `openrouter/...` → always `OPENROUTER_API_KEY`
|
|
166
|
+
- `--gateway direct` forces the direct key (errors if it's missing); `--gateway openrouter` forces OpenRouter (errors if that key is missing); omit `--gateway` (or pass `auto`) for the default direct-first behavior described above
|
|
161
167
|
|
|
162
168
|
---
|
|
163
169
|
|
|
@@ -252,6 +258,10 @@ amicus start \
|
|
|
252
258
|
- `--json`: With `--no-ui`, emit the run result as one stable JSON document on stdout
|
|
253
259
|
(`schemaVersion: 2`; the `summary` field is the model's output).
|
|
254
260
|
- `--no-validate-model`: Skip the model-catalog pre-flight check (validation is on by default).
|
|
261
|
+
- `--gateway <mode>`: Routing override for this launch — `auto` (default, direct-first: your
|
|
262
|
+
direct provider key when present, else OpenRouter), `direct` (require a direct provider key,
|
|
263
|
+
errors if missing), or `openrouter` (force OpenRouter, errors if that key is missing). Omit to
|
|
264
|
+
fall back to the `routing.prefer` config default (`"direct"` unless changed).
|
|
255
265
|
- `--agent <agent>`: Agent mode (controls tool permissions). If omitted, defaults to
|
|
256
266
|
**Chat** in interactive mode and **Build** in headless (`--no-ui`) mode — `chat`
|
|
257
267
|
stalls without user interaction, so headless runs need an agent that doesn't wait
|
|
@@ -281,17 +291,26 @@ The CLI validates all inputs **before** launching the sidecar. Invalid inputs fa
|
|
|
281
291
|
| `--timeout` | Must be positive number | `Error: --timeout must be a positive number` |
|
|
282
292
|
| `--context-turns` | Must be positive number | `Error: --context-turns must be a positive number` |
|
|
283
293
|
| `--context-since` | Must match format: `30m`, `2h`, `1d` | `Error: --context-since must be in format: 30m, 2h, or 1d` |
|
|
284
|
-
|
|
|
294
|
+
| `--gateway` | If provided, must be one of `auto`, `direct`, `openrouter` | `Error: --gateway must be one of: auto, direct, openrouter` |
|
|
295
|
+
| API Key | The router must be able to resolve *some* usable key for the requested model (see below) | Reason sentence + fix hint, e.g. `No API key was found for this vendor via any gateway.` / `Add a provider key or an OpenRouter key.` |
|
|
285
296
|
|
|
286
297
|
**API Key Requirements by Provider:**
|
|
287
298
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
|
294
|
-
|
|
299
|
+
Bare canonical ids (`google/...`, `openai/...`, `anthropic/...`, `deepseek/...`) are policy-routed —
|
|
300
|
+
the direct key for that vendor if configured, else `OPENROUTER_API_KEY` as fallback. Only
|
|
301
|
+
`openrouter/...` (the explicit override) and gateway-only vendors (Qwen, Grok, Mistral, GLM, …)
|
|
302
|
+
strictly require `OPENROUTER_API_KEY`:
|
|
303
|
+
|
|
304
|
+
| Model Prefix | Env Var Amicus Will Use | Notes |
|
|
305
|
+
|--------------|--------------------------|-------|
|
|
306
|
+
| `openrouter/...` | `OPENROUTER_API_KEY` | Always required for this form |
|
|
307
|
+
| `google/...` (bare) | `GOOGLE_GENERATIVE_AI_API_KEY`, else `OPENROUTER_API_KEY` | Errors only if neither is set |
|
|
308
|
+
| `openai/...` (bare) | `OPENAI_API_KEY`, else `OPENROUTER_API_KEY` | Errors only if neither is set |
|
|
309
|
+
| `anthropic/...` (bare) | `ANTHROPIC_API_KEY`, else `OPENROUTER_API_KEY` | Errors only if neither is set |
|
|
310
|
+
| `deepseek/...` (bare) | `DEEPSEEK_API_KEY`, else `OPENROUTER_API_KEY` | Errors only if neither is set |
|
|
311
|
+
|
|
312
|
+
`--gateway direct` narrows this to "the direct key specifically" (errors if only OpenRouter is
|
|
313
|
+
configured); `--gateway openrouter` narrows it to `OPENROUTER_API_KEY` specifically.
|
|
295
314
|
|
|
296
315
|
**Handling validation errors:**
|
|
297
316
|
|
|
@@ -310,8 +329,8 @@ amicus start --model gemini --prompt "Task" --agent Build
|
|
|
310
329
|
# Fix: Provide a non-empty briefing
|
|
311
330
|
amicus start --model gemini --prompt "Detailed task description"
|
|
312
331
|
|
|
313
|
-
# Error:
|
|
314
|
-
# Fix:
|
|
332
|
+
# Error: No API key was found for this vendor via any gateway.
|
|
333
|
+
# Fix: add a direct provider key or an OpenRouter key
|
|
315
334
|
export OPENROUTER_API_KEY=sk-or-your-key
|
|
316
335
|
amicus start --model gemini --prompt "Task"
|
|
317
336
|
```
|
|
@@ -325,10 +344,11 @@ amicus fanout --models "gemini,gpt,deepseek" --prompt-file ./briefing.md --json
|
|
|
325
344
|
Runs the same prompt on every listed model in parallel (one shared engine server, headless),
|
|
326
345
|
then prints ONE JSON wave document: `status` (`complete`|`partial`|`error`), `counts`, and
|
|
327
346
|
`legs[]` where each leg's `summary` is that model's answer. Exit codes: 0 complete, 2 partial,
|
|
328
|
-
1 error/aborted. Aliases and full `provider/model` IDs both work
|
|
347
|
+
1 error/aborted. Aliases and full `provider/model` IDs both work — bare canonical ids route
|
|
348
|
+
direct-first, `openrouter/provider/model` forces OpenRouter; duplicates are allowed
|
|
329
349
|
(distinct legs). `--wave-id <id>` pins leg IDs to `<id>-1..N`. Shared per-leg knobs: `--agent`,
|
|
330
350
|
`--thinking`, `--timeout`, `--summary-length`, `--no-context`, `--context-*`, `--mcp*`,
|
|
331
|
-
`--no-validate-model`, `--cwd`.
|
|
351
|
+
`--no-validate-model`, `--gateway`, `--cwd`.
|
|
332
352
|
|
|
333
353
|
### Inspect the Model Catalog
|
|
334
354
|
|
|
@@ -386,7 +406,10 @@ Starts a **new** sidecar session that inherits the old session's conversation as
|
|
|
386
406
|
- `--prompt`: New task description for the continuation
|
|
387
407
|
|
|
388
408
|
**Optional:**
|
|
389
|
-
- `--model <model>`: Override model (defaults to original session's model)
|
|
409
|
+
- `--model <model>`: Override model (defaults to original session's model) — bare canonical ids
|
|
410
|
+
route direct-first; `openrouter/provider/model` forces OpenRouter
|
|
411
|
+
- `--gateway <mode>`: Routing override for the new session (`auto`|`direct`|`openrouter`), same
|
|
412
|
+
semantics as `start`. Only meaningful when `--model` is also given
|
|
390
413
|
- `--context-turns <N>`: Max turns from previous session to include (default: 50)
|
|
391
414
|
- `--context-max-tokens <N>`: Max tokens for context (default: 80000)
|
|
392
415
|
- `--no-ui`: Run in autonomous mode
|
|
@@ -427,7 +450,9 @@ Use short aliases (run `amicus models` to see the live catalog, and `amicus mode
|
|
|
427
450
|
- `--model deepseek` -- DeepSeek
|
|
428
451
|
- Omit `--model` entirely to use your configured default
|
|
429
452
|
|
|
430
|
-
Full model strings also work: `--model
|
|
453
|
+
Full model strings also work: `--model provider/model-id` (bare canonical, direct-first — e.g.
|
|
454
|
+
`--model anthropic/claude-opus-4.8`) or `--model openrouter/provider/model-id` (explicit
|
|
455
|
+
force-OpenRouter override).
|
|
431
456
|
|
|
432
457
|
### Verifying Model Names
|
|
433
458
|
|
|
@@ -891,8 +916,8 @@ echo $ANTHROPIC_API_KEY # For Anthropic models
|
|
|
891
916
|
### "401 Unauthorized" or authentication errors
|
|
892
917
|
|
|
893
918
|
1. Verify you're using the correct model name format:
|
|
894
|
-
-
|
|
895
|
-
-
|
|
919
|
+
- Bare canonical (policy-routed, direct-first): `provider/model`
|
|
920
|
+
- Explicit force-OpenRouter override: `openrouter/provider/model`
|
|
896
921
|
|
|
897
922
|
2. Check your API key is valid and has credits
|
|
898
923
|
|
|
@@ -967,11 +992,14 @@ amicus start --model ... --prompt "..." --agent Explore
|
|
|
967
992
|
amicus start --model ... --prompt "..." --agent MyCustomAgent
|
|
968
993
|
```
|
|
969
994
|
|
|
970
|
-
**"
|
|
995
|
+
**"No API key was found for this vendor via any gateway." / "No API key is configured for this vendor's direct API."**
|
|
971
996
|
|
|
972
|
-
|
|
997
|
+
A bare canonical id needs *some* usable key for the vendor — either its direct key or
|
|
998
|
+
`OPENROUTER_API_KEY` as a fallback. `--gateway direct` narrows that to "the direct key
|
|
999
|
+
specifically" (fails if only OpenRouter is configured); `--gateway openrouter` narrows it to
|
|
1000
|
+
`OPENROUTER_API_KEY` specifically:
|
|
973
1001
|
```bash
|
|
974
|
-
#
|
|
1002
|
+
# OPENROUTER_API_KEY is the fallback for every direct-capable vendor
|
|
975
1003
|
export OPENROUTER_API_KEY=sk-or-your-key
|
|
976
1004
|
|
|
977
1005
|
# For Google models (google/...)
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const { resolveLaunchModel } = require('./utils/start-helpers');
|
|
14
14
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
15
15
|
const { requireNoUiForJson, requireValidTaskId } = require('./utils/cli-preflight');
|
|
16
|
+
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Handle 'amicus resume' command
|
|
@@ -70,11 +71,35 @@ async function handleContinue(args) {
|
|
|
70
71
|
|
|
71
72
|
requireNoUiForJson(args, useJson);
|
|
72
73
|
|
|
73
|
-
//
|
|
74
|
+
// FIX 4 (#61 whole-branch review, cheap parity): handleStart validates
|
|
75
|
+
// --gateway via validateStartArgs (cli.js) — continue never did, so a
|
|
76
|
+
// typo'd value silently fell through to resolveGatewayMode's pass-through
|
|
77
|
+
// instead of failing fast with a clear error. Checked unconditionally
|
|
78
|
+
// (not just when --model is also given) since --gateway alone is still a
|
|
79
|
+
// user-facing typo worth catching.
|
|
80
|
+
if (args.gateway !== undefined && !GATEWAY_MODES.includes(args.gateway)) {
|
|
81
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` }));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// F5/#61 Task 7.3: an explicitly passed --model routes through the gateway
|
|
85
|
+
// router exactly like start (resolveLaunchModel), so --gateway / direct-first
|
|
86
|
+
// policy / structured route errors apply here too. The NO-`--model` case
|
|
87
|
+
// (inherit the prior session's model) is unchanged below — that IS the
|
|
88
|
+
// "preserve prior route" behavior: the prior concrete id is reused verbatim,
|
|
89
|
+
// never re-resolved.
|
|
90
|
+
// #61 Task 5.2 (best-effort provenance): stash the freshly-resolved
|
|
91
|
+
// gateway/resolutionVersion so the NEW session's metadata can record them.
|
|
92
|
+
// These stay undefined on the inherit-prior-model path (no fresh routing
|
|
93
|
+
// happened there), so the metadata writer simply omits them — the prior
|
|
94
|
+
// concrete model id being reused verbatim already prevents a silent gateway
|
|
95
|
+
// change for a continued session without a fresh --model.
|
|
96
|
+
let routeGateway;
|
|
97
|
+
let routeResolutionVersion;
|
|
74
98
|
if (args.model !== undefined) {
|
|
75
|
-
const { model,
|
|
99
|
+
const { model, gateway, provenance } = await resolveLaunchModel(args);
|
|
76
100
|
args.model = model;
|
|
77
|
-
|
|
101
|
+
routeGateway = gateway;
|
|
102
|
+
routeResolutionVersion = provenance && provenance.resolutionVersion;
|
|
78
103
|
}
|
|
79
104
|
|
|
80
105
|
const { continueAmicus } = require('./index');
|
|
@@ -91,6 +116,8 @@ async function handleContinue(args) {
|
|
|
91
116
|
headless: args['no-ui'],
|
|
92
117
|
timeout: args.timeout,
|
|
93
118
|
json: useJson,
|
|
119
|
+
gateway: routeGateway,
|
|
120
|
+
resolutionVersion: routeResolutionVersion,
|
|
94
121
|
});
|
|
95
122
|
} catch (err) {
|
|
96
123
|
// Same rationale as handleResume above: continueSidecar/loadPreviousSession
|
package/src/cli-handlers-run.js
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
const { validateStartArgs } = require('./cli');
|
|
16
16
|
const { validateTaskId } = require('./utils/validators');
|
|
17
|
-
const {
|
|
17
|
+
const { resolveLaunchModel } = require('./utils/start-helpers');
|
|
18
18
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
19
19
|
const { requireNoUiForJson } = require('./utils/cli-preflight');
|
|
20
|
+
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Handle 'sidecar start' command
|
|
@@ -43,9 +44,8 @@ async function handleStart(args) {
|
|
|
43
44
|
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --max-cost must be a positive number' }));
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
const { model, alias } =
|
|
47
|
+
const { model, alias } = await resolveLaunchModel(args);
|
|
47
48
|
args.model = model;
|
|
48
|
-
args.model = await validateFallbackModel(args, alias);
|
|
49
49
|
|
|
50
50
|
// Normalize agent: --agent takes precedence, otherwise use --mode
|
|
51
51
|
args.agent = args.agent || args.mode;
|
|
@@ -108,6 +108,14 @@ async function handleStart(args) {
|
|
|
108
108
|
async function handleFanout(args) {
|
|
109
109
|
const useJson = !!args.json;
|
|
110
110
|
|
|
111
|
+
// FIX 4 (#61 whole-branch review, cheap parity): handleStart validates
|
|
112
|
+
// --gateway via validateStartArgs (cli.js) — fanout never did, so a typo'd
|
|
113
|
+
// value silently fell through to resolveGatewayMode's pass-through instead
|
|
114
|
+
// of failing fast with a clear error.
|
|
115
|
+
if (args.gateway !== undefined && !GATEWAY_MODES.includes(args.gateway)) {
|
|
116
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` }));
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
112
120
|
const promptRes = resolvePromptSource(args);
|
|
113
121
|
if (promptRes.error) {
|
|
@@ -161,7 +169,7 @@ async function handleFanout(args) {
|
|
|
161
169
|
|
|
162
170
|
// Direct require — the src/index.js public re-export is added later (Task 13)
|
|
163
171
|
const { runFanout } = require('./sidecar/fanout');
|
|
164
|
-
const { loadConfig } = require('./utils/config');
|
|
172
|
+
const { loadConfig, resolveGatewayMode } = require('./utils/config');
|
|
165
173
|
const cfg = loadConfig() || {};
|
|
166
174
|
const { exitCode } = await runFanout({
|
|
167
175
|
models: args.models,
|
|
@@ -187,6 +195,9 @@ async function handleFanout(args) {
|
|
|
187
195
|
noMcp: args['no-mcp'],
|
|
188
196
|
excludeMcp: args['exclude-mcp'],
|
|
189
197
|
noValidateModel: args['no-validate-model'],
|
|
198
|
+
// #61 Task 7.3: --gateway merged with routing.prefer, applied per leg
|
|
199
|
+
// by validateFanoutModels' router call.
|
|
200
|
+
gatewayMode: resolveGatewayMode(args.gateway),
|
|
190
201
|
json: !!args.json,
|
|
191
202
|
client: args.client,
|
|
192
203
|
maxCost: args['max-cost'] !== null && args['max-cost'] !== undefined ? args['max-cost'] : cfg.maxCost,
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
} = require('./utils/validators');
|
|
19
19
|
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
20
20
|
const { logger } = require('./utils/logger');
|
|
21
|
+
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Default values per spec §4.1
|
|
@@ -245,6 +246,15 @@ function validateStartArgs(args) {
|
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
// Validate --gateway (if provided) — #61 Task 7.1. resolveGatewayMode()
|
|
250
|
+
// already treats any non-'direct'/'openrouter' string as a pass-through
|
|
251
|
+
// (effectively silent auto fallback), so this pre-flight check exists
|
|
252
|
+
// purely to catch typos with a clear error instead of letting them slip
|
|
253
|
+
// through unnoticed.
|
|
254
|
+
if (args.gateway !== undefined && !GATEWAY_MODES.includes(args.gateway)) {
|
|
255
|
+
return { valid: false, error: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` };
|
|
256
|
+
}
|
|
257
|
+
|
|
248
258
|
// Validate MCP spec format (if provided)
|
|
249
259
|
const mcpCheck = validateMcpSpec(args.mcp);
|
|
250
260
|
if (!mcpCheck.valid) {
|
|
@@ -401,6 +411,7 @@ Options for 'start':
|
|
|
401
411
|
--exclude-mcp <name> Exclude specific MCP server (repeatable)
|
|
402
412
|
--validate-model (Deprecated: validation is on by default)
|
|
403
413
|
--no-validate-model Skip model-catalog validation before launch
|
|
414
|
+
--gateway <mode> Routing: auto (direct-first), direct, or openrouter
|
|
404
415
|
--position <pos> Window position: right (default), left, center
|
|
405
416
|
`,
|
|
406
417
|
fanout: `
|
|
@@ -415,6 +426,7 @@ Options for 'fanout':
|
|
|
415
426
|
--json Emit the wave result as stable JSON on stdout
|
|
416
427
|
--max-cost <$> Refuse the wave if the estimated total exceeds $ (soft ceiling)
|
|
417
428
|
--no-cost-gate Disable the budget gate (per-$/Mtok threshold + ceiling) for this run
|
|
429
|
+
--gateway <mode> Routing: auto (direct-first), direct, or openrouter
|
|
418
430
|
Shared per-leg knobs: --agent, --thinking, --timeout, --summary-length,
|
|
419
431
|
--no-context, --context-*, --mcp*, --no-validate-model, --cwd
|
|
420
432
|
Exit codes: 0 all legs complete, 2 partial, 1 none complete / hard failure
|
|
@@ -456,6 +468,7 @@ Options for 'continue':
|
|
|
456
468
|
<task_id> Required. Session to build on (positional)
|
|
457
469
|
--prompt <text> Required. Briefing for the new session
|
|
458
470
|
--model <model> Optional. Override the model (alias or provider/model)
|
|
471
|
+
--gateway <mode> Routing when --model is given: auto (direct-first), direct, or openrouter
|
|
459
472
|
--cwd <path> Project directory (default: cwd)
|
|
460
473
|
--no-ui Run without GUI (autonomous mode)
|
|
461
474
|
--json With --no-ui: emit the run result as stable JSON
|