Repository navigation
Routing Engine
Canonical source:
docs/03-architecture/routing-engine.md— this page is rendered from it on every publish; edit the source.
The routing engine is the decision-making core of ClawAI. It determines which AI provider and model should handle each user message based on the routing mode, active policies, task characteristics, privacy requirements, and cost constraints. The engine lives in the routing-service (port 4004) and is triggered asynchronously via the message.created RabbitMQ event.
The router education layer extends this engine with bounded feedback learning from thumbs signals, judge outcomes, execution outcomes, fallback behavior, and replay telemetry. That layer is documented in router-education-feedback-learning.md.
ClawAI supports 7 routing modes. The mode is set at the thread level (ChatThread.routingMode) and can be overridden by routing policies.
The intelligent routing mode. Uses a local Ollama model to analyze the user's message and select the optimal provider/model combination.
-
Router model: Configurable via
OLLAMA_ROUTER_MODEL(default:gemma3:4b) - Temperature: 0 (deterministic)
- Output: Zod-validated structured JSON
-
Timeout: 10 seconds (configurable via
OLLAMA_ROUTER_TIMEOUT_MS) - Fallback: If Ollama times out or returns invalid output, heuristic rules are applied
User explicitly selects a provider and model. The routing engine respects the forced selection without modification.
- Reads
forcedProviderandforcedModelfrom the message or thread settings - No routing logic executed
- Confidence: 1.0 (user-directed)
- Still records a routing decision for audit purposes
All messages are routed to the local Ollama instance. No data leaves the user's machine.
- Provider:
local-ollama - Model:
gemma3:4b(default, or user's preferred local model) - No cloud fallback
- Confidence: 1.0
When a user attaches images to a thread that resolves to LOCAL_ONLY (or to PRIVACY_FIRST when the cloud fallback is not taken), the orchestrator must decide whether the local model can actually see those images. The rule is drop + warn by default, never silent forwarding:
-
Vision-capability probe — the chat orchestrator asks
ollama-service(per-model registry) which installed models advertisesupportsVision=true. A model is considered vision-capable iff its name matchesOLLAMA_MULTIMODAL_MODEL_PATTERNS(currently:llava,bakllava,moondream,minicpm-v,cogvlm,llama3.2-vision,*-vision,*-multimodal). The probe is bounded byLOCAL_VISION_MODEL_DETECTION_TIMEOUT_MS(default 3000 ms); on timeout the registry is treated as having no vision model. -
Decision matrix:
- At least one installed local model is vision-capable → forward images to that model (route may upgrade the selected model from text-only to the vision-capable one).
- No vision-capable local model installed AND
ALLOW_LOCAL_ONLY_ATTACHMENTS_WITHOUT_VISION=false(default) → drop the images from the prompt, send the text-only portion to the local model, and surface the i18n warningchat.localOnly.imagesDroppedto the user (alongside a Model Catalog deep-link tollava/moondream). - No vision-capable local model installed AND
ALLOW_LOCAL_ONLY_ATTACHMENTS_WITHOUT_VISION=true→ forward images as base64 anyway. The text-only model will ignore them; users opting in accept the lossy behavior.
-
Why drop, not silently forward — Ollama's text-only models accept the
imagesfield on chat requests and silently discard it. Without the warning, a user pasting a screenshot intoLOCAL_ONLYmode would get a confidently wrong answer that pretended to "see" the image. The drop+warn path keepsLOCAL_ONLYhonest about its capability ceiling. -
Audit + transparency — each dropped attachment is recorded as a
FileDeliveryEntrywith modeOMITTED_NO_VISIONon the assistant message'smetadata.fileDelivery, so the chat UI can render an "omitted (no local vision model)" chip per dropped file. This is the same mechanism documented indocs/03-architecture/compare-file-attachments.md, extended to the LOCAL_ONLY path.
Prefers local processing. Falls back to the most privacy-respecting cloud provider only if local Ollama is unhealthy.
- Primary:
local-ollama/gemma3:4b - Fallback:
anthropic/claude-sonnet-4(Anthropic has strong data privacy commitments) - Privacy class: HIGH
The LOCAL_ONLY + attachments rule above applies verbatim to PRIVACY_FIRST whenever the local primary is healthy and the cloud fallback is not taken. Once the cloud fallback engages (e.g., the user did not opt into the local-only forward), the cloud provider's native vision support handles attachments normally and no drop/warn is emitted.
Optimized for fastest response time. Routes to the provider/model combination with the lowest expected latency.
- Default:
openai/gpt-4o-mini - Rationale: OpenAI's smaller models have consistently low latency
- Cost class: LOW
Routes to the most capable reasoning model available.
- Default:
anthropic/claude-opus-4 - Used for complex analysis, architecture decisions, deep reasoning tasks
- Cost class: HIGH
Minimizes cost. Uses local Ollama when healthy, otherwise selects the cheapest cloud option.
- Primary:
local-ollama/gemma3:4b(free) - Fallback: Cheapest available cloud model
- Cost class: LOW
AUTO mode uses a 5-stage pipeline. Each stage can short-circuit the pipeline by returning a routing decision. Stages are ordered by priority and computational cost (cheapest/fastest first, Ollama LLM call last).
Before calling the Ollama router LLM, the engine runs deterministic keyword detection:
- Privacy enforcement (30 keywords) -- forces local, no cloud fallback
- Image detection (70+ keywords, 5 detection layers) -- routes to image provider
- File generation detection (7 phrases + 162 verb/format combos) -- routes to FILE_GENERATION
- Category detection (1650+ keywords across 33 categories) -- routes to category-specific local model
If any of these stages match, the Ollama router is never called, saving 1-10 seconds of latency.
When no keyword match is found, the routing service sends a dynamically-built prompt to the Ollama router model. The prompt is constructed by PromptBuilderManager and includes only currently installed and healthy models.
The prompt builder also adds learned routing priors from recent telemetry, connector-level weights, replay summaries, and the latest router education snapshot. That keeps the router honest on long-tail prompts without replacing the deterministic category and privacy gates.
The router now maintains historical model/topic profiles built from:
- explicit message feedback (
thumbs up/thumbs down) - judge decisions and judge confidence
- execution success/failure
- latency and cost outcomes
- fallback success
Those signals materialize into versioned calibration snapshots that the router can use as bounded priors. Learned priors can refine a route, but they cannot override privacy enforcement, router-only safety, or deterministic image/file routing.
The router's response is validated against a strict Zod schema:
{
provider: string (min 1 char),
model: string (min 1 char),
confidence: number (0-1),
reason: string (min 1 char)
}
If the response fails Zod validation, the system immediately falls back to heuristic routing.
Router request sent
|
+-- Response within 10s --> Validate with Zod
| |
| Valid ------+----- Invalid
| | |
| Use decision Heuristic fallback
|
+-- Timeout (10s) --> Heuristic fallback
The 10-second timeout is configurable via OLLAMA_ROUTER_TIMEOUT_MS. This prevents slow model inference from blocking the entire message pipeline.
Every message in AUTO mode passes through a 5-stage pipeline. Each stage can short-circuit and return a decision, skipping later stages.
Stage 1: Privacy Enforcement
→ 30 privacy keywords scanned (medical, SSN, confidential, etc.)
→ If ANY match: force local-ollama, confidence 0.95, NO cloud fallback
→ Privacy is never overridden by any later stage
Stage 2: Image Generation Detection
→ 70 exact-match keywords + verb/noun combo detection
→ 12 image verbs x 34 image nouns = 408 combinations
→ 21 art style indicators (photorealistic, watercolor, pixel art, etc.)
→ 18 strong image nouns that indicate visual output
→ Reference-based detection (recreate/reproduce + this/attached)
→ Routes to IMAGE_GEMINI > IMAGE_OPENAI > IMAGE_LOCAL
Stage 3: File Generation Detection
→ 7 exact phrases (export as, save as, download as, etc.)
→ 9 verbs x 18 format words = 162 verb+format combinations
→ Routes to FILE_GENERATION / auto
Stage 4: Category Detection (1650+ keywords across 33 capability classes)
→ Scans message against all 33 keyword arrays
→ Maps detected category to a LocalModelRole
→ Finds installed model with that role via PromptBuilderManager
→ If found: routes to that local model, confidence 0.85
Stage 5: Ollama Router + Heuristic Fallback
→ Sends message to Ollama router model with dynamic prompt
→ If Ollama responds with valid JSON in 10s: use its decision
→ If Ollama fails: heuristic fallback (short messages → local, else cloud priority)
→ Heuristic order: EXPERT → Anthropic reasoning; healthy local; then Ollama Cloud (included, no credit) for
non-COMPLEX/EXPERT turns when its connector is CONFIRMED healthy; else the credit-model priority list
The routing engine classifies messages into 33 capability classes, each backed by a dedicated keyword array in routing.constants.ts (2274 lines). This enables fine-grained routing to the most appropriate model. The top 15 classes by keyword count are shown below; the remaining 18 classes cover HR, Education, Sales, Logistics, Hospitality, Science, Government, Finance, Executive, and other specialty domains.
| # | Capability Class | Keyword Count | Example Keywords | Routes To (Local Role) |
|---|---|---|---|---|
| 1 | Coding | 100 | code, debug, typescript, prisma, jest, SOLID, design pattern | LOCAL_CODING |
| 2 | Reasoning | 21 | prove, solve, theorem, probability, chain of thought | LOCAL_REASONING |
| 3 | Thinking | 15 | research, investigate, deep dive, trade-offs, pros and cons | LOCAL_THINKING |
| 4 | Infrastructure | 33 | terraform, kubernetes, docker, AWS, Lambda, VPC, helm | LOCAL_CODING |
| 5 | Data Analysis | 33 | pandas, matplotlib, ETL, BigQuery, Spark, window function | LOCAL_REASONING |
| 6 | Business | 30 | user story, KPI, ROI, roadmap, SWOT, pitch deck, go-to-market | LOCAL_FILE_GENERATION |
| 7 | Creative Writing | 26 | blog post, screenplay, narrative, tagline, press release | LOCAL_FALLBACK_CHAT |
| 8 | Security | 25 | CVE, XSS, OWASP, penetration test, threat model, WAF | LOCAL_CODING |
| 9 | Medical | 19 | clinical, diagnosis, HIPAA, ICD-10, adverse event | LOCAL_REASONING |
| 10 | Legal | 21 | contract, NDA, GDPR, patent, jurisdiction, case law | LOCAL_REASONING |
| 11 | Translation | 12 | translate, localize, i18n, multilingual, convert to English | LOCAL_FALLBACK_CHAT |
| 12 | Image Generation | 70+ | generate image, draw, sketch, logo, portrait, watercolor | LOCAL_IMAGE_GENERATION |
| 13 | File Generation | 34 | export as PDF, create CSV, generate report, save to file | LOCAL_FILE_GENERATION |
| 14 | Privacy (enforcement) | 30 | medical, SSN, confidential, private key, PII, attorney-client | Forces local (no role) |
| 15 | General Chat | 0 (default) | Everything that matches no other class | LOCAL_FALLBACK_CHAT |
Total keyword count: 1650+ unique keywords across 33 capability classes (2274 lines in routing.constants.ts), plus hundreds of verb/noun combinations for image and file detection.
The CATEGORY_TO_ROLE_MAP constant maps each detected category to a LocalModelRole:
| Category | LocalModelRole | Rationale |
|---|---|---|
| coding | LOCAL_CODING | Code generation, debugging, refactoring |
| reasoning | LOCAL_REASONING | Mathematical proofs, logic, step-by-step analysis |
| thinking | LOCAL_THINKING | Research, deep investigation, comparative analysis |
| infrastructure | LOCAL_CODING | DevOps and infra tasks need code-aware models |
| data-analysis | LOCAL_REASONING | Data tasks need analytical reasoning |
| business | LOCAL_FILE_GENERATION | Business tasks often produce documents |
| creative-writing | LOCAL_FALLBACK_CHAT | Creative tasks use general chat models |
| security | LOCAL_CODING | Security audits need code-aware models |
| medical | LOCAL_REASONING | Medical queries need careful reasoning |
| legal | LOCAL_REASONING | Legal analysis needs structured reasoning |
| translation | LOCAL_FALLBACK_CHAT | Translation uses general language models |
| image-generation | LOCAL_IMAGE_GENERATION | Routes to diffusion models |
| file-generation | LOCAL_FILE_GENERATION | Routes to structured output models |
| chat | LOCAL_FALLBACK_CHAT | Default general conversation |
Privacy detection is the highest-priority check in the routing pipeline. It runs BEFORE any other detection and cannot be overridden.
medical, patient, diagnosis, health record, PHI, HIPAA, tax return, salary, SSN, social security, credit card, bank account, password, private key, secret, confidential, NDA, attorney-client, privilege, personal data, PII, financial statement, investment portfolio, insurance claim, disability, mental health, therapy, counseling, genetic, criminal record
- Detection: Case-insensitive substring match against all 30 keywords
-
Action: Force
local-ollama/gemma3:4b, confidence 0.95 - Fallback chain: Filtered to LOCAL_PROVIDER only (cloud entries removed)
-
Reason tags:
['auto', 'privacy_enforced', 'local_only'] -
Privacy class:
local - Zero cloud exposure guarantee: Even the fallback chain contains no cloud providers
Each capability class has a dedicated detect*Request(message) method in RoutingManager:
- The message is lowercased once
- Each keyword in the class array is checked via
String.includes()(case-insensitive) - First match wins -- no need to match all keywords
- Categories are checked in priority order (security > medical > legal > coding > infrastructure > data-analysis > reasoning > thinking > business > translation > creative-writing)
When a message matches multiple categories (e.g., "debug this Kubernetes deployment"), the first matching category in priority order wins:
- Security (force to LOCAL_CODING)
- Medical (force to LOCAL_REASONING)
- Legal (force to LOCAL_REASONING)
- Coding (LOCAL_CODING)
- Infrastructure (LOCAL_CODING)
- Data Analysis (LOCAL_REASONING)
- Reasoning (LOCAL_REASONING)
- Thinking (LOCAL_THINKING)
- Business (LOCAL_FILE_GENERATION)
- Translation (LOCAL_FALLBACK_CHAT)
- Creative Writing (LOCAL_FALLBACK_CHAT)
Image detection uses 5 independent detection layers. Any single layer matching triggers image routing:
-
Exact keyword match: 70 phrases from
IMAGE_KEYWORDSarray - Verb + noun combo: 12 verbs x 34 nouns (generate+image, create+portrait, etc.)
- Strong noun + context: 18 strong nouns (logo, poster, mascot) + any image verb or word
- Art style indicators: 21 style keywords (photorealistic, watercolor, pixel art, cyberpunk, etc.)
- Reference-based: recreate/reproduce/remake + this/attached
| Detection Source | Confidence | Notes |
|---|---|---|
| Privacy enforcement | 0.95 | Highest -- never overridden |
| Image exact keyword | 0.95 | High confidence for explicit image requests |
| Image verb+noun combo | 0.95 | Same confidence as exact match |
| File generation exact phrase | 0.95 | "export as PDF" is unambiguous |
| File generation verb+format combo | 0.90 | "generate" + "pdf" may be ambiguous |
| Category keyword match | 0.85 | Strong signal from domain-specific keywords |
| Ollama router (high match) | 0.80 - 0.99 | Depends on router model confidence |
| Ollama router (uncertain) | 0.50 - 0.79 | Router model was less sure |
| Heuristic fallback (cloud) | 0.75 | Best available cloud provider |
| Heuristic fallback (local, short msg) | 0.60 | Short message + local available |
| Heuristic fallback (no healthy connector) | 0.50 | Best-effort, no health data |
| Ultimate fallback (no cloud) | 0.40 | No cloud available at all |
| Default fallback | 0.20 | Nothing matched |
| MANUAL_MODEL | 1.00 | User explicitly chose |
Routing policies allow administrators to define rules that override or influence routing decisions. Policies are stored in the RoutingPolicy table and evaluated in priority order.
RoutingPolicy {
id: UUID
name: string
routingMode: RoutingMode
priority: number (lower = higher priority)
config: JSON {
conditions: [
{ field: "messageLength", operator: "gt", value: 500 },
{ field: "hasFiles", operator: "eq", value: true }
],
action: {
forceProvider: "google",
forceModel: "gemini-2.5-flash",
overrideMode: null
}
}
isActive: boolean
createdAt, updatedAt
}
1. Load all active policies, sorted by priority (ascending)
2. For each policy:
a. Evaluate all conditions against the current message context
b. If ALL conditions match:
- Apply the policy's action
- Stop evaluating further policies (first match wins)
3. If no policy matches:
- Proceed with normal routing mode logic
| Field | Type | Description |
|---|---|---|
messageLength |
number | Character count of user message |
hasFiles |
boolean | Whether files are attached |
fileCount |
number | Number of attached files |
threadRoutingMode |
string | Current thread routing mode |
userRole |
string | ADMIN, OPERATOR, or VIEWER |
timeOfDay |
number | Hour (0-23) in server timezone |
provider |
string | Specific provider name |
| Task Category | Primary Provider | Primary Model | Fallback Provider | Fallback Model |
|---|---|---|---|---|
| Coding / debugging | anthropic | claude-sonnet-4 | openai | gpt-4o |
| Architecture / reasoning | anthropic | claude-opus-4 | gemini-2.5-pro | |
| Image / video / web | gemini-2.5-flash | openai | gpt-4o | |
| Math / algorithms | deepseek | deepseek-chat | local-ollama | phi3:mini |
| Creative writing | openai | gpt-4o-mini | anthropic | claude-sonnet-4 |
| Simple Q&A | local-ollama | gemma3:4b | openai | gpt-4o-mini |
| File / data analysis | gemini-2.5-flash | anthropic | claude-sonnet-4 | |
| Privacy-sensitive | local-ollama | gemma3:4b | (none - never cloud) | (none) |
| General / unknown | openai | gpt-4o-mini | local-ollama | gemma3:4b |
Before selecting a provider, the routing engine checks:
- Connector status: Must be ACTIVE (not DISABLED or ERROR)
- Recent health check: Last health check must be within the configured interval
- Model availability: The specific model must be synced and available
- Local Ollama: Must be reachable and have the target model loaded
If the selected provider is unhealthy, the system uses the fallback. If the fallback is also unhealthy, it falls through to local Ollama (which is always the last resort).
Every routing decision is persisted for observability and analytics.
RoutingDecision {
id: UUID
messageId: UUID
threadId: UUID
userId: UUID
routingMode: RoutingMode
selectedProvider: string
selectedModel: string
confidence: float
reasonTags: string[]
privacyClass: enum
costClass: enum
fallbackProvider: string | null
fallbackModel: string | null
policyId: UUID | null // which policy was applied, if any
heuristicUsed: boolean // true if Ollama router was bypassed
latencyMs: number // time taken for routing decision
createdAt: DateTime
}
The frontend displays routing decisions in the RoutingTransparency component:
- Provider and model used (with badge)
- Confidence score (color-coded: green > 0.7, yellow > 0.4, red < 0.4)
- Reason tags (e.g., "coding", "privacy", "low-latency")
- Privacy class and cost class indicators
- Whether fallback was used
- Whether heuristic routing was applied (indicates Ollama router was skipped)
The routing engine builds a fallback chain for every decision:
1. Selected provider/model (from router or heuristic)
|
+--(fail)--> 2. Fallback provider/model (from routing decision)
|
+--(fail)--> 3. Local Ollama / gemma3:4b
|
+--(fail)--> 4. Error returned to user
The chat-service's ChatExecutionManager triggers a fallback when:
- HTTP connection timeout
- HTTP 429 (rate limited)
- HTTP 500, 502, 503 (server errors)
- Network unreachable
- Invalid or empty response body
- Response parsing failure
- Each fallback attempt is logged with the reason for the previous failure
- The final
ChatMessagerecords the actual provider/model used (which may differ from the selected one) - The
metadataJSON field stores the full fallback chain: which providers were tried and why each failed - If a fallback was used, the SSE event includes
fallbackUsed: trueso the frontend can display this to the user
| Environment Variable | Default | Description |
|---|---|---|
OLLAMA_ROUTER_MODEL |
gemma3:4b |
Ollama model used for AUTO routing |
OLLAMA_ROUTER_TIMEOUT_MS |
10000 |
Timeout for Ollama router inference |
OLLAMA_BASE_URL |
http://ollama:11434 |
Ollama API endpoint |
Final validation of the routing engine across 150 prompts and 500+ total experiments.
- 150-prompt final validation: 99.1% accuracy (114/115 valid responses correctly routed)
- 33 detection categories with 1650+ keywords across 2274 lines of routing constants
- 115 models in catalog across 13 domains
- All 40 routing unit tests pass as a quality gate
Privacy enforcement achieved 100% accuracy across all sensitive domains:
| Domain | Enforcement Rate |
|---|---|
| Medical | 100% |
| Legal | 100% |
| Finance | 100% |
| Government | 100% |
| Executive | 100% |
Zero privacy-sensitive messages were sent to cloud providers during any validation round. The 30 privacy keywords plus domain-specific detection ensure all sensitive content stays on local infrastructure.
| Category | Accuracy |
|---|---|
| Engineering | 100% |
| Data / ML | 100% |
| Security | 100% |
| Business | 100% |
| Creative | 100% |
| Science | 100% |
| Logistics | 100% |
| Hospitality | 100% |
| HR / Education / Sales | 100% |
| Specialty | 100% |
| General | 100% |
| Privacy | 97%+ |
The routing engine was refined across 5 experiment rounds:
| Round | Image Accuracy | Overall Notes |
|---|---|---|
| Round 1 | 33% | Initial keyword set too narrow |
| Round 2 | 67% | Added verb/noun combos |
| Round 3 | 85% | Added art style indicators |
| Round 4 | 95% | Added strong noun detection |
| Round 5 | 100% | Final 5-layer detection system |
The routing engine detects multimodal message content before category-based routing, ensuring that messages requiring specific model capabilities (audio, video, PDF, OCR, web-search, vision) are routed to cloud providers that support them — rather than falling into local categories.
Modality detection follows a strict priority hierarchy (highest to lowest):
- AUDIO_INPUT — transcription, voice, speech, audio file keywords → GEMINI (primary)
- VIDEO_INPUT — video clip, watch video, YouTube, stream → GEMINI (primary)
- PDF_INPUT — .pdf attachment, extract from PDF → GEMINI or ANTHROPIC
- OCR — scan document, extract text from image, read photo → GEMINI
- WEB_SEARCH — search the web, look up online, browse → GEMINI or OPENAI
- IMAGE_INPUT — analyze this image, describe photo → GEMINI (or local-ollama as last resort)
| Capability | Priority Order |
|---|---|
| AUDIO_INPUT | GEMINI → ANTHROPIC → OPENAI |
| VIDEO_INPUT | GEMINI → OPENAI |
| PDF_INPUT | GEMINI → ANTHROPIC → OPENAI |
| OCR | GEMINI → ANTHROPIC → OPENAI |
| WEB_SEARCH | GEMINI → OPENAI |
| IMAGE_INPUT | GEMINI → ANTHROPIC → OPENAI → GROK → local-ollama |
The capability router skips unhealthy providers and selects the first healthy one. If no cloud provider is healthy for the capability, returns null and routing continues to category-based detection.
ModelCapability.TOOL_CALLING is the seventh capability, and it is detected
differently from the six above on purpose.
The six modality capabilities are inferred from message text. TOOL_CALLING is
driven by an explicit RoutingContext.requiresToolCalling flag, set by callers
whose request will execute through the Runtime V2 agent lane and therefore
needs a model that can emit native tool calls. Whether a run is an agent run is
something the caller knows; re-guessing it from prose would re-introduce
exactly the unreliability that makes keyword routing fragile.
The flag is checked before the modality patterns, so an agent prompt that
happens to say "show me an image of the architecture" does not lose its tool
requirement to a substring match. Callers reach it through
POST /routing/evaluate → requiresToolCalling on evaluateRouteSchema.
CAPABILITY_PROVIDER_PRIORITY[TOOL_CALLING] has always existed in the provider
matrix, but nothing could return the capability that selects it — the entire
branch was unreachable until this flag landed.
Rank, do not filter. When no tool-capable provider is healthy the router
returns null and ordinary routing proceeds, exactly as it does for the
modality capabilities. Hard-filtering on a capability flag would turn a data
gap — an unhealthy connector, a registry row not yet backfilled — into a total
agent-run outage. The run proceeds on the best available model and the drift
guard reports a degraded lane rather than nothing running at all.
RouterModelRegistry.supportsTools is now populated by
prisma/seed-router-models.ts from an explicit, auditable list of model keys.
Before that every one of the 23 seeded profiles sat on the column default of
false, which made the flag uniformly useless. The seed sets it on update
as well as create, so re-seeding backfills already-deployed rows.
Capability routing runs BEFORE category detection in handleAuto(). This prevents multimodal messages from matching local categories (e.g., "analyze this video clip" previously matched the "reasoning" category and was routed locally before this fix).
When message.created carries attachments (requiredModalities), every AUTO
path ranks by modality fit — not only the cloud router. The cloud router orders
its candidates DIRECT → TRANSFORMED → DEGRADED before it picks
(selectCloudRouterCandidates). The capability, privacy-local, Ollama-router,
category and heuristic paths pick first; RoutingManager.withModalityFit then
re-orders [selected, ...fallbackChain] with the same modalityFitOf tiers
(rankDecisionByModalityFit, stable). A model that cannot see the attachment is
ranked below capable ones, never removed — chat-service still delivers it a
transcript / frames / helper-vision text. The decision carries
modalityFit:<fit> and, when the pick changed, modality_fit_reranked; the
routing log names the before and after pick. Turns without attachments are
untouched and read nothing.
Every routing decision includes automatic complexity classification and a human-readable explanation.
| Class | Criteria | Routing Influence |
|---|---|---|
| SIMPLE | Short messages (< 30 chars), greetings, single-word queries | Favors cheap/fast providers |
| MEDIUM | Standard queries, multi-sentence, single topic | Standard routing applies |
| COMPLEX | Long messages (> 200 chars), multi-intent, technical depth | Favors capable providers |
| EXPERT | Deep technical, architecture, medical/legal + complex | Forces high-reasoning providers |
| Field | Type | Description |
|---|---|---|
complexityClass |
Enum | SIMPLE / MEDIUM / COMPLEX / EXPERT |
explanation |
JSON | Structured explanation: summary + reasoning factors |
routingDurationMs |
Int | Time taken for the routing decision (ms) |
{
"summary": "Routed to ANTHROPIC/claude-sonnet-4 for complex coding task",
"factors": [
{
"label": "Complexity",
"value": "COMPLEX",
"reason": "Multi-step implementation request with architecture decisions"
},
{ "label": "Category", "value": "coding", "reason": "Contains 8 coding keywords" },
{
"label": "Privacy",
"value": "cloud-safe",
"reason": "No privacy-sensitive content detected"
},
{
"label": "Provider",
"value": "ANTHROPIC",
"reason": "Best capability match for coding tasks"
}
]
}The explanation is surfaced in the RoutingTransparency component under "Why this model?" and helps users understand routing decisions without needing to interpret raw reason tags.
Grok is registered as a cloud provider in the routing engine's fallback chain and capability matrix.
GROK is included in allCloudProviders within buildFallbackChain():
- Local-primary routes: GROK can be a cloud fallback when local Ollama fails
- Cloud-primary routes: GROK is included as a cross-cloud fallback (skipped if GROK is the primary)
-
Unhealthy GROK: Excluded from chain if
connectorHealth.GROK === false
GROK is priced at $3.00/$15.00 per 1M input/output tokens — the same tier as Anthropic. In cost-sorted chains, GROK appears after GEMINI ($0.075/$0.30) and OPENAI ($0.15/$0.60).
The inferProvider() method maps grok-* model names to the GROK provider, enabling MANUAL_MODEL mode to work correctly when users specify a Grok model (e.g., grok-3, grok-3-mini, grok-2-vision-1212).
After the LLM responds, the QualityCheckManager (in claw-chat-service) scores the response quality. If the score falls below the weak threshold (0.4), the system automatically re-routes to the next candidate in the fallback chain.
| Check | Threshold | Penalty |
|---|---|---|
| Response too short | < 20 chars | -0.40 |
| Too few words | < 5 words | -0.30 |
| Error/refusal pattern | 9 patterns | -0.50 |
| Excessive repetition | > 60% trigram overlap | -0.30 |
| Echo response | > 80% similarity to prompt | -0.50 |
- Max 2 re-route attempts per message (prevents loops)
- Image and file generation responses are exempt
- Only the final accepted response is stored (weak responses are discarded)
- Re-routing uses the existing fallback chain, not a separate path
- Frontend shows an amber "Re-routed from {provider}/{model}" badge
See Auto Re-Routing Spec for full details.
In AUTO, handleAuto calls NamedModelRequestManager.resolve right after the privacy
check and before image/file/capability/cloud routing. It reads only when the prompt
contains a directive word (use, ask, with, @ …), loads routing's own
ModelDeployment catalog, drops providers known down and models the plan does not allow, then
findNamedModelRequest matches catalog ids (separators read as spaces, date and
preview suffixes optional, longest name wins) and NAMED_MODEL_ALIASES ("nano banana",
"veo", "grok imagine", provider names). resolveNamedModel picks the model that fits what is
asked (picture, video or chat, via classifyImageIntent / classifyVideoIntent). The
decision carries user_named_model and named_model_<capability>; an image-output model is
moved to its IMAGE_* provider exactly as a manual pick is.
Not routed: a bare mention, a comparison ("GPT vs Gemini"), a negated clause, a name inside a pasted body, a habit ("I use claude"), a model the plan does not allow, a local runtime.
The target model reads the task, not the instruction (2026-10-03). stripNamedModelDirective
removes "use nano banana to" / "ask grok about" (and a trailing "with X"), and the decision carries
namedModelPrompt on message.routed. chat-service replaces the final user turn in the assembled
context with it (withNamedModelPrompt), so chat, callImageService, file and video prompts all read
the stripped text; the stored message stays what the user typed.
A named model that cannot answer is never a silent fallback. resolve returns an outcome with a
notice (NOT_CONFIGURED no deployment of that provider, NOT_IN_PLAN the fitting model is outside
the plan, CONNECTOR_DOWN, NO_FITTING_MODEL e.g. grok asked for a picture and has none). It rides the
decision as namedModelNotice; chat-service tells the answering model (system prompt plus the final
user turn, withNamedModelNotice) to open with one short sentence in the user's language saying the
named model was not used and why. A bare provider name picks the provider's general chat model over a
build/code/agent variant, newest id first.
generationRequestText (shared-utilities) also drops sentences that only MENTION generation: a
supplementary opener ("say also …", "dis aussi …", "قل أيضا …"), an ability description ("our app can
create files", "supports generating images"), a verb list ("create files, documents, pdf … create
videos"), the topic of a writing task ("write a post about how teams create videos"), and bullet items
under such a line. Image, file (detectFileIntent) and video (classifyVideoIntent) detection all
read this filtered text, so one guard covers the three; a keyword hit is only a candidate and a
genuine imperative ("can you generate …", "also create an image …", "write a post and draw a cat")
survives. No extra model call: the deterministic guard decides, and every LLM that reads the message
(cloud router, Ollama router prompts, the platform identity block) is told a mention is a text task.
detectImageGenerationSignals is whole-word (Unicode lookarounds) and proximity-based
(verb and image word within 6 words). A supplementary note or a writing task takes the strict
path (isExplicitImageRequest). Regression: the LinkedIn-post context paste.
| Stage | Where | Status |
|---|---|---|
| Research planner (plan, crawl/search, follow-up, answer) | chat-service ResearchOrchestratorManager, ResearchGateService
|
Live on researchMode=AUTO; gathers evidence, the user's (or named) model answers |
| Vision helper (understand, then generate) | chat-service VisionHelperManager
|
Live, images only |
| Pipeline / decompose / escalation chain | chat-service managers, POST pipeline, decompose, escalation-chain
|
Live only when the user picks the mode; routing never selects them |
| Live workflow selector | routing LiveWorkflowSelectorManager
|
Live: SEARCH_FIRST or DIRECT_LLM only |
| Workflow orchestrator and handlers, AI route planner, multi-intent splitter | routing workflows/, intelligence/
|
Scaffolding or shadow only; no live caller |
A named-model turn composes with the planner by order, not by a new stage: research runs before routing, then the named model answers with the evidence attached.
ClawAI Wiki · source-controlled under wiki/ · generated/mirrored from the current repository knowledge layer.
ClawAI Wiki
Product
- Product-Vision
- Flagship-Features
- Business-Overview
- Requirements-Register
- Drift-Log
- Runtime-Managed-Connector-Providers
- Threads
- ADR-Index
Architecture
- System-at-a-Glance
- Architecture-Map
- System-Architecture
- Message-Flow
- End-to-End-Data-Flow
- Routing-Engine
- Event-Bus
- Security-Architecture
Platform
- Backend-Services
- Frontend-Architecture
- Shared-Packages
- Database-Reference
- API-Reference
- Provider-Catalog
- Memory-and-Context-Architecture
- File-Support-Architecture
- Research-and-Web-Crawling
Runtime & Ops
- Docker-Guide
- Nginx-Reference
- CI-CD-Pipeline
- Logging-and-Observability
- Operational-Runbooks
- Troubleshooting
Quality
Engineering OS
- AI-Native-Engineering-OS
- Generated-Manifests
- Rules-Catalog
- Skills-Catalog
- Context-Layer
- Engineering-Memory
- Tooling-and-Automation
- Scripts-and-Operations
- ESLint-and-Architecture-Enforcement
- Extended-Work-Skills
Agents
Reference