Skip to main content
awerouter

awerouter: Smart LLM Router aweskill companion

Route cheap/fast tasks to Flash, hard decisions to Pro.

Transparent same-protocol proxy that routes coding-agent requests by structural signals — no keyword guessing, no LLM classifier. Speaks Anthropic Messages, OpenAI Chat Completions, and OpenAI Responses.

English · 简体中文

Ko-fi

Version Python License

Status pip Platform Downloads Stars

Transparent proxy that splits coding-agent traffic across providers by cost and capability. Same-protocol passthrough — no translation. Optional per-profile tool-result compression (RTK, off by default).

Support Tools

awerouter works best alongside two companion tools:

  • aweskill — CLI skill package manager for AI agents. Installs the awerouter skill so your agent can manage routing in natural language.
  • aweswitch — Agent profile switcher. Launches Claude Code, Codex, or OpenCode sessions with a profile that points BASE_URL at the awerouter daemon.

aweskill lets the agent manage routing by operating skills; aweswitch lets you launch sessions through it. Configure awerouter once, then start any agent against it with aweswitch <profile>.

Install & Usage

Let AI agent install and configure

If you are working in Claude Code, Codex, Cursor, or another coding agent, tell it:

Read https://github.com/mugpeng/awerouter/blob/main/README.ai.md and follow it to install and configure awerouter.

The agent will install the CLI, init config, help you add profiles, and install the awerouter skill via aweskill for ongoing routing management.

After setup, you can tell the agent things like:

"Add a stepfun flash provider and a pro profile." "List my awerouter profiles." "Tune longContextThreshold from my usage." "Explain my usage savings."

The agent can run read-only commands (list, config show, usage stats, usage calibrate, usage savings) and edit config directly, but it will not run awerouter serve (long-lived daemon), awerouter add (interactive wizard), awerouter restore (overwrites config), awerouter usage clean (deletes logs), or awerouter self-update (upgrades the installation). To start the daemon, run it in your own terminal:

awerouter serve cc-router-1

awerouter skill

Install the awerouter skill via aweskill to let AI agents manage routing with natural language:

  • List, inspect, add, and edit routing profiles
  • Edit providers.json (endpoints/auth) and routing.json (strategy) separately
  • Read usage stats / usage calibrate / usage savings and suggest threshold changes
  • Guide environment-variable setup for ${ENV_VAR} auth references

After install, you can tell the agent things like "Add a GLM provider for the openai-chat group", "Raise longContextThreshold to 12000", or "Show me which provider handles my web_search traffic". The agent reads the config, makes changes, and verifies with awerouter config show / awerouter list.

Launch through aweswitch

Once awerouter is configured, launch any agent through it by pointing an aweswitch profile at the daemon.

Example: launch OpenCode through awerouter

Start the daemon with an openai-chat profile in one terminal:

awerouter serve oc-router-1

Add an aweswitch OpenCode profile pointing at it:

{
  "profiles": {
    "opencode": {
      "oc-awerouter": {
        "env": {
          "OPENCODE_BASE_URL": "http://127.0.0.1:20128/v1",
          "OPENCODE_API_KEY": "sk-any-non-empty-value",
          "OPENCODE_NAME": "awerouter",
          "OPENCODE_MODEL": "auto"
        }
      }
    }
  }
}
aweswitch oc-awerouter

With OPENCODE_MODEL set to auto, awerouter routes each request by structural signals — the upstream provider receives the actual model id from routing.json destinations, not auto. Claude Code works the same way via an anthropic profile (ANTHROPIC_MODEL=auto).

Manual install and usage

Install from PyPI:

pip install awerouter

Quick Start:

# 1. Init config (creates ~/.config/awerouter/{providers,routing}.json)
awerouter init

# 2. Interactively add a profile (writes both files, references stay consistent)
awerouter add
#    or edit by hand: providers.json for keys (${ENV_VAR}), routing.json for flash/pro

# 3. Start the daemon (profile name optional when only one exists)
awerouter serve [cc-router-1]     # shorthand: awerouter cc-router-1

# 4. Point CC at it — the serve banner prints both lines below
export ANTHROPIC_BASE_URL=http://127.0.0.1:20128
# aweswitch profile env: ANTHROPIC_MODEL=auto, _HAIKU_=flash, _OPUS_=pro

Config

Two files in ~/.config/awerouter/ (override with AWEROUTER_CONFIG_DIR):

providers.json — endpoints + keys, grouped by wire protocol (redacted in config show):

{
  "anthropic": {
    "stepfun":   { "base_url": "https://api.stepfun.com/step_plan", "auth": "${STEPFUN_AUTH_TOKEN}" },
    "anthropic": { "base_url": "https://api.anthropic.com",          "auth": "${ANTHROPIC_KEY}" }
  },
  "openai-chat": {
    "stepfun": { "base_url": "https://api.stepfun.com/step_plan/v1", "auth": "${STEPFUN_AUTH_TOKEN}" }
  },
  "openai-responses": {
    "openai": { "base_url": "https://api.openai.com/v1", "auth": "${OPENAI_API_KEY}" }
  }
}

Three protocols are supported. base_url uses each native client's convention — copy it verbatim from your client config; awerouter appends the endpoint path the same way the native client would:

Protocol id base_url style Endpoint
anthropic ANTHROPIC_BASE_URL (no /v1) base_url + /v1/messages
openai-chat OPENAI_BASE_URL (includes version segment) base_url + /chat/completions
openai-responses OPENAI_BASE_URL (includes version segment) base_url + /responses

The same provider often uses a different path per protocol — GLM for instance: https://open.bigmodel.cn/api/coding/paas/v4 for chat completions but https://open.bigmodel.cn/api/v1 for responses. That's why each protocol group carries its own base_url.

The auth header is auto-detected from base_url: anthropic.comx-api-key (bare token); everyone else → Authorization (auto-prefixes Bearer ). No auth_header field needed unless the heuristic is wrong.

routing.json — strategy, no secrets (safe to commit):

{
  "settings": {
    "backgroundModel": "flash",
    "thinkModel": "pro",
    "toolRouting": {
      "webSearch": "pro",
      "search": "flash",
      "edit": "pro",
      "mechanical": "flash"
    },
    "longContextAuto": {
      "percentile": 95,
      "windowDays": 7,
      "minSamples": 50,
      "fallbackThreshold": 8000
    }
  },
  "cc-router-1": {
    "protocol": "anthropic",
    "port": 20128,
    "longContextThreshold": 8000,
    "destinations": {
      "flash": "stepfun,step-3.7-flash",
      "pro":   "anthropic,claude-opus-5"
    }
  }
}

settings is optional (defaults: flash/pro). It maps the model ids CC sends for the background (Haiku) and think (Opus) tiers; every tool-keyed routing rule (L1 webSearch included) lives in settings.toolRouting — the legacy top-level webSearchModel still works as a fallback. The main loop uses auto — routed by difficulty by L3. Set these in your aweswitch profile: ANTHROPIC_DEFAULT_HAIKU_MODEL=flash, ANTHROPIC_MODEL=auto, ANTHROPIC_DEFAULT_OPUS_MODEL=pro.

longContextThreshold is an integer, or "auto" to calibrate it from this profile's own traffic: at every serve start, awerouter takes the percentile of the profile's L3 effective-token distribution over the trailing windowDays as the threshold. With fewer than minSamples L3 requests in the window (fresh profile, quiet week) the fallbackThreshold applies instead. All four knobs live in settings.longContextAuto and are optional — the banner always prints what was picked and why. Note the percentile sets the flash/pro split, not flash's capability ceiling: if your flash model degrades on very long contexts, keep a manual threshold.

Keys reference ${ENV_VAR} syntax. Missing env vars die with a clear message at startup.

Profile-based routing: routing.json groups configs under profile ids (like aweswitch). awerouter serve <profile> starts one; with a single profile it auto-selects. protocol maps the profile to a providers.json group and decides which endpoint it serves — the serve banner prints the matching client env (ANTHROPIC_BASE_URL for Claude Code, OPENAI_BASE_URL / Codex wire_api for the openai protocols). Note: openai clients are single-model, so L2 tier labels effectively never fire for them — openai traffic routes by L1 + L3 with a flash default.

Ports: the optional port field pins a profile's listen port (awerouter list shows it); precedence: --port flag > profile port > 20128 default. An explicitly chosen port that is already in use fails loudly — clients hardcode it, it must not silently move. Without one, serve takes the first free port scanning up from 20128: the first instance gets 20128, the next 20129, and so on — the assignment follows start order, not the profile. For the one-instance-at-a-time swap workflow, leave profiles portless and point clients at 20128.

How It Routes

First-match-wins pipeline, evaluated per request:

Layer Signal Decision
L1 Capability web_search tool in body toolRouting.webSearch (default pro; legacy webSearchModel still works)
L2 Tier label model == c1/flash or c1/think flash / pro respectively
L3 Difficulty token count (all request content) > threshold, or has image pro; else fall through
L4 Tool phase trailing tool batch is edit-class (edit/write/apply_patch/...), search-class (grep/glob/ls/list), or mechanical (todo_write/task) edit → pro; search → flash; mechanical → flash (settings.toolRouting, null disables a rule)

CC's /model picker sets the tier model id (c1/flash / c1/pro / c1/think). awerouter reads it and routes accordingly — no keyword parsing, no LLM classifier.

L4 keys on what the agent just did: search results feed cheap mechanical next steps (list the next glob, read a hit), while a fresh edit means code is being written or verified; todo/subagent turns are bookkeeping. The signal is the trailing parallel batch of tool calls and takes its strongest phase (edit > search > mechanical), so [Grep, Edit] and [Edit, Grep] route identically. Shell-wrapped calls (codex exec_command/shell) are classified by their command text — search binaries count as search, apply_patch counts as edit. L4 sits below L3 on purpose — a session already above longContextThreshold stays pro no matter which tool just ran, so flash never sees contexts it may degrade on and the long-context crossing stays one-way flash→pro (below the threshold, sessions may alternate flash↔pro by phase). Search-class tool names reuse the same set as the searchResultDiscount detection (claude-code's Grep/Glob/LS, opencode's grep/glob/list); edit-class covers Edit/Write/NotebookEdit/apply_patch/replace_in_file and friends, matched case-insensitively.

All tool-keyed rules live in one block — settings.toolRouting (webSearch/search/edit/mechanical) — and the serve banner prints the active mapping on one tool -> ... line.

Token Saver (RTK)

Coding agents resubmit the whole conversation every turn, and most of it is tool output — git diffs, grep hits, directory listings, build logs. A profile can opt into RTK compression, which rewrites that text in place before routing and forwarding:

"cc-router-1": {
  "protocol": "anthropic",
  "longContextThreshold": 8000,
  "rtk": true,
  "destinations": { "flash": "stepfun,step-3.7-flash", "pro": "anthropic,claude-opus-5" }
}
  • What it touches: tool_result / tool-message content only — never user prompts or model replies. Rule-based filters (git diff/status/log, grep, find, tree, ls, build output, …) auto-detect the format and compress it; unrecognized content, anything under 500 chars, and error results (is_error) pass through untouched.
  • Fail-open: any failure leaves the body as-is — the worst case is fewer tokens saved, never a broken request.
  • Deterministic: the same history compresses to the same bytes every turn, so provider prompt-cache prefixes survive.
  • Per-request escape hatch: send X-Awerouter-Token-Saver: off to forward one request uncompressed (e.g. debugging an agent that needs full diff/log detail).
  • Compression runs before routing, and /v1/messages/count_tokens is compressed too, so L3 decisions and usage logs match what is actually billed. After enabling RTK, re-run usage calibrate — a threshold tuned on uncompressed traffic over-triggers pro ("auto" self-corrects after its window).

Compression is a port of rtk (Apache 2.0) via 9router's JS port (MIT); the request log records the estimated saved tokens per request.

Commands

awerouter init                        # create default config from templates
awerouter add                         # interactively add a profile (pick category and providers)
awerouter list                        # list profiles (name, protocol, port, flash, pro, threshold)
awerouter serve [PROFILE] [--port N] [--host 127.0.0.1]  # port: --port > profile 'port' > 20128
awerouter <PROFILE>                   # shorthand for serve PROFILE
awerouter restore [providers|routing] # restore a config file from its .bak backup
awerouter self-update [--check]        # upgrade to the latest PyPI release (--check: versions only)
awerouter config path                 # print both config file paths
awerouter config show [PROFILE]       # redacted config; PROFILE = its providers + entry only
awerouter config edit [providers|routing]  # open one file in $EDITOR (backs up to .bak first)
awerouter usage stats [--since ..] [--profile ..]
awerouter usage clean                 # delete saved request logs (asks to confirm)
awerouter usage log [--lines 20] [--all] [--tokens] [--since ..] [--profile ..]
awerouter usage tokens [--since ..] [--profile ..]
awerouter usage calibrate [--since ..] [--profile ..]
awerouter usage savings [--since ..] [--profile ..]

All usage subcommands read the same request log. log, stats, tokens, calibrate, and savings take --since (today, yesterday, 7d, or YYYY-MM-DD, local time) and --profile directly — e.g. awerouter usage stats --since today --profile cc-1; clean deletes everything and takes no window options.

usage stats aggregates the log per profile (with its wire protocol): label/agent/destination/provider/model breakdowns with percentages, error and fallback counts, latency percentiles (first byte and total) per destination/provider/model, and estimated request tokens (all request content: messages, system prompt, tools, tool I/O). usage clean deletes the saved logs (requests.jsonl + rotated backup) after a confirmation prompt. usage log shows entries verbatim — the last 20 by default, or every entry with --all; each line includes the protocol served and the calling agent, detected from the client's User-Agent header (claude-cli/...claude-code, codex_cli_rs/...codex, opencode/...opencode). --tokens swaps the status/latency/model-in columns for the per-type token breakdown of each request (msg/sys/tools/results/calls/think); entries logged before per-type counting show only the total.

usage tokens aggregates those per-type breakdowns: input-token totals and share by content type (messages, system prompt, tool definitions, tool results, tool-call arguments, thinking) — useful for seeing how much of a request's tokens are environment constants (system prompt + tool definitions) versus conversation.

config edit and the add wizard snapshot the target file to <name>.json.bak before every write; awerouter restore [providers|routing] copies a backup back (with confirmation, then validates the restored config). config path prints the two config file paths; config show [PROFILE] shows the redacted full config, or just one profile's providers and routing entry.

self-update upgrades the installed package — pipx installs use pipx upgrade awerouter, everything else pip install --upgrade; restart running serve instances afterwards. Every command also checks PyPI in a background thread (at most once a day, cached as update-check.json in the config dir) and prints a one-line reminder after the command when a newer release exists — also throttled to once a day; AWEROUTER_NO_UPDATE_CHECK=1 disables the check entirely. The serve banner shows the same update hint from the cached check.

usage calibrate shows the request-token distribution of L3 traffic (the threshold-sensitive layer; all request content — messages, system prompt, tools, tool I/O) and suggests candidate longContextThreshold values at p90/p95/p99, plus what "auto" would pick under settings.longContextAuto. Run it after some real traffic, then either edit routing.json or switch the profile to "auto" and let serve calibrate on each start.

usage savings is the token accounting view: how many request-input tokens each tier consumed and how many pro input tokens routing offloaded to flash vs a pro-only baseline. A cache-sensitivity section brackets the offload between "all cache reads" and "all full price" (Anthropic-style ~0.1x read / 1.25x write / 5-min TTL) and shows your switch cadence vs the TTL — a cache-warm pro-only baseline would have billed those tokens at cache-read prices. The output ends with ready-to-fill formulas using the measured token counts — substitute your providers' input prices (per 1M tokens) and read off the saved amount (output tokens, flash-side caching, and capability-mismatch turns are not modeled).

Troubleshooting

CC shows 502 status code (no body) right after launch — a shell proxy (Clash etc.) is hijacking loopback traffic. Requests to 127.0.0.1:20128 go into the proxy, whose 127.0.0.1 is itself, so nothing is listening and the proxy returns an empty 502. serve prints a warning when it detects this; fix it by exempting loopback in your shell config:

export no_proxy=127.0.0.1,localhost NO_PROXY=127.0.0.1,localhost

Then open a new terminal and relaunch CC.

Development

git clone https://github.com/mugpeng/awerouter
cd awerouter
pip install -e ".[dev]"
pytest

See docs/CONTRIBUTING.md for architecture notes, config semantics, and the release process.

Support

If awerouter saves you money, consider supporting it:

  • ⭐ Star the repo — it helps others find it.
  • Ko-fi — buy me a coffee.
  • 💬 WeChat — scan the QR code below.

WeChat Pay

awerouter is free and open source. Sponsors keep it maintained — thank you.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

awerouter-0.4.6.tar.gz (263.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

awerouter-0.4.6-py3-none-any.whl (64.8 kB view details)

Uploaded Python 3

File details

Details for the file awerouter-0.4.6.tar.gz.

File metadata

  • Download URL: awerouter-0.4.6.tar.gz
  • Upload date:
  • Size: 263.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.15

File hashes

Hashes for awerouter-0.4.6.tar.gz
Algorithm Hash digest
SHA256 d1dcaf88cfb88dd195e61c4c53981a49c5946795c002e8e35428458afe337e57
MD5 4217435f98731a1b08189890ec749995
BLAKE2b-256 332e3b62d266b03182eebefd33ebc1826c5a177b2d9211d9b838a567e023ee85

See more details on using hashes here.

File details

Details for the file awerouter-0.4.6-py3-none-any.whl.

File metadata

  • Download URL: awerouter-0.4.6-py3-none-any.whl
  • Upload date:
  • Size: 64.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.15

File hashes

Hashes for awerouter-0.4.6-py3-none-any.whl
Algorithm Hash digest
SHA256 d638b21e42e19d12c8838a4b5f867703dc96fd47500bbe5209569ee6d9e47942
MD5 e0e17ceed9baad3d6a5df6651a56c960
BLAKE2b-256 f1f2a256ff463eaca170aa87e99b57c7db1fff5d37bb40dae68fbde5a0b86a3e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page