Merge pull request #6539 from Hmbown/feat/router-jev

feat(auto): official model routing with /router and Jev (OpenRouter or TypeSafe)
This commit is contained in:
Hunter Bown authored and GitHub committed 2026-09-24 15:37:47 -07:00
commit 283695cf25
39 files changed
+3782 -77

No files matched your search

+9
View File
@@ -16,6 +16,15 @@ Planned for Codewhale v0.10.1: a reliability and first-run release. Turns that
stall now say so, approvals keep what you approved, plugin suggestions are
quieter, and Fleet runs can be checked before they spend anything.
### Added
- Official model routing: `/router` (also `/model router`) sets up the Auto
router with presets: Jev (TypeSafe's decision model, via OpenRouter or a
TypeSafe key), your provider's fast tier, Off, or Custom. Each preset makes
one test call before it saves, `/status` shows the router's choice, cost and
latency, and a failing router is shown as failing
([#6525](https://github.com/Hmbown/Codewhale/issues/6525)).
### Fixed
- A turn that stops producing output now reports itself: the turn loop records
+37
View File
@@ -526,6 +526,43 @@
"CmdModeDescription": "Canvia els permisos o obre el selector de mode",
"CmdModelDescription": "Canvia o mostra el model actual",
"CmdModelsDescription": "Obtén els IDs de model en viu de l'API activa",
"CmdRouterDescription": "Configura el router per torn del mode Auto",
"RouterSetupTitle": "Router de models",
"RouterPresetJevHint": "Model de decisió Jev via {route} · uns $0.00002 per torn · la teva última petició i el context recent van a {route}",
"RouterPresetFastHint": "{model} sense raonament · fa servir la teva clau de {provider}",
"RouterPresetOffHint": "Sense crida al router · cada torn Auto fa servir el model per defecte",
"RouterPresetOffCostSavingHint": "Sense crida al router · [auto] cost_saving manté els torns Auto al nivell ràpid",
"RouterPresetCustomHint": "Mostra el TOML de [auto.router] per editar-lo a mà",
"RouterPresetUnavailable": "No disponible: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe va aturar les altes noves el 2026-09-22; OpenRouter és la ruta per defecte per als usuaris nous.",
"RouterTestSaveHint": "S'ha fet una crida de prova amb una petició d'exemple. Enter desa aquest router; Esc el descarta.",
"RouterTestHeader": "Prova del router ({args})",
"RouterTestNotMade": "No s'ha fet la crida de prova: {reason}",
"RouterTestWouldRoute": "Enviaria \"{request}\" a {provider} / {model} · {latency} ms",
"RouterTestDecision": "Decisió: {tier} · {reason}",
"RouterTestChoice": "Tria: {choice} ({probabilities}) · confiança {confidence} (mín. {min})",
"RouterTestCostReported": "Cost: ${cost} (informat pel proveïdor)",
"RouterTestCostUnreported": "Cost: el proveïdor no l'ha informat",
"RouterTestUsage": "Ús: {input} d'entrada · {output} de sortida",
"RouterTestFailing": "El router falla: {reason}",
"RouterSaved": "Router desat ({router}) a {path}",
"RouterNotSaved": "No s'ha desat el router: {error}",
"RouterRuntimeNotReloaded": "Router desat, però els fils d'execució mantenen el router anterior fins que reiniciïs: {error}",
"RouterCurrent": "Actual: {router}",
"RouterCurrentFailing": "Actual: {router} · falla: {reason}",
"RouterSummary": "router {kind} · {provider} / {model}",
"RouterSummaryThinking": "router {kind} · {provider} / {model} · raonament {thinking}",
"RouterPresetFastLabel": "Nivell ràpid",
"RouterPresetCustomLabel": "Personalitzat",
"RouterNoKey": "no hi ha clau d'API de {route}",
"RouterNoFastTier": "el proveïdor actiu no té cap nivell ràpid executable",
"RouterNeedKey": "afegeix una clau d'OpenRouter (OPENROUTER_API_KEY) o de TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "els routers personalitzats s'editen a mà",
"RouterCustomHelpIntro": "Edita [auto.router] a config.toml, per exemple:",
"RouterCustomHelpOutro": "Elimina la taula (o executa /router off) perquè cada torn Auto faci servir el model per defecte.",
"RouterActionTest": "prova",
"RouterActionSave": "desa",
"RouterActionDiscard": "descarta",
"ModelsListHeader": "Models de {provider} (predeterminat: {model})",
"ModelsListHint": "Catàleg en memòria cau; executa `codewhale models --update` per actualitzar els proveïdors configurats.",
"ModelsUpdateSummary": "Catàlegs de models: {updated} actualitzats, {loaded} carregats des de Codex, {failed} amb errors, {skipped} omesos.",
+37
View File
@@ -526,6 +526,43 @@
"CmdModeDescription": "Berechtigungen wechseln oder die Modusauswahl öffnen",
"CmdModelDescription": "Modell wechseln oder aktuelles anzeigen",
"CmdModelsDescription": "Live-Modell-IDs von der aktiven API abrufen",
"CmdRouterDescription": "Den Router pro Runde für das Auto-Modellrouting einrichten",
"RouterSetupTitle": "Modell-Router",
"RouterPresetJevHint": "Jev-Entscheidungsmodell über {route} · etwa $0.00002 pro Runde · deine letzte Anfrage und der jüngste Kontext gehen an {route}",
"RouterPresetFastHint": "{model} ohne Denkmodus · nutzt deinen {provider}-Schlüssel",
"RouterPresetOffHint": "Kein Router-Aufruf · jede Auto-Runde nutzt das Standardmodell",
"RouterPresetOffCostSavingHint": "Kein Router-Aufruf · [auto] cost_saving hält Auto-Runden auf der schnellen Stufe",
"RouterPresetCustomHint": "Das TOML für [auto.router] zum manuellen Bearbeiten anzeigen",
"RouterPresetUnavailable": "Nicht verfügbar: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe hat neue Registrierungen am 2026-09-22 pausiert; OpenRouter ist der Standardweg für neue Nutzer.",
"RouterTestSaveHint": "Ein Testaufruf wurde mit einer Beispielanfrage gemacht. Enter speichert diesen Router; Esc verwirft ihn.",
"RouterTestHeader": "Router-Test ({args})",
"RouterTestNotMade": "Kein Testaufruf erfolgt: {reason}",
"RouterTestWouldRoute": "Würde \"{request}\" an {provider} / {model} leiten · {latency} ms",
"RouterTestDecision": "Entscheidung: {tier} · {reason}",
"RouterTestChoice": "Wahl: {choice} ({probabilities}) · Konfidenz {confidence} (min. {min})",
"RouterTestCostReported": "Kosten: ${cost} (vom Anbieter gemeldet)",
"RouterTestCostUnreported": "Kosten: vom Anbieter nicht gemeldet",
"RouterTestUsage": "Nutzung: {input} ein · {output} aus",
"RouterTestFailing": "Router schlägt fehl: {reason}",
"RouterSaved": "Router gespeichert ({router}) in {path}",
"RouterNotSaved": "Router nicht gespeichert: {error}",
"RouterRuntimeNotReloaded": "Router gespeichert, aber Runtime-Threads nutzen bis zum Neustart den vorherigen Router: {error}",
"RouterCurrent": "Aktuell: {router}",
"RouterCurrentFailing": "Aktuell: {router} · Fehler: {reason}",
"RouterSummary": "{kind}-Router · {provider} / {model}",
"RouterSummaryThinking": "{kind}-Router · {provider} / {model} · Denkmodus {thinking}",
"RouterPresetFastLabel": "Schnelle Stufe",
"RouterPresetCustomLabel": "Benutzerdefiniert",
"RouterNoKey": "kein {route}-API-Schlüssel",
"RouterNoFastTier": "der aktive Anbieter hat keine lauffähige schnelle Stufe",
"RouterNeedKey": "füge einen OpenRouter-Schlüssel (OPENROUTER_API_KEY) oder einen TypeSafe-Schlüssel (TYPESAFE_API_KEY) hinzu",
"RouterCustomByHand": "eigene Router werden von Hand bearbeitet",
"RouterCustomHelpIntro": "Bearbeite [auto.router] in config.toml, zum Beispiel:",
"RouterCustomHelpOutro": "Entferne die Tabelle (oder führe /router off aus), damit jede Auto-Runde das Standardmodell nutzt.",
"RouterActionTest": "testen",
"RouterActionSave": "speichern",
"RouterActionDiscard": "verwerfen",
"ModelsListHeader": "Modelle von {provider} (Standard: {model})",
"ModelsListHint": "Zwischengespeicherter Katalog; mit `codewhale models --update` die eingerichteten Anbieter aktualisieren.",
"ModelsUpdateSummary": "Modellkataloge: {updated} aktualisiert, {loaded} aus Codex geladen, {failed} fehlgeschlagen, {skipped} übersprungen.",
+37
View File
@@ -529,6 +529,43 @@
"CmdModeDescription": "Switch permissions or open the mode picker",
"CmdModelDescription": "Switch or show the current model",
"CmdModelsDescription": "List live model IDs from the active API",
"CmdRouterDescription": "Set up the per-turn router for Auto model routing",
"RouterSetupTitle": "Model router",
"RouterPresetJevHint": "Jev decision model via {route} · about $0.00002 per turn · your latest request and recent context go to {route}",
"RouterPresetFastHint": "{model} with thinking off · uses your {provider} key",
"RouterPresetOffHint": "No router call · every Auto turn uses the default model",
"RouterPresetOffCostSavingHint": "No router call · [auto] cost_saving keeps Auto turns on the fast tier",
"RouterPresetCustomHint": "Show the [auto.router] TOML to edit by hand",
"RouterPresetUnavailable": "Unavailable: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe paused new signups on 2026-09-22; OpenRouter is the default route for new users.",
"RouterTestSaveHint": "One test call was made with a sample request. Enter saves this router; Esc discards it.",
"RouterTestHeader": "Router test ({args})",
"RouterTestNotMade": "Test call not made: {reason}",
"RouterTestWouldRoute": "Would route \"{request}\" to {provider} / {model} · {latency} ms",
"RouterTestDecision": "Decision: {tier} · {reason}",
"RouterTestChoice": "Choice: {choice} ({probabilities}) · confidence {confidence} (min {min})",
"RouterTestCostReported": "Cost: ${cost} (provider-reported)",
"RouterTestCostUnreported": "Cost: not reported by the provider",
"RouterTestUsage": "Usage: {input} in · {output} out",
"RouterTestFailing": "Router failing: {reason}",
"RouterSaved": "Router saved ({router}) to {path}",
"RouterNotSaved": "Router not saved: {error}",
"RouterRuntimeNotReloaded": "Router saved, but runtime threads keep the previous router until restart: {error}",
"RouterCurrent": "Current: {router}",
"RouterCurrentFailing": "Current: {router} · failing: {reason}",
"RouterSummary": "{kind} router · {provider} / {model}",
"RouterSummaryThinking": "{kind} router · {provider} / {model} · thinking {thinking}",
"RouterPresetFastLabel": "Fast tier",
"RouterPresetCustomLabel": "Custom",
"RouterNoKey": "no {route} API key",
"RouterNoFastTier": "the active provider has no runnable fast tier",
"RouterNeedKey": "add an OpenRouter key (OPENROUTER_API_KEY) or a TypeSafe key (TYPESAFE_API_KEY)",
"RouterCustomByHand": "custom routers are edited by hand",
"RouterCustomHelpIntro": "Edit [auto.router] in config.toml, for example:",
"RouterCustomHelpOutro": "Remove the table (or run /router off) to route every Auto turn to the default model.",
"RouterActionTest": "test",
"RouterActionSave": "save",
"RouterActionDiscard": "discard",
"ModelsListHeader": "{provider} models (default: {model})",
"ModelsListHint": "Cached catalog; run `codewhale models --update` to refresh configured providers.",
"ModelsUpdateSummary": "Model catalogs: {updated} updated, {loaded} loaded from Codex, {failed} failed, {skipped} skipped.",
+37
View File
@@ -529,6 +529,43 @@
"CmdModeDescription": "Cambiar permisos o abrir el selector de modo",
"CmdModelDescription": "Cambiar o mostrar el modelo actual",
"CmdModelsDescription": "Obtener IDs de modelo en vivo de la API activa",
"CmdRouterDescription": "Configura el enrutador por turno del modo Auto",
"RouterSetupTitle": "Enrutador de modelos",
"RouterPresetJevHint": "Modelo de decisión Jev vía {route} · unos $0.00002 por turno · tu última solicitud y el contexto reciente se envían a {route}",
"RouterPresetFastHint": "{model} sin razonamiento · usa tu clave de {provider}",
"RouterPresetOffHint": "Sin llamada al enrutador · cada turno Auto usa el modelo predeterminado",
"RouterPresetOffCostSavingHint": "Sin llamada al enrutador · [auto] cost_saving mantiene los turnos Auto en el nivel rápido",
"RouterPresetCustomHint": "Muestra el TOML de [auto.router] para editarlo a mano",
"RouterPresetUnavailable": "No disponible: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe pausó los registros nuevos el 2026-09-22; OpenRouter es la ruta predeterminada para usuarios nuevos.",
"RouterTestSaveHint": "Se hizo una llamada de prueba con una solicitud de ejemplo. Enter guarda este enrutador; Esc lo descarta.",
"RouterTestHeader": "Prueba del enrutador ({args})",
"RouterTestNotMade": "No se hizo la llamada de prueba: {reason}",
"RouterTestWouldRoute": "Enviaría \"{request}\" a {provider} / {model} · {latency} ms",
"RouterTestDecision": "Decisión: {tier} · {reason}",
"RouterTestChoice": "Elección: {choice} ({probabilities}) · confianza {confidence} (mín. {min})",
"RouterTestCostReported": "Costo: ${cost} (informado por el proveedor)",
"RouterTestCostUnreported": "Costo: el proveedor no lo informó",
"RouterTestUsage": "Uso: {input} de entrada · {output} de salida",
"RouterTestFailing": "El enrutador falla: {reason}",
"RouterSaved": "Enrutador guardado ({router}) en {path}",
"RouterNotSaved": "No se guardó el enrutador: {error}",
"RouterRuntimeNotReloaded": "Enrutador guardado, pero los hilos de ejecución conservan el anterior hasta reiniciar: {error}",
"RouterCurrent": "Actual: {router}",
"RouterCurrentFailing": "Actual: {router} · falla: {reason}",
"RouterSummary": "enrutador {kind} · {provider} / {model}",
"RouterSummaryThinking": "enrutador {kind} · {provider} / {model} · razonamiento {thinking}",
"RouterPresetFastLabel": "Nivel rápido",
"RouterPresetCustomLabel": "Personalizado",
"RouterNoKey": "no hay clave de API de {route}",
"RouterNoFastTier": "el proveedor activo no tiene un nivel rápido ejecutable",
"RouterNeedKey": "agrega una clave de OpenRouter (OPENROUTER_API_KEY) o de TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "los enrutadores personalizados se editan a mano",
"RouterCustomHelpIntro": "Edita [auto.router] en config.toml, por ejemplo:",
"RouterCustomHelpOutro": "Quita la tabla (o ejecuta /router off) para que cada turno Auto use el modelo predeterminado.",
"RouterActionTest": "probar",
"RouterActionSave": "guardar",
"RouterActionDiscard": "descartar",
"ModelsListHeader": "Modelos de {provider} (predeterminado: {model})",
"ModelsListHint": "Catálogo en caché; ejecuta `codewhale models --update` para actualizar los proveedores configurados.",
"ModelsUpdateSummary": "Catálogos de modelos: {updated} actualizados, {loaded} cargados desde Codex, {failed} fallidos, {skipped} omitidos.",
+37
View File
@@ -526,6 +526,43 @@
"CmdModeDescription": "Changer les permissions ou ouvrir le sélecteur de mode",
"CmdModelDescription": "Changer ou afficher le modèle actuel",
"CmdModelsDescription": "Récupérer les IDs de modèles en direct depuis l'API active",
"CmdRouterDescription": "Configurer le routeur par tour du mode Auto",
"RouterSetupTitle": "Routeur de modèles",
"RouterPresetJevHint": "Modèle de décision Jev via {route} · environ $0.00002 par tour · votre dernière requête et le contexte récent sont envoyés à {route}",
"RouterPresetFastHint": "{model} sans raisonnement · utilise votre clé {provider}",
"RouterPresetOffHint": "Aucun appel au routeur · chaque tour Auto utilise le modèle par défaut",
"RouterPresetOffCostSavingHint": "Aucun appel au routeur · [auto] cost_saving garde les tours Auto sur le niveau rapide",
"RouterPresetCustomHint": "Afficher le TOML de [auto.router] à modifier à la main",
"RouterPresetUnavailable": "Indisponible : {reason}",
"RouterTypesafeSignupPaused": "TypeSafe a suspendu les nouvelles inscriptions le 2026-09-22 ; OpenRouter est la route par défaut pour les nouveaux utilisateurs.",
"RouterTestSaveHint": "Un appel de test a été fait avec une requête d'exemple. Enter enregistre ce routeur ; Esc l'abandonne.",
"RouterTestHeader": "Test du routeur ({args})",
"RouterTestNotMade": "Aucun appel de test effectué : {reason}",
"RouterTestWouldRoute": "Enverrait « {request} » vers {provider} / {model} · {latency} ms",
"RouterTestDecision": "Décision : {tier} · {reason}",
"RouterTestChoice": "Choix : {choice} ({probabilities}) · confiance {confidence} (min {min})",
"RouterTestCostReported": "Coût : ${cost} (déclaré par le fournisseur)",
"RouterTestCostUnreported": "Coût : non déclaré par le fournisseur",
"RouterTestUsage": "Utilisation : {input} en entrée · {output} en sortie",
"RouterTestFailing": "Échec du routeur : {reason}",
"RouterSaved": "Routeur enregistré ({router}) dans {path}",
"RouterNotSaved": "Routeur non enregistré : {error}",
"RouterRuntimeNotReloaded": "Routeur enregistré, mais les threads d'exécution gardent l'ancien routeur jusqu'au redémarrage : {error}",
"RouterCurrent": "Actuel : {router}",
"RouterCurrentFailing": "Actuel : {router} · en échec : {reason}",
"RouterSummary": "routeur {kind} · {provider} / {model}",
"RouterSummaryThinking": "routeur {kind} · {provider} / {model} · réflexion {thinking}",
"RouterPresetFastLabel": "Niveau rapide",
"RouterPresetCustomLabel": "Personnalisé",
"RouterNoKey": "aucune clé d'API {route}",
"RouterNoFastTier": "le fournisseur actif n'a aucun niveau rapide exécutable",
"RouterNeedKey": "ajoutez une clé OpenRouter (OPENROUTER_API_KEY) ou une clé TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "les routeurs personnalisés se modifient à la main",
"RouterCustomHelpIntro": "Modifiez [auto.router] dans config.toml, par exemple :",
"RouterCustomHelpOutro": "Supprimez la table (ou lancez /router off) pour que chaque tour Auto utilise le modèle par défaut.",
"RouterActionTest": "tester",
"RouterActionSave": "enregistrer",
"RouterActionDiscard": "abandonner",
"ModelsListHeader": "Modèles de {provider} (par défaut : {model})",
"ModelsListHint": "Catalogue en cache ; lancez `codewhale models --update` pour actualiser les fournisseurs configurés.",
"ModelsUpdateSummary": "Catalogues de modèles : {updated} actualisés, {loaded} chargés depuis Codex, {failed} en échec, {skipped} ignorés.",
+37
View File
@@ -526,6 +526,43 @@
"CmdModeDescription": "अनुमतियाँ बदलें या मोड पिकर खोलें",
"CmdModelDescription": "वर्तमान मॉडल बदलें या देखें",
"CmdModelsDescription": "सक्रिय API से लाइव मॉडल ID लाएँ",
"CmdRouterDescription": "Auto मॉडल रूटिंग के लिए हर टर्न का राउटर सेट करें",
"RouterSetupTitle": "मॉडल राउटर",
"RouterPresetJevHint": "{route} के ज़रिए Jev निर्णय मॉडल · हर टर्न लगभग $0.00002 · आपका नवीनतम अनुरोध और हाल का संदर्भ {route} को जाता है",
"RouterPresetFastHint": "{model}, सोच बंद · आपकी {provider} कुंजी का उपयोग करता है",
"RouterPresetOffHint": "कोई राउटर कॉल नहीं · हर Auto टर्न डिफ़ॉल्ट मॉडल का उपयोग करता है",
"RouterPresetOffCostSavingHint": "कोई राउटर कॉल नहीं · [auto] cost_saving Auto टर्न को तेज़ स्तर पर रखता है",
"RouterPresetCustomHint": "हाथ से बदलने के लिए [auto.router] का TOML दिखाएँ",
"RouterPresetUnavailable": "उपलब्ध नहीं: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe ने 2026-09-22 को नए साइनअप रोक दिए; नए उपयोगकर्ताओं के लिए OpenRouter डिफ़ॉल्ट रास्ता है।",
"RouterTestSaveHint": "एक नमूना अनुरोध के साथ एक परीक्षण कॉल की गई। Enter इस राउटर को सहेजता है; Esc इसे छोड़ देता है।",
"RouterTestHeader": "राउटर परीक्षण ({args})",
"RouterTestNotMade": "परीक्षण कॉल नहीं की गई: {reason}",
"RouterTestWouldRoute": "\"{request}\" को {provider} / {model} पर भेजेगा · {latency} ms",
"RouterTestDecision": "निर्णय: {tier} · {reason}",
"RouterTestChoice": "चुनाव: {choice} ({probabilities}) · विश्वास {confidence} (न्यूनतम {min})",
"RouterTestCostReported": "लागत: ${cost} (प्रदाता द्वारा बताई गई)",
"RouterTestCostUnreported": "लागत: प्रदाता ने नहीं बताई",
"RouterTestUsage": "उपयोग: {input} इनपुट · {output} आउटपुट",
"RouterTestFailing": "राउटर विफल: {reason}",
"RouterSaved": "राउटर सहेजा गया ({router}) — {path}",
"RouterNotSaved": "राउटर सहेजा नहीं गया: {error}",
"RouterRuntimeNotReloaded": "राउटर सहेजा गया, पर रनटाइम थ्रेड रीस्टार्ट तक पिछला राउटर इस्तेमाल करेंगे: {error}",
"RouterCurrent": "वर्तमान: {router}",
"RouterCurrentFailing": "वर्तमान: {router} · विफल: {reason}",
"RouterSummary": "{kind} राउटर · {provider} / {model}",
"RouterSummaryThinking": "{kind} राउटर · {provider} / {model} · थिंकिंग {thinking}",
"RouterPresetFastLabel": "तेज़ स्तर",
"RouterPresetCustomLabel": "कस्टम",
"RouterNoKey": "{route} API कुंजी नहीं है",
"RouterNoFastTier": "सक्रिय प्रदाता के पास चलने योग्य तेज़ स्तर नहीं है",
"RouterNeedKey": "OpenRouter कुंजी (OPENROUTER_API_KEY) या TypeSafe कुंजी (TYPESAFE_API_KEY) जोड़ें",
"RouterCustomByHand": "कस्टम राउटर हाथ से संपादित किए जाते हैं",
"RouterCustomHelpIntro": "config.toml में [auto.router] संपादित करें, उदाहरण के लिए:",
"RouterCustomHelpOutro": "हर Auto टर्न को डिफ़ॉल्ट मॉडल पर भेजने के लिए तालिका हटाएँ (या /router off चलाएँ)।",
"RouterActionTest": "परीक्षण",
"RouterActionSave": "सहेजें",
"RouterActionDiscard": "छोड़ें",
"ModelsListHeader": "{provider} के मॉडल (डिफ़ॉल्ट: {model})",
"ModelsListHint": "कैश में सहेजी सूची; कॉन्फ़िगर किए प्रदाताओं को अपडेट करने के लिए `codewhale models --update` चलाएँ।",
"ModelsUpdateSummary": "मॉडल कैटलॉग: {updated} अपडेट हुए, {loaded} Codex से लोड हुए, {failed} विफल हुए, {skipped} छोड़े गए।",
+37
View File
@@ -526,6 +526,43 @@
"CmdModeDescription": "Ganti izin atau buka pemilih mode",
"CmdModelDescription": "Ganti atau lihat model saat ini",
"CmdModelsDescription": "Ambil ID model langsung dari API aktif",
"CmdRouterDescription": "Atur router per giliran untuk perutean model Auto",
"RouterSetupTitle": "Router model",
"RouterPresetJevHint": "Model keputusan Jev lewat {route} · sekitar $0.00002 per giliran · permintaan terakhir dan konteks terbaru Anda dikirim ke {route}",
"RouterPresetFastHint": "{model} tanpa penalaran · memakai kunci {provider} Anda",
"RouterPresetOffHint": "Tanpa panggilan router · setiap giliran Auto memakai model bawaan",
"RouterPresetOffCostSavingHint": "Tanpa panggilan router · [auto] cost_saving menjaga giliran Auto di tingkat cepat",
"RouterPresetCustomHint": "Tampilkan TOML [auto.router] untuk diedit manual",
"RouterPresetUnavailable": "Tidak tersedia: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe menjeda pendaftaran baru pada 2026-09-22; OpenRouter adalah rute bawaan untuk pengguna baru.",
"RouterTestSaveHint": "Satu panggilan uji dilakukan dengan permintaan contoh. Enter menyimpan router ini; Esc membuangnya.",
"RouterTestHeader": "Uji router ({args})",
"RouterTestNotMade": "Panggilan uji tidak dilakukan: {reason}",
"RouterTestWouldRoute": "Akan mengarahkan \"{request}\" ke {provider} / {model} · {latency} ms",
"RouterTestDecision": "Keputusan: {tier} · {reason}",
"RouterTestChoice": "Pilihan: {choice} ({probabilities}) · keyakinan {confidence} (min {min})",
"RouterTestCostReported": "Biaya: ${cost} (dilaporkan penyedia)",
"RouterTestCostUnreported": "Biaya: tidak dilaporkan penyedia",
"RouterTestUsage": "Pemakaian: {input} masuk · {output} keluar",
"RouterTestFailing": "Router gagal: {reason}",
"RouterSaved": "Router disimpan ({router}) ke {path}",
"RouterNotSaved": "Router tidak disimpan: {error}",
"RouterRuntimeNotReloaded": "Router disimpan, tetapi thread runtime memakai router lama sampai dimulai ulang: {error}",
"RouterCurrent": "Saat ini: {router}",
"RouterCurrentFailing": "Saat ini: {router} · gagal: {reason}",
"RouterSummary": "router {kind} · {provider} / {model}",
"RouterSummaryThinking": "router {kind} · {provider} / {model} · penalaran {thinking}",
"RouterPresetFastLabel": "Tingkat cepat",
"RouterPresetCustomLabel": "Kustom",
"RouterNoKey": "tidak ada kunci API {route}",
"RouterNoFastTier": "penyedia aktif tidak punya tingkat cepat yang bisa dijalankan",
"RouterNeedKey": "tambahkan kunci OpenRouter (OPENROUTER_API_KEY) atau kunci TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "router kustom diedit secara manual",
"RouterCustomHelpIntro": "Edit [auto.router] di config.toml, misalnya:",
"RouterCustomHelpOutro": "Hapus tabel itu (atau jalankan /router off) agar setiap giliran Auto memakai model bawaan.",
"RouterActionTest": "uji",
"RouterActionSave": "simpan",
"RouterActionDiscard": "buang",
"ModelsListHeader": "Model {provider} (bawaan: {model})",
"ModelsListHint": "Katalog tersimpan; jalankan `codewhale models --update` untuk memperbarui penyedia yang dikonfigurasi.",
"ModelsUpdateSummary": "Katalog model: {updated} diperbarui, {loaded} dimuat dari Codex, {failed} gagal, {skipped} dilewati.",
+37
View File
@@ -529,6 +529,43 @@
"CmdModeDescription": "権限を切り替えるか、モードピッカーを開く",
"CmdModelDescription": "現在のモデルを切り替え・確認",
"CmdModelsDescription": "アクティブな API からライブのモデル ID を取得する",
"CmdRouterDescription": "Auto モデルルーティングのターンごとのルーターを設定",
"RouterSetupTitle": "モデルルーター",
"RouterPresetJevHint": "{route} 経由の Jev 判定モデル · 1 ターンあたり約 $0.00002 · 最新のリクエストと直近のコンテキストが {route} に送られます",
"RouterPresetFastHint": "{model}(思考オフ)· {provider} のキーを使用",
"RouterPresetOffHint": "ルーター呼び出しなし · Auto の各ターンは既定のモデルを使用",
"RouterPresetOffCostSavingHint": "ルーター呼び出しなし · [auto] cost_saving により Auto のターンは高速ティアのまま",
"RouterPresetCustomHint": "[auto.router] の TOML を表示して手動で編集",
"RouterPresetUnavailable": "利用不可: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe は 2026-09-22 に新規登録を停止しました。新規ユーザーの既定ルートは OpenRouter です。",
"RouterTestSaveHint": "サンプルリクエストでテスト呼び出しを 1 回行いました。Enter でこのルーターを保存、Esc で破棄します。",
"RouterTestHeader": "ルーターのテスト ({args})",
"RouterTestNotMade": "テスト呼び出しは行われませんでした: {reason}",
"RouterTestWouldRoute": "\"{request}\" を {provider} / {model} に振り分けます · {latency} ms",
"RouterTestDecision": "判定: {tier} · {reason}",
"RouterTestChoice": "選択: {choice} ({probabilities}) · 確信度 {confidence} (最小 {min})",
"RouterTestCostReported": "費用: ${cost} (プロバイダー報告値)",
"RouterTestCostUnreported": "費用: プロバイダーから報告なし",
"RouterTestUsage": "使用量: 入力 {input} · 出力 {output}",
"RouterTestFailing": "ルーターが失敗しています: {reason}",
"RouterSaved": "ルーターを保存しました ({router}): {path}",
"RouterNotSaved": "ルーターを保存できませんでした: {error}",
"RouterRuntimeNotReloaded": "ルーターを保存しましたが、ランタイムスレッドは再起動まで以前のルーターを使います: {error}",
"RouterCurrent": "現在: {router}",
"RouterCurrentFailing": "現在: {router} · 失敗: {reason}",
"RouterSummary": "{kind} ルーター · {provider} / {model}",
"RouterSummaryThinking": "{kind} ルーター · {provider} / {model} · 思考 {thinking}",
"RouterPresetFastLabel": "高速ティア",
"RouterPresetCustomLabel": "カスタム",
"RouterNoKey": "{route} の API キーがありません",
"RouterNoFastTier": "アクティブなプロバイダーに実行可能な高速ティアがありません",
"RouterNeedKey": "OpenRouter のキー (OPENROUTER_API_KEY) か TypeSafe のキー (TYPESAFE_API_KEY) を追加してください",
"RouterCustomByHand": "カスタムルーターは手動で編集します",
"RouterCustomHelpIntro": "config.toml の [auto.router] を編集してください。例:",
"RouterCustomHelpOutro": "テーブルを削除する (または /router off を実行する) と、Auto の各ターンは既定のモデルを使用します。",
"RouterActionTest": "テスト",
"RouterActionSave": "保存",
"RouterActionDiscard": "破棄",
"ModelsListHeader": "{provider} のモデル(既定: {model})",
"ModelsListHint": "キャッシュ済みカタログ。設定済みプロバイダーを更新するには `codewhale models --update` を実行してください。",
"ModelsUpdateSummary": "モデルカタログ: {updated} 件更新、{loaded} 件を Codex から読み込み、{failed} 件失敗、{skipped} 件スキップ。",
+37
View File
@@ -529,6 +529,43 @@
"CmdModeDescription": "권한 전환 또는 모드 선택기 열기",
"CmdModelDescription": "현재 모델을 전환하거나 확인합니다",
"CmdModelsDescription": "현재 API에서 실시간 모델 ID를 가져옵니다",
"CmdRouterDescription": "Auto 모델 라우팅의 턴별 라우터 설정",
"RouterSetupTitle": "모델 라우터",
"RouterPresetJevHint": "{route}를 통한 Jev 결정 모델 · 턴당 약 $0.00002 · 최신 요청과 최근 맥락이 {route}로 전송됩니다",
"RouterPresetFastHint": "{model}, 사고 끔 · {provider} 키 사용",
"RouterPresetOffHint": "라우터 호출 없음 · 모든 Auto 턴이 기본 모델 사용",
"RouterPresetOffCostSavingHint": "라우터 호출 없음 · [auto] cost_saving으로 Auto 턴은 빠른 티어 유지",
"RouterPresetCustomHint": "직접 편집할 [auto.router] TOML 표시",
"RouterPresetUnavailable": "사용 불가: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe는 2026-09-22에 신규 가입을 중단했습니다. 신규 사용자의 기본 경로는 OpenRouter입니다.",
"RouterTestSaveHint": "예시 요청으로 테스트 호출을 한 번 했습니다. Enter는 이 라우터를 저장하고 Esc는 버립니다.",
"RouterTestHeader": "라우터 테스트 ({args})",
"RouterTestNotMade": "테스트 호출을 하지 않았습니다: {reason}",
"RouterTestWouldRoute": "\"{request}\"을(를) {provider} / {model}(으)로 보냅니다 · {latency} ms",
"RouterTestDecision": "결정: {tier} · {reason}",
"RouterTestChoice": "선택: {choice} ({probabilities}) · 신뢰도 {confidence} (최소 {min})",
"RouterTestCostReported": "비용: ${cost} (제공자 보고)",
"RouterTestCostUnreported": "비용: 제공자가 보고하지 않음",
"RouterTestUsage": "사용량: 입력 {input} · 출력 {output}",
"RouterTestFailing": "라우터 실패: {reason}",
"RouterSaved": "라우터 저장됨 ({router}): {path}",
"RouterNotSaved": "라우터를 저장하지 못했습니다: {error}",
"RouterRuntimeNotReloaded": "라우터를 저장했지만 런타임 스레드는 재시작 전까지 이전 라우터를 사용합니다: {error}",
"RouterCurrent": "현재: {router}",
"RouterCurrentFailing": "현재: {router} · 실패: {reason}",
"RouterSummary": "{kind} 라우터 · {provider} / {model}",
"RouterSummaryThinking": "{kind} 라우터 · {provider} / {model} · 사고 {thinking}",
"RouterPresetFastLabel": "빠른 티어",
"RouterPresetCustomLabel": "사용자 지정",
"RouterNoKey": "{route} API 키 없음",
"RouterNoFastTier": "활성 제공자에 실행 가능한 빠른 티어가 없습니다",
"RouterNeedKey": "OpenRouter 키(OPENROUTER_API_KEY) 또는 TypeSafe 키(TYPESAFE_API_KEY)를 추가하세요",
"RouterCustomByHand": "사용자 지정 라우터는 직접 편집합니다",
"RouterCustomHelpIntro": "config.toml에서 [auto.router]를 편집하세요. 예:",
"RouterCustomHelpOutro": "테이블을 삭제하면(또는 /router off 실행) 모든 Auto 턴이 기본 모델을 사용합니다.",
"RouterActionTest": "테스트",
"RouterActionSave": "저장",
"RouterActionDiscard": "버리기",
"ModelsListHeader": "{provider} 모델 (기본값: {model})",
"ModelsListHint": "캐시된 카탈로그입니다. 설정된 제공업체를 갱신하려면 `codewhale models --update`를 실행하세요.",
"ModelsUpdateSummary": "모델 카탈로그: {updated}개 업데이트, {loaded}개 Codex에서 불러옴, {failed}개 실패, {skipped}개 건너뜀.",
+37
View File
@@ -529,6 +529,43 @@
"CmdModeDescription": "Alternar permissões ou abrir o seletor de modo",
"CmdModelDescription": "Trocar ou exibir o modelo atual",
"CmdModelsDescription": "Buscar IDs de modelo ao vivo da API ativa",
"CmdRouterDescription": "Configure o roteador por turno do roteamento de modelos Auto",
"RouterSetupTitle": "Roteador de modelos",
"RouterPresetJevHint": "Modelo de decisão Jev via {route} · cerca de $0.00002 por turno · sua última solicitação e o contexto recente vão para {route}",
"RouterPresetFastHint": "{model} sem raciocínio · usa sua chave de {provider}",
"RouterPresetOffHint": "Sem chamada ao roteador · cada turno Auto usa o modelo padrão",
"RouterPresetOffCostSavingHint": "Sem chamada ao roteador · [auto] cost_saving mantém os turnos Auto no nível rápido",
"RouterPresetCustomHint": "Mostrar o TOML de [auto.router] para editar à mão",
"RouterPresetUnavailable": "Indisponível: {reason}",
"RouterTypesafeSignupPaused": "A TypeSafe pausou novos cadastros em 2026-09-22; OpenRouter é a rota padrão para novos usuários.",
"RouterTestSaveHint": "Uma chamada de teste foi feita com uma solicitação de exemplo. Enter salva este roteador; Esc o descarta.",
"RouterTestHeader": "Teste do roteador ({args})",
"RouterTestNotMade": "Chamada de teste não feita: {reason}",
"RouterTestWouldRoute": "Enviaria \"{request}\" para {provider} / {model} · {latency} ms",
"RouterTestDecision": "Decisão: {tier} · {reason}",
"RouterTestChoice": "Escolha: {choice} ({probabilities}) · confiança {confidence} (mín. {min})",
"RouterTestCostReported": "Custo: ${cost} (informado pelo provedor)",
"RouterTestCostUnreported": "Custo: não informado pelo provedor",
"RouterTestUsage": "Uso: {input} de entrada · {output} de saída",
"RouterTestFailing": "Roteador falhando: {reason}",
"RouterSaved": "Roteador salvo ({router}) em {path}",
"RouterNotSaved": "Roteador não salvo: {error}",
"RouterRuntimeNotReloaded": "Roteador salvo, mas as threads de runtime mantêm o roteador anterior até reiniciar: {error}",
"RouterCurrent": "Atual: {router}",
"RouterCurrentFailing": "Atual: {router} · falhando: {reason}",
"RouterSummary": "roteador {kind} · {provider} / {model}",
"RouterSummaryThinking": "roteador {kind} · {provider} / {model} · raciocínio {thinking}",
"RouterPresetFastLabel": "Nível rápido",
"RouterPresetCustomLabel": "Personalizado",
"RouterNoKey": "sem chave de API de {route}",
"RouterNoFastTier": "o provedor ativo não tem nível rápido executável",
"RouterNeedKey": "adicione uma chave do OpenRouter (OPENROUTER_API_KEY) ou do TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "roteadores personalizados são editados à mão",
"RouterCustomHelpIntro": "Edite [auto.router] em config.toml, por exemplo:",
"RouterCustomHelpOutro": "Remova a tabela (ou rode /router off) para que cada turno Auto use o modelo padrão.",
"RouterActionTest": "testar",
"RouterActionSave": "salvar",
"RouterActionDiscard": "descartar",
"ModelsListHeader": "Modelos de {provider} (padrão: {model})",
"ModelsListHint": "Catálogo em cache; execute `codewhale models --update` para atualizar os provedores configurados.",
"ModelsUpdateSummary": "Catálogos de modelos: {updated} atualizados, {loaded} carregados do Codex, {failed} com falha, {skipped} ignorados.",
+37
View File
@@ -526,6 +526,43 @@
"CmdModeDescription": "Переключить разрешения или открыть выбор режима",
"CmdModelDescription": "Сменить или показать текущую модель",
"CmdModelsDescription": "Получить актуальные ID моделей из активного API",
"CmdRouterDescription": "Настроить роутер для каждого хода в режиме Auto",
"RouterSetupTitle": "Роутер моделей",
"RouterPresetJevHint": "Модель решений Jev через {route} · около $0.00002 за ход · ваш последний запрос и недавний контекст отправляются в {route}",
"RouterPresetFastHint": "{model} без рассуждений · использует ваш ключ {provider}",
"RouterPresetOffHint": "Без вызова роутера · каждый ход Auto использует модель по умолчанию",
"RouterPresetOffCostSavingHint": "Без вызова роутера · [auto] cost_saving держит ходы Auto на быстром уровне",
"RouterPresetCustomHint": "Показать TOML [auto.router] для ручной правки",
"RouterPresetUnavailable": "Недоступно: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe приостановил новые регистрации 2026-09-22; для новых пользователей маршрут по умолчанию — OpenRouter.",
"RouterTestSaveHint": "Сделан один тестовый вызов с примером запроса. Enter сохраняет этот роутер; Esc отменяет.",
"RouterTestHeader": "Тест роутера ({args})",
"RouterTestNotMade": "Тестовый вызов не выполнен: {reason}",
"RouterTestWouldRoute": "Направил бы «{request}» в {provider} / {model} · {latency} мс",
"RouterTestDecision": "Решение: {tier} · {reason}",
"RouterTestChoice": "Выбор: {choice} ({probabilities}) · уверенность {confidence} (мин. {min})",
"RouterTestCostReported": "Стоимость: ${cost} (по данным провайдера)",
"RouterTestCostUnreported": "Стоимость: провайдер не сообщил",
"RouterTestUsage": "Использование: {input} на входе · {output} на выходе",
"RouterTestFailing": "Роутер не работает: {reason}",
"RouterSaved": "Роутер сохранён ({router}) в {path}",
"RouterNotSaved": "Роутер не сохранён: {error}",
"RouterRuntimeNotReloaded": "Роутер сохранён, но потоки runtime используют прежний роутер до перезапуска: {error}",
"RouterCurrent": "Сейчас: {router}",
"RouterCurrentFailing": "Сейчас: {router} · ошибка: {reason}",
"RouterSummary": "роутер {kind} · {provider} / {model}",
"RouterSummaryThinking": "роутер {kind} · {provider} / {model} · рассуждение {thinking}",
"RouterPresetFastLabel": "Быстрый уровень",
"RouterPresetCustomLabel": "Свой",
"RouterNoKey": "нет API-ключа {route}",
"RouterNoFastTier": "у активного провайдера нет запускаемого быстрого уровня",
"RouterNeedKey": "добавьте ключ OpenRouter (OPENROUTER_API_KEY) или ключ TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "свои роутеры редактируются вручную",
"RouterCustomHelpIntro": "Отредактируйте [auto.router] в config.toml, например:",
"RouterCustomHelpOutro": "Удалите таблицу (или выполните /router off), чтобы каждый ход Auto использовал модель по умолчанию.",
"RouterActionTest": "проверить",
"RouterActionSave": "сохранить",
"RouterActionDiscard": "отменить",
"ModelsListHeader": "Модели {provider} (по умолчанию: {model})",
"ModelsListHint": "Сохранённый каталог; выполните `codewhale models --update`, чтобы обновить настроенных провайдеров.",
"ModelsUpdateSummary": "Каталоги моделей: обновлено {updated}, загружено из Codex {loaded}, ошибок {failed}, пропущено {skipped}.",
+37
View File
@@ -526,6 +526,43 @@
"CmdModeDescription": "Перемкнути дозволи або відкрити вибір режиму",
"CmdModelDescription": "Змінити або переглянути поточну модель",
"CmdModelsDescription": "Отримати актуальні ID моделей з активного API",
"CmdRouterDescription": "Налаштувати роутер для кожного ходу в режимі Auto",
"RouterSetupTitle": "Роутер моделей",
"RouterPresetJevHint": "Модель рішень Jev через {route} · близько $0.00002 за хід · ваш останній запит і недавній контекст надсилаються до {route}",
"RouterPresetFastHint": "{model} без міркувань · використовує ваш ключ {provider}",
"RouterPresetOffHint": "Без виклику роутера · кожен хід Auto використовує модель за замовчуванням",
"RouterPresetOffCostSavingHint": "Без виклику роутера · [auto] cost_saving тримає ходи Auto на швидкому рівні",
"RouterPresetCustomHint": "Показати TOML [auto.router] для ручного редагування",
"RouterPresetUnavailable": "Недоступно: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe призупинив нові реєстрації 2026-09-22; для нових користувачів типовий маршрут — OpenRouter.",
"RouterTestSaveHint": "Зроблено один тестовий виклик із прикладом запиту. Enter зберігає цей роутер; Esc скасовує.",
"RouterTestHeader": "Тест роутера ({args})",
"RouterTestNotMade": "Тестовий виклик не зроблено: {reason}",
"RouterTestWouldRoute": "Спрямував би «{request}» до {provider} / {model} · {latency} мс",
"RouterTestDecision": "Рішення: {tier} · {reason}",
"RouterTestChoice": "Вибір: {choice} ({probabilities}) · впевненість {confidence} (мін. {min})",
"RouterTestCostReported": "Вартість: ${cost} (за даними провайдера)",
"RouterTestCostUnreported": "Вартість: провайдер не повідомив",
"RouterTestUsage": "Використання: {input} на вході · {output} на виході",
"RouterTestFailing": "Роутер не працює: {reason}",
"RouterSaved": "Роутер збережено ({router}) у {path}",
"RouterNotSaved": "Роутер не збережено: {error}",
"RouterRuntimeNotReloaded": "Роутер збережено, але потоки runtime використовують попередній роутер до перезапуску: {error}",
"RouterCurrent": "Зараз: {router}",
"RouterCurrentFailing": "Зараз: {router} · помилка: {reason}",
"RouterSummary": "роутер {kind} · {provider} / {model}",
"RouterSummaryThinking": "роутер {kind} · {provider} / {model} · міркування {thinking}",
"RouterPresetFastLabel": "Швидкий рівень",
"RouterPresetCustomLabel": "Власний",
"RouterNoKey": "немає API-ключа {route}",
"RouterNoFastTier": "активний провайдер не має придатного швидкого рівня",
"RouterNeedKey": "додайте ключ OpenRouter (OPENROUTER_API_KEY) або ключ TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "власні роутери редагуються вручну",
"RouterCustomHelpIntro": "Відредагуйте [auto.router] у config.toml, наприклад:",
"RouterCustomHelpOutro": "Видаліть таблицю (або виконайте /router off), щоб кожен хід Auto використовував модель за замовчуванням.",
"RouterActionTest": "перевірити",
"RouterActionSave": "зберегти",
"RouterActionDiscard": "скасувати",
"ModelsListHeader": "Моделі {provider} (типова: {model})",
"ModelsListHint": "Збережений каталог; виконайте `codewhale models --update`, щоб оновити налаштованих провайдерів.",
"ModelsUpdateSummary": "Каталоги моделей: оновлено {updated}, завантажено з Codex {loaded}, помилок {failed}, пропущено {skipped}.",
+37
View File
@@ -529,6 +529,43 @@
"CmdModeDescription": "Chuyển quyền hạn hoặc mở trình chọn chế độ",
"CmdModelDescription": "Chuyển đổi hoặc xem mô hình AI hiện tại",
"CmdModelsDescription": "Lấy ID mô hình trực tiếp từ API đang dùng",
"CmdRouterDescription": "Thiết lập bộ định tuyến theo lượt cho định tuyến mô hình Auto",
"RouterSetupTitle": "Bộ định tuyến mô hình",
"RouterPresetJevHint": "Mô hình quyết định Jev qua {route} · khoảng $0.00002 mỗi lượt · yêu cầu mới nhất và ngữ cảnh gần đây của bạn được gửi tới {route}",
"RouterPresetFastHint": "{model}, tắt suy luận · dùng khóa {provider} của bạn",
"RouterPresetOffHint": "Không gọi bộ định tuyến · mỗi lượt Auto dùng mô hình mặc định",
"RouterPresetOffCostSavingHint": "Không gọi bộ định tuyến · [auto] cost_saving giữ các lượt Auto ở tầng nhanh",
"RouterPresetCustomHint": "Hiện TOML [auto.router] để sửa thủ công",
"RouterPresetUnavailable": "Không khả dụng: {reason}",
"RouterTypesafeSignupPaused": "TypeSafe đã tạm dừng đăng ký mới vào 2026-09-22; OpenRouter là tuyến mặc định cho người dùng mới.",
"RouterTestSaveHint": "Đã thực hiện một lệnh gọi thử với yêu cầu mẫu. Enter lưu bộ định tuyến này; Esc bỏ qua.",
"RouterTestHeader": "Thử bộ định tuyến ({args})",
"RouterTestNotMade": "Chưa thực hiện lệnh gọi thử: {reason}",
"RouterTestWouldRoute": "Sẽ chuyển \"{request}\" tới {provider} / {model} · {latency} ms",
"RouterTestDecision": "Quyết định: {tier} · {reason}",
"RouterTestChoice": "Lựa chọn: {choice} ({probabilities}) · độ tin cậy {confidence} (tối thiểu {min})",
"RouterTestCostReported": "Chi phí: ${cost} (do nhà cung cấp báo)",
"RouterTestCostUnreported": "Chi phí: nhà cung cấp không báo",
"RouterTestUsage": "Sử dụng: {input} vào · {output} ra",
"RouterTestFailing": "Bộ định tuyến lỗi: {reason}",
"RouterSaved": "Đã lưu bộ định tuyến ({router}) vào {path}",
"RouterNotSaved": "Chưa lưu bộ định tuyến: {error}",
"RouterRuntimeNotReloaded": "Đã lưu bộ định tuyến, nhưng các luồng runtime vẫn dùng bộ cũ cho đến khi khởi động lại: {error}",
"RouterCurrent": "Hiện tại: {router}",
"RouterCurrentFailing": "Hiện tại: {router} · lỗi: {reason}",
"RouterSummary": "bộ định tuyến {kind} · {provider} / {model}",
"RouterSummaryThinking": "bộ định tuyến {kind} · {provider} / {model} · suy luận {thinking}",
"RouterPresetFastLabel": "Tầng nhanh",
"RouterPresetCustomLabel": "Tùy chỉnh",
"RouterNoKey": "không có khóa API {route}",
"RouterNoFastTier": "nhà cung cấp đang dùng không có tầng nhanh chạy được",
"RouterNeedKey": "thêm khóa OpenRouter (OPENROUTER_API_KEY) hoặc khóa TypeSafe (TYPESAFE_API_KEY)",
"RouterCustomByHand": "bộ định tuyến tùy chỉnh được sửa thủ công",
"RouterCustomHelpIntro": "Sửa [auto.router] trong config.toml, ví dụ:",
"RouterCustomHelpOutro": "Xóa bảng (hoặc chạy /router off) để mọi lượt Auto dùng mô hình mặc định.",
"RouterActionTest": "thử",
"RouterActionSave": "lưu",
"RouterActionDiscard": "bỏ",
"ModelsListHeader": "Mô hình của {provider} (mặc định: {model})",
"ModelsListHint": "Danh mục đã lưu; chạy `codewhale models --update` để cập nhật các nhà cung cấp đã cấu hình.",
"ModelsUpdateSummary": "Danh mục mô hình: {updated} đã cập nhật, {loaded} đã tải từ Codex, {failed} thất bại, {skipped} đã bỏ qua.",
+37
View File
@@ -529,6 +529,43 @@
"CmdModeDescription": "切换权限或打开模式选择器",
"CmdModelDescription": "切换或查看当前模型",
"CmdModelsDescription": "工具箱:从当前 API 获取实时模型 ID",
"CmdRouterDescription": "设置 Auto 模型路由的逐轮路由器",
"RouterSetupTitle": "模型路由器",
"RouterPresetJevHint": "经由 {route} 的 Jev 决策模型 · 每轮约 $0.00002 · 你的最新请求和近期上下文会发送到 {route}",
"RouterPresetFastHint": "{model},关闭思考 · 使用你的 {provider} 密钥",
"RouterPresetOffHint": "不调用路由器 · 每个 Auto 轮次都使用默认模型",
"RouterPresetOffCostSavingHint": "不调用路由器 · [auto] cost_saving 让 Auto 轮次保持在快速档",
"RouterPresetCustomHint": "显示 [auto.router] 的 TOML 以便手动编辑",
"RouterPresetUnavailable": "不可用:{reason}",
"RouterTypesafeSignupPaused": "TypeSafe 已于 2026-09-22 暂停新用户注册;新用户默认使用 OpenRouter 路线。",
"RouterTestSaveHint": "已用示例请求进行一次测试调用。Enter 保存此路由器;Esc 放弃。",
"RouterTestHeader": "路由器测试 ({args})",
"RouterTestNotMade": "未进行测试调用:{reason}",
"RouterTestWouldRoute": "会把 \"{request}\" 路由到 {provider} / {model} · {latency} ms",
"RouterTestDecision": "决策:{tier} · {reason}",
"RouterTestChoice": "选择:{choice} ({probabilities}) · 置信度 {confidence} (最低 {min})",
"RouterTestCostReported": "费用:${cost} (提供商报告)",
"RouterTestCostUnreported": "费用:提供商未报告",
"RouterTestUsage": "用量:输入 {input} · 输出 {output}",
"RouterTestFailing": "路由器失败:{reason}",
"RouterSaved": "路由器已保存 ({router}) 到 {path}",
"RouterNotSaved": "路由器未保存:{error}",
"RouterRuntimeNotReloaded": "路由器已保存,但运行时线程在重启前仍使用之前的路由器:{error}",
"RouterCurrent": "当前:{router}",
"RouterCurrentFailing": "当前:{router} · 失败:{reason}",
"RouterSummary": "{kind} 路由器 · {provider} / {model}",
"RouterSummaryThinking": "{kind} 路由器 · {provider} / {model} · 思考 {thinking}",
"RouterPresetFastLabel": "快速档",
"RouterPresetCustomLabel": "自定义",
"RouterNoKey": "没有 {route} API 密钥",
"RouterNoFastTier": "当前提供商没有可运行的快速档",
"RouterNeedKey": "添加 OpenRouter 密钥 (OPENROUTER_API_KEY) 或 TypeSafe 密钥 (TYPESAFE_API_KEY)",
"RouterCustomByHand": "自定义路由器需手动编辑",
"RouterCustomHelpIntro": "编辑 config.toml 中的 [auto.router],例如:",
"RouterCustomHelpOutro": "删除该表(或运行 /router off),每个 Auto 轮次都会使用默认模型。",
"RouterActionTest": "测试",
"RouterActionSave": "保存",
"RouterActionDiscard": "放弃",
"ModelsListHeader": "{provider} 的模型(默认:{model})",
"ModelsListHint": "已缓存的目录;运行 `codewhale models --update` 可更新已配置的提供商。",
"ModelsUpdateSummary": "模型目录:{updated} 个已更新,{loaded} 个从 Codex 加载,{failed} 个失败,{skipped} 个已跳过。",
+37
View File
@@ -401,6 +401,43 @@
"CmdModelDbDescription": "參考:瀏覽內置的模型資料庫",
"CmdModelDescription": "切換或檢視目前模型",
"CmdModelsDescription": "工具箱:從目前 API 取得實時模型 ID",
"CmdRouterDescription": "設定 Auto 模型路由的逐輪路由器",
"RouterSetupTitle": "模型路由器",
"RouterPresetJevHint": "經由 {route} 的 Jev 決策模型 · 每輪約 $0.00002 · 你的最新請求與近期上下文會傳送到 {route}",
"RouterPresetFastHint": "{model},關閉思考 · 使用你的 {provider} 金鑰",
"RouterPresetOffHint": "不呼叫路由器 · 每個 Auto 輪次都使用預設模型",
"RouterPresetOffCostSavingHint": "不呼叫路由器 · [auto] cost_saving 讓 Auto 輪次維持在快速檔",
"RouterPresetCustomHint": "顯示 [auto.router] 的 TOML 以便手動編輯",
"RouterPresetUnavailable": "無法使用:{reason}",
"RouterTypesafeSignupPaused": "TypeSafe 已於 2026-09-22 暫停新用戶註冊;新用戶預設使用 OpenRouter 路線。",
"RouterTestSaveHint": "已用範例請求進行一次測試呼叫。Enter 儲存此路由器;Esc 放棄。",
"RouterTestHeader": "路由器測試 ({args})",
"RouterTestNotMade": "未進行測試呼叫:{reason}",
"RouterTestWouldRoute": "會把 \"{request}\" 路由到 {provider} / {model} · {latency} ms",
"RouterTestDecision": "決策:{tier} · {reason}",
"RouterTestChoice": "選擇:{choice} ({probabilities}) · 信心度 {confidence} (最低 {min})",
"RouterTestCostReported": "費用:${cost} (供應商回報)",
"RouterTestCostUnreported": "費用:供應商未回報",
"RouterTestUsage": "用量:輸入 {input} · 輸出 {output}",
"RouterTestFailing": "路由器失敗:{reason}",
"RouterSaved": "路由器已儲存 ({router}) 到 {path}",
"RouterNotSaved": "路由器未儲存:{error}",
"RouterRuntimeNotReloaded": "路由器已儲存,但執行階段執行緒在重新啟動前仍使用先前的路由器:{error}",
"RouterCurrent": "目前:{router}",
"RouterCurrentFailing": "目前:{router} · 失敗:{reason}",
"RouterSummary": "{kind} 路由器 · {provider} / {model}",
"RouterSummaryThinking": "{kind} 路由器 · {provider} / {model} · 思考 {thinking}",
"RouterPresetFastLabel": "快速檔",
"RouterPresetCustomLabel": "自訂",
"RouterNoKey": "沒有 {route} API 金鑰",
"RouterNoFastTier": "目前供應商沒有可執行的快速檔",
"RouterNeedKey": "新增 OpenRouter 金鑰 (OPENROUTER_API_KEY) 或 TypeSafe 金鑰 (TYPESAFE_API_KEY)",
"RouterCustomByHand": "自訂路由器需手動編輯",
"RouterCustomHelpIntro": "編輯 config.toml 中的 [auto.router],例如:",
"RouterCustomHelpOutro": "刪除該表(或執行 /router off),每個 Auto 輪次都會使用預設模型。",
"RouterActionTest": "測試",
"RouterActionSave": "儲存",
"RouterActionDiscard": "放棄",
"ModelsListHeader": "{provider} 的模型(預設:{model})",
"ModelsListHint": "已快取的目錄;執行 `codewhale models --update` 可更新已設定的提供者。",
"ModelsUpdateSummary": "模型目錄:{updated} 個已更新,{loaded} 個從 Codex 載入,{failed} 個失敗,{skipped} 個已略過。",
+74
View File
@@ -712,6 +712,43 @@ pub enum MessageId {
CmdModeDescription,
CmdModelDescription,
CmdModelsDescription,
CmdRouterDescription,
RouterSetupTitle,
RouterPresetJevHint,
RouterPresetFastHint,
RouterPresetOffHint,
RouterPresetOffCostSavingHint,
RouterPresetCustomHint,
RouterPresetUnavailable,
RouterTypesafeSignupPaused,
RouterTestSaveHint,
RouterTestHeader,
RouterTestNotMade,
RouterTestWouldRoute,
RouterTestDecision,
RouterTestChoice,
RouterTestCostReported,
RouterTestCostUnreported,
RouterTestUsage,
RouterTestFailing,
RouterSaved,
RouterNotSaved,
RouterRuntimeNotReloaded,
RouterCurrent,
RouterCurrentFailing,
RouterSummary,
RouterSummaryThinking,
RouterPresetFastLabel,
RouterPresetCustomLabel,
RouterNoKey,
RouterNoFastTier,
RouterNeedKey,
RouterCustomByHand,
RouterCustomHelpIntro,
RouterCustomHelpOutro,
RouterActionTest,
RouterActionSave,
RouterActionDiscard,
ModelsListHeader,
ModelsListHint,
ModelsUpdateSummary,
@@ -3116,6 +3153,43 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdModeDescription,
MessageId::CmdModelDescription,
MessageId::CmdModelsDescription,
MessageId::CmdRouterDescription,
MessageId::RouterSetupTitle,
MessageId::RouterPresetJevHint,
MessageId::RouterPresetFastHint,
MessageId::RouterPresetOffHint,
MessageId::RouterPresetOffCostSavingHint,
MessageId::RouterPresetCustomHint,
MessageId::RouterPresetUnavailable,
MessageId::RouterTypesafeSignupPaused,
MessageId::RouterTestSaveHint,
MessageId::RouterTestHeader,
MessageId::RouterTestNotMade,
MessageId::RouterTestWouldRoute,
MessageId::RouterTestDecision,
MessageId::RouterTestChoice,
MessageId::RouterTestCostReported,
MessageId::RouterTestCostUnreported,
MessageId::RouterTestUsage,
MessageId::RouterTestFailing,
MessageId::RouterSaved,
MessageId::RouterNotSaved,
MessageId::RouterRuntimeNotReloaded,
MessageId::RouterCurrent,
MessageId::RouterCurrentFailing,
MessageId::RouterSummary,
MessageId::RouterSummaryThinking,
MessageId::RouterPresetFastLabel,
MessageId::RouterPresetCustomLabel,
MessageId::RouterNoKey,
MessageId::RouterNoFastTier,
MessageId::RouterNeedKey,
MessageId::RouterCustomByHand,
MessageId::RouterCustomHelpIntro,
MessageId::RouterCustomHelpOutro,
MessageId::RouterActionTest,
MessageId::RouterActionSave,
MessageId::RouterActionDiscard,
MessageId::ModelsListHeader,
MessageId::ModelsListHint,
MessageId::ModelsUpdateSummary,
+9
View File
@@ -16,6 +16,15 @@ Planned for Codewhale v0.10.1: a reliability and first-run release. Turns that
stall now say so, approvals keep what you approved, plugin suggestions are
quieter, and Fleet runs can be checked before they spend anything.
### Added
- Official model routing: `/router` (also `/model router`) sets up the Auto
router with presets: Jev (TypeSafe's decision model, via OpenRouter or a
TypeSafe key), your provider's fast tier, Off, or Custom. Each preset makes
one test call before it saves, `/status` shows the router's choice, cost and
latency, and a failing router is shown as failing
([#6525](https://github.com/Hmbown/Codewhale/issues/6525)).
### Fixed
- A turn that stops producing output now reports itself: the turn loop records
+8
View File
@@ -760,6 +760,13 @@ fn configured_model_bound_secret_values(config: &Config, active_api_key: &str) -
}
}
// The decision router's TypeSafe key is no chat provider's key; its env
// form must still never reach a model. (`for_decision_route` adds the key
// from every source to its own client.)
if let Ok(value) = std::env::var(system_one::TYPESAFE_API_KEY_ENV) {
push_model_bound_secret(&mut values, Some(&value));
}
push_file_backed_model_bound_secrets(&mut values);
// Replace longer values first in case one credential happens to contain
@@ -5156,6 +5163,7 @@ mod provider_native_search;
mod responses;
mod role_placement;
mod stream_entry;
pub(crate) mod system_one;
/// Longest a request may take to open its stream and deliver the first body
/// byte before the client itself times out (#6184): the header wait plus the
+290
View File
@@ -0,0 +1,290 @@
//! System One decision API on the existing client (#6525).
//!
//! `[auto.router] kind = "decision"` asks a non-generative decision model
//! (TypeSafe's Jev) one typed Choice per turn. The wire is a plain JSON
//! `POST {base}/systemone` — not chat completions — served on two routes:
//!
//! * **OpenRouter** — `{openrouter base}/systemone` with the user's OpenRouter
//! key, built exactly like every other OpenRouter client.
//! * **TypeSafe direct** — `https://api.typesafe.ai/v1/systemone` with a
//! TypeSafe key.
//!
//! This is a child of `client` so it reuses the client's auth headers, TLS,
//! redaction and bounded retry plumbing; there is no second HTTP client.
//!
//! Known limits (written down so nobody assumes them):
//! * TypeSafe is **not** an [`ApiProvider`]: it serves no chat route, so it is
//! the decision router's own endpoint + key (`TYPESAFE_API_KEY`, the
//! `typesafe` secret-store slot, or `[providers.typesafe] api_key` /
//! `api_key_env`). Its spend is shown on the Auto receipt as the
//! provider-reported cost; it is not entered in session cost totals.
//! * The routing call makes one attempt (no retry): it is bounded by the
//! router timeout, and a retried decision would arrive after the turn has
//! already fallen back.
//! * Only the `choice` answer shape is parsed; `score` / `noul` are later.
use std::collections::BTreeMap;
use serde::Deserialize;
use serde_json::value::RawValue;
use super::*;
use crate::model_routing::AutoRouterFailure;
/// TypeSafe's direct API base (the `/systemone` path is appended).
pub(crate) const TYPESAFE_DEFAULT_BASE_URL: &str = "https://api.typesafe.ai/v1";
/// Environment variable holding a TypeSafe API key.
pub(crate) const TYPESAFE_API_KEY_ENV: &str = "TYPESAFE_API_KEY";
/// Secret-store slot and `[providers.<name>]` table name for the TypeSafe key.
pub(crate) const TYPESAFE_KEY_NAME: &str = "typesafe";
/// Which endpoint serves a decision router.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum DecisionRouterRoute {
Openrouter,
Typesafe,
}
impl DecisionRouterRoute {
/// Parse an `[auto.router] provider` value for a decision router.
#[must_use]
pub(crate) fn parse(provider: &str) -> Option<Self> {
let provider = provider.trim();
if ApiProvider::parse(provider) == Some(ApiProvider::Openrouter) {
Some(Self::Openrouter)
} else if provider.eq_ignore_ascii_case(TYPESAFE_KEY_NAME)
|| provider.eq_ignore_ascii_case("typesafe-ai")
{
Some(Self::Typesafe)
} else {
None
}
}
#[must_use]
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Openrouter => "openrouter",
Self::Typesafe => TYPESAFE_KEY_NAME,
}
}
#[must_use]
pub(crate) fn display_name(self) -> &'static str {
match self {
Self::Openrouter => "OpenRouter",
Self::Typesafe => "TypeSafe",
}
}
/// Whether a credential for this route is present (never returns it).
#[must_use]
pub(crate) fn has_key(self, config: &Config) -> bool {
match self {
Self::Openrouter => crate::config::has_api_key_for(config, ApiProvider::Openrouter),
Self::Typesafe => typesafe_api_key(config).is_some(),
}
}
}
/// Resolve the TypeSafe key: environment, then the durable secret store, then
/// `[providers.typesafe] api_key` / `api_key_env`.
pub(crate) fn typesafe_api_key(config: &Config) -> Option<String> {
let non_empty = |value: String| (!value.trim().is_empty()).then(|| value.trim().to_string());
if let Some(key) = std::env::var(TYPESAFE_API_KEY_ENV).ok().and_then(non_empty) {
return Some(key);
}
if let Some(key) = crate::config::credential_secret_store()
.and_then(|store| store.get(TYPESAFE_KEY_NAME).ok().flatten())
.and_then(non_empty)
{
return Some(key);
}
let entry = config
.providers
.as_ref()
.and_then(|providers| providers.custom_provider_config(TYPESAFE_KEY_NAME))?;
entry.api_key.clone().and_then(non_empty).or_else(|| {
entry
.api_key_env
.as_deref()
.map(str::trim)
.filter(|name| !name.is_empty())
.and_then(|name| std::env::var(name).ok())
.and_then(non_empty)
})
}
/// A decoded System One response. Unknown fields are ignored.
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct SystemOneResponse {
#[serde(default)]
pub(crate) id: Option<String>,
#[serde(default)]
pub(crate) model: Option<String>,
#[serde(default)]
pub(crate) answers: BTreeMap<String, SystemOneAnswer>,
#[serde(default)]
pub(crate) usage: Option<SystemOneUsage>,
}
/// One answer. Only the `choice` subset is interpreted.
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct SystemOneAnswer {
#[serde(rename = "type", default)]
pub(crate) kind: String,
#[serde(default)]
pub(crate) choice: Option<String>,
#[serde(default)]
pub(crate) probabilities: BTreeMap<String, Option<f64>>,
#[serde(default)]
pub(crate) confidence: Option<f64>,
}
/// Provider usage. OpenRouter adds `cost`; the token-count casing differs
/// between surfaces, so both spellings are accepted.
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct SystemOneUsage {
/// Kept verbatim so the receipt never re-renders a float.
#[serde(default)]
pub(crate) cost: Option<Box<RawValue>>,
#[serde(default, alias = "inputTokens")]
pub(crate) input_tokens: u32,
#[serde(default, alias = "outputTokens")]
pub(crate) output_tokens: u32,
}
impl SystemOneUsage {
/// The provider-reported cost as its verbatim JSON decimal, when it is a
/// finite, non-negative number.
#[must_use]
pub(crate) fn reported_cost(&self) -> Option<String> {
let raw = self.cost.as_ref()?.get().trim();
let value: f64 = raw.parse().ok()?;
(value.is_finite() && value >= 0.0).then(|| raw.to_string())
}
}
impl CodewhaleClient {
/// Build the client that serves `route`.
///
/// OpenRouter is the ordinary OpenRouter client (its key, base URL,
/// attribution headers). TypeSafe re-points a clone of the active route's
/// client — keeping its retry, TLS and redaction policy — at the TypeSafe
/// endpoint with the TypeSafe key only, and drops the active provider's
/// concurrency permit so a routing call never holds a chat slot.
pub(crate) fn for_decision_route(
config: &Config,
route: DecisionRouterRoute,
base_url_override: Option<&str>,
) -> Result<Self> {
match route {
DecisionRouterRoute::Openrouter => {
let mut scoped = config.clone();
scoped.provider = Some(ApiProvider::Openrouter.as_str().to_string());
// The decision model id is not a chat route; it travels in the
// JSON body only.
scoped.default_text_model = None;
Self::new(&scoped)
}
DecisionRouterRoute::Typesafe => {
let key = typesafe_api_key(config).with_context(|| {
format!("TypeSafe API key not configured ({TYPESAFE_API_KEY_ENV})")
})?;
let base_url = base_url_override
.map(str::trim)
.filter(|url| !url.is_empty())
.unwrap_or(TYPESAFE_DEFAULT_BASE_URL)
.trim_end_matches('/')
.to_string();
validate_base_url_security(&base_url, false)?;
let mut client = Self::new(config)?;
client.http_client = Self::http_client_builder_with_auth_mode(
&key,
&HashMap::new(),
ApiProvider::Custom,
&base_url,
WireFormat::ChatCompletions,
false,
false,
)?
.build()?;
client.base_url = base_url;
// `Self::new` froze the redaction set from the chat provider's
// secrets; the TypeSafe key is none of them, so add it before
// any decision body is built from untrusted context.
let mut secrets = client.model_bound_secret_values.as_ref().clone();
push_model_bound_secret(&mut secrets, Some(&key));
client.model_bound_secret_values = Arc::new(secrets);
client.api_key = key;
client.request_concurrency = None;
client.remote_control_inference_participant = false;
Ok(client)
}
}
}
/// `POST {base}/systemone` once, isolated like the Auto chat classifier:
/// no global retry banners, no shared token bucket, no response cache.
///
/// Only a failure class leaves this function — provider error bodies can
/// echo the prompt and must never reach receipts.
///
/// `dispatched` is set once both permits are held and the request is
/// handed to the transport, so a caller whose deadline cancels this
/// future can tell a possibly-billed request from one never sent.
pub(crate) async fn system_one_decide(
&self,
body: &Value,
dispatched: &std::sync::atomic::AtomicBool,
) -> std::result::Result<SystemOneResponse, AutoRouterFailure> {
let mut isolated = self.clone();
isolated.isolated_request_state = true;
isolated.rate_limiter = Arc::new(AsyncMutex::new(TokenBucket::from_env()));
isolated.retry.max_retries = 0;
let _inference = isolated.acquire_remote_control_inference_permit().await;
let _permit = isolated.acquire_provider_request_permit().await;
let url = api_url(&isolated.base_url, "systemone");
dispatched.store(true, std::sync::atomic::Ordering::Release);
let response = isolated
.send_json_with_retry(&url, body)
.await
.map_err(|error| router_failure_from_error(&error))?;
let bytes = response
.bytes()
.await
.map_err(|_| AutoRouterFailure::Transport)?;
serde_json::from_slice(&bytes).map_err(|_| AutoRouterFailure::InvalidAnswer)
}
}
/// Collapse a client error into a non-secret failure class. The HTTP status
/// is kept where the error carries it; the body never is.
pub(crate) fn router_failure_from_error(error: &anyhow::Error) -> AutoRouterFailure {
let Some(error) = error.downcast_ref::<LlmError>() else {
return AutoRouterFailure::Transport;
};
match error {
LlmError::RateLimited { .. } => AutoRouterFailure::Http { status: 429 },
LlmError::ServerError { status, .. } | LlmError::InvalidRequest { status, .. } => {
AutoRouterFailure::Http { status: *status }
}
LlmError::AuthenticationError(_) => AutoRouterFailure::Http { status: 401 },
LlmError::AuthorizationError(_) => AutoRouterFailure::Http { status: 403 },
LlmError::QuotaExhausted(_) => AutoRouterFailure::QuotaExhausted,
LlmError::ModelError(_)
| LlmError::ContextLengthError(_)
| LlmError::ContentPolicyError(_) => AutoRouterFailure::Rejected,
LlmError::Other(message) => message
.strip_prefix("HTTP ")
.and_then(|rest| rest.split(':').next())
.and_then(|status| status.trim().parse::<u16>().ok())
.map_or(AutoRouterFailure::Transport, |status| {
AutoRouterFailure::Http { status }
}),
LlmError::NetworkError(_) | LlmError::Timeout(_) | LlmError::ParseError(_) => {
AutoRouterFailure::Transport
}
}
}
@@ -265,6 +265,13 @@ pub fn exit() -> CommandResult {
/// picker (Pro/Flash + thinking effort) per #39 — gives users a discoverable
/// way to flip both knobs without memorising the docs.
pub fn model(app: &mut App, model_name: Option<&str>) -> CommandResult {
// `/model router …` is `/router …` (#6525): one Router setup view.
if let Some(name) = model_name.map(str::trim) {
let (head, rest) = name.split_once(char::is_whitespace).unwrap_or((name, ""));
if head.eq_ignore_ascii_case("router") {
return super::router::router_command(Some(rest));
}
}
if model_name.is_some_and(|name| name.eq_ignore_ascii_case("save-default")) {
// Explicit persistence of the pending session route as the startup
// default — only an explicit command can write settings after an
@@ -33,6 +33,7 @@ mod profile;
mod provider;
mod queue;
mod rlm;
pub(in crate::commands) mod router;
mod setup;
mod stash;
mod subagents;
@@ -82,6 +83,10 @@ impl CommandGroup for CoreCommands {
model::ModelCmd::info(),
model::ModelCmd::execute,
)),
Box::new(FunctionCommand::new(
router::RouterCmd::info(),
router::RouterCmd::execute,
)),
Box::new(FunctionCommand::new(
models::ModelsCmd::info(),
models::ModelsCmd::execute,
+1 -1
View File
@@ -9,7 +9,7 @@ use super::CommandResult;
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
name: "model",
aliases: &["moxing", "route"],
usage: "/model [name|refresh]",
usage: "/model [name|refresh|router]",
description_id: MessageId::CmdModelDescription,
};
@@ -0,0 +1,37 @@
//! `/router` command (#6525): set up the per-turn router for Auto model
//! routing. `/model router …` is the same command. Both open the one Router
//! setup view (`tui::views::router_setup`) or run a preset through it.
use crate::commands::traits::{CommandInfo, RegisterCommand};
use crate::tui::app::{App, AppAction};
use crate::tui::views::router_setup::parse_router_args;
use codewhale_localization::MessageId;
use super::CommandResult;
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
name: "router",
aliases: &[],
usage: "/router [jev|fast|off|custom|save]",
description_id: MessageId::CmdRouterDescription,
};
pub(in crate::commands) struct RouterCmd;
impl RegisterCommand for RouterCmd {
fn info() -> &'static CommandInfo {
&COMMAND_INFO
}
fn execute(_app: &mut App, arg: Option<&str>) -> CommandResult {
router_command(arg)
}
}
/// Shared by `/router` and `/model router`.
pub(in crate::commands) fn router_command(arg: Option<&str>) -> CommandResult {
match parse_router_args(arg) {
Ok(request) => CommandResult::action(AppAction::RouterSetup { request }),
Err(usage) => CommandResult::error(usage),
}
}
@@ -118,6 +118,8 @@ fn save_preserves_latest_auto_route_receipt() {
reason: crate::model_routing::AutoRouteReason::LocalFallback(
crate::model_routing::AutoRouteHeuristicReason::DeclaredDefault,
),
decision: None,
router_failure: None,
};
app.set_model_selection("auto".to_string());
app.last_effective_provider = Some(crate::config::ApiProvider::Zai);
+58
View File
@@ -2617,8 +2617,53 @@ pub struct AutoRouterConfig {
/// hung local router cannot stall a turn indefinitely.
#[serde(default)]
pub timeout_secs: Option<u64>,
/// Router kind (#6525): `"chat"` (default) asks a chat model for JSON;
/// `"decision"` asks a System One decision model (Jev) a typed Choice
/// between the active provider's fast and strong tiers. Any other value
/// leaves the router unconfigured (shown as failing, never guessed).
#[serde(default)]
pub kind: Option<String>,
/// Decision routers only: below this answer confidence (0..=1, default
/// [`DEFAULT_AUTO_ROUTER_MIN_CONFIDENCE`]) the turn takes the local
/// fallback instead of the decision.
#[serde(default)]
pub min_confidence: Option<f64>,
/// Decision routers only: endpoint override for `provider = "typesafe"`
/// (default `https://api.typesafe.ai/v1`). OpenRouter decision routers use
/// the configured OpenRouter base URL.
#[serde(default)]
pub base_url: Option<String>,
}
/// `[auto.router] kind` (#6525).
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum AutoRouterKind {
/// A chat model returns `{provider, model, thinking}` JSON.
Chat,
/// A System One decision model answers a typed Choice over tiers.
Decision,
}
impl AutoRouterKind {
/// `None` (absent) is the chat default; an unknown value is `None` so the
/// caller reports the router as not configured instead of guessing.
#[must_use]
pub(crate) fn parse(raw: Option<&str>) -> Option<Self> {
match raw.map(str::trim).filter(|kind| !kind.is_empty()) {
None => Some(Self::Chat),
Some(kind) if kind.eq_ignore_ascii_case("chat") => Some(Self::Chat),
Some(kind) if kind.eq_ignore_ascii_case("decision") => Some(Self::Decision),
Some(_) => None,
}
}
}
/// Default `[auto.router] min_confidence` for decision routers: TypeSafe's
/// "below 0.5, don't act" band (for two options, the chosen tier's
/// probability must reach 0.75).
pub(crate) const DEFAULT_AUTO_ROUTER_MIN_CONFIDENCE: f64 = 0.5;
fn default_update_check_for_updates() -> bool {
true
}
@@ -4801,6 +4846,19 @@ impl Config {
.min(MAX_AUTO_ROUTER_TIMEOUT_SECS)
}
/// Decision-router confidence floor, clamped to `0..=1`; absent or
/// non-finite values use [`DEFAULT_AUTO_ROUTER_MIN_CONFIDENCE`].
#[must_use]
pub(crate) fn auto_router_min_confidence(&self) -> f64 {
self.auto
.as_ref()
.and_then(|a| a.router.as_ref())
.and_then(|r| r.min_confidence)
.filter(|value| value.is_finite())
.unwrap_or(DEFAULT_AUTO_ROUTER_MIN_CONFIDENCE)
.clamp(0.0, 1.0)
}
#[must_use]
pub fn tools_always_load(&self) -> std::collections::HashSet<String> {
self.tools
+301 -24
View File
@@ -6,8 +6,10 @@
use serde::Serialize;
use crate::client::system_one::DecisionRouterRoute;
use crate::config::{
ApiProvider, Config, has_api_key_for, normalize_model_name_for_provider, provider_capability,
ApiProvider, AutoRouterKind, Config, has_api_key_for, normalize_model_name_for_provider,
provider_capability,
};
use crate::provider_lake::models_for_provider;
@@ -50,6 +52,45 @@ pub(crate) struct ModelRouteCandidate {
pub(crate) tags: Vec<&'static str>,
}
/// Why a declared `[auto.router]` cannot run.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum AutoRouterSetupIssue {
/// `kind` is neither `"chat"` nor `"decision"`.
UnknownKind,
/// `kind = "decision"` names a provider that serves no decision API.
UnsupportedDecisionProvider,
/// `provider` or `model` is missing (or the chat provider is unknown).
Incomplete,
/// The route is complete but its credential is missing.
MissingKey,
}
impl AutoRouterSetupIssue {
#[must_use]
pub(crate) fn label(self) -> &'static str {
match self {
Self::UnknownKind => "unknown [auto.router] kind (expected \"chat\" or \"decision\")",
Self::UnsupportedDecisionProvider => {
"decision routers are served by OpenRouter or TypeSafe"
}
Self::Incomplete => "[auto.router] needs a known provider and a model",
Self::MissingKey => "no API key for the router route",
}
}
}
/// Probability or confidence (0..=1) as basis points, so receipts and the
/// inventory stay `Eq` and never re-render a float.
#[must_use]
pub(crate) fn probability_bp(value: f64) -> u16 {
if !value.is_finite() {
return 0;
}
// Bounded to 0..=10000 before the cast.
(value.clamp(0.0, 1.0) * 10_000.0).round() as u16
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ModelInventory {
pub(crate) active_provider: ApiProvider,
@@ -64,6 +105,19 @@ pub(crate) struct ModelInventory {
/// holding a provider key never elects a network classifier by itself.
pub(crate) router_configured: bool,
pub(crate) router_available: bool,
/// `[auto.router] kind` (#6525); an unknown kind leaves the router
/// unconfigured and sets [`Self::router_setup_issue`].
pub(crate) router_kind: AutoRouterKind,
/// Endpoint for a decision router (`None` for chat routers).
pub(crate) router_decision_route: Option<DecisionRouterRoute>,
/// Decision-router endpoint override (TypeSafe only).
pub(crate) router_base_url: Option<String>,
/// Decision-router confidence floor in basis points (0..=10000).
pub(crate) router_min_confidence_bp: u16,
/// Why a declared `[auto.router]` cannot run. `None` when no router is
/// declared or the router is available. A declared-but-unusable router is
/// shown as failing, never silently ignored.
pub(crate) router_setup_issue: Option<AutoRouterSetupIssue>,
/// `[auto] cross_provider = true` opt-in (#4411). When false (the
/// default), Auto routing — classifier payload included — is confined to
/// `active_provider`. The full candidate list still carries every
@@ -239,45 +293,107 @@ impl ModelInventory {
// every Auto turn, spending a user's tokens on a route they never asked
// for and privileging one provider. With no explicit `[auto.router]`,
// legacy Auto is now local/free (heuristic-only).
let explicit_router = config
.auto
.as_ref()
.and_then(|auto| auto.router.as_ref())
.and_then(|router| {
let provider = router.provider.as_deref().and_then(ApiProvider::parse)?;
let model = router
.model
.as_deref()
.map(str::trim)
.filter(|m| !m.is_empty())?;
Some((
provider,
model.to_string(),
router
.thinking
.as_deref()
.map(str::trim)
.filter(|t| !t.is_empty())
.map(str::to_string),
))
});
let router_table = config.auto.as_ref().and_then(|auto| auto.router.as_ref());
// Any populated key declares a router: a table holding only
// `timeout_secs` or `base_url` is a malformed router to diagnose, not
// an absent one to skip silently.
let router_declared = router_table.is_some_and(|router| {
router.provider.is_some()
|| router.model.is_some()
|| router.kind.is_some()
|| router.thinking.is_some()
|| router.timeout_secs.is_some()
|| router.min_confidence.is_some()
|| router.base_url.is_some()
});
let router_kind = AutoRouterKind::parse(router_table.and_then(|r| r.kind.as_deref()));
let router_model_setting = router_table
.and_then(|router| router.model.as_deref())
.map(str::trim)
.filter(|model| !model.is_empty());
let router_provider_setting = router_table
.and_then(|router| router.provider.as_deref())
.map(str::trim)
.filter(|provider| !provider.is_empty());
let mut router_setup_issue = None;
let mut router_decision_route = None;
let explicit_router = match router_kind {
None => {
router_setup_issue = Some(AutoRouterSetupIssue::UnknownKind);
None
}
Some(AutoRouterKind::Chat) => router_provider_setting
.and_then(ApiProvider::parse)
.zip(router_model_setting)
.map(|(provider, model)| {
(
provider,
model.to_string(),
router_table
.and_then(|router| router.thinking.as_deref())
.map(str::trim)
.filter(|t| !t.is_empty())
.map(str::to_string),
)
}),
Some(AutoRouterKind::Decision) => {
match router_provider_setting.map(DecisionRouterRoute::parse) {
Some(None) => {
router_setup_issue =
Some(AutoRouterSetupIssue::UnsupportedDecisionProvider);
None
}
Some(Some(route)) => router_model_setting.map(|model| {
router_decision_route = Some(route);
// A decision model has no reasoning knob, so the
// configured `thinking` is ignored. TypeSafe is not a
// chat provider; `router_provider` is only a label
// there, and `router_decision_route` is authoritative.
(ApiProvider::Openrouter, model.to_string(), None)
}),
None => None,
}
}
};
let router_configured = explicit_router.is_some();
let (router_provider, router_model, router_thinking) = explicit_router
// Kept only as an inert display/default label for the router fields;
// `router_available` below is what gates any classifier call.
.unwrap_or_else(|| (ApiProvider::Deepseek, "deepseek-v4-flash".to_string(), None));
let router_available = router_configured
&& match router_decision_route {
Some(route) => route.has_key(config),
None => has_api_key_for(config, router_provider),
};
if router_declared && router_setup_issue.is_none() && !router_available {
router_setup_issue = Some(if router_configured {
AutoRouterSetupIssue::MissingKey
} else {
AutoRouterSetupIssue::Incomplete
});
}
let cross_provider_auto = config.auto_cross_provider();
let router_timeout_secs = config.auto_router_timeout_secs();
let router_min_confidence_bp = probability_bp(config.auto_router_min_confidence());
Self {
active_provider,
router_provider,
router_configured,
router_available: router_configured && has_api_key_for(config, router_provider),
router_available,
router_model,
router_thinking,
router_timeout_secs,
router_kind: router_kind.unwrap_or(AutoRouterKind::Chat),
router_decision_route,
router_base_url: router_table
.and_then(|router| router.base_url.as_deref())
.map(str::trim)
.filter(|url| !url.is_empty())
.map(str::to_string),
router_min_confidence_bp,
router_setup_issue,
cross_provider_auto,
candidates,
}
@@ -773,6 +889,7 @@ mod tests {
model: Some("local-router".to_string()),
thinking: None,
timeout_secs: Some(15),
..Default::default()
}),
..Default::default()
}),
@@ -788,6 +905,7 @@ mod tests {
model: Some("local-router".to_string()),
thinking: None,
timeout_secs: Some(9_999),
..Default::default()
}),
..Default::default()
}),
@@ -806,6 +924,7 @@ mod tests {
model: Some("local-router".to_string()),
thinking: None,
timeout_secs: Some(0),
..Default::default()
}),
..Default::default()
}),
@@ -860,6 +979,7 @@ mod tests {
model: Some("glm-5-turbo".to_string()),
thinking: Some("low".to_string()),
timeout_secs: None,
..Default::default()
}),
}),
..Default::default()
@@ -908,6 +1028,7 @@ mod tests {
model: Some("glm-5-turbo".to_string()),
thinking: None,
timeout_secs: None,
..Default::default()
}),
cross_provider: None,
}),
@@ -983,6 +1104,11 @@ mod tests {
router_timeout_secs: 4,
router_configured: false,
router_available: false,
router_kind: AutoRouterKind::Chat,
router_decision_route: None,
router_base_url: None,
router_min_confidence_bp: 5_000,
router_setup_issue: None,
cross_provider_auto: false,
candidates: vec![ModelRouteCandidate {
provider: ApiProvider::Openai,
@@ -1147,6 +1273,7 @@ mod tests {
model: Some("deepseek-v4-flash".to_string()),
thinking: None,
timeout_secs: None,
..Default::default()
}),
}),
..zai.clone()
@@ -1283,3 +1410,153 @@ mod tests {
crate::provider_lake::clear_live_snapshot();
}
}
#[cfg(test)]
mod decision_router_inventory_tests {
use super::*;
fn with_router(router: crate::config::AutoRouterConfig, openrouter_key: bool) -> Config {
Config {
provider: Some("deepseek".to_string()),
providers: Some(crate::config::ProvidersConfig {
openrouter: crate::config::ProviderConfig {
api_key: openrouter_key.then(|| "or-test-key".to_string()),
..Default::default()
},
..Default::default()
}),
auto: Some(crate::config::AutoConfig {
cost_saving: None,
cross_provider: None,
router: Some(router),
}),
..Default::default()
}
}
fn decision(provider: &str) -> crate::config::AutoRouterConfig {
crate::config::AutoRouterConfig {
kind: Some("decision".to_string()),
provider: Some(provider.to_string()),
model: Some("typesafe/jev-1.13".to_string()),
..Default::default()
}
}
/// Tuple fields drop in order: restore the environment, then unlock.
fn hermetic() -> (
crate::test_support::EnvVarGuard,
crate::test_support::EnvVarGuard,
crate::test_support::TestEnvLock,
) {
let lock = crate::test_support::lock_test_env();
(
crate::test_support::EnvVarGuard::remove("OPENROUTER_API_KEY"),
crate::test_support::EnvVarGuard::remove("TYPESAFE_API_KEY"),
lock,
)
}
#[test]
fn decision_router_on_openrouter_needs_the_openrouter_key() {
let _env = hermetic();
let with_key = ModelInventory::from_config(&with_router(decision("openrouter"), true));
assert!(with_key.router_configured);
assert!(with_key.router_available);
assert_eq!(with_key.router_kind, AutoRouterKind::Decision);
assert_eq!(
with_key.router_decision_route,
Some(DecisionRouterRoute::Openrouter)
);
assert_eq!(
with_key.router_thinking, None,
"decision models ignore thinking"
);
assert_eq!(with_key.router_min_confidence_bp, 5_000);
assert_eq!(with_key.router_setup_issue, None);
let without_key = ModelInventory::from_config(&with_router(decision("openrouter"), false));
assert!(without_key.router_configured);
assert!(!without_key.router_available);
assert_eq!(
without_key.router_setup_issue,
Some(AutoRouterSetupIssue::MissingKey)
);
}
#[test]
fn unknown_kind_or_unsupported_decision_provider_is_not_configured() {
let _env = hermetic();
let bogus = crate::config::AutoRouterConfig {
kind: Some("bogus".to_string()),
..decision("openrouter")
};
let inventory = ModelInventory::from_config(&with_router(bogus, true));
assert!(!inventory.router_configured);
assert!(!inventory.router_available);
assert_eq!(
inventory.router_setup_issue,
Some(AutoRouterSetupIssue::UnknownKind)
);
let zai = ModelInventory::from_config(&with_router(decision("zai"), true));
assert!(!zai.router_configured);
assert_eq!(
zai.router_setup_issue,
Some(AutoRouterSetupIssue::UnsupportedDecisionProvider)
);
}
#[test]
fn a_router_table_with_only_tuning_keys_is_declared_and_incomplete() {
let _env = hermetic();
let tuning_only = [
crate::config::AutoRouterConfig {
timeout_secs: Some(3),
..Default::default()
},
crate::config::AutoRouterConfig {
min_confidence: Some(0.6),
..Default::default()
},
crate::config::AutoRouterConfig {
thinking: Some("off".to_string()),
..Default::default()
},
crate::config::AutoRouterConfig {
base_url: Some("https://api.typesafe.ai/v1".to_string()),
..Default::default()
},
];
for router in tuning_only {
let inventory = ModelInventory::from_config(&with_router(router.clone(), true));
assert!(!inventory.router_available, "{router:?}");
assert_eq!(
inventory.router_setup_issue,
Some(AutoRouterSetupIssue::Incomplete),
"{router:?}"
);
}
}
#[test]
fn min_confidence_is_clamped() {
let _env = hermetic();
for (configured, expected) in [
(Some(2.0), 10_000),
(Some(-1.0), 0),
(Some(0.35), 3_500),
(None, 5_000),
] {
let router = crate::config::AutoRouterConfig {
min_confidence: configured,
..decision("openrouter")
};
assert_eq!(
ModelInventory::from_config(&with_router(router, true)).router_min_confidence_bp,
expected,
"{configured:?}"
);
}
}
}
File diff suppressed because it is too large. Load diff
+16
View File
@@ -1584,6 +1584,22 @@ impl TaskManager {
&self.execution_lease.scope
}
/// Apply `edit` to the runtime threads' authoritative config and reload
/// it, so runtime-chat and queued runtime turns see a setting the UI just
/// persisted instead of their startup snapshot. No runtime manager is a
/// no-op.
pub(crate) async fn reload_runtime_config_with(
&self,
edit: impl FnOnce(&mut crate::config::Config),
) -> Result<()> {
let Some(runtime) = &self.runtime_threads else {
return Ok(());
};
let mut config = runtime.read_config().clone();
edit(&mut config);
runtime.reload_config(config).await.map(|_| ())
}
pub(crate) fn session_store_binding(
&self,
) -> Option<crate::runtime_threads::RuntimeStoreBinding> {
+2
View File
@@ -582,6 +582,8 @@ fn auto_reasoning_change_invalidates_the_previous_route_and_receipt() {
reason: crate::model_routing::AutoRouteReason::LocalFallback(
crate::model_routing::AutoRouteHeuristicReason::DeclaredDefault,
),
decision: None,
router_failure: None,
});
app.last_effective_reasoning_effort =
Some(EffectiveReasoningEffort::Tier(ReasoningEffort::Max));
+5
View File
@@ -675,6 +675,11 @@ pub enum AppAction {
content: String,
command: String,
},
/// Router setup (`/router`, `/model router`, #6525): open the view, test a
/// preset with one routing call, or save one to `[auto.router]`.
RouterSetup {
request: crate::tui::views::router_setup::RouterRequest,
},
/// Live remaining-credit lookup for prepaid providers (`/balance`).
FetchBalance,
FetchModels,
+112
View File
@@ -1585,6 +1585,7 @@ fn turn_route_lines(app: &App) -> Vec<String> {
lines.push(format!("Auto pair: {pair}"));
lines.push(format!("Auto scope: {}", receipt.scope.label()));
lines.push(format!("Auto data: {}", receipt.data_path.label()));
lines.extend(auto_router_receipt_lines(receipt));
}
let session = &app.session;
@@ -1643,6 +1644,55 @@ enum ResultDetail {
Compact,
}
/// `/status` lines for the router behind an Auto receipt (#6525): the
/// decision-model answer, its cost and latency, and a failing router.
fn auto_router_receipt_lines(receipt: &crate::model_routing::AutoRouteReceipt) -> Vec<String> {
let percent = |bp: u16| format!("{}%", (u32::from(bp) + 50) / 100);
let mut lines = Vec::new();
if let Some(decision) = receipt.decision.as_ref() {
let cost = decision.provider_reported_cost_usd.as_deref().map_or_else(
|| "cost not reported".to_string(),
|cost| format!("${cost} (provider-reported)"),
);
let model = decision
.response_model
.as_deref()
.map(|model| format!(" · {model}"))
.unwrap_or_default();
lines.push(format!(
"Auto router: decision model{model} · {} ms · {cost}",
decision.latency_ms
));
let mut probabilities: Vec<(&String, &u16)> = decision.probabilities_bp.iter().collect();
probabilities.sort_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0)));
let probabilities = probabilities
.into_iter()
.map(|(option, bp)| format!("{option} {}", percent(*bp)))
.collect::<Vec<_>>()
.join(" · ");
let mut choice = format!(
"Auto choice: {} ({probabilities}) · confidence {} (min {})",
decision.choice,
percent(decision.confidence_bp),
percent(decision.min_confidence_bp)
);
if decision.cost_saving_kept_fast {
choice.push_str(" · cost-saving kept fast");
}
if let Some(thinking) = decision.thinking.as_deref() {
choice.push_str(&format!(" · thinking {thinking}"));
}
lines.push(choice);
}
if let Some(failure) = receipt.router_failure {
lines.push(format!(
"Auto router: failing — {} (used the local fallback)",
failure.label()
));
}
lines
}
fn cleaned_turn_text(text: &str, detail: ResultDetail, max_width: usize) -> String {
if detail == ResultDetail::Compact {
return one_line_summary(text, max_width);
@@ -1743,6 +1793,66 @@ mod tests {
assert!(joined.contains("still failing"), "{joined}");
}
#[test]
fn turn_route_lines_show_decision_evidence_and_a_failing_router() {
let mut app = test_app();
app.auto_model = true;
app.last_effective_provider = Some(crate::config::ApiProvider::Deepseek);
app.last_effective_model = Some("deepseek-v4-pro".to_string());
let decision = crate::model_routing::AutoRouteDecisionEvidence {
choice: "strong".to_string(),
probabilities_bp: [("fast".to_string(), 1800), ("strong".to_string(), 8200)]
.into_iter()
.collect(),
confidence_bp: 6400,
min_confidence_bp: 5000,
cost_saving_kept_fast: false,
thinking: Some("max".to_string()),
provider_reported_cost_usd: Some("0.000019992".to_string()),
latency_ms: 180,
response_model: Some("typesafe/jev-1.13-20260917".to_string()),
};
let receipt = crate::model_routing::AutoRouteReceipt {
tier: crate::model_routing::AutoRouteTier::Strong,
pair: crate::model_routing::AutoRoutePair {
strong: "deepseek-v4-pro".to_string(),
fast: Some("deepseek-v4-flash".to_string()),
},
scope: crate::model_routing::AutoRouteScope::ActiveProvider,
data_path: crate::model_routing::AutoRouteDataPath::Decision {
route: crate::client::system_one::DecisionRouterRoute::Openrouter,
model: "typesafe/jev-1.13".to_string(),
},
reason: crate::model_routing::AutoRouteReason::ClassifierRecommendation,
decision: Some(decision),
router_failure: None,
};
app.last_auto_route_receipt = Some(receipt.clone());
let joined = turn_route_lines(&app).join("\n");
assert!(joined.contains("-> OpenRouter / typesafe/jev-1.13 (decision model)"));
assert!(joined.contains(
"Auto router: decision model · typesafe/jev-1.13-20260917 · 180 ms · $0.000019992 (provider-reported)"
));
assert!(joined.contains(
"Auto choice: strong (strong 82% · fast 18%) · confidence 64% (min 50%) · thinking max"
));
assert!(!joined.contains("failing"));
app.last_auto_route_receipt = Some(crate::model_routing::AutoRouteReceipt {
reason: crate::model_routing::AutoRouteReason::ClassifierFallback(
crate::model_routing::AutoRouteHeuristicReason::DeclaredDefault,
),
decision: None,
router_failure: Some(crate::model_routing::AutoRouterFailure::Http { status: 402 }),
..receipt
});
let joined = turn_route_lines(&app).join("\n");
assert!(
joined.contains("Auto router: failing — HTTP 402 (used the local fallback)"),
"{joined}"
);
}
#[test]
fn turn_route_lines_include_truthful_auto_receipt() {
let mut app = test_app();
@@ -1761,6 +1871,8 @@ mod tests {
model: "deepseek-v4-flash".to_string(),
},
reason: crate::model_routing::AutoRouteReason::ClassifierRecommendation,
decision: None,
router_failure: None,
});
let joined = turn_route_lines(&app).join("\n");
+9
View File
@@ -1719,6 +1719,15 @@ pub(crate) async fn apply_command_result(
app.status_message = Some(format!("Could not cancel {agent_id}"));
}
}
AppAction::RouterSetup { request } => {
crate::tui::views::router_setup::handle_router_request(
app,
config,
task_manager,
request,
)
.await;
}
AppAction::FetchBalance => {
let provider = app.api_provider;
if !crate::config::provider_has_balance_api(provider) {
+4
View File
@@ -17102,6 +17102,8 @@ fn turn_started_route_is_captured_before_cancel_suppression() {
reason: crate::model_routing::AutoRouteReason::LocalFallback(
crate::model_routing::AutoRouteHeuristicReason::DeclaredDefault,
),
decision: None,
router_failure: None,
});
let created_at = chrono::Utc::now();
let event = EngineEvent::TurnStarted {
@@ -22117,6 +22119,8 @@ fn auto_route_receipt_survives_session_snapshot_and_restore() {
reason: crate::model_routing::AutoRouteReason::LocalFallback(
crate::model_routing::AutoRouteHeuristicReason::ComplexRequest,
),
decision: None,
router_failure: None,
};
let mut app = create_test_app();
app.set_model_selection("auto".to_string());
+3
View File
@@ -38,6 +38,7 @@ pub mod fleet_roster;
pub mod fleet_setup;
pub mod mode_picker;
pub mod route_save_prompt;
pub(crate) mod router_setup;
pub mod skills_manager;
pub mod status_picker;
pub mod workflows_manager;
@@ -85,6 +86,8 @@ pub enum ModalKind {
/// "Resume this session?" over the launch card. Resuming replaces the
/// whole session context, so it asks first.
LaunchResumeConfirm,
/// Router setup (`/router`, `/model router`): presets for `[auto.router]`.
RouterSetup,
}
/// Clear and paint a modal popup with an opaque surface.
File diff suppressed because it is too large. Load diff
+51 -3
View File
@@ -706,8 +706,55 @@ A classifier call happens only when `[auto.router]` names both `provider` and
`ModelInventory::from_config` (`crates/tui/src/model_inventory.rs`). If either
condition fails, or the classifier call errors or times out, the local
fallback decides: the default model, or the fast sibling under `cost_saving`.
That is a fallback, not a failure. The turn's route receipt
(`/status` → Auto) records which path was taken.
The turn's route receipt (`/status` → Auto) records which path was taken, and a
router you configured that cannot run or fails (missing key, HTTP error,
timeout, invalid answer) is shown as `Auto router: failing — …` rather than
silently ignored.
#### Set up model routing
`/router` (also `/model router`) opens one Router setup view with these
presets. Each writes only `[auto.router]`; none is ever chosen for you.
| Preset | What it writes | Cost and privacy |
| --- | --- | --- |
| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |
| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |
| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |
| `/router custom` | Nothing; prints the TOML to edit | — |
Choosing Jev or Fast makes **one test call** with a fixed sample request and
shows the tier it picked, the probabilities and confidence, the latency and the
provider-reported cost. `Enter` (or `/router save <preset>`) then saves through
the normal config writer; `Esc` discards it. TypeSafe paused new signups on
2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is
read from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or
`[providers.typesafe] api_key` / `api_key_env`.
#### Decision routers (`kind = "decision"`)
A decision router asks a non-generative decision model one typed question per
turn — a Choice between the active provider's `fast` and `strong` tiers, plus a
thinking level — and gets calibrated probabilities back. No prose is parsed.
```toml
[auto.router]
kind = "decision" # default "chat"
provider = "openrouter" # or "typesafe"
model = "typesafe/jev-1.13" # "~typesafe/jev-latest" also works; TypeSafe direct: "jev-latest"
timeout_secs = 2
min_confidence = 0.5 # default 0.5, clamped to 0..1
```
- The router is called only when the active provider has a runnable strong/fast
pair; otherwise there is no call and no spend.
- An answer below `min_confidence` takes the local fallback. Under
`[auto] cost_saving`, a `strong` answer also needs a probability of at least
0.75, or the turn stays on the fast tier.
- An unknown `kind`, or a decision `provider` other than `openrouter` /
`typesafe`, leaves the router unconfigured and shown as failing.
- `thinking` is ignored for decision routers. OpenRouter spend is recorded like
any routed usage; TypeSafe-direct spend appears on the receipt only.
Two `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):
@@ -728,7 +775,8 @@ cross_provider = false # default false
is configured to use. The classifier is only shown that provider's models,
and the fallback never leaves it. Setting `cross_provider = true` lets the
classifier choose among every runnable provider. There is no interactive
toggle; it has to be set in config.
toggle for `cross_provider`; it has to be set in config. A decision router
always chooses within the active provider.
To bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.
To only scaffold MCP, run `codewhale mcp init`.
+32 -1
View File
@@ -313,7 +313,38 @@ model = "glm-5-turbo"
thinking = "off" # 可选;默认 off
```
分类器调用只在 `[auto.router]` 已设置**且**该 provider 有 key 时发生——`router_available = router_configured && has_api_key_for(...)`(`crates/tui/src/model_inventory.rs:206-218`)。任一条件不满足意味着由启发式决定,而不是失败。回合的路由回执(`/status` → Auto)记录是哪一种。
分类器调用只在 `[auto.router]` 已设置**且**该 provider 有 key 时发生——`router_available = router_configured && has_api_key_for(...)`(`crates/tui/src/model_inventory.rs`)。任一条件不满足、或分类器调用出错/超时时,由本地回退决定。回合的路由回执(`/status` → Auto)记录是哪一种;你配置了但无法运行或失败的路由器(缺 key、HTTP 错误、超时、无效回答)会显示为 `Auto router: failing — …`,而不是被静默忽略。
#### 设置模型路由
`/router`(也可用 `/model router`)打开同一个 Router 设置视图,提供以下预设。每个预设只写入 `[auto.router]`,不会被自动选中。
| 预设 | 写入内容 | 成本与隐私 |
| --- | --- | --- |
| `/router jev` | TypeSafe 的决策模型 Jev,经由 OpenRouter(`typesafe/jev-1.13`)或 TypeSafe 直连,取决于你有哪个 key | 每轮约 $0.00002(每百万输入 token $0.042,输出免费)。你的最新请求和最多六行近期上下文会发送到 OpenRouter → TypeSafe(或 TypeSafe)。 |
| `/router fast` | 当前 provider 可运行的快速档,关闭思考 | 使用你已有的 key;分类器看到同样的请求文本。 |
| `/router off` | 删除 `[auto.router]` | 不调用路由器;Auto 回合使用默认模型,`[auto] cost_saving = true` 时使用快速档(Off 不改动该设置)。 |
| `/router custom` | 不写入;打印可手动编辑的 TOML | — |
选择 Jev 或 Fast 会用固定的示例请求做**一次测试调用**,并显示它选择的档位、概率与置信度、延迟和 provider 报告的费用。随后按 `Enter`(或 `/router save <预设>`)通过常规配置写入器保存;按 `Esc` 放弃。TypeSafe 已于 2026-09-22 暂停新用户注册,因此 OpenRouter 是新用户的默认路线。TypeSafe key 读取自 `TYPESAFE_API_KEY`、`typesafe` 密钥存储条目,或 `[providers.typesafe] api_key` / `api_key_env`。
#### 决策路由器(`kind = "decision"`)
决策路由器每回合向一个非生成式决策模型提一个有类型的问题——在当前 provider 的 `fast` 和 `strong` 档之间做 Choice,外加一个思考档位——并得到校准过的概率。不解析任何散文。
```toml
[auto.router]
kind = "decision" # 默认 "chat"
provider = "openrouter" # 或 "typesafe"
model = "typesafe/jev-1.13" # 也可用 "~typesafe/jev-latest";TypeSafe 直连用 "jev-latest"
timeout_secs = 2
min_confidence = 0.5 # 默认 0.5,限制在 0..1
```
- 只有当前 provider 有可运行的 strong/fast 档位对时才会调用路由器;否则不调用、不花费。
- 置信度低于 `min_confidence` 的回答走本地回退。在 `[auto] cost_saving` 下,回答 `strong` 还需要至少 0.75 的概率,否则回合留在快速档。
- 未知的 `kind`,或决策路由器的 `provider` 不是 `openrouter` / `typesafe`,会让路由器处于未配置状态并显示为 failing。
- 决策路由器忽略 `thinking`。OpenRouter 的花费像其他路由用量一样记录;TypeSafe 直连的花费只显示在回执上。
要在解析后的路径引导(bootstrap) MCP 和 skills 目录,运行 `codewhale setup`。要只搭建 MCP,运行 `codewhale mcp init`。
+7
View File
@@ -27,6 +27,13 @@ export const CHANGELOG: ChangelogRelease[] = [
"unreleased": true,
"compareUrl": "https://github.com/Hmbown/CodeWhale/compare/v0.10.0...HEAD",
"sections": [
{
"heading": "Added",
"items": [
"Official model routing: /router (also /model router) sets up the Auto router with presets: Jev (TypeSafe's decision model, via OpenRouter or a TypeSafe key), your provider's fast tier, Off, or Custom. Each preset makes one test call before it saves, /status shows the router's choice, cost and latency, and a failing router is shown as failing (#6525)."
],
"itemCount": 1
},
{
"heading": "Fixed",
"items": [