amicus 4.1.1 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.1.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,25 @@ 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
+
8
27
  ## [4.1.1] - 2026-07-21
9
28
 
10
29
  ### Fixed
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.1",
332
+ "version": "4.1.2",
333
333
  "model": "google/gemini-2.5-flash",
334
334
  "phase": "terminal"
335
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.1.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": [
@@ -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] = toCanonicalDefault(pick.routes.openrouter || Object.values(pick.routes)[0]);
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; }
@@ -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
- * `openrouter/<vendor>/<model>` form — broadening the catalog to cover BOTH
265
- * routes the router might pick, regardless of which session created the
266
- * server. This does NOT change what the alias itself resolves to (still
267
- * bare, still direct-first) — it only widens what's pre-registered.
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.values(aliases)) {
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). Gateway-only
293
- // aliases (already `openrouter/...`, e.g. grok/qwen/x-ai) are untouched
294
- // OpenRouter is their only possible route anyway, already covered above.
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)) {
@@ -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). The overlaid route is run through
71
- * `toCanonicalDefault` so a direct-capable vendor (e.g. google, openai)
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
- seeds[r.alias] = toCanonicalDefault(r.routes.openrouter);
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 };