mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
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:
39 files changed
+3782
-77
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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} छोड़े गए।",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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} 件スキップ。",
|
||||
|
||||
@@ -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}개 건너뜀.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}.",
|
||||
|
||||
@@ -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}.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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} 个已跳过。",
|
||||
|
||||
@@ -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} 個已略過。",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1149
-48
File diff suppressed because it is too large.
Load diff
@@ -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> {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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`。
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in new issue
Block a user