Native path
The harness path runs the foreign model's own loop in OpenCode, Codex, Pi, or Reasonix. The native path instead runs foreign models inside Claude Code's own loop, tools, and permission modes, through a local routing proxy.
The usual setup is sonata route auto once per project (sonata init offers it), then launching plain claude exactly as before. Each session routes itself at start, the router comes up on its own, and Remote Control keeps working. sonata code is for a one-off routed session somewhere you have not set routing up.
sonata serve— runs the router, and a managed LiteLLM child when any configured model needs one. Anthropic-native gateways go straight through the router and start none. The child comes up withservewhen the config already needs it, and otherwise lazily, on the first request that does.sonata route auto— routes every plainclaudesession in the project and keeps Remote Control. Instead of leaving the routing env in the file, a SessionStart hook writes it just after the session launches and a SessionEnd hook removes it again, so each session launches from a clean file. Claude Code decides Remote Control once, at launch, fromANTHROPIC_BASE_URL; it re-reads the settingsenvon every request. Auto mode lives in that gap. Concurrent sessions are counted, so one ending never cuts another's routing.sonata route manualremoves the pair. A session that dies without running its SessionEnd hook leaves routing on — the next launch loses Remote Control once;sonata route offresets it.sonata route on— routes every plainclaudesession by leavingANTHROPIC_BASE_URLin.claude/settings.local.json, with a SessionStart hook that keeps the router up, so editor integrations,.mcp.jsonentries and shell aliases all go through the proxy too. Every routed session loses Remote Control untilsonata route off;sonata route statusreports which mode is on. Preferautounless something launches sessions in a way its hooks cannot reach.sonata code— launches a single routed Claude Code session (ANTHROPIC_BASE_URLpoints at the local router), starting the router in the background if it is down. No settings are written; that session loses Remote Control.sonata auth— manages per-gateway keys that the router forwards to LiteLLM; keys live in the store and are never logged or put in a conversation. A stored key is only ever sent to a host bound to its gateway name — the provider's own endpoint, a host you confirmed withsonata auth trust <gateway> <host>, or the machine config's own URL for that name — so a project config pointing the name elsewhere gets no key and a named 502 naming the config that set the URL and the trust command, instead of your credential.sonata auth addbinds nothing from a config; init binds the URLs typed in its own runsonata auth login <gateway>— starts LiteLLM's device login for an OAuth gateway. The login runs into a fresh directory that replaces the old one only on success, so a failed login never damages the working credential
The claude harness adapter completes the picture: it dispatches a foreign-on-Claude-loop session through sonata dispatch, running headless claude -p with no TUI. Native dispatches assume sonata serve is already up.
Two consequences worth knowing. First, ANTHROPIC_BASE_URL is process-wide and isFirstPartyAnthropicBaseUrl gates Remote Control, so a session launched routed — by sonata code, or under sonata route on — loses Remote Control. route auto avoids that by routing only after launch.
Second, behind a gateway Claude Code cannot verify that a model really has the 1M window it claims natively, so it budgets those models — Sonnet 5, the Fable models, Opus 4.7+ — at 200K instead. The window silently shrinks by 80%; the only clue is a number in /context. An id carrying an explicit [1m] is immune, because there is nothing left to verify: "model": "opus[1m]" keeps its full window where a bare opus does not. sonata doctor prints the fix when it applies.
Your own Claude Code account
A tier can rank Claude models served by your own Claude Code account:
[native.gateways."claude"]
provider = "anthropic"
auth = "session"
[models."opus"]
gateway = "claude"
id = "claude-opus-4-1"
The router forwards the session's own credential, and only to api.anthropic.com — any other base_url is refused. Usage is priced at Anthropic's list rates; work your Claude subscription paid for is shown as $x sub and never counts toward [budget]. A key named exactly like a model your main session uses (claude-opus-4-1) also catches that session's own turns when it is routed.
OpenCode Zen and Go. The two are separate endpoints on one opencode.ai account: Zen (pay as you go) is https://opencode.ai/zen/v1, Go (the subscription) is https://opencode.ai/zen/go/v1 (models.dev now lists both under /inference, and both paths answer). Both route a conversation by an x-opencode-session header and refuse a request without one (400 MissingSessionID). Since 0.13.1 the router sends it — a stable id per conversation — and LiteLLM forwards it only to models on an opencode.ai gateway; no other client x-* header reaches them. Earlier versions sent none, so no native request to an opencode.ai gateway could succeed.
Subscription plans
Several vendors sell a flat-rate coding or token plan beside a metered API, reached with an ordinary API key. Sonata reports a plan's work as covered: valued at list price in sonata usage (shown as $x sub), never billed per token, and left out of [budget]. Usage beyond a plan's allowance that the vendor bills per token is not something a response says, so sonata cannot see it — keep [budget] for metered gateways in mind if your plan overflows to a balance.
A plan with an endpoint of its own is recognised from the gateway's base_url, with nothing to configure:
| Plan | Endpoint |
|---|---|
| OpenCode Go | opencode.ai/zen/go, opencode.ai/inference/go |
| Z.ai / Zhipu GLM Coding Plan | api.z.ai/api/coding, open.bigmodel.cn/api/coding |
| Kimi For Coding | api.kimi.com/coding, api.kimi.ai/coding |
| Alibaba Coding Plan | coding-intl.dashscope.aliyuncs.com, coding.dashscope.aliyuncs.com |
| Alibaba Token Plan | token-plan.<region>.maas.aliyuncs.com |
| Volcengine Ark Coding / Agent Plan | ark.cn-beijing.volces.com/api/coding, /api/plan |
| Tencent Coding / Token Plan | api.lkeap.cloud.tencent.com/coding, /plan |
| Xiaomi Token Plan | token-plan-<region>.xiaomimimo.com |
| StepFun Step Plan | api.stepfun.ai/step_plan, api.stepfun.com/step_plan |
| KUAE Cloud Coding Plan | coding-plan-endpoint.kuaecloud.net |
The same vendors' pay-as-you-go endpoints (api.z.ai/api/paas, api.moonshot.ai, opencode.ai/zen/v1, …) stay metered. The list follows models.dev's provider catalogue, which opencode also reads.
Some plans share their pay-as-you-go endpoint, and only the key says which you have: MiniMax Token Plan, Synthetic, Chutes, NanoGPT, Ollama Cloud, Cerebras Code, Umans AI. Sonata treats those as metered, and sonata doctor names them. If yours is a plan key, say so on the gateway:
[native.gateways."minimax"]
base_url = "https://api.minimax.io/anthropic"
provider = "anthropic"
billing = "subscription"billing = "metered" does the opposite, for an endpoint sonata gets wrong. sonata doctor lists every gateway it counts as a subscription and why.
ChatGPT (codex-oauth), GitHub Copilot (copilot-oauth) and your Claude account (auth = "session") are subscriptions through their own logins. A SuperGrok subscription includes no API access, so xAI is always metered.
Check a plan's terms before routing it through sonata. Some limit the plan to the vendor's listed tools — Z.ai's Coding Plan terms, for one, exclude "SDK-based access or other third-party integrations".
See also: Codex subscription auth, for how the codex-oauth gateway type authenticates against a ChatGPT subscription rather than a metered API key.
The loop panel
The sonata-loop skill folder sonata init installs is also a Claude Code mod, which Claude Code loads from the skills folder by itself. While a loop runs it shows a side panel (toggle it with /sonata-loop panel; any other arguments start a loop as before):
- Tasks from the loop's plan, one line each, with a state stroke:
━━active,── done,· · queued, and─ ─for a condition (failed 1x, an escalation). The orchestrator reports these through the panel'ssonata_looptool; nothing is inferred. - Agents under their task, one line each: role and tier, the model the router served, tokens, elapsed time and state. A running agent adds one line with its current tool, step count and how long ago its last step was; one silent for 90 s reads
─ ─ stalled. - Model and cost come from the router.
≈means two agents on one alias ran at once and the router cannot say whose request was whose, so neither is given a cost. A dash orcost unknownmeans unpriced or unattributable, never$0. - Tokens come from Claude Code itself, so they show even when the router cannot be read.
- Detail: select a row (click, or focus the pane with
ctrl+x taband press enter) to open, beneath it, its token breakdown (input, output, cache read, cache write) and the edits it applied. A task's detail covers its whole agent tree. Only appliedEditandWritecalls are diffed; whenBashran too, the detail says so.
The panel reads the router at http://127.0.0.1:4100 by default. With a different [native.ports] router, set the mod's router_url option. When the router is not reachable the footer says so and the panel keeps showing what it last reported, with its age.
The panel can also be installed as a plugin from this repo's marketplace:
claude plugin marketplace add zhihaohong52/sonata
claude plugin install sonata-loop@sonataClaude Code loads one plugin per name, and the installed plugin wins over any skills-folder copy, so once it is present sonata init and sonata sync write only the skill text and remove the panel files from their copies. Update it with claude plugin update sonata-loop@sonata; sonata doctor says when its version is behind the package's.