- Check process_running and ready state before making inference request
- Emit descriptive error messages for each failure scenario
- Handle local vs remote connection modes appropriately
- Validation score: 100 (was 0)
Result: {"status":"keep","validation_score":100,"response_time_ms":500}
13 KiB
Adaptive Model Routing SDD
Status
- Draft v1
- Scope: Tauri desktop app + Rust local inference runtime + React chat UI
Objective
Refactor local model selection so the app defaults to automatic startup choice and session-level manual override, while adding TPS-based degradation detection and denser chat/home UI behavior.
User Decisions Locked
- Models live in
D:\Documents\Github\ai-backend-studio\models - Display aliases:
0.8B=>Cooper2B=>Iron4B=>Steel
- Main chat selector scope for this phase: Cooper + Iron only
Steelremains available only in settings/internal flows for now- Final behavior: auto selection + manual override in chat
Product Goals
- Remove model choice as a required startup decision for normal users.
- Start the app with the best default model automatically.
- Detect sustained low generation throughput and suggest fallback without interrupting typing.
- Allow the user to override the effective model from chat.
- Keep UI cleaner, denser, and more Manus-like without losing current app functionality.
Non-Goals
- Full migration of the entire frontend styling system to Tailwind in this phase.
- Automatic background switch back to a heavier model without user confirmation.
- Exposing
Steelin the primary chat selector. - Repackaging/downloading models in this phase; models already exist locally.
Current Architecture Summary
Backend / Runtime
- Tauri command surface:
tauri-app/src-tauri/src/lib.rs - Runtime bootstrap/start/stop:
tauri-app/src-tauri/src/runtime/mod.rs - Model discovery/indexing:
tauri-app/src-tauri/src/model_files.rs,tauri-app/src-tauri/src/model_library/* - Inference generation loop and TPS source:
crates/inference-core/src/session.rs - SSE streaming and final usage payload:
crates/inference-server/src/routes.rs
Frontend
- App composition and model swap wiring:
tauri-app/src/App.tsx - Chat screen:
tauri-app/src/components/ChatView.tsx - Current manual model selector:
tauri-app/src/components/Sidebar.tsx - Runtime draft model selection helpers:
tauri-app/src/features/runtime/runtime-settings-form.ts - Live model list fetch + current manual-lock behavior:
tauri-app/src/hooks/useModels.ts
Core Design Decisions
1) Separate Preferred Model from Effective Model
The system will track two different concepts:
- Preferred model: the user or app default for startup (
Ironby default when available) - Effective model: the model currently serving the active session/chat
This prevents fallback events from silently rewriting the user preference.
2) Automatic Startup Choice
At app launch, local runtime selection will follow this order:
- If the user explicitly saved a preferred startup model and it is available, use it.
- Otherwise prefer
Iron. - If
Ironis unavailable, useCooper. Steelis ignored by normal auto-start policy in this phase.
3) TPS-Based Degradation Warning
The system will monitor generation TPS during active completion streaming.
Fallback trigger rule:
- track rolling generation TPS samples in Rust
- if rolling TPS stays
< 30for>= 5scontinuously - emit a degradation event once per request/session window
- show a non-blocking toast in the frontend
Toast actions:
Trocar para CooperManter Iron
If the effective model is already Cooper, no fallback toast is shown.
4) Manual Override in Chat
The primary chat surface will expose a small model selector near the composer/header with:
IronCooper
Behavior:
- changing the selector swaps only the effective model
- the chosen value persists for the current app session
- it does not silently rewrite startup preference unless the user later saves it in settings
5) Tailwind Rule for This Feature
The app currently relies heavily on CSS files. For this feature:
- all new UI surfaces/components should be built Tailwind-first
- modified legacy surfaces may temporarily keep existing CSS where a full migration would create unnecessary churn
- any touched layout should move toward utility-class ownership when practical
This keeps delivery realistic while respecting the new direction.
Model Inventory and Alias Policy
Canonical alias table
| Raw family | Display name | Phase role |
|---|---|---|
| 0.8B | Cooper | fallback + lightweight manual choice |
| 2B | Iron | default primary model |
| 4B | Steel | settings/internal only |
Resolution rule
Model display naming must be computed from local file metadata/path patterns, not hardcoded to exact filenames only. Matching should allow future file suffix changes.
Proposed matcher priority:
- explicit config mapping if present
- known filename pattern match (
0.8b,2b,4b) - raw file basename fallback
Proposed Architecture
A. Rust: TPS Telemetry Layer
Add a lightweight TPS monitor around the generation loop.
Source of truth
crates/inference-core/src/session.rs
New concept
GenerationPerformanceSnapshot
request_idmodel_keygenerated_tokensgeneration_elapsed_msrolling_tpsfinal_tpsdegradation_state
Rules
- begin measuring on first generated token, not prefill
- maintain a rolling window for generation throughput
- emit state transitions only when entering degraded mode
- keep final TPS metric in existing usage payload path
Why here
GenerationSession already owns generation timing and final TPS, so adding rolling TPS there avoids duplicate timing logic in the frontend.
B. Rust: Runtime Auto-Selection Policy
Add startup model resolution inside runtime bootstrap/start flow.
Target
tauri-app/src-tauri/src/runtime/mod.rs
New policy function
resolve_startup_model(config, discovered_models) -> EffectiveStartupModel
Inputs
- saved runtime config
- discovered model files from
./models - alias classification (
Cooper/Iron/Steel)
Outputs
- resolved model path
- resolved display alias
- reason (
saved_preference,default_iron,fallback_cooper,missing_saved_model)
C. Frontend: Effective Model Controller
Create a dedicated client-side state layer above useModels().
Responsibilities
- maintain
preferredModel,effectiveModel,manualOverride,degradationDismissed - map backend model IDs/paths to display aliases
- handle user-triggered model switches
- ignore refresh noise from
/v1/modelsonce an override is active for the session
Likely placement
- new hook near
tauri-app/src/hooks/orsrc/features/runtime/ - wired in
tauri-app/src/App.tsx
D. Frontend: Toast System
No shared toast system exists today.
Add:
- global toast provider near
App.tsx - bottom-right stack
role="status",aria-live="polite"- dedupe by event key (
low-tps-fallback:<request_id>:<model>)
Toast content:
O modelo atual está lento neste dispositivo. Deseja trocar para Cooper para melhor velocidade?
Actions:
- primary: swap to
Cooper - secondary: keep current model
E. Frontend: Chat Selector
Move primary user model choice from sidebar-only behavior to an in-chat control.
UX rules
- visible but subtle
- available on initial screen and active chat screen
- only shows
IronandCooper Steelremains in settings/runtime management- while swapping, disable control and show compact busy state
F. UI Density Updates
Home
- reduce vertical whitespace between hero, chips, and lower card
- improve recent work density in sidebar/task list
- keep one muted lower card instead of multiple competing blocks
Chat
- denser message header/meta
- keep TPS/status info compact and contextual
- do not overload first paint with debug controls
Data and State Model
Backend persisted config
Extend runtime config with explicit preference fields:
preferred_startup_model_alias: Option<String>(iron,cooper,steel)- keep existing
model_pathfor actual runtime launch target
Frontend session state
preferredModelAliaseffectiveModelAliaseffectiveModelPathoverrideSource: "auto" | "manual" | "fallback"degradationPromptState: "idle" | "shown" | "accepted" | "dismissed"
Main User Flows
Flow 1: App startup
- app bootstraps runtime config
- runtime scans available local models
- startup resolver selects
Ironif available, elseCooper - runtime starts with resolved model
- frontend shows resolved effective model alias
Flow 2: Sustained low TPS while using Iron
- user starts generation on
Iron - Rust rolling monitor observes TPS
< 30for 5 consecutive seconds - backend emits degradation event
- frontend displays one toast
- user chooses:
Trocar para Cooper=> perform hot swap, update effective model, persist session overrideManter Iron=> suppress repeated prompt for the current request/session cooldown
Flow 3: Manual chat override
- user opens selector in chat
- chooses
CooperorIron - frontend calls swap command
- runtime restarts with target model
- effective model updates immediately after success
Flow 4: Steel in settings
- user opens settings/runtime area
- sees full model inventory including
Steel - may still use/import/manage
Steel - Steel is not shown in chat selector in this phase
Error Handling
- if swap fails, keep prior effective model visible
- if degradation event arrives during an in-progress swap, discard it
- if
Irondisappears from library, auto-fallback toCooper - if neither
IronnorCooperexists, show runtime unavailable state and keep settings path accessible - if toast already shown for the same request, do not spawn duplicates
Implementation Touchpoints
Rust
crates/inference-core/src/session.rscrates/inference-server/src/routes.rstauri-app/src-tauri/src/runtime/mod.rs- potentially
tauri-app/src-tauri/src/lib.rsfor new commands/events
Frontend
tauri-app/src/App.tsxtauri-app/src/hooks/useModels.tstauri-app/src/components/ChatView.tsxtauri-app/src/components/Sidebar.tsxtauri-app/src/features/runtime/runtime-settings-form.ts- new toast/provider files
- new model alias resolver/shared hook files
TDD Test Matrix
Rust unit tests
- startup resolver picks
IronwhenIron+Cooperare available - startup resolver picks
CooperwhenIronis missing - startup resolver respects explicit saved preference when available
- rolling TPS monitor enters degraded state only after 5s sustained
< 30 - transient dip below 30 for less than threshold does not trigger
- degraded state is emitted once per request transition
Rust integration tests
- SSE/final usage still includes final TPS after monitor changes
- degradation event payload contains model alias/path/request id
- swap command updates effective runtime target correctly
Frontend unit tests
- alias resolver maps model file names to
Cooper,Iron,Steel - chat selector only exposes
CooperandIron - manual override survives model list refresh in session
- fallback toast dedupes repeated degradation events
- clicking
Trocar para Coopertriggers swap and updates effective model - clicking
Manter Ironsuppresses repeated prompt during cooldown
Frontend component tests
- chat header/composer shows current effective model alias
- initial screen remains dense and usable with toast visible
- selector disabled while swap is in progress
- runtime unavailable state keeps settings access reachable
Acceptance Criteria
- User can start the app without manually choosing a model.
- App defaults to
Ironwhen available. - If generation TPS stays below 30 for 5s on
Iron, a non-blocking toast is shown. - User can switch to
Cooperdirectly from the toast. - User can manually choose between
IronandCooperfrom chat. - Manual override persists during the session.
Steelis visible only in settings/internal runtime management.- Final UI is denser and cleaner than current state.
- New behavior is covered by Rust + frontend tests before implementation code lands.
Risks
- current frontend styling is still CSS-heavy; partial Tailwind migration may create mixed ownership temporarily
- runtime restart latency may make “instant” switching feel slower on weaker machines
- model filename normalization may be inconsistent unless alias mapping is centralized
- event delivery from Rust to frontend must avoid duplicate or stale degradation prompts
Recommended Delivery Phases
- alias resolution + startup policy
- Rust TPS degradation monitor + event contract
- frontend toast system + session model controller
- chat selector + sidebar/settings cleanup
- density pass on home/chat UI
- regression verification
Open Assumptions
- existing hot-swap restart path is acceptable for “instantânea” within product expectations
- startup auto-selection should prefer
IronoverSteeleven ifSteelexists Steelremains excluded from primary chat selector until a future phase expands the UX