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 +10 -0
- package/dist/app/package.json +1 -1
- package/dist/router/src/admin.js +71 -10
- package/dist/router/src/index.js +7 -1
- package/dist/router/src/providers/anthropic-models.js +125 -0
- package/dist/router/src/proxy.js +7 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +7 -1
- package/dist/ui/presets-fallback.js +5 -2
- package/docs/ARCHITECTURE.md +12 -0
- package/package.json +1 -1
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
|
package/dist/app/package.json
CHANGED
package/dist/router/src/admin.js
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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") {
|
package/dist/router/src/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/router/src/proxy.js
CHANGED
|
@@ -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
|
+
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
|
-
|
|
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 = [
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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.
|
|
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": [
|