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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.1.0",
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.0",
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.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.
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.0",
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": "jest --testPathIgnorePatterns='worktrees' --testMatch='**/tests/**/*.integration.test.js'",
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.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.
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
 
@@ -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.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.
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.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
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.8`) or `--model openrouter/provider/model-id` (explicit
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' });
@@ -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 judges = verdict.council || [];
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: judges, claudeInCouncil: verdict.claudeInCouncil === true,
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
- if (wouldRevote && !costCeiling) {
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 };
@@ -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
- runState.updateStage(o.runDir, 'debate-revote', { status: 'complete', completedAt: now() });
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.8; ' +
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.8) is the canonical,
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.5-flash',
34
- google: 'google/gemini-3.5-flash' } },
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