amicus 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +94 -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/utils/curated-models.js +22 -20
- package/src/utils/model-fetcher.js +6 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
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,100 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.1.1] - 2026-07-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **The integration tier is tested again, and now something actually watches it.** All 14
|
|
13
|
+
`tests/**/*.integration.test.js` files were unreachable from every gate — `jest.config.js` excludes
|
|
14
|
+
them from `npm test`, no workflow ran `npm run test:integration`, and the pre-push hook ran only the
|
|
15
|
+
unit suite. Six tests had been failing unnoticed behind a single dead model alias: three E2E suites
|
|
16
|
+
passed `--model gemini-flash`, which is not a live alias (`tryResolveModel('gemini-flash')` returns
|
|
17
|
+
`Unknown model alias`). They now pass `gemini`.
|
|
18
|
+
- **`--claude-review` reports no longer grow a blank `claude` judge column, or a false self-vote `*`.**
|
|
19
|
+
`report.js`'s `toModel()` reused `verdict.council` (the street-cred universe, which legitimately
|
|
20
|
+
includes `claude` on a `--claude-review` run) as the adjudication-matrix judge roster too. Claude is
|
|
21
|
+
judged but never judges, so the matrix grew an extra column that always rendered blank, or a bare
|
|
22
|
+
`*` on a row Claude itself raised — asserting Claude voted for its own finding, the opposite of the
|
|
23
|
+
documented guarantee. `judges` is now filtered out of `council` independently (gated on the
|
|
24
|
+
`claudeInCouncil` flag plus the `claude` name, never on whether a model cast any adjudications, so a
|
|
25
|
+
genuinely dead/unstructured bench judge with zero votes still keeps its blank column).
|
|
26
|
+
- **`debate-revote` now checkpoints `skipped`, not a false `complete`, when the re-vote wave never
|
|
27
|
+
launches** (nothing was defended/amended, or the cost ceiling skipped it). **This is a
|
|
28
|
+
consumer-visible `run.json` value change**: anything parsing `stages[].status` for this stage — the
|
|
29
|
+
Council Review Action's stage-ladder footer, `amicus status`, or a user script — now sees `skipped`
|
|
30
|
+
instead of `complete` on a run that hits this path. Neither the Action's footer nor `amicus
|
|
31
|
+
status`'s human/JSON renderers special-case `complete`, so both already print whatever value is
|
|
32
|
+
there correctly; a script that hard-coded an expectation of `complete` for this stage should treat
|
|
33
|
+
`skipped` as the equivalent no-op, the same way `run-chair.js`'s existing chair-skipped-over-budget
|
|
34
|
+
convention already works.
|
|
35
|
+
- **`amicus council verdict --render` now writes `report.html` at `0o600`**, matching every sibling
|
|
36
|
+
writer (`run-assemble.js`, `mcp-server.js`'s `amicus_verdict` `render:true` path, `run-launch.js`).
|
|
37
|
+
It previously wrote with no explicit mode, which falls back to the process umask (typically `0o644`
|
|
38
|
+
on POSIX — group/world-readable) whenever the target directory had no pre-existing `report.html`,
|
|
39
|
+
i.e. exactly the fresh-`--out-dir` case, for a file that holds model output.
|
|
40
|
+
- **Dropped the false `amicus wait` CLI claim.** `docs/usage.md` and `docs/council.md` both documented
|
|
41
|
+
`amicus status|wait|abort <councilRunId>` as working CLI commands. There is no CLI `wait` (verified
|
|
42
|
+
against `bin/amicus.js`'s command dispatch) — only `status` and `abort` genuinely resolve council
|
|
43
|
+
runs via the sessions-dir pointer file. Both docs now point readers at the MCP `amicus_wait` tool
|
|
44
|
+
instead.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- **Keyless integration job in CI.** `.github/workflows/ci.yml` gained an `integration` job that runs
|
|
49
|
+
`npm run test:integration` with no secrets on every push and PR, so the tier is permanently watched
|
|
50
|
+
for free (~51 assertions, ~10s). `npm run test:integration` now goes through
|
|
51
|
+
`scripts/run-integration-keyless.js`, which strips every provider credential and sandboxes
|
|
52
|
+
`HOME`/`USERPROFILE` before spawning jest — the money-spending suites self-skip and the script cannot
|
|
53
|
+
bill even on a machine with keys on disk. The scrub derives its key names from the engine's own
|
|
54
|
+
`PROVIDER_ENV_MAP`, so it covers providers and paid suites added later without maintenance.
|
|
55
|
+
- **`npm run test:integration:live`** — the paid rail, split out so the CI job cannot silently start
|
|
56
|
+
billing if secrets are ever added to it. Run by the new `.github/workflows/integration-live.yml`
|
|
57
|
+
(`workflow_dispatch` only, carries `secrets.OPENROUTER_API_KEY`) and by the release checklist in
|
|
58
|
+
`docs/publishing.md`.
|
|
59
|
+
|
|
60
|
+
### Fixed (docs)
|
|
61
|
+
|
|
62
|
+
- `docs/testing.md` and `CLAUDE.md` both claimed a pre-push integration gate that has never existed;
|
|
63
|
+
the pre-push hook runs the unit suite only, deliberately, so a local push never spends money. Both
|
|
64
|
+
now describe the real rails, and `.husky/pre-push`'s stale "until the 'Fix integration tests' task
|
|
65
|
+
lands" comment is replaced with the reason the hook stays unit-only.
|
|
66
|
+
- **`amicus doctor` no longer warns about Amicus's own shipped defaults — on a fresh install, and in
|
|
67
|
+
the keyless `model-drift.yml` CI check.** Both previously reported `⚠ Model aliases: 2 stale: opus,
|
|
68
|
+
haiku` (and the scheduled Model Drift Check ran red) with no user config involved.
|
|
69
|
+
`toDefaultAliases()` built each alias's pinned id by string-stripping the `openrouter/` prefix
|
|
70
|
+
instead of routing through the module's own `directFormFor()`, so it emitted OpenRouter's dot ids
|
|
71
|
+
for Anthropic — `anthropic/claude-opus-4.8`, `anthropic/claude-haiku-4.5` — which the direct API
|
|
72
|
+
rejects, and invented a bare `anthropic/claude-fable-5` for a model OpenRouter serves exclusively.
|
|
73
|
+
Defaults now come from `toGatewayRoutes()`, so an alias resolves to its authored direct form when
|
|
74
|
+
one exists (`anthropic/claude-opus-4-8`, `anthropic/claude-haiku-4-5-20251001`) and to its
|
|
75
|
+
OpenRouter route when none does (`openrouter/anthropic/claude-fable-5`). `fable`'s *recorded* form
|
|
76
|
+
changes, but this is **not a routing change** — OpenRouter was already the only gateway that serves
|
|
77
|
+
it, so a `fable` run resolves and routes identically before and after; only the id `doctor`/`amicus
|
|
78
|
+
models` display and compare against is different. Council artifacts are unaffected either way — they
|
|
79
|
+
record alias strings, not resolved ids.
|
|
80
|
+
- **This fix does not reach everyone who already ran `amicus setup`.** `createDefaultConfig()`
|
|
81
|
+
(`src/sidecar/setup.js`) persists the *entire* default alias map into `config.json` at setup time,
|
|
82
|
+
and `getEffectiveAliases()` lets that persisted user config win over the shipped defaults — so
|
|
83
|
+
anyone who ran `amicus setup` on Amicus ≤4.1.0 has the old, broken ids frozen on disk
|
|
84
|
+
(`source: user-config`), which `findStaleAliases()` never suppresses. Measured: such a user sees
|
|
85
|
+
**4 stale** aliases after upgrading to 4.1.1 — `opus`, `haiku`, `fable` (the pre-fix ids) plus
|
|
86
|
+
`gemini` (this release's own pin move, see Changed below) — not zero, and `amicus doctor` /
|
|
87
|
+
the Model Drift Check will keep warning for them. **There is no code fix for this in 4.1.1**; a
|
|
88
|
+
self-healing config migration is a 4.2 candidate. If `amicus doctor` still warns after upgrading,
|
|
89
|
+
re-run `amicus setup`, or fix individual aliases by hand, e.g.
|
|
90
|
+
`amicus setup --add-alias opus=anthropic/claude-opus-4-8`.
|
|
91
|
+
- The offline Anthropic model floor now also lists `anthropic/claude-haiku-4-5-20251001`, the dated id
|
|
92
|
+
Anthropic's own listing returns. Without it, keyless and OpenRouter-only users saw the shipped
|
|
93
|
+
`haiku` default reported stale against the shipped floor.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- **Curated `gemini` pin moves from Gemini 3.5 Flash to Gemini 3.6 Flash on both gateways.** This is a
|
|
98
|
+
**silent model change, not merely a drift-notice cleanup**: a user with no `gemini` alias override of
|
|
99
|
+
their own starts talking to a different underlying model the next time they use the `gemini` alias
|
|
100
|
+
after upgrading. It also clears the pinned-fallback drift notice from `amicus models --check`.
|
|
101
|
+
|
|
8
102
|
## [4.1.0] - 2026-07-21
|
|
9
103
|
|
|
10
104
|
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.1",
|
|
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.1",
|
|
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.
|
|
@@ -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
|
|