diff --git a/diagnostic/index.html b/diagnostic/index.html
index a39d25e..445325f 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -611,19 +611,27 @@
/shared/config/runtime.json als brainModel persistiert.
Bei Aenderung: aria-brain restarten (Reparatur-Section oben), damit's greift.
- ARIA laeuft ueber dein Claude-Max-Abo (CLI) — waehlbar ist der Tier
- (Opus/Sonnet/Haiku), nicht eine feste Version. Die CLI nimmt automatisch das
- jeweils aktuelle Modell des Tiers.
+ ARIA laeuft ueber dein Claude-Max-Abo (CLI). Du hast zwei Möglichkeiten:
+
+ Automatisch (Opus/Sonnet/Haiku/Fable) — folgt immer der neuesten
+ Version dieses Tiers, ohne dass du etwas pflegen musst. Für den Alltag die
+ richtige Wahl.
+
+ Feste Version (z.B. „Opus 5.5") — genau dieses Modell, auch wenn
+ später ein neueres kommt.
- Die Auswahlliste kommt aus /shared/config/models.json — dort kannst du
- Tiers/Beschreibungen anpassen (kein Neustart noetig, danach „↻ Aktualisieren").
+ Die Liste kommt aus Anthropics eigenem Modell-Katalog
+ (downloads.claude.ai) — 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
+ /shared/config/models.json.
Aktives Model:
-
+
@@ -7899,17 +7907,46 @@
// Kuratierte Tier-Liste vom Proxy holen (via Diagnostic-Server) und ins
// 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 st = document.getElementById('model-status');
if (!sel) return;
+ if (force && st) { st.textContent = 'Hole Modell-Liste von Anthropic …'; st.style.color = '#8888AA'; }
try {
- const r = await fetch('/api/models-list');
+ const r = await fetch('/api/models-list' + (force ? '?refresh=1' : ''));
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Fehler');
_modelListCache = d.models || [];
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');
opt.value = m.id;
opt.textContent = m.displayName || m.id;
@@ -7920,6 +7957,10 @@
opt.value = ''; opt.textContent = '(keine Modelle vom Proxy)';
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();
// Aktuelles Model vom Brain holen → markiert die richtige Option
loadModel();
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 9b0dd83..5d172dd 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -2204,22 +2204,27 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ ok: false, error: err.message }));
}
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
// Sprachmodell-Dropdown. ARIA laeuft ueber das Max-Abo/CLI, waehlbar ist
// der Tier (opus/sonnet/haiku), keine feste Version.
(async () => {
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 models = (d.data || []).map(m => ({
id: m.id,
tier: m.tier || m.id,
displayName: m.display_name || m.id,
description: m.description || "",
+ // section: tier | main | overflow — die UI gruppiert danach.
+ section: m.section || "tier",
}));
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) {
res.writeHead(502, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: String(err && err.message || err) }));
diff --git a/proxy-patches/openai-to-cli.js b/proxy-patches/openai-to-cli.js
index d2b87d9..516efc4 100644
--- a/proxy-patches/openai-to-cli.js
+++ b/proxy-patches/openai-to-cli.js
@@ -15,29 +15,47 @@
* (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 = {
- "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",
"sonnet": "sonnet",
"haiku": "haiku",
"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",
- // 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--.
+// 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) {
if (MODEL_MAP[model]) return MODEL_MAP[model];
const stripped = (model || "").replace(/^claude-code-cli\//, "");
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";
}
diff --git a/proxy-patches/routes.js b/proxy-patches/routes.js
index 2a5418f..c628647 100644
--- a/proxy-patches/routes.js
+++ b/proxy-patches/routes.js
@@ -552,19 +552,144 @@ function _loadModels() {
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..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 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({
object: "list",
- data: models.map(m => ({
+ catalog_ok: catalog.length > 0,
+ data: [...tiers, ...exact].map(m => ({
id: m.id,
object: "model",
owned_by: "anthropic",
created,
- tier: m.tier || m.id,
- display_name: m.display_name || m.id,
- description: m.description || "",
+ tier: m.tier,
+ display_name: m.display_name,
+ description: m.description,
+ section: m.section,
})),
});
}