amicus 4.1.0 → 4.1.2
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 +113 -0
- package/README.md +2 -2
- package/package.json +3 -2
- package/skills/second-opinion/SKILL.md +1 -1
- package/skills/sidecar/SKILL.md +5 -5
- package/src/cli-handlers-council.js +1 -1
- package/src/council/report.js +14 -2
- package/src/council/run-debate.js +6 -2
- package/src/council/run.js +5 -1
- package/src/mcp-tools.js +2 -2
- package/src/sidecar/setup.js +2 -3
- package/src/utils/config.js +29 -9
- package/src/utils/curated-models.js +22 -20
- package/src/utils/model-fetcher.js +6 -0
- package/src/utils/quick-picks.js +35 -12
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
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,119 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.1.2] - 2026-07-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Claude model aliases no longer drift between the direct API and OpenRouter.** The two
|
|
13
|
+
gateways spell the same model differently — OpenRouter serves `anthropic/claude-opus-4.8`,
|
|
14
|
+
the direct Anthropic API serves `anthropic/claude-opus-4-8` — and three places converted
|
|
15
|
+
between the two by adding or removing the `openrouter/` prefix. That is only sound when the
|
|
16
|
+
rest of the id is identical, which for Claude it is not. Two user-visible consequences: a fresh
|
|
17
|
+
`amicus setup` wrote an `opus` alias that `amicus doctor` then reported as stale (the exact
|
|
18
|
+
warning 4.1.1 shipped to remove — and it fired even with an Anthropic-only catalog, not just
|
|
19
|
+
for OpenRouter users), and the pre-registered fallback catalog handed to a long-lived shared
|
|
20
|
+
server carried two ids OpenRouter does not serve while omitting the two it does. All three
|
|
21
|
+
sites now read each gateway's authored route instead of deriving one from the other, and an
|
|
22
|
+
alias you have overridden yourself is left alone rather than inheriting a curated route.
|
|
23
|
+
Affects `opus` and `haiku`; every other alias is spelled identically on both gateways or is
|
|
24
|
+
OpenRouter-only. Default routing is direct-first and was never affected — no run selected a
|
|
25
|
+
wrong model.
|
|
26
|
+
|
|
27
|
+
## [4.1.1] - 2026-07-21
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **The integration tier is tested again, and now something actually watches it.** All 14
|
|
32
|
+
`tests/**/*.integration.test.js` files were unreachable from every gate — `jest.config.js` excludes
|
|
33
|
+
them from `npm test`, no workflow ran `npm run test:integration`, and the pre-push hook ran only the
|
|
34
|
+
unit suite. Six tests had been failing unnoticed behind a single dead model alias: three E2E suites
|
|
35
|
+
passed `--model gemini-flash`, which is not a live alias (`tryResolveModel('gemini-flash')` returns
|
|
36
|
+
`Unknown model alias`). They now pass `gemini`.
|
|
37
|
+
- **`--claude-review` reports no longer grow a blank `claude` judge column, or a false self-vote `*`.**
|
|
38
|
+
`report.js`'s `toModel()` reused `verdict.council` (the street-cred universe, which legitimately
|
|
39
|
+
includes `claude` on a `--claude-review` run) as the adjudication-matrix judge roster too. Claude is
|
|
40
|
+
judged but never judges, so the matrix grew an extra column that always rendered blank, or a bare
|
|
41
|
+
`*` on a row Claude itself raised — asserting Claude voted for its own finding, the opposite of the
|
|
42
|
+
documented guarantee. `judges` is now filtered out of `council` independently (gated on the
|
|
43
|
+
`claudeInCouncil` flag plus the `claude` name, never on whether a model cast any adjudications, so a
|
|
44
|
+
genuinely dead/unstructured bench judge with zero votes still keeps its blank column).
|
|
45
|
+
- **`debate-revote` now checkpoints `skipped`, not a false `complete`, when the re-vote wave never
|
|
46
|
+
launches** (nothing was defended/amended, or the cost ceiling skipped it). **This is a
|
|
47
|
+
consumer-visible `run.json` value change**: anything parsing `stages[].status` for this stage — the
|
|
48
|
+
Council Review Action's stage-ladder footer, `amicus status`, or a user script — now sees `skipped`
|
|
49
|
+
instead of `complete` on a run that hits this path. Neither the Action's footer nor `amicus
|
|
50
|
+
status`'s human/JSON renderers special-case `complete`, so both already print whatever value is
|
|
51
|
+
there correctly; a script that hard-coded an expectation of `complete` for this stage should treat
|
|
52
|
+
`skipped` as the equivalent no-op, the same way `run-chair.js`'s existing chair-skipped-over-budget
|
|
53
|
+
convention already works.
|
|
54
|
+
- **`amicus council verdict --render` now writes `report.html` at `0o600`**, matching every sibling
|
|
55
|
+
writer (`run-assemble.js`, `mcp-server.js`'s `amicus_verdict` `render:true` path, `run-launch.js`).
|
|
56
|
+
It previously wrote with no explicit mode, which falls back to the process umask (typically `0o644`
|
|
57
|
+
on POSIX — group/world-readable) whenever the target directory had no pre-existing `report.html`,
|
|
58
|
+
i.e. exactly the fresh-`--out-dir` case, for a file that holds model output.
|
|
59
|
+
- **Dropped the false `amicus wait` CLI claim.** `docs/usage.md` and `docs/council.md` both documented
|
|
60
|
+
`amicus status|wait|abort <councilRunId>` as working CLI commands. There is no CLI `wait` (verified
|
|
61
|
+
against `bin/amicus.js`'s command dispatch) — only `status` and `abort` genuinely resolve council
|
|
62
|
+
runs via the sessions-dir pointer file. Both docs now point readers at the MCP `amicus_wait` tool
|
|
63
|
+
instead.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **Keyless integration job in CI.** `.github/workflows/ci.yml` gained an `integration` job that runs
|
|
68
|
+
`npm run test:integration` with no secrets on every push and PR, so the tier is permanently watched
|
|
69
|
+
for free (~51 assertions, ~10s). `npm run test:integration` now goes through
|
|
70
|
+
`scripts/run-integration-keyless.js`, which strips every provider credential and sandboxes
|
|
71
|
+
`HOME`/`USERPROFILE` before spawning jest — the money-spending suites self-skip and the script cannot
|
|
72
|
+
bill even on a machine with keys on disk. The scrub derives its key names from the engine's own
|
|
73
|
+
`PROVIDER_ENV_MAP`, so it covers providers and paid suites added later without maintenance.
|
|
74
|
+
- **`npm run test:integration:live`** — the paid rail, split out so the CI job cannot silently start
|
|
75
|
+
billing if secrets are ever added to it. Run by the new `.github/workflows/integration-live.yml`
|
|
76
|
+
(`workflow_dispatch` only, carries `secrets.OPENROUTER_API_KEY`) and by the release checklist in
|
|
77
|
+
`docs/publishing.md`.
|
|
78
|
+
|
|
79
|
+
### Fixed (docs)
|
|
80
|
+
|
|
81
|
+
- `docs/testing.md` and `CLAUDE.md` both claimed a pre-push integration gate that has never existed;
|
|
82
|
+
the pre-push hook runs the unit suite only, deliberately, so a local push never spends money. Both
|
|
83
|
+
now describe the real rails, and `.husky/pre-push`'s stale "until the 'Fix integration tests' task
|
|
84
|
+
lands" comment is replaced with the reason the hook stays unit-only.
|
|
85
|
+
- **`amicus doctor` no longer warns about Amicus's own shipped defaults — on a fresh install, and in
|
|
86
|
+
the keyless `model-drift.yml` CI check.** Both previously reported `⚠ Model aliases: 2 stale: opus,
|
|
87
|
+
haiku` (and the scheduled Model Drift Check ran red) with no user config involved.
|
|
88
|
+
`toDefaultAliases()` built each alias's pinned id by string-stripping the `openrouter/` prefix
|
|
89
|
+
instead of routing through the module's own `directFormFor()`, so it emitted OpenRouter's dot ids
|
|
90
|
+
for Anthropic — `anthropic/claude-opus-4.8`, `anthropic/claude-haiku-4.5` — which the direct API
|
|
91
|
+
rejects, and invented a bare `anthropic/claude-fable-5` for a model OpenRouter serves exclusively.
|
|
92
|
+
Defaults now come from `toGatewayRoutes()`, so an alias resolves to its authored direct form when
|
|
93
|
+
one exists (`anthropic/claude-opus-4-8`, `anthropic/claude-haiku-4-5-20251001`) and to its
|
|
94
|
+
OpenRouter route when none does (`openrouter/anthropic/claude-fable-5`). `fable`'s *recorded* form
|
|
95
|
+
changes, but this is **not a routing change** — OpenRouter was already the only gateway that serves
|
|
96
|
+
it, so a `fable` run resolves and routes identically before and after; only the id `doctor`/`amicus
|
|
97
|
+
models` display and compare against is different. Council artifacts are unaffected either way — they
|
|
98
|
+
record alias strings, not resolved ids.
|
|
99
|
+
- **This fix does not reach everyone who already ran `amicus setup`.** `createDefaultConfig()`
|
|
100
|
+
(`src/sidecar/setup.js`) persists the *entire* default alias map into `config.json` at setup time,
|
|
101
|
+
and `getEffectiveAliases()` lets that persisted user config win over the shipped defaults — so
|
|
102
|
+
anyone who ran `amicus setup` on Amicus ≤4.1.0 has the old, broken ids frozen on disk
|
|
103
|
+
(`source: user-config`), which `findStaleAliases()` never suppresses. Measured: such a user sees
|
|
104
|
+
**4 stale** aliases after upgrading to 4.1.1 — `opus`, `haiku`, `fable` (the pre-fix ids) plus
|
|
105
|
+
`gemini` (this release's own pin move, see Changed below) — not zero, and `amicus doctor` /
|
|
106
|
+
the Model Drift Check will keep warning for them. **There is no code fix for this in 4.1.1**; a
|
|
107
|
+
self-healing config migration is a 4.2 candidate. If `amicus doctor` still warns after upgrading,
|
|
108
|
+
re-run `amicus setup`, or fix individual aliases by hand, e.g.
|
|
109
|
+
`amicus setup --add-alias opus=anthropic/claude-opus-4-8`.
|
|
110
|
+
- The offline Anthropic model floor now also lists `anthropic/claude-haiku-4-5-20251001`, the dated id
|
|
111
|
+
Anthropic's own listing returns. Without it, keyless and OpenRouter-only users saw the shipped
|
|
112
|
+
`haiku` default reported stale against the shipped floor.
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- **Curated `gemini` pin moves from Gemini 3.5 Flash to Gemini 3.6 Flash on both gateways.** This is a
|
|
117
|
+
**silent model change, not merely a drift-notice cleanup**: a user with no `gemini` alias override of
|
|
118
|
+
their own starts talking to a different underlying model the next time they use the `gemini` alias
|
|
119
|
+
after upgrading. It also clears the pinned-fallback drift notice from `amicus models --check`.
|
|
120
|
+
|
|
8
121
|
## [4.1.0] - 2026-07-21
|
|
9
122
|
|
|
10
123
|
The `second-opinion` skill stops hand-driving councils. Stages 1–3 and the Stage-5 artifacts
|
package/README.md
CHANGED
|
@@ -329,7 +329,7 @@ $ amicus status demo123 --json
|
|
|
329
329
|
"taskId": "demo123",
|
|
330
330
|
"status": "complete",
|
|
331
331
|
"elapsed": "5m 0s",
|
|
332
|
-
"version": "4.1.
|
|
332
|
+
"version": "4.1.2",
|
|
333
333
|
"model": "google/gemini-2.5-flash",
|
|
334
334
|
"phase": "terminal"
|
|
335
335
|
}
|
|
@@ -352,7 +352,7 @@ amicus models --search gemini # filter by substring
|
|
|
352
352
|
|
|
353
353
|
### Routing
|
|
354
354
|
|
|
355
|
-
- **Bare `provider/model`** (e.g. `openai/gpt-5.5`, `anthropic/claude-opus-4
|
|
355
|
+
- **Bare `provider/model`** (e.g. `openai/gpt-5.5`, `anthropic/claude-opus-4-8`, `google/gemini-3.6-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.
|
|
356
356
|
- **`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, …).
|
|
357
357
|
- **`--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.
|
|
358
358
|
- **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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
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": [
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"start": "node --experimental-top-level-await --experimental-vm-modules bin/amicus.js",
|
|
51
51
|
"test": "jest",
|
|
52
|
-
"test:integration": "
|
|
52
|
+
"test:integration": "node scripts/run-integration-keyless.js",
|
|
53
|
+
"test:integration:live": "jest --testPathIgnorePatterns='worktrees' --testMatch='**/tests/**/*.integration.test.js'",
|
|
53
54
|
"test:all": "jest --testPathIgnorePatterns='/node_modules/' --testPathIgnorePatterns='worktrees' && node scripts/mark-test-passed.js",
|
|
54
55
|
"test:e2e:mcp": "jest tests/mcp-repomix-e2e.integration.test.js --testTimeout=180000 --forceExit",
|
|
55
56
|
"posttest": "node scripts/mark-test-passed.js",
|
|
@@ -513,7 +513,7 @@ Use these together with `amicus council stats` (the ledger — authoritative qua
|
|
|
513
513
|
|
|
514
514
|
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.
|
|
515
515
|
|
|
516
|
-
**Model naming for council members.** Name bench members by alias (`gemini`, `gpt`, `deepseek`, `opus`, …) or by full `provider/model` id — both work with `--models`. A bare canonical id (e.g. `anthropic/claude-opus-4
|
|
516
|
+
**Model naming for council members.** Name bench members by alias (`gemini`, `gpt`, `deepseek`, `opus`, …) or by full `provider/model` id — both work with `--models`. 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` overrides routing for the whole run if the user asks for it; leave it unset (`auto`) by default.
|
|
517
517
|
|
|
518
518
|
---
|
|
519
519
|
|
package/skills/sidecar/SKILL.md
CHANGED
|
@@ -79,8 +79,8 @@ Amicus uses the OpenCode SDK to communicate with LLM providers. You need to conf
|
|
|
79
79
|
|
|
80
80
|
Read this before picking a model string — it decides both routing and which credentials get used:
|
|
81
81
|
|
|
82
|
-
- **Bare `provider/model`** (e.g. `openai/gpt-5.5`, `anthropic/claude-opus-4
|
|
83
|
-
- **`openrouter/provider/model`** (e.g. `openrouter/google/gemini-3.
|
|
82
|
+
- **Bare `provider/model`** (e.g. `openai/gpt-5.5`, `anthropic/claude-opus-4-8`, `google/gemini-3.6-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.6-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
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
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
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.
|
|
@@ -116,8 +116,8 @@ amicus start --model gemini --prompt "Say hello" --no-ui
|
|
|
116
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:
|
|
117
117
|
```bash
|
|
118
118
|
amicus start --model gemini --prompt "..." # alias
|
|
119
|
-
amicus start --model google/gemini-3.
|
|
120
|
-
amicus start --model openrouter/google/gemini-3.
|
|
119
|
+
amicus start --model google/gemini-3.6-flash --prompt "..." # bare canonical, falls back to OpenRouter
|
|
120
|
+
amicus start --model openrouter/google/gemini-3.6-flash --prompt "..." # explicit override — same result here
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
### Option B: Direct API Keys (Provider-Specific)
|
|
@@ -451,7 +451,7 @@ Use short aliases (run `amicus models` to see the live catalog, and `amicus mode
|
|
|
451
451
|
- Omit `--model` entirely to use your configured default
|
|
452
452
|
|
|
453
453
|
Full model strings also work: `--model provider/model-id` (bare canonical, direct-first — e.g.
|
|
454
|
-
`--model anthropic/claude-opus-4
|
|
454
|
+
`--model anthropic/claude-opus-4-8`) or `--model openrouter/provider/model-id` (explicit
|
|
455
455
|
force-OpenRouter override).
|
|
456
456
|
|
|
457
457
|
### Verifying Model Names
|
|
@@ -181,7 +181,7 @@ function runVerdict(args, useJson) {
|
|
|
181
181
|
// v4.1 §4.5c: refresh report.html next to the decided verdict.
|
|
182
182
|
try {
|
|
183
183
|
const html = buildReport({ verdict }, { format: 'html' });
|
|
184
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'report.html'), html);
|
|
184
|
+
fs.writeFileSync(path.join(path.dirname(outPath), 'report.html'), html, { mode: 0o600 });
|
|
185
185
|
} catch (e) {
|
|
186
186
|
return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `verdict written but render failed: ${e.message}`,
|
|
187
187
|
hint: 'the verdict.json is valid; re-run `amicus council report <verdict.json> --html` manually' });
|
package/src/council/report.js
CHANGED
|
@@ -20,7 +20,19 @@ function toModel(verdict, wave) {
|
|
|
20
20
|
if (!verdict || !Array.isArray(verdict.findings)) {
|
|
21
21
|
throw new Error('verdict.json must have a findings[] array');
|
|
22
22
|
}
|
|
23
|
-
const
|
|
23
|
+
const council = verdict.council || [];
|
|
24
|
+
// 'council' (verdict.council / meta.models) is the street-cred universe and
|
|
25
|
+
// legitimately includes 'claude' on a --claude-review run (run-assemble.js:
|
|
26
|
+
// 123-125, docs/council.md:326). 'judges' is the adjudication-matrix column
|
|
27
|
+
// set: SKILL.md:482 / run-stages.js:162-163 guarantee Claude is judged but
|
|
28
|
+
// never judges, so its reserved seat must never grow a matrix column — filter
|
|
29
|
+
// it out ONLY when claudeInCouncil is true. This is name+flag gated, not
|
|
30
|
+
// vote-derived: a bench judge that cast zero adjudications (dead/unstructured
|
|
31
|
+
// leg, run-stages.js:204-207) is still in council/judges and must still get
|
|
32
|
+
// its (blank) column — deriving the roster from "who actually voted" would
|
|
33
|
+
// silently delete that column too and break the byte-unchanged-artifact
|
|
34
|
+
// contract for degraded v4.0.1-shaped runs.
|
|
35
|
+
const judges = verdict.claudeInCouncil === true ? council.filter(j => j !== 'claude') : council;
|
|
24
36
|
const findings = verdict.findings.map((f) => {
|
|
25
37
|
const byJudge = {};
|
|
26
38
|
for (const j of judges) { byJudge[j] = null; }
|
|
@@ -61,7 +73,7 @@ function toModel(verdict, wave) {
|
|
|
61
73
|
return {
|
|
62
74
|
header: {
|
|
63
75
|
runType: verdict.runType || 'review', runId: verdict.runId, date: verdict.date,
|
|
64
|
-
chair: verdict.chair, council
|
|
76
|
+
chair: verdict.chair, council, claudeInCouncil: verdict.claudeInCouncil === true,
|
|
65
77
|
},
|
|
66
78
|
tierCounts: verdict.tierCounts || { Confirmed: 0, Contested: 0, Singleton: 0, Disputed: 0 },
|
|
67
79
|
judges, findings, debate,
|
|
@@ -223,7 +223,11 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
223
223
|
// degradation branch (spec §5.7); skipping because there is simply nothing to re-vote is NOT.
|
|
224
224
|
const wouldRevote = defendedOrAmended.length > 0 && judges.length > 0;
|
|
225
225
|
const costCeiling = ctx.overBudget() && wouldRevote;
|
|
226
|
-
|
|
226
|
+
// run.js needs to know whether the wave actually launched so it can
|
|
227
|
+
// checkpoint debate-revote 'skipped' (not a false 'complete') when nothing
|
|
228
|
+
// was defended/amended, or the cost ceiling skipped it (spec §5.7).
|
|
229
|
+
const revoteLaunched = wouldRevote && !costCeiling;
|
|
230
|
+
if (revoteLaunched) {
|
|
227
231
|
const rv = await runRevoteWave(ctx, judges, defendedOrAmended);
|
|
228
232
|
if (rv.aborted) { return { aborted: rv.aborted, contested, disputed }; }
|
|
229
233
|
revoteByJudge = rv.byJudge;
|
|
@@ -283,7 +287,7 @@ async function runDebate(ctx, { provisionalRecord, tallyInput }) {
|
|
|
283
287
|
|
|
284
288
|
return { debatedInput, debateFindings, debateSummary, addendumOutcomes,
|
|
285
289
|
defenseLegs: defenseResults.map(d => d.leg), revoteLegs, verdictChanges,
|
|
286
|
-
degraded, aborted: null };
|
|
290
|
+
degraded, aborted: null, revoteLaunched };
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
module.exports = { runDebate, nothingToDebate, disputingJudges, debateTargets };
|
package/src/council/run.js
CHANGED
|
@@ -198,7 +198,11 @@ async function runCouncil(options, deps = {}) {
|
|
|
198
198
|
// run-debate owns debate-revote's running/waveId/waveIds checkpoint — only it
|
|
199
199
|
// knows whether the wave launched. Never advertise a `-rv` id here: a skipped
|
|
200
200
|
// re-vote would leave the abort cascade chasing the v4.0 lens `-s1` phantom.
|
|
201
|
-
|
|
201
|
+
// Mirror run-chair.js's 'skipped' convention (no startedAt) when nothing was
|
|
202
|
+
// defended/amended or the cost ceiling skipped it — 'complete' would report
|
|
203
|
+
// work that never happened.
|
|
204
|
+
runState.updateStage(o.runDir, 'debate-revote', dbg.revoteLaunched
|
|
205
|
+
? { status: 'complete', completedAt: now() } : { status: 'skipped', completedAt: now() });
|
|
202
206
|
({ debatedInput, debateFindings, debateSummary } = dbg);
|
|
203
207
|
debatedRecord = tally(debatedInput);
|
|
204
208
|
// Defensive truthiness guard: `[]` is truthy in JS, so an empty outcomes
|
package/src/mcp-tools.js
CHANGED
|
@@ -57,7 +57,7 @@ function getTools() {
|
|
|
57
57
|
inputSchema: {
|
|
58
58
|
model: safeModel.optional().describe(
|
|
59
59
|
`Short alias (${aliasNames}) or full model ID ` +
|
|
60
|
-
'(bare provider/model is canonical and routes direct-first, e.g. anthropic/claude-opus-4
|
|
60
|
+
'(bare provider/model is canonical and routes direct-first, e.g. anthropic/claude-opus-4-8; ' +
|
|
61
61
|
'openrouter/provider/model forces OpenRouter). ' +
|
|
62
62
|
'If omitted, uses the configured default. Call amicus_guide to see all aliases.'
|
|
63
63
|
),
|
|
@@ -581,7 +581,7 @@ Include: Objective, Background, Files of interest, Success criteria, Constraints
|
|
|
581
581
|
|-------|-------|
|
|
582
582
|
${aliasRows}
|
|
583
583
|
|
|
584
|
-
Or use a full model ID. Bare \`provider/model\` (e.g. anthropic/claude-opus-4
|
|
584
|
+
Or use a full model ID. Bare \`provider/model\` (e.g. anthropic/claude-opus-4-8) is the canonical,
|
|
585
585
|
policy-routed form — it routes direct-first (your direct provider key if configured, else
|
|
586
586
|
OpenRouter). \`openrouter/provider/model\` is an explicit override that forces OpenRouter. The
|
|
587
587
|
\`gateway\` param (or \`routing.prefer\` in config.json) controls this per call or globally.
|
package/src/sidecar/setup.js
CHANGED
|
@@ -353,8 +353,7 @@ async function runReadlineSetup() {
|
|
|
353
353
|
return;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
const { resolveQuickPicks, toLiveSeedAliases } = require('../utils/quick-picks');
|
|
357
|
-
const { toCanonicalDefault } = require('../utils/curated-models');
|
|
356
|
+
const { resolveQuickPicks, toLiveSeedAliases, toStorableRoute } = require('../utils/quick-picks');
|
|
358
357
|
const picks = resolveQuickPicks(catalog);
|
|
359
358
|
|
|
360
359
|
console.log('Choose your default model:');
|
|
@@ -386,7 +385,7 @@ async function runReadlineSetup() {
|
|
|
386
385
|
// choice), but the alias's VALUE must stay the vendor phase's tier choice --
|
|
387
386
|
// skip the curated-flagship upgrade so it isn't discarded.
|
|
388
387
|
if (pick && !chosen.noUpgrade && !vendorAliasesWritten.has(chosen.alias)) {
|
|
389
|
-
cfg.aliases[chosen.alias] =
|
|
388
|
+
cfg.aliases[chosen.alias] = toStorableRoute(pick);
|
|
390
389
|
} else if (cfg.aliases[chosen.alias] === undefined) {
|
|
391
390
|
const fallback = getDefaultAliases()[chosen.alias];
|
|
392
391
|
if (fallback !== undefined) { cfg.aliases[chosen.alias] = fallback; }
|
package/src/utils/config.js
CHANGED
|
@@ -12,8 +12,9 @@ const { autoRepairAlias } = require('./alias-resolver');
|
|
|
12
12
|
const { isDirectProvider } = require('./provider-registry');
|
|
13
13
|
|
|
14
14
|
/** Default model alias map — derived from the curated-models single source (F5) */
|
|
15
|
-
const { toDefaultAliases } = require('./curated-models');
|
|
15
|
+
const { toDefaultAliases, toGatewayRoutes } = require('./curated-models');
|
|
16
16
|
const DEFAULT_ALIASES = toDefaultAliases();
|
|
17
|
+
const CURATED_ROUTES = toGatewayRoutes();
|
|
17
18
|
|
|
18
19
|
/** Built-in council benches (B23) — consulted only when a name is absent from user config. */
|
|
19
20
|
const { resolveBuiltinCouncil } = require('./council-presets');
|
|
@@ -261,10 +262,19 @@ function tryResolveModel(modelArg) {
|
|
|
261
262
|
* threading only that first session's resolved id is insufficient. So for
|
|
262
263
|
* every alias that resolves to a BARE direct-capable-vendor id (post-#61
|
|
263
264
|
* default aliases are bare, e.g. `openai/gpt-5.5`), this ALSO registers the
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
265
|
+
* alias's OpenRouter form — broadening the catalog to cover BOTH routes the
|
|
266
|
+
* router might pick, regardless of which session created the server. This
|
|
267
|
+
* does NOT change what the alias itself resolves to (still bare, still
|
|
268
|
+
* direct-first) — it only widens what's pre-registered.
|
|
269
|
+
*
|
|
270
|
+
* The mirror is read from the alias's authored gateway route, NOT built by
|
|
271
|
+
* prepending `openrouter/`. For DIVERGENT_VENDORS the two ids are different
|
|
272
|
+
* strings, not merely differently prefixed — OpenRouter serves
|
|
273
|
+
* `anthropic/claude-opus-4.8` while the direct API serves
|
|
274
|
+
* `anthropic/claude-opus-4-8`. v4.1.1 made `toDefaultAliases()` emit the
|
|
275
|
+
* direct form, at which point prepending produced an id OpenRouter does not
|
|
276
|
+
* serve (fixed in v4.1.2). Aliases the user has overridden fall back to the
|
|
277
|
+
* prefix form, since no authored gateway route describes them.
|
|
268
278
|
* @param {string[]} [resolvedRoutes] executable model id(s) actually launched
|
|
269
279
|
* @returns {object} e.g. { openrouter: { models: { "x-ai/grok-4.3": {}, ... } } } */
|
|
270
280
|
function buildProviderModels(resolvedRoutes = []) {
|
|
@@ -285,13 +295,23 @@ function buildProviderModels(resolvedRoutes = []) {
|
|
|
285
295
|
providers[providerID].models[modelID] = {};
|
|
286
296
|
};
|
|
287
297
|
|
|
288
|
-
for (const fullModel of Object.
|
|
298
|
+
for (const [alias, fullModel] of Object.entries(aliases)) {
|
|
289
299
|
addRoute(fullModel);
|
|
290
300
|
|
|
291
301
|
// Broaden: a bare direct-capable-vendor route also gets an OpenRouter
|
|
292
|
-
// mirror registered (see catalog-broadening note above).
|
|
293
|
-
//
|
|
294
|
-
//
|
|
302
|
+
// mirror registered (see catalog-broadening note above). Prefer the
|
|
303
|
+
// alias's AUTHORED OpenRouter route — for divergent vendors it is a
|
|
304
|
+
// different id, not a prefixed one. Guarded on the alias still holding its
|
|
305
|
+
// shipped direct value so a user override never inherits a curated route.
|
|
306
|
+
const curated = CURATED_ROUTES[alias];
|
|
307
|
+
if (curated && curated.openrouter && curated.direct === fullModel) {
|
|
308
|
+
addRoute(curated.openrouter);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Fallback for user-defined aliases. Gateway-only aliases (already
|
|
313
|
+
// `openrouter/...`, e.g. grok/qwen/x-ai) are untouched — OpenRouter is
|
|
314
|
+
// their only possible route anyway, already covered above.
|
|
295
315
|
if (typeof fullModel === 'string' && !fullModel.startsWith('openrouter/')) {
|
|
296
316
|
const vendor = fullModel.split('/')[0];
|
|
297
317
|
if (isDirectProvider(vendor)) {
|
|
@@ -30,8 +30,8 @@ const FAMILIES = [
|
|
|
30
30
|
vendorPath: 'google',
|
|
31
31
|
idPattern: /^gemini-[\d.]+-flash(-preview|-exp|-latest)?$/,
|
|
32
32
|
directProviders: ['google'],
|
|
33
|
-
fallback: { openrouter: 'openrouter/google/gemini-3.
|
|
34
|
-
google: 'google/gemini-3.
|
|
33
|
+
fallback: { openrouter: 'openrouter/google/gemini-3.6-flash',
|
|
34
|
+
google: 'google/gemini-3.6-flash' } },
|
|
35
35
|
{ alias: 'gemini-pro', label: 'Gemini Pro-class', blurb: 'advanced reasoning',
|
|
36
36
|
vendorPath: 'google',
|
|
37
37
|
idPattern: /^gemini-[\d.]+-pro(-preview|-exp|-latest)?$/,
|
|
@@ -119,24 +119,6 @@ function toCanonicalDefault(route) {
|
|
|
119
119
|
return route;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
* @returns {Object<string,string>} alias → pinned route, direct-first for
|
|
124
|
-
* direct-capable vendors (bare `vendor/model`), openrouter-prefixed for
|
|
125
|
-
* gateway-only vendors. STATIC — runtime-safe.
|
|
126
|
-
*/
|
|
127
|
-
function toDefaultAliases() {
|
|
128
|
-
const out = {};
|
|
129
|
-
for (const f of FAMILIES) {
|
|
130
|
-
const route = f.fallback.openrouter || Object.values(f.fallback)[0];
|
|
131
|
-
out[f.alias] = toCanonicalDefault(route);
|
|
132
|
-
}
|
|
133
|
-
for (const e of CARDLESS) {
|
|
134
|
-
const route = e.routes.openrouter || Object.values(e.routes)[0];
|
|
135
|
-
out[e.alias] = toCanonicalDefault(route);
|
|
136
|
-
}
|
|
137
|
-
return out;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
122
|
/**
|
|
141
123
|
* @returns {Array<{alias,provider,model}>} every pinned route, flattened (for the alias audit).
|
|
142
124
|
*/
|
|
@@ -213,6 +195,26 @@ function toGatewayRoutes() {
|
|
|
213
195
|
return out;
|
|
214
196
|
}
|
|
215
197
|
|
|
198
|
+
/**
|
|
199
|
+
* @returns {Object<string,string>} alias → the SINGLE pinned route used for
|
|
200
|
+
* display and `config.default`: the alias's authored direct form when one
|
|
201
|
+
* exists, else its OpenRouter route. STATIC — runtime-safe, never networks.
|
|
202
|
+
*
|
|
203
|
+
* Derived from `toGatewayRoutes()` on purpose, so the two builders can never
|
|
204
|
+
* disagree. It previously string-stripped `openrouter/` itself, which emitted
|
|
205
|
+
* OpenRouter's dot ids for divergent vendors (`anthropic/claude-opus-4.8` —
|
|
206
|
+
* the direct API only serves the dash form) and invented a bare direct id for
|
|
207
|
+
* OpenRouter-only models (`fable`). Both made `amicus doctor` and `amicus
|
|
208
|
+
* models --check` warn about the product's own shipped defaults.
|
|
209
|
+
*/
|
|
210
|
+
function toDefaultAliases() {
|
|
211
|
+
const out = {};
|
|
212
|
+
for (const [alias, routes] of Object.entries(toGatewayRoutes())) {
|
|
213
|
+
out[alias] = routes.direct || routes.openrouter;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
216
218
|
module.exports = {
|
|
217
219
|
getFamilies, toDefaultAliases, toCanonicalDefault, listCuratedRoutes, toGatewayRoutes, DIVERGENT_VENDORS
|
|
218
220
|
};
|
|
@@ -19,6 +19,12 @@ const ANTHROPIC_MODELS = [
|
|
|
19
19
|
{ id: 'anthropic/claude-opus-4-8', name: 'Claude Opus 4.8', contextLength: null, pricing: null },
|
|
20
20
|
{ id: 'anthropic/claude-sonnet-5', name: 'Claude Sonnet 5', contextLength: null, pricing: null },
|
|
21
21
|
{ id: 'anthropic/claude-haiku-4-5', name: 'Claude Haiku 4.5', contextLength: null, pricing: null },
|
|
22
|
+
// Dated snapshot: the id Anthropic's /v1/models actually lists, and the
|
|
23
|
+
// `haiku` direct route curated-models.js authors. Without it the floor
|
|
24
|
+
// (the only anthropic/ rows a keyless or OpenRouter-only user ever has)
|
|
25
|
+
// reports the shipped `haiku` default as stale.
|
|
26
|
+
{ id: 'anthropic/claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5 (2025-10-01)',
|
|
27
|
+
contextLength: null, pricing: null },
|
|
22
28
|
{ id: 'anthropic/claude-sonnet-4-6', name: 'Claude Sonnet 4.6', contextLength: null, pricing: null }
|
|
23
29
|
];
|
|
24
30
|
|
package/src/utils/quick-picks.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const { getFamilies, toDefaultAliases, toCanonicalDefault } = require('./curated-models');
|
|
13
|
+
const { getFamilies, toDefaultAliases, toCanonicalDefault, DIVERGENT_VENDORS } = require('./curated-models');
|
|
14
14
|
|
|
15
15
|
const MARKER_RE = /(-preview|-exp|-beta|-latest|:free)+$/;
|
|
16
16
|
|
|
@@ -45,7 +45,7 @@ function pickCurrent(catalog, nsPrefix, vendorPath, idPattern) {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* @param {Array<{id:string}>} catalog
|
|
48
|
-
* @returns {Array<{alias,label,blurb,source:'live'|'fallback',routes:Object<string,string>}>}
|
|
48
|
+
* @returns {Array<{alias,label,blurb,vendorPath,source:'live'|'fallback',routes:Object<string,string>}>}
|
|
49
49
|
* `routes` may be empty if a family defines no fallback and the catalog has no match.
|
|
50
50
|
*/
|
|
51
51
|
function resolveQuickPicks(catalog) {
|
|
@@ -60,30 +60,53 @@ function resolveQuickPicks(catalog) {
|
|
|
60
60
|
if (direct) { routes[p] = direct; live = true; }
|
|
61
61
|
else if (f.fallback[p]) { routes[p] = f.fallback[p]; }
|
|
62
62
|
}
|
|
63
|
-
return { alias: f.alias, label: f.label, blurb: f.blurb, routes,
|
|
63
|
+
return { alias: f.alias, label: f.label, blurb: f.blurb, vendorPath: f.vendorPath, routes,
|
|
64
64
|
source: live ? 'live' : 'fallback' };
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The single route value a wizard may STORE for a resolved quick pick.
|
|
70
|
+
*
|
|
71
|
+
* For a direct-capable vendor the OpenRouter pick is canonicalised to bare
|
|
72
|
+
* `vendor/model` so it stays direct-first via the gateway router — otherwise a
|
|
73
|
+
* fresh `amicus setup` with a live catalog would silently defeat the
|
|
74
|
+
* direct-first default `toDefaultAliases()` establishes.
|
|
75
|
+
*
|
|
76
|
+
* For a DIVERGENT vendor the direct id is never DERIVED from the OpenRouter
|
|
77
|
+
* one: they are different strings, not differently prefixed (OpenRouter serves
|
|
78
|
+
* `anthropic/claude-opus-4.8`, the direct API `anthropic/claude-opus-4-8`).
|
|
79
|
+
* Stripping the prefix there fabricates an id the direct API rejects, which
|
|
80
|
+
* `amicus doctor` then reports as a stale alias. The row's own direct route is
|
|
81
|
+
* used verbatim, falling back to the intact `openrouter/` form when the
|
|
82
|
+
* catalog offered no direct pick. Mirrors the guard already used at
|
|
83
|
+
* `provider-default-picker.js:82,143,220`.
|
|
84
|
+
* @param {{vendorPath?:string, routes?:Object<string,string>}} pick
|
|
85
|
+
* @returns {string|undefined}
|
|
86
|
+
*/
|
|
87
|
+
function toStorableRoute(pick) {
|
|
88
|
+
const routes = (pick && pick.routes) || {};
|
|
89
|
+
if (pick && DIVERGENT_VENDORS.has(pick.vendorPath)) {
|
|
90
|
+
return routes[pick.vendorPath] || routes.openrouter;
|
|
91
|
+
}
|
|
92
|
+
return toCanonicalDefault(routes.openrouter || Object.values(routes)[0]);
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
/**
|
|
69
96
|
* Seed map for fresh configs: static defaults overlaid with live family
|
|
70
|
-
* routes (cardless aliases stay pinned).
|
|
71
|
-
*
|
|
72
|
-
* lands as bare `vendor/model` (direct-first via the gateway router)
|
|
73
|
-
* instead of the raw `openrouter/<vendor>/<rest>` pick — otherwise a fresh
|
|
74
|
-
* `amicus setup` with a live catalog would silently defeat the direct-first
|
|
75
|
-
* default `toDefaultAliases()` establishes. Gateway-only vendors are
|
|
76
|
-
* returned unchanged by `toCanonicalDefault`.
|
|
97
|
+
* routes (cardless aliases stay pinned). See `toStorableRoute` for why the
|
|
98
|
+
* overlaid value is not a raw prefix strip.
|
|
77
99
|
* @returns {Object<string,string>}
|
|
78
100
|
*/
|
|
79
101
|
function toLiveSeedAliases(catalog) {
|
|
80
102
|
const seeds = toDefaultAliases();
|
|
81
103
|
for (const r of resolveQuickPicks(catalog || [])) {
|
|
82
104
|
if (r.source === 'live' && r.routes.openrouter) {
|
|
83
|
-
|
|
105
|
+
const stored = toStorableRoute(r);
|
|
106
|
+
if (stored) { seeds[r.alias] = stored; }
|
|
84
107
|
}
|
|
85
108
|
}
|
|
86
109
|
return seeds;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
module.exports = { compareIdsDesc, pickCurrent, resolveQuickPicks, toLiveSeedAliases };
|
|
112
|
+
module.exports = { compareIdsDesc, pickCurrent, resolveQuickPicks, toLiveSeedAliases, toStorableRoute };
|