You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make provider/model switching atomic. The TUI, slash commands, fallback switch, model picker, and engine restart path must resolve a complete route candidate before mutating app state, config state, persisted settings, compaction budgets, or engine clients.
This issue implements the route-resolution mutation gate described in #2608.
Architecture contract
A route candidate is runtime-derived. It is not a catalog row and not a naked (provider, model) pair.
Only the route resolver may construct a ready candidate. Engine-facing code should move toward accepting this candidate rather than provider/model strings.
Route resolution must be driven by explicit user choice, saved config, Fleet role/slot/loadout policy, hard capability requirements, fallback policy, or an explicit user-enabled auto router. It must not inspect freeform prompt text and silently change provider, model, Fleet model class, or reasoning mode.
Wire Id And Namespace Rule
Provider-prefixed or organization-prefixed model strings are provider wire ids or namespace hints, not global provider-selection proof.
Examples:
deepseek-ai/DeepSeek-V4-Pro may be a Together wire id.
deepseek/deepseek-v4-pro may be an OpenRouter wire id.
anthropic/claude-* may be an aggregator wire id rather than direct Anthropic provider state.
A custom OpenAI-compatible endpoint may legitimately use a string that resembles a hosted catalog namespace.
The resolver must interpret these strings only inside provider scope unless the user explicitly invoked global search. Do not infer provider switching from deepseek-ai/, deepseek/, anthropic/, openai/, qwen/, or similar prefixes.
Resolution order
Explicit invocation wins: --provider, --model, --base-url, request fields, or wrapper env values are highest priority and session-scoped unless explicitly saved.
Resolve provider first: explicit provider, then saved/default provider, then legacy base URL inference, then default provider.
Treat auto as an explicit opt-in sentinel, never a model ID. Resolve it from metadata/policy such as Fleet role, command purpose, capability requirements, cost/latency preference, and context limits. Do not resolve it by hidden prompt-content classification.
If a concrete model is explicit:
try active provider first;
if no explicit provider was supplied, allow routing to a unique authenticated/catalog provider match;
if multiple providers match, reject as ambiguous and ask for a provider;
if no catalog match, keep active provider and apply that provider's pass-through/validation rules.
If no explicit model:
use [providers.<active>].model first;
then root/project default only if coherent for active provider or endpoint is pass-through/custom;
then provider default.
Map logical/canonical model to provider wire ID using provider-scoped offerings.
Custom model IDs are never auto-picked. They are honored only when explicit or saved for that provider/endpoint.
Local rejection vs provider deferral
Reject locally only when CodeWhale is confident the route is invalid:
empty model or invalid provider;
ambiguous concrete model across multiple configured providers;
official DeepSeek provider with a non-DeepSeek model;
direct non-DeepSeek provider with a known foreign route, such as zai + deepseek-v4-pro;
a request wire protocol CodeWhale cannot serialize correctly.
Defer syntactically valid unknown IDs to provider API when provider is a hosted aggregator, local runtime, custom/OpenAI-compatible endpoint, or account/region/tier/live-catalog dependent.
Required behavior
/provider <target> with no model must not inherit stale previous-provider models.
/provider <target> <model> validates and normalizes within target provider scope before mutation.
/model cross-provider route rows, if present, must emit explicit provider-scoped refs.
Fallback chain switches go through the same resolver.
Persistence happens only after candidate resolution and client construction succeed.
On failure, provider, model, config, engine, header/footer/status, model pass-through flag, and compaction budget remain on the previous route.
Acceptance criteria
Add a route resolver API that returns ReadyRouteCandidate or structured validation errors.
Provider picker, slash /provider, fallback switch, and model-picker cross-provider rows all use that resolver.
Rollback tests inject validation/client-construction failures and assert app/config/engine state is unchanged.
Route explanation includes logical model, wire model when different, endpoint, auth source class, wire protocol, capability summary, and validation messages.
No new per-provider/model match tables outside catalog/offering data.
Verification
cargo test -p codewhale-tui route_resolver
cargo test -p codewhale-tui config::tests::validate_route
cargo test -p codewhale-tui commands::groups::core::provider
cargo test -p codewhale-tui tui::model_picker
Goal
Make provider/model switching atomic. The TUI, slash commands, fallback switch, model picker, and engine restart path must resolve a complete route candidate before mutating app state, config state, persisted settings, compaction budgets, or engine clients.
This issue implements the route-resolution mutation gate described in #2608.
Architecture contract
A route candidate is runtime-derived. It is not a catalog row and not a naked
(provider, model)pair.Target shape:
Only the route resolver may construct a ready candidate. Engine-facing code should move toward accepting this candidate rather than provider/model strings.
Route resolution must be driven by explicit user choice, saved config, Fleet role/slot/loadout policy, hard capability requirements, fallback policy, or an explicit user-enabled
autorouter. It must not inspect freeform prompt text and silently change provider, model, Fleet model class, or reasoning mode.Wire Id And Namespace Rule
Provider-prefixed or organization-prefixed model strings are provider wire ids or namespace hints, not global provider-selection proof.
Examples:
deepseek-ai/DeepSeek-V4-Promay be a Together wire id.deepseek/deepseek-v4-promay be an OpenRouter wire id.anthropic/claude-*may be an aggregator wire id rather than direct Anthropic provider state.The resolver must interpret these strings only inside provider scope unless the user explicitly invoked global search. Do not infer provider switching from
deepseek-ai/,deepseek/,anthropic/,openai/,qwen/, or similar prefixes.Resolution order
--provider,--model,--base-url, request fields, or wrapper env values are highest priority and session-scoped unless explicitly saved.autoas an explicit opt-in sentinel, never a model ID. Resolve it from metadata/policy such as Fleet role, command purpose, capability requirements, cost/latency preference, and context limits. Do not resolve it by hidden prompt-content classification.[providers.<active>].modelfirst;Local rejection vs provider deferral
Reject locally only when CodeWhale is confident the route is invalid:
zai + deepseek-v4-pro;Defer syntactically valid unknown IDs to provider API when provider is a hosted aggregator, local runtime, custom/OpenAI-compatible endpoint, or account/region/tier/live-catalog dependent.
Required behavior
/provider <target>with no model must not inherit stale previous-provider models./provider <target> <model>validates and normalizes within target provider scope before mutation./modelcross-provider route rows, if present, must emit explicit provider-scoped refs.Acceptance criteria
ReadyRouteCandidateor structured validation errors./provider, fallback switch, and model-picker cross-provider rows all use that resolver.Verification
Manual smoke:
Expected: every switch either applies one coherent route or fails before any visible/runtime state changes.