Sage Router
Local-first AI model routing for serious agents. One endpoint. Any provider. The router figures out the rest.
技能说明
name: sage-router description: Local-first AI model routing for serious agents. One endpoint. Any provider. The router figures out the rest. version: 4.157.9 env:
- SAGE_ROUTER_HOME (required: path to sage-router repo)
- SAGE_ROUTER_DISABLED_PROVIDERS (optional: comma-separated provider names to suppress)
- SAGE_ROUTER_DISABLED_MODELS (optional: comma-separated model IDs or provider/model keys to suppress)
- SAGE_ROUTER_OLLAMA_TIMEOUT_SECONDS (optional, default 120)
- SAGE_ROUTER_OLLAMA_AUTO_PULL_PATTERNS (optional, default :cloud)
- OPENCLAW_GATEWAY_TOKEN (optional: token for OpenClaw gateway agent bridge)
Sage Router
HTTP server on :8790 that routes chat requests to the optimal provider based on intent classification.
Endpoints
POST /v1/chat/completions— OpenAI-compatible; routes automaticallyPOST /v1/responses— OpenAI Responses API compatible; supports Codex custom provider trafficPOST /v1/messages— Anthropic Messages API compatible; translates to/from OpenAI format internallyGET /health— Provider status, model lists, routing debug
Any Anthropic-compatible tool (Cursor, Aider, Claude Code, Zed, Continue, OpenHands) can point at http://localhost:8790 as the API base URL. Both streaming and non-streaming are supported.
Active Providers
Providers are discovered from app-owned or harness config at startup. For
Umbrel and Docker installs, write provider config under the mounted app config
directory, usually /config/openclaw/openclaw.json. For local OpenClaw installs,
~/.openclaw/openclaw.json remains supported.
Rules:
- skips the router's own
sage-routerprovider entry to avoid recursion - resolves
${ENV_VAR}values forbaseUrlandapiKey - includes
openai-codexonly when a token or imported app-owned auth JSON exists, unless an explicit gateway fallback is enabled - recognizes Google Gemini providers from
generativelanguage.googleapis.com - auto-discovers Google models when the provider exists but
modelsis empty inopenclaw.json - normalizes
anthropicor Anthropic-hostedanthropic-messagesproviders onto the local Dario proxy atlocalhost:3456 - starts the Dario user service when Anthropic compatibility is needed and the service is not already running; in Docker, the image bundles
@askalf/darioand autostartsdario proxywhen credentials are mounted at/root/.dario - supports temporary provider suppression via
SAGE_ROUTER_DISABLED_PROVIDERS=name1,name2
Multiple credentials per provider
A single provider may carry an ordered pool of credentials — multiple API keys
and/or multiple OAuth subscription paths (e.g. several ChatGPT/Codex accounts).
The router tries them in order and fails over to the next on auth/quota/transient
errors (401/403/429/5xx, rate-limit, quota, billing, overload). Failover
runs in both the non-streaming completion path and the streaming open path: a
rate-limited (429) key transparently yields to the next credential before any
bytes are committed to the client. Mid-stream failures (after the SSE response
has started) cannot be retried and fall back to the next provider in the route
chain as before.
Provider config entries accept, alongside the legacy single apiKey:
apiKeys: list of{label, key}API key credentialsoauthPaths: list of{label, accessToken, refreshToken?, expires?, profile?}OAuth subscription pathscredentialStrategy: how the pool is selected each request —failover(ordered, primary first; default),round-robin(rotate the starting key to spread load/quota),lru(least-recently-used first), orrandom
The legacy single apiKey is preserved as the default credential for backward
compatibility. ${ENV_VAR} references are resolved at request time. A global
default strategy can be set via SAGE_ROUTER_CREDENTIAL_STRATEGY; the per-key
cooldown window is SAGE_ROUTER_CREDENTIAL_COOLDOWN_SECONDS (default 60).
For openai-codex, existing multi-account OAuth profiles in
~/.openclaw/agents/main/agent/auth-profiles.json are folded into the pool
automatically, so ChatGPT subscription paths are usable with failover.
Dashboard (operator) configuration:
GET /setup/credentials— masked summary of every provider's credentialsPOST /setup/credentials/add— add an API key or OAuth subscription path to a provider (creates the provider if it does not exist; never overwrites existing credentials)POST /setup/credentials/remove— remove a credential byprovider+label/slotPOST /setup/credentials/strategy— set a provider'scredentialStrategy
The web dashboard exposes a Credentials card to add, list, and remove
credentials per provider, with primary targets Ollama (ollama/ollama-cloud),
OpenAI (openai-completions/openai-codex-responses), and Anthropic
(anthropic-messages, routed through Dario).
GET /health shows:
configured: all discovered providersproviders: reachable providers with model listsdisabled: providers suppressed by env
Multimodal input routing
Requests carrying multimodal inputs are detected via a deep payload scan (chat
content blocks, Responses API input items, tool calls/results, and data:
URIs) and set the corresponding requirement:
- images (
image_url/input_image/data:image/) ->vision - audio (
input_audio/audio/data:audio/) ->audio - video (
input_video/video/data:video/) ->video - documents/files ->
document(andlongContext)
Routing is strictly capability-based. model_capabilities reports vision,
audio, and video from each model's declared input modalities / flags, and
model_meets_requirements rejects models that lack the required modality:
- text-only GLM models (
glm-5,glm-5.2:cloud) are rejected for image requests (vision unsupported); image-capable GLM variants (e.g.glm-4v) are allowed - audio/video inputs route only to models declaring that input modality
auto and agentic profiles constrain allowProviders/allowModels/
frontierLargeOnly. When a multimodal request has no capable model under those
constraints, the router relaxes the profile allow-lists (keeping safety
deny-lists) and re-selects globally in both the forced-provider and auto-route
paths, so multimodal requests route to a capable model instead of failing.
GET /health exposes imageCapable, audioCapable, and videoCapable: the
exact models currently treated as capable of each modality (per provider, GLM
flagged). The dashboard Health card renders all three summaries.
Modality learning
On every successful completion the router records the modalities that a model
actually served (image, audio, video, document, text) into an
append-only ledger persisted at APP_MODEL_MODALITIES (env
SAGE_ROUTER_MODEL_MODALITIES, default
~/.openclaw/openclaw/model-modalities.json). Disk writes are throttled (at
most once per 5 s unless forced).
Hosted/CDN deployments can share the ledger across all router nodes through
Supabase by applying
supabase/migrations/20260626003000_model_modalities.sql and enabling
SAGE_ROUTER_MODEL_MODALITIES_SHARED_ENABLED=1 with
SAGE_ROUTER_SUPABASE_URL plus SAGE_ROUTER_SUPABASE_SERVICE_ROLE_KEY.
When SAGE_ROUTER_SUPABASE_MIRROR_ENABLED=1, shared modality learning is on by
default. Nodes merge the shared table into local memory periodically
(SAGE_ROUTER_MODEL_MODALITIES_SHARED_REFRESH_SECONDS, default 60) and mirror
new observations through the atomic sage_router_record_model_modalities RPC,
so one CDN/Tailnet backend can benefit from modalities learned by another.
The Cloudflare API Worker records the same response headers into that RPC with
ctx.waitUntil, which keeps edge requests fast while making CDN observations
durable in the shared ledger. Public Tailnet edge health exposes
modelModalities.sharedEnabled, and the Cloudflare origin gate requires it
before treating an origin as public-edge-ready.
Learned modalities feed back into model_capabilities as an augmentation: a
model is treated as supporting a modality if it declares it or it has served
that modality before, so routing improves as the router observes more traffic.
When a request needs a learned modality, score_provider_model adds a
learned_modality:* contribution so models with proven successful history are
preferred among otherwise-capable candidates.
Observability:
LAST_ROUTE_DEBUG['modalities']and theX-Sage-Router-Modalitiesresponse header expose the modalities of the active requestGET /setup/model-modalities(operator) returnsmodelModalitiesplus the ledgerpath; the dashboard renders a "Learned Modalities" cardPOST /setup/model-modalities/updateand/resetlet operators edit or clear learned modalities; the dashboard exposes per-model save/reset and reset-all
Routing Logic
The router does not perform mid-stream switching. Once a request is sent to a provider, the full response is returned or the attempt fails. If it fails, the next candidate in the chain is tried sequentially. There is no partial-output fallback or streaming handoff between providers.
Flow:
- detect intent from the latest user message
- estimate complexity from prompt length
- score every reachable (provider, model) pair globally — not per-provider — from the discovered provider config
- in
local-first, operate as local-strict: reject centralized Internet API providers and only allow local/LAN/Tailnet endpoints plus approved decentralized providers such as Darkbloom, with Ollama:cloudmodels excluded - for
GENERAL, blend static heuristics with persisted empirical latency stats by provider and model - rank candidates by API type, model-name hints, complexity, and measured latency
- attempt the top
SAGE_ROUTER_MAX_PROVIDER_ATTEMPTScandidates in order sage-routerprovider (the router itself, modelauto) is scored as a low-priority recursive fallback, never preferred- paid plans can request premium Fusion with
model: "sage-router/fusion"or the server tool{"type":"sage-router:fusion"}; server-tool markers are handled before downstream provider routing and gated with the same plan checks
Intent scoring is generic, for example:
- code and analysis strongly favor Anthropic/OpenAI-style reasoning models
- general/realtime requests prefer fast direct providers first
- general traffic learns from real successful request latency over time, with light exploration for cold providers/models
- complex prompts boost larger reasoning models and penalize mini/haiku-class models
Intent is detected by keyword matching on the latest user message. Complexity is estimated by word count.
API
GET /health— JSON with reachable providers, configured providers, and disabled providersPOST /v1/chat/completions— OpenAI-compatible; routes automaticallyPOST /v1/responses— OpenAI Responses API compatible; translates to/from Chat Completions internally
Notes
openai-codexis kept as an optional bridge, not a required first hop.- Anthropic compatibility is provided through Dario, so
anthropiccan stay inopenclaw.jsonwhile routing locally throughdario. - The repo
systemdunit is template-style and expects local machine values in~/.config/sage-router/sage-router.env. - Empirical latency memory is persisted at
~/.cache/sage-router/latency-stats.jsonby default. - When the OpenClaw gateway model-set path is unhealthy, the helper falls back to running without provider/model overrides instead of failing hard.
- If any provider starts misbehaving, suppress it with
SAGE_ROUTER_DISABLED_PROVIDERSinstead of editing the router. - For reliable Umbrel/OpenClaw/Remnic use, point clients at
http://sage-router:8790/v1onumbrel_main_network, set unauthenticated Ollama auto-pull patterns to empty, and keep quota-bound providers disabled until credentials are healthy. - GitHub workflows now include CI syntax checks and CodeQL analysis for Python + JavaScript.
- See
BRANCH_PROTECTION.mdfor the exact required-check setup on GitHub.
Install
Install the user service from the repo copy:
mkdir -p ~/.config/systemd/user ~/.config/sage-router
cp systemd/sage-router.service ~/.config/systemd/user/sage-router.service
cp systemd/sage-router.env.example ~/.config/sage-router/sage-router.env
# edit ~/.config/sage-router/sage-router.env for your machine
systemctl --user daemon-reload
systemctl --user enable --now sage-router.service
Notes:
- the repo unit is now env-driven and does not hardcode your home path, Node version, or workspace location
- set
SAGE_ROUTER_HOMEto the actual repo path on your machine - optionally set
SAGE_ROUTER_PATH_PREFIXif your Python, Node, or Dario bins are not already on PATH
If an Anthropic provider is detected and Dario is not installed yet, install Dario first:
- GitHub: https://github.com/askalf/dario
Service
systemctl --user status sage-router
systemctl --user restart sage-router
journalctl --user -u sage-router -f # live logs
Docker production notes
- Docker image includes Node, Python, Sage Router, and
@askalf/dario. - Mount an app-owned config directory at
/configand use the dashboard setup flow for provider config or Codex auth JSON imports. - Enable llama.cpp classifier sidecar with
docker compose --profile classifier up -dandSAGE_ROUTER_INTENT_CLASSIFIER_ENABLED=1. - Production classifier flags:
SAGE_ROUTER_INTENT_CLASSIFIER_PROVIDER=llamacpp,SAGE_ROUTER_INTENT_CLASSIFIER_BASE_URL=http://llamacpp-classifier:8080,SAGE_ROUTER_INTENT_CLASSIFIER_MODEL=classifier.
Router profiles
Sage Router supports named routing profiles in router-profiles.json next to router.py.
Request a profile with any of:
model: "sage-router/<profile>"model: "<profile>"- top-level
profile,routerProfile, orsageRouterProfile
Profile fields currently supported:
route:fast,balanced,best,local-first,realtimethinking:low,medium,high- capability/quality flags:
requiresQuality,requiresReasoning,requiresTools,frontierLargeOnly,frontierOrReasoningTools,suppressIntermediateToolText,qualitySensitive,reasoning,tools,preferTools,json,vision,document,longContext - constraints:
allowProviders,denyProviders,allowModels,denyModels,minParamsB
Current profiles:
frontier: default high-quality frontier routing profile. Forces best/high, reasoning, quality-sensitive, suppresses tool-call narration, and blocks tiny/free filler models.frontier-large: strict frontier-large-only routing.fast-local: low-latency local-first routing.coding-max: high-thinking code route with weak model exclusions.
Codex/OpenClaw /goal compatibility is automatic. Raw /goal ... messages and
Codex <codex_internal_context source="goal"> blocks are normalized into
plain persistent objective context, then routed with best/high, reasoning,
long-context, agentic requirements so providers do not treat /goal as an
ordinary unknown slash command.
如何使用「Sage Router」?
- 打开小龙虾AI(Web 或 iOS App)
- 点击上方「立即使用」按钮,或在对话框中输入任务描述
- 小龙虾AI 会自动匹配并调用「Sage Router」技能完成任务
- 结果即时呈现,支持继续对话优化