clauderipple 0.8.8 → 0.8.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.9 — 2026-10-08
4
+
5
+ ### Fixed
6
+
7
+ - **New Claude models appear on their own.** The Claude model list was built into ClaudeRipple,
8
+ so Opus 5.5, Sonnet 5.5 and Haiku 5.5 never showed up. It now comes from Anthropic's model list
9
+ for your account, refreshed hourly. When a newer model ships in a family you already use (say
10
+ Opus 5.5 after Opus 5), it is added to your Claude provider on its own. Untick it and it stays
11
+ unticked.
12
+
3
13
  ## 0.8.8 — 2026-10-07
4
14
 
5
15
  ### Fixed
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "private": true,
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "type": "commonjs"
5
5
  }
@@ -30,6 +30,7 @@ import { CHATGPT_FALLBACK_MODELS } from "./providers/chatgpt/catalog.js";
30
30
  import { measureModel, refusedByPlan } from "./capabilities.js";
31
31
  import { catalogEntry } from "./catalog.js";
32
32
  import { ClaudeCodeAuthStore, nativeAnthropicHeaders } from "./providers/anthropic.js";
33
+ import { fetchClaudeModels, newClaudeReleases } from "./providers/anthropic-models.js";
33
34
  import { codexEnabled, codexHome } from "../../cli/src/codex.js";
34
35
  import { openBrowser } from "../../cli/src/browser.js";
35
36
  import { caTrusted, currentAppProxy } from "../../cli/src/picker.js";
@@ -171,18 +172,27 @@ export function effortLevelsForModel(cfg, modelId) {
171
172
  }
172
173
  return undefined;
173
174
  }
175
+ // Only for when Anthropic's own list cannot be had (no sign-in yet, offline): the live list is
176
+ // `deps.claudeModels`, and a release after this table is cut appears there by itself (2026-10-08).
174
177
  const CLAUDE_MODEL_FALLBACK = [
175
- { id: "claude-fable-5-1", name: "Fable 5.1" },
178
+ { id: "claude-haiku-5-5", name: "Haiku 5.5" },
179
+ { id: "claude-sonnet-5-5", name: "Sonnet 5.5" },
176
180
  { id: "claude-opus-5-5", name: "Opus 5.5" },
181
+ { id: "claude-fable-5-1", name: "Fable 5.1" },
177
182
  { id: "claude-opus-5", name: "Opus 5" },
178
183
  { id: "claude-sonnet-5", name: "Sonnet 5" },
179
- { id: "claude-haiku-4-5", name: "Haiku 4.5" },
180
184
  { id: "claude-fable-5", name: "Fable 5" },
181
185
  { id: "claude-opus-4-8", name: "Opus 4.8" },
182
186
  { id: "claude-opus-4-7", name: "Opus 4.7" },
183
- { id: "claude-opus-4-6", name: "Opus 4.6" },
184
187
  { id: "claude-sonnet-4-6", name: "Sonnet 4.6" },
188
+ { id: "claude-opus-4-6", name: "Opus 4.6" },
189
+ { id: "claude-haiku-4-5", name: "Haiku 4.5" },
185
190
  ];
191
+ /** The account's Claude models, or the table above when Anthropic's list cannot be had. */
192
+ async function claudeModelList(deps) {
193
+ const live = await deps.claudeModels?.().catch(() => null);
194
+ return live?.length ? live.map(({ line: _line, createdAt: _createdAt, ...model }) => ({ ...model, name: model.name ?? model.id })) : CLAUDE_MODEL_FALLBACK;
195
+ }
186
196
  export function adminPort(cfg) {
187
197
  return cfg.admin?.port ?? cfg.listen.port + 1;
188
198
  }
@@ -638,7 +648,9 @@ function claudeAuthStore(deps) {
638
648
  return new ClaudeCodeAuthStore(undefined, { home: homeDir(), ...(deps.observedClaudeCodeAuth ? { observed: deps.observedClaudeCodeAuth } : {}) });
639
649
  }
640
650
  async function probeAnthropicApiKey(apiKey, probeFetch = fetchWithTimeout) {
641
- const models = CLAUDE_MODEL_FALLBACK;
651
+ // The key's own model list where it answers; a key that cannot list is still tested below.
652
+ const listed = await fetchClaudeModels("https://api.anthropic.com", { "x-api-key": apiKey }, probeFetch).catch(() => []);
653
+ const models = listed.length ? listed.map(({ line: _line, createdAt: _createdAt, ...model }) => model) : CLAUDE_MODEL_FALLBACK;
642
654
  const label = "messages endpoint";
643
655
  try {
644
656
  const response = await probeFetch("https://api.anthropic.com/v1/messages", {
@@ -661,14 +673,14 @@ async function probeAnthropicApiKey(apiKey, probeFetch = fetchWithTimeout) {
661
673
  return { ok: false, auth: "unreachable", models, error: `${label}: ${errorText(e)}` };
662
674
  }
663
675
  }
664
- function probeClaudeCodeAuth(deps) {
676
+ async function probeClaudeCodeAuth(deps) {
665
677
  const source = claudeAuthStore(deps).describeSource();
666
678
  // Our own accounts are reported separately: a Claude Desktop session can outrank them, and without
667
679
  // this the screen would answer a finished sign-in with the source it was already showing.
668
680
  const accounts = listClaudeAccounts(homeDir());
669
681
  const hasUsableAccount = accounts.some((account) => !account.needsReauth && account.expiresAt > Date.now());
670
682
  const signedIn = accounts.length > 0 ? "oauth" : readClaudeAuthFile(homeDir())?.source ?? null;
671
- return { ok: source !== null || hasUsableAccount, auth: source !== null || hasUsableAccount ? "ok" : "missing", source, signedIn, accountCount: accounts.length, models: CLAUDE_MODEL_FALLBACK };
683
+ return { ok: source !== null || hasUsableAccount, auth: source !== null || hasUsableAccount ? "ok" : "missing", source, signedIn, accountCount: accounts.length, models: await claudeModelList(deps) };
672
684
  }
673
685
  /** The reasoning levels a `GET /v1beta/models` entry says the model takes, or none to offer. */
674
686
  function googleEffortLevels(entry) {
@@ -1089,6 +1101,55 @@ export function measureUnsettledModels(deps) {
1089
1101
  }
1090
1102
  return started;
1091
1103
  }
1104
+ /**
1105
+ * Declare each Claude release the account has gained on every native Anthropic provider that
1106
+ * declares an older model of the same family (`newClaudeReleases`). The declared list was whatever
1107
+ * was ticked when the provider was saved, so Opus 5.5, Sonnet 5.5 and Haiku 5.5 stayed off it for
1108
+ * weeks while Claude Code had them (2026-10-08). A model offered once is remembered in
1109
+ * `claude-models-offered.json` and never offered again, so unticking a new model sticks.
1110
+ * Returns the `provider/model` ids it declared.
1111
+ */
1112
+ export async function adoptNewClaudeModels(deps) {
1113
+ const live = await deps.claudeModels?.().catch(() => null);
1114
+ if (!live?.length)
1115
+ return [];
1116
+ const stateFile = path.join(homeDir(), "claude-models-offered.json");
1117
+ let offered = new Set();
1118
+ try {
1119
+ const saved = JSON.parse(fs.readFileSync(stateFile, "utf8"));
1120
+ if (Array.isArray(saved.offered))
1121
+ offered = new Set(saved.offered.filter((id) => typeof id === "string"));
1122
+ }
1123
+ catch { /* first run: nothing offered yet */ }
1124
+ let current;
1125
+ try {
1126
+ current = JSON.parse(fs.readFileSync(deps.configFile, "utf8"));
1127
+ }
1128
+ catch (e) {
1129
+ deps.log.warn(`admin: claude models not adopted: could not read config: ${errorText(e)}`);
1130
+ return [];
1131
+ }
1132
+ const added = [];
1133
+ for (const [name, provider] of Object.entries(current.providers)) {
1134
+ if (provider.type !== "anthropic" || !provider.models?.length)
1135
+ continue;
1136
+ for (const model of newClaudeReleases(live, provider.models.map((entry) => entry.id), offered)) {
1137
+ provider.models.push({ id: model.id, name: model.name ?? model.id });
1138
+ added.push(`${name}/${model.id}`);
1139
+ }
1140
+ }
1141
+ if (added.length) {
1142
+ const errors = validate(current);
1143
+ if (errors.length > 0) {
1144
+ deps.log.warn(`admin: claude models not adopted: config did not validate: ${errors[0]}`);
1145
+ return [];
1146
+ }
1147
+ writeConfigFile(deps.configFile, current);
1148
+ deps.log.info(`admin: declared new Claude model(s): ${added.join(", ")}`);
1149
+ }
1150
+ fs.writeFileSync(stateFile, JSON.stringify({ offered: [...new Set([...offered, ...live.map((model) => model.id)])].sort() }, null, 2) + "\n");
1151
+ return added;
1152
+ }
1092
1153
  /** Run one provider measurement job to completion. Detached: the request that started it has already answered. */
1093
1154
  async function runMeasureJob(jobId, providerName, models, deps) {
1094
1155
  const job = MEASURE_JOBS.get(jobId);
@@ -1145,7 +1206,7 @@ async function runMeasureJob(jobId, providerName, models, deps) {
1145
1206
  MEASURE_JOBS.set(jobId, { ...job, state: "failed", error: errorText(e), finishedAt: Date.now() });
1146
1207
  }
1147
1208
  }
1148
- function pickerModels(deps) {
1209
+ async function pickerModels(deps) {
1149
1210
  const last = deps.picker?.().last;
1150
1211
  if (last && typeof last === "object" && Array.isArray(last.surfaces)) {
1151
1212
  const entries = last.surfaces
@@ -1171,7 +1232,7 @@ function pickerModels(deps) {
1171
1232
  if (models.length > 0)
1172
1233
  return { models, source: "picker" };
1173
1234
  }
1174
- return { models: CLAUDE_MODEL_FALLBACK, source: "fallback" };
1235
+ return { models: await claudeModelList(deps), source: "fallback" };
1175
1236
  }
1176
1237
  function tailLines(file, n) {
1177
1238
  if (!fs.existsSync(file))
@@ -1293,7 +1354,7 @@ export function startAdmin(deps) {
1293
1354
  return;
1294
1355
  }
1295
1356
  if (pathname === "/api/claude-models" && method === "GET") {
1296
- sendJson(res, 200, pickerModels(deps));
1357
+ sendJson(res, 200, await pickerModels(deps));
1297
1358
  return;
1298
1359
  }
1299
1360
  if (pathname === "/api/effort-levels" && method === "GET") {
@@ -1324,7 +1385,7 @@ export function startAdmin(deps) {
1324
1385
  const probe = parsed;
1325
1386
  if (probe.type === "anthropic") {
1326
1387
  if (probe.auth === "claude-code") {
1327
- sendJson(res, 200, probeClaudeCodeAuth(deps));
1388
+ sendJson(res, 200, await probeClaudeCodeAuth(deps));
1328
1389
  return;
1329
1390
  }
1330
1391
  if (probe.auth === "api-key") {
@@ -14,7 +14,7 @@ import { CertStore } from "./certs.js";
14
14
  import { Logger } from "./log.js";
15
15
  import { UpstreamHealth, EXIT_UPSTREAM_UNREACHABLE } from "./health.js";
16
16
  import { Proxy } from "./proxy.js";
17
- import { measureUnsettledModels, startAdmin } from "./admin.js";
17
+ import { adoptNewClaudeModels, measureUnsettledModels, startAdmin } from "./admin.js";
18
18
  import { loadModelCatalog } from "./catalog.js";
19
19
  import { RequestLog } from "./requestlog.js";
20
20
  import { OpenAiIngress } from "./ingress/server.js";
@@ -164,6 +164,7 @@ proxy
164
164
  picker: () => ({ enabled: !!store.get().picker?.enabled, hosts: terminateHosts(store.get()).slice(1), last: proxy.lastPickerInjection }),
165
165
  observedClaudeCodeAuth,
166
166
  claudeUsage: () => proxy.claudeUsage.snapshot(),
167
+ claudeModels: () => proxy.claudeModels.list(),
167
168
  image: (req, signal) => proxy.generateImage(req, signal),
168
169
  modelCatalog: () => loadModelCatalog(),
169
170
  shutdown: () => beginDrain("shutdown requested"),
@@ -174,6 +175,11 @@ proxy
174
175
  // A minute in, once start-up traffic has settled: a few one-token requests per model whose
175
176
  // effort nobody has measured yet, so effort works without the owner re-saving the provider.
176
177
  setTimeout(() => measureUnsettledModels(adminDeps), 60_000).unref();
178
+ // Claude releases reach the declared models without a ClaudeRipple release: checked a minute in,
179
+ // then every six hours (the catalogue itself is cached for an hour).
180
+ const adopt = () => void adoptNewClaudeModels(adminDeps).catch((e) => log.warn(`claude models: ${e.message}`));
181
+ setTimeout(adopt, 60_000).unref();
182
+ setInterval(adopt, 6 * 60 * 60_000).unref();
177
183
  })
178
184
  .catch((e) => {
179
185
  log.error(`listen failed: ${e.message}`);
@@ -0,0 +1,125 @@
1
+ const EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"];
2
+ /**
3
+ * The active models in a `/v1/models` body. A dated id (`claude-haiku-4-5-20251001`) is folded to
4
+ * its alias, which the API takes as well and which is the id the rest of ClaudeRipple and Claude
5
+ * Code's own aliases use.
6
+ */
7
+ export function claudeModelsFromBody(body) {
8
+ const data = body?.data;
9
+ if (!Array.isArray(data))
10
+ return [];
11
+ const out = [];
12
+ for (const raw of data) {
13
+ if (!raw || typeof raw !== "object")
14
+ continue;
15
+ const m = raw;
16
+ if (typeof m.id !== "string" || !m.id.startsWith("claude-"))
17
+ continue;
18
+ if (m.lifecycle !== undefined && m.lifecycle !== "active")
19
+ continue;
20
+ const id = m.id.replace(/-\d{8}$/, "");
21
+ if (out.some((entry) => entry.id === id))
22
+ continue;
23
+ const name = typeof m.display_name === "string" ? m.display_name.replace(/^Claude\s+/, "") : id;
24
+ const effort = m.capabilities?.effort;
25
+ const supported = (key) => {
26
+ const value = effort?.[key];
27
+ return typeof value === "object" && value !== null && value.supported === true;
28
+ };
29
+ const entry = { id, name };
30
+ // Haiku 4.5 reports the effort block with every level unsupported: it takes no effort at all.
31
+ if (effort && typeof effort === "object")
32
+ entry.effortLevels = effort.supported === true ? EFFORT_ORDER.filter(supported) : [];
33
+ if (typeof m.max_input_tokens === "number" && m.max_input_tokens > 0)
34
+ entry.contextWindow = m.max_input_tokens;
35
+ if (typeof m.line === "string")
36
+ entry.line = m.line;
37
+ const created = typeof m.created_at === "string" ? Date.parse(m.created_at) : NaN;
38
+ if (Number.isFinite(created))
39
+ entry.createdAt = created;
40
+ out.push(entry);
41
+ }
42
+ return out;
43
+ }
44
+ /**
45
+ * The account's model list, fetched at most once per `ttlMs` (an hour by default: Anthropic ships a
46
+ * model every few weeks). A failed lookup keeps the last good list, and null means none was ever
47
+ * had, so a caller falls back to its own.
48
+ */
49
+ export class ClaudeModelCatalog {
50
+ last = null;
51
+ fetchedAt = Number.NEGATIVE_INFINITY;
52
+ inFlight = null;
53
+ source;
54
+ opts;
55
+ constructor(source, opts) {
56
+ this.source = source;
57
+ this.opts = opts;
58
+ }
59
+ list() {
60
+ const now = (this.opts.now ?? Date.now)();
61
+ if (now - this.fetchedAt < (this.opts.ttlMs ?? 60 * 60_000))
62
+ return Promise.resolve(this.last);
63
+ this.inFlight ??= this.fetch().finally(() => {
64
+ this.fetchedAt = (this.opts.now ?? Date.now)();
65
+ this.inFlight = null;
66
+ });
67
+ return this.inFlight;
68
+ }
69
+ async fetch() {
70
+ let credential;
71
+ try {
72
+ credential = (await this.source.credentials())[0];
73
+ }
74
+ catch (e) {
75
+ this.opts.warn?.(`claude models: no credentials: ${e.message}`);
76
+ return this.last;
77
+ }
78
+ if (!credential)
79
+ return this.last;
80
+ try {
81
+ const models = await fetchClaudeModels(`https://${this.opts.upstream()}`, credential.headers, this.opts.fetch, this.opts.timeoutMs);
82
+ if (models.length)
83
+ this.last = models;
84
+ }
85
+ catch (e) {
86
+ this.opts.warn?.(`claude models: ${e.message}`);
87
+ }
88
+ return this.last;
89
+ }
90
+ }
91
+ /** One `GET /v1/models` with the given credentials. Throws on anything but a usable answer. */
92
+ export async function fetchClaudeModels(origin, headers, fetchImpl = fetch, timeoutMs = 8000) {
93
+ const res = await fetchImpl(`${origin}/v1/models?limit=100`, {
94
+ method: "GET",
95
+ headers: { "anthropic-version": "2023-06-01", ...headers, accept: "application/json" },
96
+ signal: AbortSignal.timeout(timeoutMs),
97
+ });
98
+ // The status alone: the body of an auth failure is not worth the risk of echoing.
99
+ if (!res.ok)
100
+ throw new Error(`HTTP ${res.status}`);
101
+ return claudeModelsFromBody(await res.json());
102
+ }
103
+ /**
104
+ * The models to add to a provider's declared list: each one released after the newest model it
105
+ * already declares in the same family, and not offered before. A family the provider does not
106
+ * declare at all is left out — someone who unticked every Sonnet did not ask for the next one — and
107
+ * so is anything in `offered`, so a model unticked after it arrived does not come back.
108
+ */
109
+ export function newClaudeReleases(live, declared, offered) {
110
+ const byId = new Map(live.map((model) => [model.id, model]));
111
+ const newest = new Map();
112
+ for (const id of declared) {
113
+ const model = byId.get(id.replace(/-\d{8}$/, ""));
114
+ if (!model?.line || model.createdAt === undefined)
115
+ continue;
116
+ newest.set(model.line, Math.max(newest.get(model.line) ?? Number.NEGATIVE_INFINITY, model.createdAt));
117
+ }
118
+ const declaredIds = new Set(declared.map((id) => id.replace(/-\d{8}$/, "")));
119
+ return live.filter((model) => {
120
+ if (declaredIds.has(model.id) || offered.has(model.id) || !model.line || model.createdAt === undefined)
121
+ return false;
122
+ const floor = newest.get(model.line);
123
+ return floor !== undefined && model.createdAt > floor;
124
+ });
125
+ }
@@ -37,6 +37,7 @@ import { effortLevelsForModel } from "./admin.js";
37
37
  import { ResponseUsageTap } from "./requestlog.js";
38
38
  import { credentialHeaderValues, redactErrorText, redactHeaders } from "./redact.js";
39
39
  import { ClaudeAccountAuthPool } from "./providers/anthropic-account-pool.js";
40
+ import { ClaudeModelCatalog } from "./providers/anthropic-models.js";
40
41
  import { ClaudeUsage } from "./providers/anthropic-usage.js";
41
42
  import { dropForeignThinking } from "./thinking.js";
42
43
  const MAX_BODY = 64 * 1024 * 1024;
@@ -188,6 +189,8 @@ export class Proxy {
188
189
  claudeAccounts;
189
190
  /** Each Claude subscription account's usage, for the admin status (never a token). */
190
191
  claudeUsage;
192
+ /** The Claude models the signed-in account can use, from Anthropic's own list. */
193
+ claudeModels;
191
194
  deps;
192
195
  /**
193
196
  * Cooldowns, quarantines and conversation stickiness for provider credentials. In memory: a
@@ -357,6 +360,10 @@ export class Proxy {
357
360
  upstream: () => this.deps.config().upstream,
358
361
  warn: (message) => deps.log.warn(message),
359
362
  });
363
+ this.claudeModels = new ClaudeModelCatalog(this.claudeAccounts, {
364
+ upstream: () => this.deps.config().upstream,
365
+ warn: (message) => deps.log.warn(message),
366
+ });
360
367
  this.httpServer = http.createServer({ maxHeaderSize: 64 * 1024 }, (req, res) => {
361
368
  void this.handle(req, res);
362
369
  });
@@ -5,4 +5,4 @@
5
5
  // from the manifests. Until 0.1.1 the router and the CLI each carried their own literal, and both
6
6
  // still said "0.1.0" in the 0.1.1 release: after an update nobody could tell which router was
7
7
  // running, and the tray app had nothing to compare (2026-09-15, reported from a Windows install).
8
- export const VERSION = "0.8.8";
8
+ export const VERSION = "0.8.9";
package/dist/ui/app.js CHANGED
@@ -1795,7 +1795,10 @@ function openAnthropicProviderForm(options) {
1795
1795
  response.error ? el("div", { class: "small", text: response.error.replace(/^no-credits:\s*/, "") }) : null,
1796
1796
  ].filter(Boolean));
1797
1797
  if (Array.isArray(response.models) && response.models.length) {
1798
- foundModels = response.models;
1798
+ // Anthropic's own list, so a release shows up here without a ClaudeRipple update. A saved
1799
+ // model the list no longer carries stays visible and ticked rather than dropped on save.
1800
+ const listed = new Set(response.models.map((model) => model.id));
1801
+ foundModels = [...response.models, ...modelsOf(existing).filter((model) => !listed.has(model.id))];
1799
1802
  selected = new Set(existing ? modelsOf(existing).map((model) => model.id) : foundModels.map((model) => model.id));
1800
1803
  renderModels();
1801
1804
  }
@@ -1803,6 +1806,9 @@ function openAnthropicProviderForm(options) {
1803
1806
  finally { if (formActive) probeButton.disabled = false; }
1804
1807
  }
1805
1808
  probeButton.addEventListener("click", () => void runProbe());
1809
+ // The Claude login needs nothing typed, so fetch the account's model list as soon as the form
1810
+ // opens: a saved provider otherwise showed only what was ticked when it was first saved.
1811
+ if (auth.value === "claude-code") setTimeout(() => void runProbe(), 0);
1806
1812
  const modeHint = hint("");
1807
1813
  function syncModeHint() { modeHint.textContent = auth.value === "claude-code" && accountPool ? t("providers.anthropicPoolRouting") : t("providers.anthropicIngressOnly"); }
1808
1814
  auth.addEventListener("change", syncModeHint);
@@ -3,15 +3,18 @@
3
3
  // Used only while the router is starting up or an older router does not yet expose
4
4
  // /api/presets and /api/claude-models. The API response replaces these lists.
5
5
  const FALLBACK_CLAUDE_MODELS = [
6
+ { id: "claude-haiku-5-5", name: "Haiku 5.5" },
7
+ { id: "claude-sonnet-5-5", name: "Sonnet 5.5" },
8
+ { id: "claude-opus-5-5", name: "Opus 5.5" },
6
9
  { id: "claude-fable-5-1", name: "Fable 5.1" },
7
10
  { id: "claude-opus-5", name: "Opus 5" },
8
11
  { id: "claude-sonnet-5", name: "Sonnet 5" },
9
- { id: "claude-haiku-4-5", name: "Haiku 4.5" },
10
12
  { id: "claude-fable-5", name: "Fable 5" },
11
13
  { id: "claude-opus-4-8", name: "Opus 4.8" },
12
14
  { id: "claude-opus-4-7", name: "Opus 4.7" },
13
- { id: "claude-opus-4-6", name: "Opus 4.6" },
14
15
  { id: "claude-sonnet-4-6", name: "Sonnet 4.6" },
16
+ { id: "claude-opus-4-6", name: "Opus 4.6" },
17
+ { id: "claude-haiku-4-5", name: "Haiku 4.5" },
15
18
  ];
16
19
 
17
20
  const CHATGPT_MODELS = [
@@ -208,6 +208,18 @@ request went, and a `Pane` holds the request log.
208
208
 
209
209
  ## 4. Provider adapters
210
210
 
211
+ - **The Claude model list is Anthropic's own (2026-10-08).** The native `anthropic`
212
+ provider's models came from a constant in admin.ts, so each release after it was cut
213
+ was missing from the provider form, the mapping table and the declared list until a
214
+ ClaudeRipple release caught up — Opus 5.5 (Sep 21), Sonnet 5.5 (Sep 28) and Haiku 5.5
215
+ (Oct 7) all were, while Claude Code itself had them. `GET /v1/models` answers the Claude
216
+ Code login (the observed headers) as well as an API key, newest first, with each model's
217
+ effort ladder, context window and lifecycle (measured: 14 active models). The router
218
+ caches it for an hour (`ClaudeModelCatalog`); the constant is only the fallback when no
219
+ sign-in answers. A release is also **declared** on its own, every six hours: added to a
220
+ native provider that already declares an older model of the same family, and remembered
221
+ in `claude-models-offered.json` so a model unticked afterwards is not added again.
222
+
211
223
  - **Declared models route themselves (2026-09-19).** A provider's `models` list —
212
224
  filled by the probe from the vendor's `/models`, ticked in the GUI — now decides
213
225
  routing when no rule matched: a model exactly one provider carries goes there,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauderipple",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "Use GPT (any ChatGPT plan with Codex), DeepSeek and 400+ other models in the Claude Desktop Code tab and Claude Code while staying signed in to your Claude subscription — no third-party gateway mode. Several ChatGPT accounts that take over when one runs out, in Codex too.",
5
5
  "license": "GPL-3.0-only",
6
6
  "workspaces": [