fix(models): Modell-Liste kommt jetzt von Anthropic — und feste Versionen greifen wirklich

Zwei Fehler. Der zweite war der schlimmere.

1) Die Liste war handgepflegt und damit garantiert veraltet.
   Sie kannte "Opus (aktuell: Opus 5)" — waehrend es laengst Opus 5.5,
   Sonnet 5.5 und Fable 5.1 gab. Der Knopf "↻ Aktualisieren" las nur eine
   lokale Datei neu und aktualisierte damit gar nichts.

   Jetzt kommt die Liste aus Anthropics eigenem Modell-Katalog
   (downloads.claude.ai/model-catalog/v1/catalog.json, Surface 'cc') —
   dieselbe Quelle, aus der sich Claude Code selbst bedient. Oeffentlich,
   ohne API-Key (die Models-API braucht einen; mit dem Max-Abo gibt es keinen).
   6h-Cache plus Datei-Cache, der auch offline und bei abgelaufenem TTL
   einspringt: eine etwas alte echte Liste ist besser als keine.

   Die Tier-Aliase (opus/sonnet/haiku/fable) bleiben vorne und bleiben die
   Empfehlung — sie folgen automatisch der neuesten Version. Darunter die
   exakten Versionen, aeltere ausdruecklich als solche markiert.

2) Eine feste Version liess sich faktisch nicht waehlen.
   extractModel() hatte eine MODEL_MAP und fiel fuer alles Unbekannte STILL
   auf "opus" zurueck. Wer "claude-opus-5-5" setzte, bekam ohne jede Meldung
   das Tier-Default. Schlimmer noch: claude-opus-5 und claude-fable-5 standen
   selbst in der Map und wurden auf ihr Tier eingeschmolzen — eine bewusst
   gewaehlte aeltere Version wurde also durch die neueste ersetzt.

   Volle Modellnamen gehen jetzt unveraendert an die CLI. Die versteht sie
   ("--model: an alias for the latest model or a model's full name") und
   meldet sich selbst klar, wenn sie einen Namen nicht kennt — ehrlicher als
   ein stilles Downgrade. Nur echter Muell faellt noch auf "opus", und das
   wird geloggt.

Nachgewiesen gegen den LIVE-Katalog und die echte CLI im Proxy-Container:
20 Pruefungen gruen, u.a. dass alle 12 gelieferten exakten Versionen den Proxy
unveraendert passieren. `claude --print --model claude-opus-5-5` antwortet.
Stefans aktive Einstellung ist der Alias "opus" — am laufenden Betrieb aendert
sich dadurch nichts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:47:34 +02:00
co-authored by Claude Opus 5
parent 4c0d8ce3fe
commit 09d06011d5
4 changed files with 218 additions and 29 deletions
+50 -9
View File
@@ -611,19 +611,27 @@
<code>/shared/config/runtime.json</code> als <code>brainModel</code> persistiert. <code>/shared/config/runtime.json</code> als <code>brainModel</code> persistiert.
Bei Aenderung: <strong>aria-brain restarten</strong> (Reparatur-Section oben), damit's greift. Bei Aenderung: <strong>aria-brain restarten</strong> (Reparatur-Section oben), damit's greift.
<br><br> <br><br>
ARIA laeuft ueber dein Claude-Max-Abo (CLI) — waehlbar ist der <strong>Tier</strong> ARIA laeuft ueber dein Claude-Max-Abo (CLI). Du hast zwei Möglichkeiten:
(Opus/Sonnet/Haiku), nicht eine feste Version. Die CLI nimmt automatisch das <br>
jeweils aktuelle Modell des Tiers. <strong>Automatisch</strong> (Opus/Sonnet/Haiku/Fable) — folgt immer der neuesten
Version dieses Tiers, ohne dass du etwas pflegen musst. Für den Alltag die
richtige Wahl.
<br>
<strong>Feste Version</strong> (z.B. „Opus 5.5") — genau dieses Modell, auch wenn
später ein neueres kommt.
<br><br> <br><br>
Die Auswahlliste kommt aus <code>/shared/config/models.json</code> — dort kannst du Die Liste kommt aus Anthropics eigenem <strong>Modell-Katalog</strong>
Tiers/Beschreibungen anpassen (kein Neustart noetig, danach „↻ Aktualisieren"). (<code>downloads.claude.ai</code>) — dieselbe Quelle, aus der sich Claude Code
selbst bedient. „↻ Aktualisieren" holt sie frisch; sonst gilt ein 6-Stunden-Cache.
Die automatischen Tiers kommen weiterhin aus
<code>/shared/config/models.json</code>.
</div> </div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;"> <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span style="font-size:12px;color:#8888AA;white-space:nowrap;">Aktives Model:</span> <span style="font-size:12px;color:#8888AA;white-space:nowrap;">Aktives Model:</span>
<select id="setting-model-select" onchange="onModelSelectChange()" style="flex:1;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-family:inherit;font-size:12px;"> <select id="setting-model-select" onchange="onModelSelectChange()" style="flex:1;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-family:inherit;font-size:12px;">
<option value="">(lade Liste…)</option> <option value="">(lade Liste…)</option>
</select> </select>
<button class="btn secondary" onclick="loadModelList()" title="Liste vom Proxy neu holen" style="padding:4px 8px;font-size:10px;">↻ Aktualisieren</button> <button class="btn secondary" onclick="loadModelList(true)" title="Modell-Liste frisch von Anthropic holen (Modell-Katalog)" style="padding:4px 8px;font-size:10px;">↻ Aktualisieren</button>
<button class="btn" onclick="saveModel()" style="padding:4px 8px;font-size:10px;">Setzen</button> <button class="btn" onclick="saveModel()" style="padding:4px 8px;font-size:10px;">Setzen</button>
</div> </div>
<div id="model-desc" style="font-size:10px;color:#6a6a88;margin-bottom:6px;min-height:12px;"></div> <div id="model-desc" style="font-size:10px;color:#6a6a88;margin-bottom:6px;min-height:12px;"></div>
@@ -7899,17 +7907,46 @@
// Kuratierte Tier-Liste vom Proxy holen (via Diagnostic-Server) und ins // Kuratierte Tier-Liste vom Proxy holen (via Diagnostic-Server) und ins
// Dropdown fuellen. Danach get_model, damit das aktive Model markiert wird. // Dropdown fuellen. Danach get_model, damit das aktive Model markiert wird.
async function loadModelList() { // force=true (Knopf „↻ Aktualisieren") holt die Liste frisch von Anthropic
// statt aus dem Proxy-Cache.
async function loadModelList(force) {
const sel = document.getElementById('setting-model-select'); const sel = document.getElementById('setting-model-select');
const st = document.getElementById('model-status'); const st = document.getElementById('model-status');
if (!sel) return; if (!sel) return;
if (force && st) { st.textContent = 'Hole Modell-Liste von Anthropic …'; st.style.color = '#8888AA'; }
try { try {
const r = await fetch('/api/models-list'); const r = await fetch('/api/models-list' + (force ? '?refresh=1' : ''));
const d = await r.json(); const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Fehler'); if (!d.ok) throw new Error(d.error || 'Fehler');
_modelListCache = d.models || []; _modelListCache = d.models || [];
sel.innerHTML = ''; sel.innerHTML = '';
for (const m of _modelListCache) { // Nach Herkunft gruppieren, damit die Wahl klar ist:
// Tier — folgt automatisch der neuesten Version (Empfehlung)
// main — die aktuell von Anthropic vorgeschlagenen Versionen
// overflow— aeltere Versionen, nur wenn man sie bewusst will
const GROUPS = [
['tier', 'Automatisch (folgt der neuesten Version)'],
['main', 'Aktuelle Versionen'],
['overflow', 'Ältere Versionen'],
];
for (const [section, label] of GROUPS) {
const inGroup = _modelListCache.filter(m => (m.section || 'tier') === section);
if (!inGroup.length) continue;
const g = document.createElement('optgroup');
g.label = label;
for (const m of inGroup) {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.displayName || m.id;
g.appendChild(opt);
}
sel.appendChild(g);
}
// Unbekannte Sektionen nicht verschlucken (aeltere Proxy-Version
// liefert gar kein section-Feld -> die landen oben unter 'tier').
const known = new Set(GROUPS.map(g => g[0]));
const rest = _modelListCache.filter(m => !known.has(m.section || 'tier'));
for (const m of rest) {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = m.id; opt.value = m.id;
opt.textContent = m.displayName || m.id; opt.textContent = m.displayName || m.id;
@@ -7920,6 +7957,10 @@
opt.value = ''; opt.textContent = '(keine Modelle vom Proxy)'; opt.value = ''; opt.textContent = '(keine Modelle vom Proxy)';
sel.appendChild(opt); sel.appendChild(opt);
} }
if (st && d.catalogOk === false) {
st.textContent = 'Anthropic-Katalog nicht erreichbar — nur die automatischen Tiers verfügbar.';
st.style.color = '#FFD60A';
}
onModelSelectChange(); onModelSelectChange();
// Aktuelles Model vom Brain holen → markiert die richtige Option // Aktuelles Model vom Brain holen → markiert die richtige Option
loadModel(); loadModel();
+8 -3
View File
@@ -2204,22 +2204,27 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ ok: false, error: err.message })); res.end(JSON.stringify({ ok: false, error: err.message }));
} }
return; return;
} else if (req.url === "/api/models-list" && req.method === "GET") { } else if (req.url.startsWith("/api/models-list") && req.method === "GET") {
// Kuratierte Model-Liste vom Proxy (/v1/models) — Tier-Auswahl fuers // Kuratierte Model-Liste vom Proxy (/v1/models) — Tier-Auswahl fuers
// Sprachmodell-Dropdown. ARIA laeuft ueber das Max-Abo/CLI, waehlbar ist // Sprachmodell-Dropdown. ARIA laeuft ueber das Max-Abo/CLI, waehlbar ist
// der Tier (opus/sonnet/haiku), keine feste Version. // der Tier (opus/sonnet/haiku), keine feste Version.
(async () => { (async () => {
try { try {
const r = await fetch(`${PROXY_URL}/v1/models`); // ?refresh=1 weiterreichen: der Proxy umgeht damit seinen Katalog-Cache
// und holt die Liste frisch von Anthropic („↻ Aktualisieren").
const refresh = /[?&]refresh=1\b/.test(req.url) ? "?refresh=1" : "";
const r = await fetch(`${PROXY_URL}/v1/models${refresh}`);
const d = await r.json(); const d = await r.json();
const models = (d.data || []).map(m => ({ const models = (d.data || []).map(m => ({
id: m.id, id: m.id,
tier: m.tier || m.id, tier: m.tier || m.id,
displayName: m.display_name || m.id, displayName: m.display_name || m.id,
description: m.description || "", description: m.description || "",
// section: tier | main | overflow — die UI gruppiert danach.
section: m.section || "tier",
})); }));
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, models })); res.end(JSON.stringify({ ok: true, models, catalogOk: d.catalog_ok !== false }));
} catch (err) { } catch (err) {
res.writeHead(502, { "Content-Type": "application/json" }); res.writeHead(502, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: String(err && err.message || err) })); res.end(JSON.stringify({ ok: false, error: String(err && err.message || err) }));
+29 -11
View File
@@ -15,29 +15,47 @@
* (siehe docker-compose.yml proxy-Block). * (siehe docker-compose.yml proxy-Block).
*/ */
// NUR echte Tier-Aliase. Die CLI loest sie selbst auf die aktuelle Version des
// Tiers auf.
//
// Hier standen frueher auch volle Versionen (claude-opus-5 → "opus",
// claude-fable-5 → "fable", ...). Das war falsch: wer bewusst eine BESTIMMTE
// Version waehlte, bekam still das Tier-Default — also unter Umstaenden ein
// ganz anderes Modell als ausgewaehlt. Volle Namen gehen jetzt unveraendert an
// die CLI (siehe extractModel), die versteht sie.
const MODEL_MAP = { const MODEL_MAP = {
"claude-opus-4": "opus",
"claude-sonnet-4": "sonnet",
"claude-haiku-4": "haiku",
"claude-code-cli/claude-opus-4": "opus",
"claude-code-cli/claude-sonnet-4": "sonnet",
"claude-code-cli/claude-haiku-4": "haiku",
"opus": "opus", "opus": "opus",
"sonnet": "sonnet", "sonnet": "sonnet",
"haiku": "haiku", "haiku": "haiku",
"fable": "fable", "fable": "fable",
"claude-fable-5": "fable", "claude-code-cli/opus": "opus",
"claude-code-cli/sonnet": "sonnet",
"claude-code-cli/haiku": "haiku",
"claude-code-cli/fable": "fable", "claude-code-cli/fable": "fable",
// Volle aktuelle IDs (falls die App/Diagnostic sie mal direkt setzt)
"claude-opus-5": "opus",
"claude-sonnet-5": "sonnet",
"claude-haiku-4-5": "haiku",
}; };
// Echte Claude-Modellnamen: claude-<familie>-<zahlen/suffixe>.
// Die CLI nimmt laut `claude --help` "an alias for the latest model (e.g.
// 'fable', 'opus', or 'sonnet') or a model's full name" — exakte Versionen
// darf man also direkt durchreichen.
const FULL_MODEL_RE = /^claude-[a-z]+-[0-9][0-9a-z.-]*$/;
export function extractModel(model) { export function extractModel(model) {
if (MODEL_MAP[model]) return MODEL_MAP[model]; if (MODEL_MAP[model]) return MODEL_MAP[model];
const stripped = (model || "").replace(/^claude-code-cli\//, ""); const stripped = (model || "").replace(/^claude-code-cli\//, "");
if (MODEL_MAP[stripped]) return MODEL_MAP[stripped]; if (MODEL_MAP[stripped]) return MODEL_MAP[stripped];
// Exakte Version durchreichen statt sie auf einen Tier einzuschmelzen.
//
// Vorher landete JEDER unbekannte Name still auf "opus". Damit war eine
// neuere Version faktisch nicht waehlbar: wer "claude-opus-5-5" setzte,
// bekam ohne jede Meldung das Tier-Default — die Einstellung sah gesetzt
// aus und tat nichts. Kennt die CLI den Namen nicht, sagt SIE das deutlich
// ("isn't described by this version's model catalog"), und das ist die
// ehrlichere Antwort als ein stilles Downgrade.
if (FULL_MODEL_RE.test(stripped)) return stripped;
if (stripped) {
console.error(`[aria-model] Unbekannter Modellname ${JSON.stringify(stripped)} — nutze 'opus'`);
}
return "opus"; return "opus";
} }
+131 -6
View File
@@ -552,19 +552,144 @@ function _loadModels() {
return DEFAULT_MODELS; return DEFAULT_MODELS;
} }
export function handleModels(_req, res) { // ── Echte Modell-Liste von Anthropic ────────────────────────────────
//
// Anthropic veroeffentlicht einen signierten Modell-Katalog, aus dem auch
// Claude Code selbst seinen Modell-Auswaehler fuellt. Oeffentlich erreichbar,
// KEIN API-Key noetig (die Models-API /v1/models braucht einen; mit dem
// Max-Abo gibt es keinen).
//
// Warum ueberhaupt: die handgepflegte Liste hier veraltete garantiert. Sie
// kannte Opus 5 — waehrend im Katalog laengst Opus 5.5, Sonnet 5.5 und
// Fable 5.1 standen. Ein "↻ Aktualisieren"-Knopf, der nur eine lokale Datei
// neu liest, aktualisiert nichts.
//
// Struktur: surfaces.<surface>.model_selector_config[].models[]
// = { id, name, short_name, description, section }
// 'cc' ist die Claude-Code-Oberflaeche — genau die, durch die ARIA laeuft.
// section 'main' = die vorgeschlagenen, 'overflow' = aeltere/weitere.
const CATALOG_URL = process.env.ARIA_MODEL_CATALOG_URL
|| "https://downloads.claude.ai/model-catalog/v1/catalog.json";
const CATALOG_CACHE_FILE = "/shared/config/model_catalog_cache.json";
const CATALOG_TTL_MS = 6 * 60 * 60 * 1000; // 6h — der Katalog gilt eine Woche
const CATALOG_SURFACE = "cc";
let _catalogMem = null; // { at, models }
function _parseCatalog(doc) {
const out = [];
const surfaces = (doc && doc.surfaces && doc.surfaces[CATALOG_SURFACE]) || null;
const configs = surfaces && surfaces.model_selector_config;
if (!Array.isArray(configs)) return out;
for (const cfg of configs) {
for (const m of (cfg.models || [])) {
if (!m || typeof m.id !== "string") continue;
out.push({
id: m.id,
tier: m.short_name ? String(m.short_name).toLowerCase() : m.id,
display_name: m.name || m.id,
description: m.description || "",
section: m.section || "main",
});
}
}
return out;
}
function _readCatalogCache() {
try {
const c = JSON.parse(fs.readFileSync(CATALOG_CACHE_FILE, "utf-8"));
if (Array.isArray(c.models) && c.models.length) return c;
} catch (_) { /* kein Cache */ }
return null;
}
/**
* Liefert die Katalog-Modelle. `force` umgeht den TTL (Diagnostic-Knopf).
* Schlaegt der Abruf fehl, gilt der Cache — auch ein abgelaufener, denn eine
* etwas alte echte Liste ist besser als gar keine.
*/
async function _catalogModels(force) {
const now = Date.now();
if (!force && _catalogMem && (now - _catalogMem.at) < CATALOG_TTL_MS) {
return _catalogMem.models;
}
if (!force) {
const cached = _readCatalogCache();
if (cached && (now - (cached.at || 0)) < CATALOG_TTL_MS) {
_catalogMem = { at: cached.at, models: cached.models };
return cached.models;
}
}
try {
const ctl = AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined;
const r = await fetch(CATALOG_URL, { signal: ctl });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const doc = await r.json();
const models = _parseCatalog(doc);
if (!models.length) throw new Error("Katalog enthaelt keine Modelle fuer Surface " + CATALOG_SURFACE);
_catalogMem = { at: now, models };
try {
fs.mkdirSync("/shared/config", { recursive: true });
fs.writeFileSync(CATALOG_CACHE_FILE, JSON.stringify({
at: now, version: doc.version, expires_at: doc.expires_at, models,
}, null, 2));
} catch (e) {
console.error("[aria-models] Katalog-Cache schreiben fehlgeschlagen:", e && e.message);
}
console.error(`[aria-models] Katalog geladen: ${models.length} Modelle (Version ${doc.version})`);
return models;
} catch (e) {
console.error("[aria-models] Katalog-Abruf fehlgeschlagen:", e && e.message);
const cached = _readCatalogCache();
if (cached) {
console.error("[aria-models] nutze Cache vom " + new Date(cached.at || 0).toISOString());
_catalogMem = { at: cached.at, models: cached.models };
return cached.models;
}
return [];
}
}
export async function handleModels(req, res) {
const created = Math.floor(Date.now() / 1000); const created = Math.floor(Date.now() / 1000);
const models = _loadModels(); const force = !!(req && req.query && (req.query.refresh === "1" || req.query.refresh === "true"));
// 1) Tier-Aliase zuerst: sie zeigen IMMER auf die aktuelle Version ihres
// Tiers, ganz ohne Pflege. Fuer den Alltag die richtige Wahl.
const tiers = _loadModels().map(m => ({
id: m.id,
tier: m.tier || m.id,
display_name: m.display_name || m.id,
description: m.description || "",
section: "tier",
}));
// 2) Danach die exakten Versionen aus dem Anthropic-Katalog.
const catalog = await _catalogModels(force);
const seen = new Set(tiers.map(t => t.id));
const exact = catalog.filter(m => !seen.has(m.id)).map(m => ({
id: m.id,
tier: m.tier,
display_name: m.display_name
+ (m.section === "main" ? "" : " (ältere Version)"),
description: m.description
|| (m.section === "main" ? "" : "Aeltere Version — nur waehlen wenn gewollt."),
section: m.section,
}));
res.json({ res.json({
object: "list", object: "list",
data: models.map(m => ({ catalog_ok: catalog.length > 0,
data: [...tiers, ...exact].map(m => ({
id: m.id, id: m.id,
object: "model", object: "model",
owned_by: "anthropic", owned_by: "anthropic",
created, created,
tier: m.tier || m.id, tier: m.tier,
display_name: m.display_name || m.id, display_name: m.display_name,
description: m.description || "", description: m.description,
section: m.section,
})), })),
}); });
} }