Skill: earlvanze/sage-router (version 4.154.0)

--- name: sage-router description: Local-first AI model routing for serious agents. One endpoint. Any provider. The router figures out the rest. 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 `:8788` that routes chat requests to the optimal provider based on intent classification. ## Endpoints - `POST /v1/chat/completions` — OpenAI-compatible; routes automatically - `POST /v1/messages` — Anthropic Messages API compatible; translates to/from OpenAI format internally - `GET /health` — Provider status, model lists, routing debug Any Anthropic-compatible tool (Cursor, Aider, Claude Code, Zed, Continue, OpenHands) can point at `http://localhost:8788` as the API base URL. Both streaming and non-streaming are supported. ## Active Providers Providers are discovered from `~/.openclaw/openclaw.json` at startup. Rules: - skips the router's own `sage-router` provider entry to avoid recursion - resolves `${ENV_VAR}` values for `baseUrl` and `apiKey` - includes OpenClaw gateway `openai-codex` as a virtual provider when the auth profile exists - recognizes Google Gemini providers from `generativelanguage.googleapis.com` - auto-discovers Google models when the provider exists but `models` is empty in `openclaw.json` - normalizes `anthropic` or Anthropic-hosted `anthropic-messages` providers onto the local Dario proxy at `localhost:3456` - starts the Dario user service when Anthropic compatibility is needed and the service is not already running; in Docker, the image bundles `@askalf/dario` and autostarts `dario proxy` when credentials are mounted at `/root/.dario` - supports temporary provider suppression via `SAGE_ROUTER_DISABLED_PROVIDERS=name1,name2` `GET /health` shows: - `configured`: all discovered providers - `providers`: reachable providers with model lists - `disabled`: providers suppressed by env ## 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 `openclaw.json` - 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 `:cloud` models 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 [REDACTED_SECRET] candidates in order - `sage-router` provider (the router itself, model `auto`) is scored as a low-priority recursive fallback, never preferred 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 providers - `POST /v1/chat/completions` — OpenAI-compatible; routes automatically ## Notes - `openai-codex` is kept as an optional bridge, not a required first hop. - Anthropic compatibility is provided through Dario, so `anthropic` can stay in `openclaw.json` while routing locally through `dario`. - The repo `systemd` unit 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.json` by 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_PROVIDERS` instead of editing the router. - GitHub workflows now include CI syntax checks and CodeQL analysis for Python + JavaScript. - See `BRANCH_PROTECTION.md` for the exact required-check setup on GitHub. - `provider-profiles.json` includes a `grok-sso` template for the OpenClaw xAI auth plugin's local SuperGrok-backed proxy. ## Install Install the user service from the repo copy: ```bash 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_HOME` to the actual repo path on your machine - optionally set `SAGE_ROUTER_PATH_PREFIX` if 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 ```bash 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 host Dario credentials as `~/.dario:/root/.dario` for Anthropic-compatible Claude routing. - Enable llama.cpp classifier sidecar with `docker compose --profile classifier up -d` and [REDACTED_SECRET]. - Production classifier flags: [REDACTED_SECRET], `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`, or `sageRouterProfile` Profile fields currently supported: - `route`: `fast`, `balanced`, `best`, `local-first`, `realtime` - `thinking`: `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.

path: BRANCH_PROTECTION.md
content: # Branch protection guide

Use this after the workflows have run at least once on GitHub so the status checks exist.

## Goal
Require the automated checks before anything merges to `master`.

## Recommended GitHub settings
Path: GitHub repo → Settings → Rules → Rulesets (or Branches) → `master`

Enable:
- Require a pull request before merging
- Require status checks to pass before merging
- Require branches to be up to date before merging
- Block force pushes
- Block deletions

## Required status checks
Add these checks:
- `CI / validate`
- `CodeQL / Analyze (python)`
- `CodeQL / Analyze (javascript)`

## Notes
- If GitHub shows slightly different check names, use the exact names from the first successful workflow run.
- If the default branch changes from `master`, update the workflow branch filters too.
- If you later add tests or lint jobs, add them here as required checks as well.


path: DEPLOY_CF.md
content: # CloudFlare Deployment Guide

Sage Router Docker image can be deployed to CloudFlare via:

## Option 1 — CloudFlare Container Registry (CCR)

Push to CCR, then deploy as a Cloudflare Container Workload:

```bash
# Authenticate
cloudflare auth login
cloudflare containers auth

# Tag and push
docker tag sage-router:test
registry.connect.cloudflare.com/your-account/sage-router:latest
docker push registry.connect.cloudflare.com/your-account/sage-router:latest

# Deploy via wrangler
wrangler containers deploy sage-router --image registry.connect.cloudflare.com/your-account/sage-router:latest
```

Then configure `PORT=8788` and mount `~/.openclaw` via a secrets volume or Workers KV for `openclaw.json`.

---

## Option 2 — Docker + Cloudflare Tunnel (lightweight)

Run the container on a home/lightsail VPS, expose via `cloudflared tunnel`:

```bash
# On your host
docker run -d --restart=always \
  --name sage-router \
  -p 127.0.0.1:8788:8788 \
  -v ~/.openclaw:/config:ro \
  sage-router:test

# Cloudflare Tunnel (zero-config, no open ports)
cloudflared tunnel --url http://localhost:8788
# Copy the returned https://xxx.trycloudflare.com URL
# Use that as OPENAI_BASE_URL in your AI tools
```

This gives you a permanent `*.trycloudflare.com` URL you can point tools at — no firewall config, no static IP.

---

## Option 3 — Fly.io (recommended for stateless-ish routing)

Fly.io supports Docker with volume mounts for config:

```bash
fly launch --image sage-router:test --name sage-router
fly secrets set SAGE_ROUTER_HOME=/config
fly volumes create sage_config --size 1
fly deploy

# Scale
fly scale count 2 --region ord,lax
```

---

## Option 4 — Self-hosted Linux + Caddy reverse proxy

Traditional VPS deployment with automatic TLS:

```bash
# On your VPS
docker run -d --restart=always \
  --name sage-router \
  -p 127.0.0.1:8788:8788 \
  -v ~/.openclaw:/config:ro \
  sage-router:test

# Caddyfile
# sage-router.example.com {
#   reverse_proxy localhost:8788
# }
```

---

## GitHub Container Registry (ghcr.io)

If you have the image published to `ghcr.io`:

```bash
docker pull ghcr.io/earlvanze/sage-router:latest
docker run -d --restart=always \
  --name sage-router \
  -p 8788:8788 \
  -v ~/.openclaw:/config:ro \
  ghcr.io/earlvanze/sage-router:latest
```

Build and push with:
```bash
docker build -t ghcr.io/earlvanze/sage-router:latest .
docker push ghcr.io/earlvanze/sage-router:latest
```


path: GROK_SSO.md
content: # Grok SSO local proxy

The old `xai-grok-auth` install path appears dead, so this repo now ships a local replacement: `grok_sso_proxy.py`.

It exposes an OpenAI-compatible endpoint at:

- `http://127.0.0.1:18923/v1/chat/completions`
- `http://127.0.0.1:18923/v1/models`
- `http://127.0.0.1:18923/health`

## What it does

- uses Grok web/session cookies instead of xAI API credits
- translates OpenAI chat-completions requests into Grok's web conversation endpoint
- auto-loads into Sage Router as `grok-sso` **only when `/health` reports `ready: true`**

## Current limits

- chat completions only
- no OpenAI-style tool calling
- no OpenAI streaming passthrough
- each request is treated like a fresh Grok conversation

## Cookie sources

The proxy checks these in order:

1. `GROK_SSO_PROXY_COOKIE_HEADER`
2. `GROK_SSO_PROXY_COOKIE_JSON`
3. Chromium cookie DB at `GROK_SSO_PROXY_BROWSER_COOKIE_DB`

Default browser path:

- `~/.config/BraveSoftware/Brave-Browser/Default/Cookies`

Default required cookie set for readiness:

- `sso`

Recommended additional cookies if present:

- `sso-rw`
- `x-anonuserid`
- `x-challenge`
- `x-signature`

## Run manually

```bash
cd /home/umbrel/.openclaw/workspace/skills/sage-router
python3 grok_sso_proxy.py --port 18923
```

Check readiness:

```bash
python3 - <<'PY'
import json, urllib.request
print(json.load(urllib.request.urlopen('http://127.0.0.1:18923/health')))
PY
```

## Example cookie JSON

```json
{
  "sso": "...",
  "sso-rw": "...",
  "x-anonuserid": "...",
  "x-challenge": "...",
  "x-signature": "..."
}
```

Then point the proxy at it:

```bash
export GROK_SSO_PROXY_COOKIE_JSON=/absolute/path/to/grok-cookies.json
python3 grok_sso_proxy.py --port 18923
```

## Systemd

See:

- `systemd/grok-sso-proxy.service`
- `systemd/grok-sso-proxy.env.example`

## Sage Router behavior

`provider-profiles.json` already contains the `grok-sso` overlay:

- base URL: `http://127.0.0.1:18923/v1`
- provider id: `grok-sso`

Sage Router now checks `/health` and only loads the overlay when the local proxy is both reachable and ready.


path: README.md
content: # Sage Router

**Local-first AI model routing for serious agents.**

One endpoint. Any provider. The router figures out the rest.

[![ClawHub](https://img.shields.io/badge/ClawHub-v3.26.31-blue)](https://clawhub.ai/earlvanze/sage-router)
[![GitHub](https://img.shields.io/badge/GitHub-earlvanze%2Fsage--router-black)](https://github.com/earlvanze/sage-router)

---

## What This Is

Sage Router is a **local-first, self-hosted AI model gateway** that intelligently routes requests to the best available model based on intent, latency, and capability — not just price.

Sage Router optimizes for **getting the job done**:

- **Intent-based routing**: Code tasks go to coding models, creative tasks to creative models, reasoning tasks to reasoning models
- **Automatic fallback**: If one provider fails or hits rate limits, it seamlessly tries the next
- **Dynamic discovery**: New models from Ollama, Anthropic, OpenAI, Google, NVIDIA NIM / NVIDIA Cloud, and OpenClaw are auto-detected — no config updates needed
- **Zero API lock-in**: Use any subscription or key you already have (Ollama, Claude, OpenAI, Gemini, NVIDIA NIM, GitHub Copilot)
- **Debuggable routing**: Surface the selected provider/model in headers, `/health`, or optional debug output

---

## Quick Start

### Installation (OpenClaw)

```bash
openclaw skill add sage-router --from clawhub
openclaw skill configure sage-router
```

### Manual Installation

```bash
git clone https://github.com/earlvanze/sage-router.git
cd sage-router
pip install -r requirements.txt  # if any
python3 router.py --port 8788
```

### Configure Your Tools

Point any OpenAI-compatible tool at Sage Router:

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=irrelevant  # Sage Router uses your configured provider auth
```

Or for Gemini CLI:

```bash
export GOOGLE_GEMINI_BASE_URL=http://localhost:8788
export GEMINI_API_KEY=routed
```

Or for Anthropic tools:

```bash
export ANTHROPIC_BASE_URL=http://localhost:8788
export ANTHROPIC_API_KEY=irrelevant
```

---

## Integration Guides

- [Codex CLI](docs/integrations/codex.md)
- [Claude Code](docs/integrations/claude-code.md)
- [OpenClaw](docs/integrations/openclaw.md)
- [Hermes](docs/integrations/hermes.md)
- [Pi agents](docs/integrations/pi.md)
- [Cursor](docs/integrations/cursor.md)
- [Aider](docs/integrations/aider.md)
- [Continue](docs/integrations/continue.md)
- [OpenHands](docs/integrations/openhands.md)
- [Ollama and Ollama Cloud](docs/integrations/ollama.md)
- [NVIDIA NIM / NVIDIA Cloud](docs/integrations/nvidia-nim.md)
- [OpenAI-compatible clients](docs/integrations/openai-compatible.md)
- [Anthropic-compatible clients](docs/integrations/anthropic-compatible.md)
- [Harness fallback](docs/integrations/harness-fallback.md)

---

## Supported API Formats

| Endpoint | Format | Used By |
|----------|--------|---------|
| `POST /v1/chat/completions` | OpenAI | OpenAI SDK, Aider, Continue, Zed |
| `POST /v1/messages` | Anthropic | Cursor, Claude Code, Claude Desktop |
| `POST /v1beta/models/{model}:generateContent` | Google | Gemini CLI |
| `POST /v1beta/models/{model}:streamGenerateContent` | Google | Gemini CLI (streaming) |
| `GET /v1beta/models` | Google | Gemini CLI (model discovery) |
| `POST /chat/completions` | OpenAI | Legacy/short path |

---

## How Routing Works

Sage Router analyzes every request for:

1. **Intent**: CODE, CHAT, REASONING, CREATIVE, REFACTOR, DOCUMENTATION
2. **Complexity**: LOW, MEDIUM, HIGH, UNKNOWN
3. **Requirements**: reasoning, json, tools, longContext, streaming
4. **Thinking level**: off, low, medium, high

Then it scores all available models and selects the optimal chain:

```
Request: "Refactor this Python function"
  → Intent: CODE, Complexity: MEDIUM
  → Route Mode: balanced
  → Selected Chain:
    1. ollama/glm-5.1:cloud            (best score for CODE + available)
    2. openai-codex/gpt-5.5            (fallback)
    3. ollama/kimi-k2:cloud            (fallback)
    4. openai/gpt-4.1                  (last resort)
```

If the first model fails or times out, it automatically tries the next. No manual retry needed.

### Model Selection Pseudocode

The core selection path lives in `router.py` across `normalize_requirements`,
`prepare_route`, `select_model`, `score_provider_model`, and `route_request`.
In pseudocode:

```text
function route_request(payload):
    messages = payload.messages
    thinking = normalize_thinking(payload.reasoning / payload.thinking)
    route_mode = payload.routeMode or "balanced"
    requirements = normalize_requirements(payload)

    latest_prompt = last user/developer message text
    intent = classify_intent(latest_prompt)        # code, analysis, general, creative, etc.
    complexity = estimate_complexity(latest_prompt)
    estimated_tokens = estimate_prompt_tokens(messages)

    if caller forced a provider/model:
        candidate_chain = validate_forced_route_against_requirements()
    else:
        candidate_chain = []
        rejected = []

        for each provider in configured providers:
            if provider is disabled: continue
            if provider is Ollama: refresh discovered model list
            if provider has no models or endpoint is unreachable: continue

            for each model in provider.models:
                if local-first mode and provider is approved decentralized infrastructure: allow
                if local-first mode and provider endpoint is not LAN/Tailnet/local: reject
                if local-first mode and provider is a known cloud/SSO proxy: reject
                if local-first mode and model is an Ollama Cloud model: reject
                if model is not chat-capable: reject
                if model does not satisfy hard requirements
                   such as JSON, tools, streaming, reasoning, or long context: reject

                score = base score for intent + provider API type
                score += model-name intent hints
                score += / -= context-window fit
                score += / -= provider/model family preferences
                score += / -= route mode preference
                         # fast, realtime, best, local-first, balanced
                score += / -= thinking-level preference
                         # high favors larger reasoning models, low favors lightweight models
                score += / -= current health/cooldown signal
                score += / -= empirical latency/success adjustment

                if tools were supplied but not forced:
                    score += soft bonus for models with tool support

                candidate_chain.append((score, provider, model))

        sort candidate_chain by score descending, then provider/model name for stability
        candidate_chain = top MAX_PROVIDER_ATTEMPTS

    for provider, model in candidate_chain:
        response = call_provider(provider, model, payload)
        if response has visible text or valid tool calls:
            record successful route event
            return response
        record failure and try next candidate

    record failed route event
    return provider_failure_error
```

Hard filters happen before scoring, so an otherwise high-scoring model is never
selected when it cannot satisfy explicit requirements like forced tool calling or
JSON output. Soft preferences, such as attached optional tools, influence the
score without unnecessarily shrinking the candidate pool.

---

## Supported Providers

Configure any number of providers in `openclaw.json` or via environment variables:

### Ollama (Local)

```json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434",
      "models": ["auto-discover"],
      "api": "ollama"
    }
  }
}
```

Models are auto-discovered via `/api/tags`.

### Anthropic (Claude)

```json
{
  "providers": {
    "anthropic": {
      "baseUrl": "https://api.anthropic.com",
      "apiKey": "${ANTHROPIC_API_KEY}",
      "models": ["claude-opus-4", "claude-sonnet-4", "claude-haiku-4"],
      "api": "anthropic-messages"
    }
  }
}
```

**Pro tip**: Route Claude subscription usage through [Dario](https://github.com/askalf/dario) to avoid burning API credits when available.

### OpenAI

```json
{
  "providers": {
    "openai": {
      "baseUrl": "https://api.openai.com/v1",
      "apiKey": "${OPENAI_API_KEY}",
      "models": ["auto-discover"],
      "api": "openai-completions"
    }
  }
}
```

Models are auto-discovered via `/v1/models`.


### Darkbloom

Darkbloom is OpenAI-compatible at `https://api.darkbloom.dev`. If `DARKBLOOM_API_KEY` is present in `~/.openclaw/.env` or the skill-local `.env`, Sage Router loads it automatically through the bundled `darkbloom` provider profile.

```json
{
  "providers": {
    "darkbloom": {
      "baseUrl": "https://api.darkbloom.dev",
      "apiKey": "${DARKBLOOM_API_KEY}",
      "models": "auto-discover",
      "api": "openai-completions"
    }
  }
}
```

Models are auto-discovered via `/v1/models`. Chat requests route through `/v1/chat/completions`.

### Google Gemini

```json
{
  "providers": {
    "google": {
      "baseUrl": "https://generativelanguage.googleapis.com/v1beta",
      "apiKey": "${GEMINI_API_KEY}",
      "models": ["auto-discover"],
      "api": "google-generative-ai"
    }
  }
}
```

Models are auto-discovered via the Gemini API.

### GitHub Copilot

```json
{
  "providers": {
    "github-copilot": {
      "baseUrl": "https://api.githubcopilot.com",
      "apiKey": "${GITHUB_COPILOT_TOKEN}",
      "models": ["auto-discover"],
      "api": "openai-completions"
    }
  }
}
```

Models are auto-discovered via Copilot's `/v1/models`.
```

### xAI (Grok)

**API Key mode** (recommended for production):
```json
{
  "providers": {
    "xai": {
      "baseUrl": "https://api.x.ai/v1",
      "apiKey": "${XAI_API_KEY}",
      "models": ["auto-discover"],
      "api": "openai-completions"
    }
  }
}
```
Models are auto-discovered via `/v1/models`. Supports tool calling, streaming, and passthrough.

**SSO/SuperGrok mode** (local proxy, no API costs):
Sage Router can route through a local Grok SSO proxy instead of burning xAI API credits.

- local proxy provider: `grok-sso`
- typical base URL: `http://127.0.0.1:18923/v1`
- if the old OpenClaw `xai-grok-auth` plugin still exists for you, that works
- this repo also ships a bundled replacement proxy: `grok_sso_proxy.py`
- Sage Router will auto-load the `grok-sso` overlay from `provider-profiles.json` only when the local proxy `/health` reports `ready: true`
- SSO mode is chat-only and intentionally **does not support OpenAI-style tool calling or streaming**
- xAI API-key mode keeps tool support

See `provider-profiles.json` for the `grok-sso` template and `GROK_SSO.md` for setup.

### NVIDIA NIM / NVIDIA Cloud

```json
{
  "plugins": {
    "entries": {
      "nvidia": {
        "enabled": true,
        "config": {
          "autoDiscovery": {
            "enabled": true,
            "base_url": "integrate.api.nvidia.com/v1",
            "api_key": "$NVIDIA_API_KEY"
          }
        }
      }
    }
  }
}
```

Models are auto-discovered from NVIDIA NIM / NVIDIA Cloud when `NVIDIA_API_KEY` is present. This is useful for GPU-accelerated hosted inference and NVIDIA-backed model endpoints without changing agent configuration.

### OpenClaw Gateway

```json
{
  "providers": {
    "openai-codex": {
      "baseUrl": "http://127.0.0.1:8788",
      "models": ["auto-discover"],
      "api": "openclaw-gateway"
    }
  }
}
```

Models are auto-discovered via the gateway's `/v1/models` endpoint.
```

---

## Docker / Production deployment

The Docker image bundles Sage Router plus Dario for Anthropic-compatible Claude requests. Dario credentials are read from `~/.dario`, mounted into the container at `/root/.dario`.

```bash
# Router only, with Dario available for Anthropic-compatible requests
docker compose up -d --build

# Router + llama.cpp GPU classifier sidecar
SAGE_ROUTER_INTENT_CLASSIFIER_ENABLED=1 \
SAGE_ROUTER_MODELS_DIR=/path/to/gguf-models \
docker compose --profile classifier up -d --build
```

Key production flags:

```bash
SAGE_ROUTER_OPENROUTER_FREE_ONLY=1
SAGE_ROUTER_DARIO_AUTOSTART=1
SAGE_ROUTER_INTENT_CLASSIFIER_ENABLED=1
SAGE_ROUTER_INTENT_CLASSIFIER_PROVIDER=llamacpp
SAGE_ROUTER_INTENT_CLASSIFIER_BASE_URL=http://llamacpp-classifier:8080
SAGE_ROUTER_INTENT_CLASSIFIER_MODEL=classifier
SAGE_ROUTER_INTENT_CLASSIFIER_MODEL_PATH=/models/qwen2.5-0.5b-instruct-q4_K_M.gguf
SAGE_ROUTER_INTENT_CLASSIFIER_N_GPU_LAYERS=999
```

The classifier backend speaks OpenAI-compatible llama.cpp server API (`/v1/chat/completions`), so it can be run as a sidecar, on Cyber GPU, or replaced by any compatible local inference server.


## Provider Feature Matrix

| Provider | Dynamic Discovery | Force Model | Passthrough | Auth Method |
|----------|-------------------|-------------|-------------|-------------|
| **Ollama** | ✅ `/api/tags` | ✅ | ✅ | Local socket |
| **Google Gemini** | ✅ `/v1beta/models` | ✅ | ✅ | API key |
| **Anthropic** | ✅ Via Dario | ✅ | ✅ | API key |
| **OpenAI** | ✅ `/v1/models` | ✅ | ✅ | API key |
| **GitHub Copilot** | ✅ `/v1/models` | ✅ | ✅ | Token |
| **NVIDIA NIM / Cloud** | ✅ auto-discovery | ✅ | ✅ | API key |
| **OpenClaw Gateway** | ✅ `/v1/models` | ✅ | ✅ | Gateway token |
| **xAI/Grok (API)** | ✅ `/v1/models` | ✅ | ✅ | API key |
| **xAI/Grok (SSO)** | ❌ SSO proxy | ❌ | ❌ | Cookie/SSO |

**Dynamic Discovery**: Models are auto-fetched from provider API  
**Force Model**: Request specific model via `"model": "provider/model"`  
**Passthrough**: Any model name accepted (even if not in discovered list)

---

## Route Modes

Control how Sage Router selects models:

| Mode | Behavior |
|------|----------|
| `fast` | Prefer local models, minimize latency |
| `balanced` | Balance capability and speed |
| `best` | Always pick the best model for the task, regardless of latency |
| `local-first` / `local-strict` | Local-strict mode. Only use local, LAN, Tailnet, or approved decentralized provider endpoints. Reject centralized Internet APIs such as OpenAI, Anthropic/Dario, Google, NVIDIA Cloud, Copilot, xAI/Grok SSO, OpenRouter, etc. Darkbloom is allowed as decentralized infrastructure. Ollama models ending in `:cloud` are still excluded even if the Ollama endpoint is localhost. |

Set via request: `{"route": "fast"}` or header: `X-Route-Mode: fast`

---

## Thinking Levels

Control reasoning depth per request:

| Level | Description |
|-------|-------------|
| `off` | No reasoning, maximum speed |
| `low` | Minimal reasoning |
| `medium` | Standard reasoning (default) |
| `high` | Deep reasoning for complex tasks |

Set via request: `{"thinking": "high"}` or `{"reasoning": "high"}`

## Debug Mode

To surface routing info back in the response payload, send:

```json
{
  "debug": true
}
```

or:

```json
{
  "routeDebug": true
}
```

Current behavior:
- response headers always include `X-Sage-Router-*` routing metadata
- `/health` exposes the last selected provider/model and attempts
- debug mode adds `sage_router` metadata to the JSON response
- for plain text responses, debug mode also prefixes the visible content with the selected `provider/model`

---

## Health Endpoint

```bash
curl http://localhost:8788/health
```

Returns:
- Configured providers
- Available models
- Last route decision
- Reasoning capabilities by provider
- Selected provider/model, attempt history, and rejection reasons for the last request

Every routed response also includes headers like:
- `X-Sage-Router-Model`
- `X-Sage-Router-Provider`
- `X-Sage-Router-Intent`
- `X-Sage-Router-Request-Id`

Use these when you need to know exactly which model answered.

## Streaming Note

Sage Router currently supports compatibility streaming wrappers for clients that require SSE, but it does not yet do true token-by-token passthrough across heterogeneous providers.

That means stream-shaped responses work for client compatibility, but they may still arrive buffered after the selected provider finishes.

---

## Why Sage Router?



## Configuration

### Environment Variables

```bash
# Provider API keys (used for auto-discovery)
ANTHROPIC_API_KEY=sk-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=AIza...
NVIDIA_API_KEY=nvapi-...
OLLAMA_HOST=http://localhost:11434

# Router behavior
SAGE_ROUTER_DEFAULT_MODE=balanced
SAGE_ROUTER_TIMEOUT=60
```

### OpenClaw Config (`~/.openclaw/openclaw.json`)

```json
{
  "models": {
    "providers": {
      "ollama": {
        "baseUrl": "http://localhost:11434",
        "models": [{"id": "llama3.2:latest", "name": "Llama 3.2"}],
        "api": "ollama"
      },
      "anthropic": {
        "baseUrl": "https://api.anthropic.com",
        "apiKey": "${ANTHROPIC_API_KEY}",
        "models": [{"id": "claude-sonnet-4", "name": "Claude Sonnet"}],
        "api": "anthropic-messages"
      }
    }
  }
}
```

---

## Development

```bash
# Run locally
python3 router.py --port 8788

# Run tests
pytest tests/

# Enable debug logging
LOG_LEVEL=DEBUG python3 router.py
```

---

## Roadmap

### Completed

- [x] Multi-modal support (vision-capable model detection and image payload routing)
- [x] Tool/function calling proxy (OpenAI, Ollama, and Anthropic-compatible tool-call normalization)
- [x] Cloudflare Pages marketing site on `https://sagerouter.dev`
- [x] Integration guides for major agent harnesses and SDK-compatible clients
- [x] Waitlist capture into AOps Supabase

### Next

- [ ] Request/response caching
- [ ] Usage analytics dashboard
- [ ] Distributed deployment mode
- [ ] CDN-hosted option / hosted reliability layer
- [ ] **Grok SSO Browser Extension** — Chrome extension to proxy SuperGrok web access via local OpenAI-compatible endpoint (blocked by anti-bot; needs extension architecture)

---

## License

MIT — Use it, fork it, improve it. PRs welcome.

---

## For Other Clankers

Built this because I was tired of:
- Switching API keys between coding agents
- Burning Claude API credits on trivial tasks
- Configuring new models in 3 different places

If you're running local AI infrastructure, Sage Router is the single endpoint that makes everything else just work.

🦞


## Router Profiles

Sage Router supports named routing profiles for reusable policy bundles. Use them when a client or agent needs a quality floor without hardcoding one model.

Request a profile with any of:

```json
{ "model": "sage-router/frontier" }
{ "model": "frontier" }
{ "profile": "frontier" }
{ "routerProfile": "coding-max" }
```

Profiles live in `router-profiles.json` and can set:

- route mode: `fast`, `balanced`, `best`, `local-first`, `realtime`
- thinking level: `low`, `medium`, `high`
- requirements: quality, reasoning, tools, JSON, vision, documents, long context
- constraints: provider/model allowlists and denylists, `minParamsB`, `frontierLargeOnly`, `frontierOrReasoningTools`, `suppressIntermediateToolText`

Bundled profiles:

- `frontier` — public-channel quality profile, high thinking, quality/reasoning required, tiny/free filler models blocked, tool-call narration suppressed
- `frontier-large` — strict frontier/large-model-only routing
- `fast-local` — low-latency local-first routing
- `coding-max` — high-thinking coding route with weak model exclusions


path: _meta.json
content: ownerId: kn7a5vdq619nymw8zq4aq95b49846rhq
slug: sage-router
version: 3.26.15
publishedAt: 1777227847391

path: deploy/gcp/HOSTED_PROVIDER_SECRETS.md
content: # Hosted provider secrets for Cloud Run Sage Router

Cloud Run Sage Router can now keep upstream provider credentials server-side. Premium client apps can send only a Sage Router subscription key via `Authorization: Bearer <client-key>`.

This does **not** deprecate pass-through user subscriptions. Bring-your-own-key/user-subscription routing remains supported and should stay available alongside hosted-provider routing. Hosted credentials are an additional managed-provider option, not a replacement.

Create these Secret Manager secrets before deploy, using dedicated hosted-router credentials, not personal/local OpenClaw keys:

- `SAGE_ROUTER_CLIENT_API_KEYS` — comma-separated client/subscription keys
- `SAGE_ROUTER_API_KEY_HASH_PEPPER` or `SAGE_ROUTER_SIGNING_SECRET` — optional pepper used when hashing newly generated Sage Router API keys
- `SAGE_ROUTER_SUPABASE_SERVICE_ROLE_KEY` — server-side Supabase REST key for customer, API key, payment intent, and analytics tables
- `SAGE_ROUTER_SUPABASE_ANON_KEY` — public Supabase Auth validation key
- `STRIPE_SECRET_KEY` or `SAGE_ROUTER_STRIPE_SECRET_KEY`
- `STRIPE_WEBHOOK_SECRET` or `SAGE_ROUTER_STRIPE_WEBHOOK_SECRET`
- `SAGE_ROUTER_STRIPE_PRICE_ID` or `STRIPE_PRICE_ID`
- `SAGE_ROUTER_PUBLIC_BASE_URL` — public web origin, for example `https://sagerouter.dev`
- `SAGE_ROUTER_API_BASE_URL` — public API origin, for example `https://api.sagerouter.dev`
- `SAGE_ROUTER_CRYPTO_PAYMENT_ADDRESS` — optional manual crypto receiving address
- `SAGE_ROUTER_OPENAI_API_KEY`
- `SAGE_ROUTER_OPENROUTER_API_KEY`
- `SAGE_ROUTER_ANTHROPIC_API_KEY`
- `SAGE_ROUTER_GOOGLE_API_KEY`
- `SAGE_ROUTER_XAI_API_KEY`
- `SAGE_ROUTER_ZAI_API_KEY`
- `NVIDIA_API_KEY` (also exposed as `SAGE_ROUTER_NVIDIA_API_KEY`)
- `SAGE_ROUTER_CLOUDFLARE_API_TOKEN`
- `OLLAMA_API_KEY` (also exposed as `SAGE_ROUTER_OLLAMA_API_KEY`)
- `SAGE_ROUTER_OPENAI_CODEX_ACCESS_TOKEN` — OpenAI Codex OAuth bearer token

Optional model allowlists, comma-separated env vars:

- `SAGE_ROUTER_OPENAI_MODELS`
- `SAGE_ROUTER_OPENROUTER_MODELS`
- `SAGE_ROUTER_ANTHROPIC_MODELS`
- `SAGE_ROUTER_GOOGLE_MODELS`
- `SAGE_ROUTER_XAI_MODELS`
- `SAGE_ROUTER_ZAI_MODELS`
- `SAGE_ROUTER_NVIDIA_MODELS`
- `SAGE_ROUTER_CLOUDFLARE_MODELS`
- `SAGE_ROUTER_OLLAMA_MODELS`
- `SAGE_ROUTER_OPENAI_CODEX_MODELS`

OpenAI Codex OAuth compatibility:

- Preferred hosted path: wire `SAGE_ROUTER_OPENAI_CODEX_ACCESS_TOKEN` from Secret Manager.
- Local/mounted compatibility: router will also read OpenClaw/Hermes-style auth profiles from `SAGE_ROUTER_OPENAI_CODEX_AUTH_PROFILE_PATH(S)` if mounted.
- OAuth access tokens are usually short-lived, so production should rotate/update this secret or mount a refresh-capable auth profile intentionally.

Cloudflare account id is configured as deploy env `SAGE_ROUTER_CLOUDFLARE_ACCOUNT_ID`.

Apply `supabase/sage_router_saas.sql` before enabling self-serve account creation in production.

Self-serve SaaS tables are configured by name through:

- `SAGE_ROUTER_SUPABASE_CUSTOMERS_TABLE` default `sage_router_customers`
- `SAGE_ROUTER_SUPABASE_API_KEYS_TABLE` default `sage_router_api_keys`
- `SAGE_ROUTER_SUPABASE_PAYMENT_INTENTS_TABLE` default `sage_router_payment_intents`

Minimum columns expected by the incremental backend:

- `sage_router_customers`: `id`, `user_id`, `email`, `plan`, `status`, `stripe_customer_id`, `stripe_subscription_id`, `created_at_epoch`, `updated_at_epoch`
- `sage_router_api_keys`: `id`, `customer_id`, `user_id`, `name`, `prefix`, `api_key_hash`, `status`, `plan`, `created_at_epoch`, `last_used_at_epoch`, `revoked_at_epoch`
- `sage_router_payment_intents`: `id`, `kind`, `customer_id`, `user_id`, `status`, `asset`, `network`, `amount`, `address`, `metadata`, `event_type`, `event_id`, `created_at_epoch`, `updated_at_epoch`

Generated API keys are returned raw once on creation. Store only `api_key_hash` server-side. Legacy/manual `SAGE_ROUTER_CLIENT_API_KEYS` remains supported for migrations and operator-issued keys.


path: deploy/gcp/README.md
content: # Sage Router on GCP Free-Tier-Style Cloud Run

This deployment is for a public Phase 2 demo of Sage Router on Google Cloud Run.

## Cost posture

- Cloud Run: `min-instances=0`, `max-instances=1`, 512Mi memory.
- No provider/customer API keys are deployed by default.
- Dario is bundled for Anthropic-compatible routing, but is not authenticated unless credentials are supplied via Secret Manager or a private runtime config.
- Ollama is bundled and started without local models so users can route to Ollama Cloud when they provide their own Ollama auth/config.
- Artifact Registry stores one small Python image.
- The service is public for demoability and exposes `/health`.


## Live Phase 2 deployment

- Project: `sage-router-demo-20260428`
- Region: `us-central1`
- Service: `sage-router`
- URL: `https://sage-router-434058661374.us-central1.run.app`
- Verified endpoints: `/`, `/health`, `/v1/models`
- Runtime boundary: Dario binary and Ollama daemon are present. Dario requires user-provided auth before Anthropic-compatible provider traffic can use it. Ollama requires user-provided Ollama Cloud auth/config for cloud inference; no local model weights are bundled.

## Deploy

```bash
export PROJECT_ID=your-gcp-project-id
export REGION=us-central1
./deploy/gcp/cloudrun-deploy.sh
```

The script enables required APIs, creates an Artifact Registry Docker repo if needed, builds the minimal Cloud Run image, deploys the service, and prints the Cloud Run URL.

## Verify

```bash
SERVICE_URL=$(gcloud run services describe sage-router --region us-central1 --format 'value(status.url)')
curl "$SERVICE_URL/health"
```

## Boundary

Do not deploy local OpenClaw configs, customer provider credentials, `.env` files, Dario credentials, or OAuth cookies to Cloud Run. This Phase 2 deployment is a public demo / credibility deployment, not a hosted customer key-custody layer.


path: deploy/gcp/cloudbuild.yaml
content: steps:
  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - -f
      - deploy/gcp/Dockerfile.cloudrun
      - -t
      - ${_IMAGE}
      - .
images:
  - ${_IMAGE}


path: deploy/gcp/cloudrun-deploy.sh
content: #!/usr/bin/env bash
set -euo pipefail

SERVICE_NAME="${SERVICE_NAME:-sage-router}"
REGION="${REGION:-us-central1}"
REPOSITORY="${REPOSITORY:-sage-router}"
IMAGE_TAG="${IMAGE_TAG:-phase2}"
PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null || true)}"

if [[ -z "${PROJECT_ID}" || "${PROJECT_ID}" == "(unset)" ]]; then
  echo "PROJECT_ID is required. Set PROJECT_ID or run: gcloud config set project YOUR_PROJECT_ID" >&2
  exit 2
fi

IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${SERVICE_NAME}:${IMAGE_TAG}"

gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com \
  firestore.googleapis.com \
  --project "${PROJECT_ID}"

if ! gcloud artifacts repositories describe "${REPOSITORY}" --location "${REGION}" --project "${PROJECT_ID}" >/dev/null 2>&1; then
  gcloud artifacts repositories create "${REPOSITORY}" \
    --repository-format=docker \
    --location "${REGION}" \
    --description "Sage Router Cloud Run images" \
    --project "${PROJECT_ID}"
fi

gcloud builds submit . \
  --project "${PROJECT_ID}" \
  --config deploy/gcp/cloudbuild.yaml \
  --substitutions _IMAGE="${IMAGE}"

gcloud run deploy "${SERVICE_NAME}" \
  --image "${IMAGE}" \
  --project "${PROJECT_ID}" \
  --region "${REGION}" \
  --platform managed \
  --allow-unauthenticated \
  --memory 512Mi \
  --cpu 1 \
  --min-instances 0 \
  --max-instances 1 \
  --concurrency 20 \
  --timeout 300 \
  --set-env-vars '^|^SAGE_ROUTER_HOME=/app|SAGE_ROUTER_BUNDLED_OLLAMA_AUTOSTART=0|SAGE_ROUTER_OLLAMA_AUTO_PULL_PATTERNS=|SAGE_ROUTER_PROFILE_OVERLAYS=ollama|SAGE_ROUTER_MAX_PROVIDER_ATTEMPTS=2|SAGE_ROUTER_OLLAMA_TIMEOUT_SECONDS=120|SAGE_ROUTER_FIRESTORE_PROJECT_ID='"${PROJECT_ID}"'|SAGE_ROUTER_SUPABASE_URL=https://awtangrlqqsdpksarhwo.supabase.co|SAGE_ROUTER_SUPABASE_AUTH_ENABLED=1|SAGE_ROUTER_SUPABASE_MIRROR_ENABLED=1|SAGE_ROUTER_CLIENT_AUTH_REQUIRED=1|SAGE_ROUTER_CORS_ORIGIN=https://sagerouter.dev,https://www.sagerouter.dev' \
  --set-secrets OLLAMA_API_KEY=OLLAMA_API_KEY:latest,SAGE_ROUTER_ANALYTICS_TOKEN=SAGE_ROUTER_ANALYTICS_TOKEN:latest,SAGE_ROUTER_SUPABASE_ANON_KEY=AOPS_SUPABASE_ANON_KEY:latest,SAGE_ROUTER_SUPABASE_SERVICE_ROLE_KEY=AOPS_SUPABASE_SERVICE_ROLE_KEY:latest,SAGE_ROUTER_CLIENT_API_KEYS=SAGE_ROUTER_CLIENT_API_KEYS:latest,NVIDIA_API_KEY=NVIDIA_API_KEY:latest

gcloud run services describe "${SERVICE_NAME}" \
  --project "${PROJECT_ID}" \
  --region "${REGION}" \
  --format 'value(status.url)'


path: docker-compose.yml
content: # ── Docker Compose for Sage Router ───────────────────────────────────────────
# Deploy via: docker compose up -d
# Or standalone: docker run -d --rm \
#     -p 8788:8788 \
#     -v ~/.openclaw:/config:ro \
#     -e PORT=8788 \
#     ghcr.io/earlvanze/sage-router:latest

name: sage-router
services:
  sage-router:
    build:
      context: .
      dockerfile: Dockerfile
    image: ghcr.io/earlvanze/sage-router:latest
    container_name: sage-router
    restart: unless-stopped
    ports:
      - "${PORT:-8788}:8788"
    volumes:
      # Mount OpenClaw config read-only so the router can discover providers
      # Adjust the source path if your openclaw config lives elsewhere
      - ~/.openclaw:/config:ro
      # Dario OAuth/accounts/cache. Mount your host ~/.dario here for Claude subscription routing.
      - ~/.dario:/root/.dario
      # Bundled Ollama model/cache directory. Cloud pulls are tiny metadata stubs; local pulls persist here.
      - ${SAGE_ROUTER_OLLAMA_DIR:-./ollama}:/root/.ollama
      # Optional llama.cpp GGUF model cache for the classifier sidecar.
      - ${SAGE_ROUTER_MODELS_DIR:-./models}:/models
      # Mount router source so local hotfixes survive container recreation even when image rebuild/pull is blocked.
      - ./router.py:/app/router.py:ro
      # Optional: mount a custom provider-profiles.json
      # - ./provider-profiles.json:/app/provider-profiles.json:ro
    environment:
      PORT: "8788"
      SAGE_ROUTER_HOME: /config
      # SAGE_ROUTER_DISABLED_PROVIDERS: "openai-codex,some-other"
      SAGE_ROUTER_OLLAMA_TIMEOUT_SECONDS: "300"
      SAGE_ROUTER_BUNDLED_OLLAMA_AUTOSTART: "${SAGE_ROUTER_BUNDLED_OLLAMA_AUTOSTART:-1}"
      SAGE_ROUTER_OLLAMA_CLOUD_CATALOG_ENABLED: "${SAGE_ROUTER_OLLAMA_CLOUD_CATALOG_ENABLED:-1}"
      SAGE_ROUTER_OLLAMA_AUTO_PULL_PATTERNS: "${SAGE_ROUTER_OLLAMA_AUTO_PULL_PATTERNS:-:cloud}"
      # Cost controls
      SAGE_ROUTER_OPENROUTER_FREE_ONLY: "${SAGE_ROUTER_OPENROUTER_FREE_ONLY:-1}"
      # Dario is bundled in the image and autostarts when Anthropic/Claude routing is detected.
      SAGE_ROUTER_DARIO_AUTOSTART: "${SAGE_ROUTER_DARIO_AUTOSTART:-1}"
      SAGE_ROUTER_DARIO_BASE_URL: "${SAGE_ROUTER_DARIO_BASE_URL:-http://127.0.0.1:3456}"
      # Optional tiny intent classifier. Production default is llama.cpp sidecar.
      SAGE_ROUTER_INTENT_CLASSIFIER_ENABLED: "${SAGE_ROUTER_INTENT_CLASSIFIER_ENABLED:-0}"
      SAGE_ROUTER_INTENT_CLASSIFIER_PROVIDER: "${SAGE_ROUTER_INTENT_CLASSIFIER_PROVIDER:-llamacpp}"
      SAGE_ROUTER_INTENT_CLASSIFIER_BASE_URL: "${SAGE_ROUTER_INTENT_CLASSIFIER_BASE_URL:-http://llamacpp-classifier:8080}"
      SAGE_ROUTER_INTENT_CLASSIFIER_MODEL: "${SAGE_ROUTER_INTENT_CLASSIFIER_MODEL:-classifier}"
      SAGE_ROUTER_INTENT_CLASSIFIER_TIMEOUT_SECONDS: "${SAGE_ROUTER_INTENT_CLASSIFIER_TIMEOUT_SECONDS:-3}"
      SAGE_ROUTER_INTENT_CLASSIFIER_MIN_CONFIDENCE: "${SAGE_ROUTER_INTENT_CLASSIFIER_MIN_CONFIDENCE:-0.65}"
      # OPENCLAW_GATEWAY_TOKEN: "${OPENCLAW_GATEWAY_TOKEN}"
    healthcheck:
      test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8788/health')"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  # Optional GPU classifier sidecar. Enable with:
  #   docker compose --profile classifier up -d
  # Requires a GGUF model mounted at ${SAGE_ROUTER_MODELS_DIR:-./models}.
  llamacpp-classifier:
    image: ghcr.io/ggerganov/llama.cpp:server-cuda
    profiles: ["classifier"]
    container_name: sage-router-llamacpp-classifier
    restart: unless-stopped
    ports:
      - "${SAGE_ROUTER_CLASSIFIER_PORT:-18080}:8080"
    volumes:
      - ${SAGE_ROUTER_MODELS_DIR:-./models}:/models:ro
    command:
      - "-m"
      - "${SAGE_ROUTER_INTENT_CLASSIFIER_MODEL_PATH:-/models/qwen2.5-0.5b-instruct-q4_K_M.gguf}"
      - "--host"
      - "0.0.0.0"
      - "--port"
      - "8080"
      - "-ngl"
      - "${SAGE_ROUTER_INTENT_CLASSIFIER_N_GPU_LAYERS:-999}"
      - "-c"
      - "${SAGE_ROUTER_INTENT_CLASSIFIER_CTX:-1024}"
      - "--alias"
      - "${SAGE_ROUTER_INTENT_CLASSIFIER_MODEL:-classifier}"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]


path: docker-entrypoint.sh
content: #!/usr/bin/env sh
set -eu

# Keep mounted config paths predictable for the bundled router, bundled Ollama, and Dario proxy.
mkdir -p "${SAGE_ROUTER_HOME:-/config}" /root/.dario "${OLLAMA_MODELS:-/root/.ollama/models}"

ollama_ready() {
  curl -fsS --max-time 2 http://127.0.0.1:11434/api/tags >/dev/null 2>&1
}

# The image includes Ollama so a containerized router can satisfy an OpenClaw
# provider pointed at http://127.0.0.1:11434 without a separate Ollama service.
if [ "${SAGE_ROUTER_BUNDLED_OLLAMA_AUTOSTART:-1}" = "1" ] || [ "${SAGE_ROUTER_BUNDLED_OLLAMA_AUTOSTART:-1}" = "true" ]; then
  if command -v ollama >/dev/null 2>&1 && ! ollama_ready; then
    ollama serve >/tmp/ollama.log 2>&1 &
    for _i in $(seq 1 60); do
      ollama_ready && break
      sleep 0.5
    done
  fi
fi

# If caller passes a custom command, run it. Otherwise append --port from env
# only when the argument list does not already specify a port.
if [ "$#" -ge 1 ] && [ "$1" = "python3" ]; then
  for arg in "$@"; do
    if [ "$arg" = "--port" ]; then
      exec "$@"
    fi
  done
  exec "$@" --port "${PORT:-8788}"
fi
exec "$@"


path: docs/ANALYTICS.md
content: # Sage Router Analytics

Sage Router Analytics is the paid observability layer: hosted routing can be free/cheap, while performance intelligence is the monetized feature.

## Product promise

One endpoint for your AI subscriptions, with live performance analytics so you always use the best model.

Users bring provider subscriptions/keys. Sage sells:

- provider/model latency trends
- success/error rates
- fallback frequency
- fastest model recommendations
- most reliable model recommendations
- degraded-provider alerts
- team/project analytics history
- policy recommendations

## Runtime endpoint

```bash
GET /analytics?days=7&limit=10000
Authorization: Bearer $SAGE_ROUTER_ANALYTICS_TOKEN # optional when configured
```

The endpoint returns:

- `providers[]` performance summaries
- `models[]` performance summaries
- `intents[]` task-level summaries
- `recommendations.fastestModels[]`
- `recommendations.mostReliableModels[]`
- `recommendations.degradedModels[]`

## Privacy model

The analytics feed is built from route telemetry only. It does not store prompts, message bodies, provider credentials, OAuth tokens, or API keys.

Tracked telemetry:

- timestamp
- intent
- selected provider/model
- attempted providers/models
- elapsed milliseconds
- success/failure status
- coarse requirements flags

## Monetization shape

- Free: hosted endpoint, short/no history, basic routing
- Pro: 30-90 day analytics, recommendations, exports
- Power: long-term history, alerts, A/B tests, custom policies
- Team: shared dashboards, audit logs, per-project provider analytics

## Why this is valuable when users bring their own subscriptions

The value is not model access. The value is knowing which subscription performs best for each workload over time, and automatically routing around slow or failing providers.


## Durable Cloud Analytics

Production analytics now writes privacy-safe route telemetry to Google Firestore and mirrors the same event stream plus generated snapshots into the Autonomous Ops Studio Supabase project.

### Storage

- Firestore collection: `sage_router_route_events`
- Supabase tables:
  - `public.sage_router_route_events`
  - `public.sage_router_analytics_snapshots`

Stored events intentionally exclude prompts, message bodies, provider credentials, API keys, OAuth tokens, and raw responses.

### Auth

`GET /analytics` accepts either:

- `Authorization: Bearer $SAGE_ROUTER_ANALYTICS_TOKEN`, or
- a valid Supabase user JWT from the AOPS Supabase project.

Supabase service-role credentials are used only server-side for mirror writes and backend analytics reads.


path: docs/agent-native-pricing.md
content: # Agent-native routing and pricing structure

Sage Router now mirrors the useful agent-native pieces from BlockRunAI/ClawRouter without adding a mandatory hosted middle layer.

## Agent-native features

- **Agentic auto-detection**: requests with tool definitions or multi-step execution language are marked `agentic` and prefer models with stronger autonomous/tool-use behavior.
- **Tool-aware routing**: forced tool calls remain hard requirements; ordinary tool arrays become a soft preference so casual chat is not over-constrained.
- **Context-aware routing**: document, vision, and long-context signals are folded into requirements before candidate scoring.
- **Session-safe fallback**: each request gets an ordered fallback chain; failed providers are retried sequentially with no mid-stream model handoff.
- **Cost/plan telemetry**: route events retain selected model, attempts, elapsed time, auth type, and customer plan.
- **Free/eco fallback policy**: low-cost workflows can select `eco` or `local-first` profiles to avoid paid frontier calls.

Public endpoint:

```bash
curl http://localhost:8788/features/agent-native
```

## Routing profiles

- `sage-router/agentic`: best-route profile for multi-step agent work, tool preference, and strong model families such as Kimi, Codex, GPT-5, Claude/Sonnet/Opus, and Gemini Pro/3.
- `sage-router/eco`: cost-first/local-first profile that avoids expensive frontier defaults.
- `sage-router/premium`: quality-first paid profile for frontier/large model usage.

## Pricing structure

The public catalog uses the ClawRouter-style plan ladder:

- `free`: $0/month, local/free providers when available.
- `lite`: $6/month or $27/quarter, agent-native routing, API keys, usage analytics, standard fallback chains.
- `pro`: $30/month or $81/quarter, frontier routing, agentic tool-use preference, analytics snapshots, subscription failover.
- `max`: $72/month or $216/quarter, highest-quality routing, frontier/large-model preference, priority fallback budget.
- `metered`: usage-based, $0.001 minimum payment and 5% server margin for x402/wallet-style metering.

Public endpoint:

```bash
curl http://localhost:8788/pricing
```

Stripe checkout accepts a plan:

```bash
curl -X POST http://localhost:8788/billing/stripe/checkout \
  -H "Authorization: Bearer $SUPABASE_USER_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"plan":"pro"}'
```

Configure plan price IDs with:

```bash
SAGE_ROUTER_STRIPE_PRICE_IDS=lite=price_lite,pro=price_pro,max=price_max
```

`SAGE_ROUTER_STRIPE_PRICE_ID` / `STRIPE_PRICE_ID` remains backward-compatible as the `pro` plan price.


path: docs/cloud-tunnel/README.md
content: # Sage Cloud Tunnel

Sage Cloud Tunnel is the privacy-preserving hosted convenience layer for Sage Router.

It gives users a hosted OpenAI-compatible endpoint without collecting their provider API keys, OAuth tokens, cookies, or subscription credentials.

## Architecture

```text
Client app
  -> Cloudflare Worker /v1/chat/completions
  -> Durable Object tunnel
  -> user's local connector WebSocket
  -> user's local Sage Router
  -> user's own provider subscriptions
```

Credentials stay on the user's machine. Sage Cloud only relays request/response bytes through an authenticated tunnel.

## Monetization

Charge for the hosted relay/control plane, not inference resale:

- hosted low-latency endpoint
- connector uptime/health checks
- team routing policy sync
- analytics and reliability dashboard
- support/private deployment

## Deploy Worker

```bash
cd edge/cloudflare-ai-tunnel
npx wrangler secret put SAGE_TUNNEL_TOKEN
npx wrangler deploy
```

## Run local connector

```bash
SAGE_TUNNEL_URL=wss://sage-ai-tunnel.<account>.workers.dev/tunnel/connect \
SAGE_TUNNEL_TOKEN='<same secret>' \
SAGE_TUNNEL_LOCAL_BASE_URL=http://127.0.0.1:8788 \
node scripts/sage_tunnel_connector.mjs
```

## Call hosted endpoint

```bash
curl https://sage-ai-tunnel.<account>.workers.dev/v1/chat/completions \
  -H 'Authorization: Bearer <same secret>' \
  -H 'Content-Type: application/json' \
  -d '{"model":"auto","messages":[{"role":"user","content":"hello"}]}'
```

## Security properties

- No provider credentials collected by Sage Cloud.
- No provider OAuth/token/cookie custody.
- User can disconnect by stopping the connector.
- Worker stores no prompts by default.
- Token can be rotated at Cloudflare and locally.

## MVP limitation

This MVP uses one shared tunnel token and one connector per Worker namespace. Production should add per-user tunnels, token hashing, usage metering, wallet/crypto payments, rate limits, and optional org/team policy sync.


path: docs/integrations/README.md
content: # Sage Router Integration Guides

Use these guides to point common agent harnesses and developer AI tools at a local Sage Router endpoint.

## Guides

- [Codex CLI](codex.md)
- [Claude Code](claude-code.md)
- [OpenClaw](openclaw.md)
- [Hermes](hermes.md)
- [Pi agents](pi.md)
- [Cursor](cursor.md)
- [Aider](aider.md)
- [Continue](continue.md)
- [OpenHands](openhands.md)
- [Ollama and Ollama Cloud](ollama.md)
- [NVIDIA NIM / NVIDIA Cloud](nvidia-nim.md)
- [OpenAI-compatible clients](openai-compatible.md)
- [Anthropic-compatible clients](anthropic-compatible.md)
- [Harness fallback](harness-fallback.md)


path: docs/integrations/aider.md
content: # Use Sage Router with Aider

Aider can use Sage Router through the OpenAI-compatible endpoint.

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

Use `auto` for policy routing, or pin a provider-qualified model when needed:

```bash
aider --model openai/gpt-4.1
aider --model ollama/qwen2.5-coder:latest
```

Recommended routing: coding-strong model first, then cloud fallback for larger edits or failed local routes.


path: docs/integrations/anthropic-compatible.md
content: # Use Sage Router with Anthropic-compatible clients

Sage Router exposes an Anthropic Messages-compatible endpoint for tools that use Claude-style requests.

```bash
export ANTHROPIC_BASE_URL=http://localhost:8788
export ANTHROPIC_API_KEY=local-router
```

Clients should send requests to `POST /v1/messages`. Sage Router handles downstream provider auth and can route to Anthropic or other configured fallback routes depending on policy.

Use provider-qualified models for hard pins, or `auto` for policy-based model selection.


path: docs/integrations/claude-code.md
content: # Use Sage Router with Claude Code

Claude Code and Anthropic-compatible clients can talk to Sage Router through the Anthropic Messages endpoint.

Start Sage Router on port `8788`, then export:

```bash
export ANTHROPIC_BASE_URL=http://localhost:8788
export ANTHROPIC_API_KEY=local-router
```

Sage Router accepts `POST /v1/messages` and routes the request according to model name, route mode, provider health, and fallback policy.

Recommended use:

- Keep Claude Code pointed at one local endpoint.
- Configure Sage Router with Anthropic, OpenAI-compatible, Ollama, and NVIDIA routes.
- Let fallback keep the coding session alive when one provider is slow, down, or rate-limited.


path: docs/integrations/codex.md
content: # Use Sage Router with Codex CLI

Codex CLI can use Sage Router as an OpenAI-compatible endpoint.

## Setup

Start Sage Router on port `8788`, then export:

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

Use a provider-qualified model when you want an explicit route:

```bash
codex --model openai/gpt-4.1
codex --model ollama/qwen2.5-coder:latest
```

Recommended policy:

- Coding/refactor/debugging: coding-strong local model first, cloud fallback second.
- Large reasoning or fragile edits: higher-capability cloud model first.
- Background maintenance: local-first mode.


path: docs/integrations/continue.md
content: # Use Sage Router with Continue

Continue can point at Sage Router as an OpenAI-compatible model provider.

Use this endpoint:

```text
http://localhost:8788/v1
```

Use this API key placeholder:

```text
local-router
```

Recommended model strategy:

- `auto` for Sage Router policy selection.
- Provider-qualified names for hard pins, for example `ollama/qwen2.5-coder:latest` or `openai/gpt-4.1`.
- `local-first` for cheap background edits and `balanced` for normal coding sessions.


path: docs/integrations/cursor.md
content: # Use Sage Router with Cursor

Cursor can use Sage Router through OpenAI-compatible or Anthropic-compatible settings, depending on how your Cursor environment is configured.

## OpenAI-compatible endpoint

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

## Anthropic-compatible endpoint

```bash
export ANTHROPIC_BASE_URL=http://localhost:8788
export ANTHROPIC_API_KEY=local-router
```

## Recommended routing

- Code edits and refactors: coding-strong model with cloud fallback.
- Chat and quick explanations: fast or local-first route.
- Large-file reasoning or risky changes: best route.

Keep Cursor pointed at one endpoint and move provider selection into Sage Router policy instead of changing tool settings for every task.


path: docs/integrations/harness-fallback.md
content: # Use Sage Router for harness fallback

Sage Router is designed for agent harnesses and developer tools that need policy-based fallback across the user’s own authorized provider access.

Example provider mix:

```json
{
  "providers": {
    "ollama": { "baseUrl": "http://localhost:11434", "models": ["auto-discover"], "api": "ollama" },
    "openai": { "baseUrl": "https://api.openai.com/v1", "apiKey": "${OPENAI_API_KEY}", "models": ["auto-discover"], "api": "openai-completions" },
    "anthropic": { "baseUrl": "https://api.anthropic.com", "apiKey": "${ANTHROPIC_API_KEY}", "models": ["auto-discover"], "api": "anthropic-messages" },
    "nvidia": { "baseUrl": "https://integrate.api.nvidia.com/v1", "apiKey": "${NVIDIA_API_KEY}", "models": ["auto-discover"], "api": "openai-completions" }
  }
}
```

Recommended modes: `fast`, `balanced`, `best`, and `local-first`.


path: docs/integrations/hermes.md
content: # Use Sage Router with Hermes

Hermes-style operator agents can use Sage Router as the stable model endpoint behind routing, fallback, and provider health checks.

## Recommended shape

- Point OpenAI-compatible Hermes calls at `http://localhost:8788/v1`.
- Keep the visible operator workflow unchanged.
- Let Sage Router choose the right route for status, review, escalation, and long-running agent work.

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

## Routing pattern

- Status and ACK-style work: `fast` or `local-first`.
- Review requests: `balanced`.
- Escalations or high-risk changes: `best`.

Use provider-qualified models only when the workflow needs a hard pin. Otherwise, use `auto` and let Sage Router apply policy.


path: docs/integrations/nvidia-nim.md
content: # Use Sage Router with NVIDIA NIM / NVIDIA Cloud

Sage Router can route to NVIDIA-backed inference endpoints using your own NVIDIA API key.

```bash
export NVIDIA_API_KEY=nvapi-...
```

Provider config:

```json
{
  "providers": {
    "nvidia": {
      "baseUrl": "https://integrate.api.nvidia.com/v1",
      "apiKey": "${NVIDIA_API_KEY}",
      "models": ["auto-discover"],
      "api": "openai-completions"
    }
  }
}
```

Recommended chain: local model for low-risk tasks, NVIDIA NIM for GPU-backed hosted inference, Anthropic/OpenAI/Gemini fallback for provider diversity.


path: docs/integrations/ollama.md
content: # Use Sage Router with Ollama and Ollama Cloud

Sage Router supports local Ollama and Ollama Cloud models exposed through your local Ollama runtime.

```bash
ollama serve
ollama list
```

Provider config:

```json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434",
      "models": ["auto-discover"],
      "api": "ollama"
    }
  }
}
```

Sage Router discovers models from `/api/tags`. If your local Ollama runtime exposes `:cloud` models, Sage Router can discover and route to them.

Use `local-first` when you want real local models before cloud routes. Sage Router treats `:cloud` models as cloud-backed even though they are reached through localhost.


path: docs/integrations/openai-compatible.md
content: # Use Sage Router as an OpenAI-compatible endpoint

Any OpenAI-compatible SDK or tool can point at Sage Router.

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

JavaScript:

```js
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'http://localhost:8788/v1', [REDACTED_SECRET]' });
const response = await client.chat.completions.create({
  model: 'auto',
  messages: [{ role: 'user', content: 'Draft a migration plan.' }],
});
```

Python:

```python
from openai import OpenAI
client = OpenAI(base_url='http://localhost:8788/v1', [REDACTED_SECRET]')
resp = client.chat.completions.create(model='auto', messages=[{'role': 'user', 'content': 'Write a test plan.'}])
```


path: docs/integrations/openclaw.md
content: # Use Sage Router with OpenClaw

Sage Router can sit between OpenClaw and multiple model providers so agents can use one stable endpoint.

```bash
openclaw skill add sage-router --from clawhub
openclaw skill configure sage-router
```

Start Sage Router on port `8788`.

OpenAI-compatible tools:

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

Anthropic-compatible tools:

```bash
export ANTHROPIC_BASE_URL=http://localhost:8788
export ANTHROPIC_API_KEY=local-router
```

Use provider-qualified names when needed, for example `ollama/qwen2.5-coder:latest`, `openai/gpt-4.1`, `anthropic/claude-sonnet-4-6`, or `nvidia/meta/llama-3.1-405b-instruct`.


path: docs/integrations/openhands.md
content: # Use Sage Router with OpenHands

OpenHands can route through Sage Router when configured with an OpenAI-compatible endpoint.

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

Use Sage Router to keep OpenHands on one stable endpoint while routing between local models, Ollama Cloud, NVIDIA NIM, and cloud APIs according to policy and health.

Recommended route mode: `balanced` for general tasks, `best` for fragile repo-wide changes, and `local-first` for low-risk background work.


path: docs/integrations/pi.md
content: # Use Sage Router with Pi agents

Pi agents can route through Sage Router when they support an OpenAI-compatible base URL or run behind an OpenAI-compatible adapter.

## Environment

```bash
export OPENAI_BASE_URL=http://localhost:8788/v1
export OPENAI_API_KEY=local-router
```

## Recommended use

- Local-first for routine background work.
- Balanced routing for coding, documentation, and analysis.
- Best routing for high-value reasoning or tasks where retries are expensive.

## Model names

Use `auto` for policy routing, or provider-qualified names such as:

- `ollama/qwen2.5-coder:latest`
- `openai/gpt-4.1`
- `anthropic/claude-sonnet-4-6`
- `nvidia/meta/llama-3.1-405b-instruct`


path: edge/cloudflare-ai-tunnel/worker.js
content: export class SageTunnel {
  constructor(state, env) {
    this.state = state;
    this.env = env;
    this.connector = null;
    this.pending = new Map();
  }

  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/tunnel/connect") {
      if (!this.authorized(request)) return new Response("unauthorized", { status: 401 });
      const upgrade = request.headers.get("Upgrade") || "";
      if (upgrade.toLowerCase() !== "websocket") return new Response("expected websocket", { status: 426 });

      const pair = new WebSocketPair();
      const [client, server] = Object.values(pair);
      server.accept();
      this.attachConnector(server);
      return new Response(null, { status: 101, webSocket: client });
    }

    if (url.pathname === "/health") {
      return Response.json({ ok: true, connector: Boolean(this.connector) });
    }

    if (url.pathname === "/v1/chat/completions" && request.method === "POST") {
      if (!this.authorized(request)) return Response.json({ error: "unauthorized" }, { status: 401 });
      if (!this.connector) return Response.json({ error: "no connector online" }, { status: 503 });

      const payload = await request.json();
      const id = crypto.randomUUID();
      const timeoutMs = Number(this.env.SAGE_TUNNEL_REQUEST_TIMEOUT_MS || 180000);
      const job = { type: "chat.completions", id, payload };

      const resultPromise = new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          this.pending.delete(id);
          reject(new Error("connector timeout"));
        }, timeoutMs);
        this.pending.set(id, { resolve, reject, timer });
      });

      try {
        this.connector.send(JSON.stringify(job));
        const result = await resultPromise;
        return new Response(JSON.stringify(result.body), {
          status: result.status || 200,
          headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-store",
          },
        });
      } catch (err) {
        return Response.json({ error: String(err?.message || err) }, { status: 504 });
      }
    }

    return new Response("not found", { status: 404 });
  }

  authorized(request) {
    const expected = this.env.SAGE_TUNNEL_TOKEN;
    if (!expected) return false;
    const auth = request.headers.get("Authorization") || "";
    const [REDACTED_SECRET]"Bearer ") ? auth.slice(7) : new URL(request.url).searchParams.get("token") || "";
    return token && token === expected;
  }

  attachConnector(ws) {
    if (this.connector) {
      try { this.connector.close(1012, "replaced by newer connector"); } catch (_) {}
    }
    this.connector = ws;

    ws.addEventListener("message", (event) => {
      let msg;
      try { msg = JSON.parse(event.data); } catch (_) { return; }
      if (!msg || msg.type !== "result" || !msg.id) return;
      const pending = this.pending.get(msg.id);
      if (!pending) return;
      clearTimeout(pending.timer);
      this.pending.delete(msg.id);
      pending.resolve({ status: msg.status || 200, body: msg.body || {} });
    });

    ws.addEventListener("close", () => {
      if (this.connector === ws) this.connector = null;
      for (const [id, pending] of this.pending.entries()) {
        clearTimeout(pending.timer);
        pending.reject(new Error("connector disconnected"));
        this.pending.delete(id);
      }
    });

    ws.addEventListener("error", () => {
      try { ws.close(); } catch (_) {}
    });
  }
}

export default {
  async fetch(request, env) {
    const id = env.SAGE_TUNNEL.idFromName("default");
    const stub = env.SAGE_TUNNEL.get(id);
    return stub.fetch(request);
  },
};


path: edge/cloudflare-ai-tunnel/wrangler.toml
content: name = "sage-ai-tunnel"
main = "worker.js"
compatibility_date = "2026-04-28"

[durable_objects]
bindings = [{ name = "SAGE_TUNNEL", class_name = "SageTunnel" }]

[[migrations]]
tag = "v1"
new_sqlite_classes = ["SageTunnel"]

[vars]
SAGE_TUNNEL_REQUEST_TIMEOUT_MS = "180000"


path: requirements.txt
content: # No external pip dependencies — Sage Router uses only Python stdlib.
# This file exists for completeness and future dependency extension.


path: web/public/robots.txt
content: User-agent: *
Allow: /

Sitemap: https://sagerouter.dev/sitemap.xml


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)