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, })), }); }