cctra
v0.8.1
Published
Local LLM provider protocol converter + plugin host. Runs a local HTTP server on 127.0.0.1:3133 that translates between OpenAI Chat / OpenAI Responses / Anthropic Messages, with a global alias table (`cctra switch` to rebind without restarting the client)
Maintainers
Readme
cctra
Local LLM provider protocol converter + plugin host
cctra runs a local HTTP server on 127.0.0.1:3133 that translates between OpenAI Chat Completions / OpenAI Responses / Anthropic Messages, with a global alias table (rebind without restarting the client) and local-path plugin support for non-standard upstream auth (OAuth, mTLS, etc.).
Quick start
# install (once)
bun add -g cctra
# or npm i -g cctra
# add a provider (interactive wizard)
cctra add
# start the server (foreground)
cctra serveEndpoints
cctra exposes exactly 3 protocol endpoints on 127.0.0.1:3133:
| Protocol | Path |
|---|---|
| Anthropic Messages | POST /v1/messages |
| OpenAI Chat Completions | POST /v1/chat/completions |
| OpenAI Responses | POST /v1/responses |
| OpenAI Models | GET /v1/models |
| Health | GET /healthz |
Aliases — the only short-name system
cctra has one place where every short name lives: the [aliases] table in ~/.config/cctra/config.toml. An alias is a name → provider/model pointer; clients send the alias as their model field and cctra routes to the upstream.
Three things to know:
- Auto-generated: when you
cctra adda provider, every model whose id is globally unique getsaliases[id] = "provider/id"for free — clients can use the short id immediately, noprovider/prefix needed. - Manual slots for stable client config: cctra pre-seeds three empty aliases —
cctra-pro/cctra-flash/cctra-vision. Bind them withcctra switch <name>and your Claude Code / Codex configs can hard-code those names forever; switching upstreams is a one-line CLI call that hot-reloads (no client restart). - Add your own:
cctra alias add <name>for empty slots,cctra alias <name> <target>to set in one shot.
cctra add # walks the wizard, auto-aliases unique ids
cctra alias # list all aliases (bound + unbound)
cctra switch cctra-pro # interactive: pick a model from the dropdown
cctra switch cctra-pro ark/doubao-seed-1-6 # non-interactive
cctra alias rm cctra-vision # remove a slot you don't usecctra ls shows everything at a glance:
cctra ls
# ALIASES
# cctra-pro → ark-sub/doubao-seed-1-6 [Ark Sub]
# doubao-seed-1-6 → ark-sub/doubao-seed-1-6 [Ark Sub]
# sonnet → or/anthropic/claude-3.5 [OpenRouter]
#
# UNBOUND
# cctra-flash
# cctra-vision
#
# OTHER MODELS
# ark-sub/doubao-1-5-pro [Ark Sub]
# ark-sub/doubao-1-5-vision [Ark Sub]Client integration
Each client picks its baseURL + model field to hit the right protocol endpoint. Two clients that speak the same protocol (e.g. Claude Code and any other Anthropic-SDK-based client) use the same baseURL — only the model field varies.
⚠️ baseURL must include the right prefix for the SDK. Anthropic SDK appends
/v1/messagesinternally (setbaseURL=http://127.0.0.1:3133). OpenAI SDK appends/chat/completionsand/responses(setbaseURL=http://127.0.0.1:3133/v1).
Claude Code
export ANTHROPIC_BASE_URL=http://127.0.0.1:3133
export ANTHROPIC_AUTH_TOKEN=anything # cctra 不验 Anthropic 客户端的 token;填任意占位即可model field can be any alias (cctra-pro / cctra-flash / provider-unique-id) or full name (provider/id). The [1m] context suffix is processed client-side by Claude Code — cctra never sees it.
Codex (and OpenAI SDK / Cursor)
export OPENAI_BASE_URL=http://127.0.0.1:3133/v1
export OPENAI_API_KEY=anything # 同上,cctra 不验 OpenAI 客户端的 keyCodex defaults to the Chat Completions path (/v1/chat/completions). For the Responses path (/v1/responses), the client must be configured to send there explicitly — cctra will route whichever path the client picks.
任意 OpenAI 兼容客户端(opencode / 其他)
baseURL = http://127.0.0.1:3133/v1. The client picks the path:
- requests to
/v1/chat/completions→ Chat path - requests to
/v1/responses→ Responses path
If the client allows custom paths, prefer /v1/responses — it's closer to cctra's canonical model and carries more forward-compat extras (per 0.6.0 parity work). opencode / Continue.dev / Aider / any custom OpenAI-SDK wrapper fall into this category.
Model field: 全名 vs alias
- Alias 短名(
cctra-pro/cctra-flash/provider-unique-id)— 推荐- 客户端写死
model: cctra-pro,服务端cctra switch cctra-pro <new-target>热切上游,client 不需要改 - 3 个预置空槽(
cctra-pro/cctra-flash/cctra-vision)走cctra alias add或cctra switch绑定
- 客户端写死
- 全名(
provider/id,如or/anthropic/claude-3.5)— 锁定上游时- 不依赖 alias 表,配置文件丢了也能 resolve
- 切上游要改 client 的
model字段
Known inter-protocol incompatibilities
cctra's canonical layer is best-effort, not lossless. When you mix protocols, expect these:
documentblocks 丢 — Anthropicdocument(PDF/图片) → Chat/Responses upstream 静默丢。仅 Anthropic↔Anthropic round-trip 安全。thinking/signaturedeltas 丢 — Chat 路径无对应字段。Anthropic↔Responses round-trip 文本保留,signature 丢。redacted_thinking降级文本 — 在 Chat/Responses 客户端呈现为[redacted_thinking]literal 字符串(0.5.1 实现)。refusal块变文本 — Chat 上游的 refusal → Anthropic 客户端看到[refusal] …普通 text block,Claude Code refusal 分支不会触发。imageparts 在 Chat/Responses 出站被 re-encode 成data:URL — 即使入站是 URL 也重编码,payload 涨;远程大图要走 Chat 上游时尤其明显。- 未知 block / item 占位 — Anthropic 未知 block →
[unknown_block:<type>];Responses 未知 item →[unknown_input_item:<type>](含web_search_call/mcp_call等 5 个内置 tool)。原 payload 保留在extras里。 stop_reason: "error"→"refusal"— 0.5.1 修复;Chat 上游content_filter让 Anthropic 客户端看到 refusal 事件。- Anthropic
system数组(带cache_control)丢 cache_control — Chat 顶层 system 是 string 无元数据空间,cache_control 元信息无法承载。
Plugin system
Add custom JS plugins for non-standard upstream auth:
cctra plugin add my-internal /path/to/my-internal.js
cctra plugin ls
cctra plugin enable my-internal
cctra plugin disable my-internal
cctra plugin rm my-internalA plugin exports:
export default {
name: "my-internal-llm",
displayName: "My Company LLM",
async getConfig(ctx) {
// OAuth / mTLS / custom header logic
return { baseUrl: "...", path: "/v1/chat/completions", apiFormat: "openai-chat", authHeader: { /* ... */ }, modelId: "..." };
},
async listModels(ctx) { return [{ id: "..." }, { id: "..." }]; },
};See examples/plugins/ for working examples.
Rectifiers (vendor-quirk workarounds)
Some upstream providers have idiosyncratic request-shape requirements that aren't part of any standard protocol. Classic example: Kimi's Anthropic-compatible endpoint (api.moonshot.cn/anthropic) only accepts thinking: { type: "enabled", budget_tokens: N } — it 400s on the effort shorthand ("high"/"medium"/"low") that Claude Code sends, and on boolean true/false. Without rectification, the request goes through cctra unchanged and Kimi rejects it.
cctra's rectify subsystem lets you attach named, composable rules to specific providers. A rule rewrites the upstream wire body right before the HTTP fetch. Currently bundled:
| Rule id | Effect |
|---|---|
| normalize-thinking-type | Coerce Anthropic thinking.type to "enabled"/"disabled" string. Any non-literal-"disabled" value (including "high"/"medium"/"low"/"xhigh"/"max"/boolean true) → "enabled". Only fires for anthropic-messages upstream. |
Two-level control: global on/off per rule + per-provider attach (whitelist). Provider not in [rectify.providers.X] → no rules run, regardless of global toggle. Rules throw → logger.warn + skip; request continues.
cctra rectify # list rules + attachments + per-rule status
cctra rectify enable normalize-thinking-type # global toggle
cctra rectify disable normalize-thinking-type
cctra rectify attach kimi normalize-thinking-type # whitelist a provider
cctra rectify detach kimi normalize-thinking-typeTOML equivalent (manual edit ~/.config/cctra/config.toml):
[rectify.rules]
"normalize-thinking-type" = true
[rectify.providers]
"kimi" = ["normalize-thinking-type"]Scope notes (v1):
- Only TS-built-in rules ship — adding a new rule is a code change (drop a file in
src/convert/upstream/rectify/rules/and add it to the registry). - Plugin upstreams do NOT participate in rectify. Plugins are arbitrary JS and handle their own quirks via
getConfig. If you need plugin-level transforms later, that's atransformRequesthook on the plugin API. - One built-in rule today. The architecture is "onion-style" internally so new rules compose left-to-right without touching the dispatcher.
CLI
cctra add # interactive provider wizard
cctra edit <name> # edit models on a provider (multiselect)
cctra alias # list all aliases (bound + unbound)
cctra alias <name> # show what an alias points to
cctra alias <name> <target> # set/create alias (target is `provider/model` or another alias)
cctra alias add <name> # create an empty alias slot
cctra alias rm <name> # remove an alias
cctra switch [<name>] [<tgt>] # interactive switch (prompts when args omitted)
cctra ls # list aliases + models
cctra show <name> # show provider / plugin details
cctra rm <name> # remove provider / plugin / model (unbinds related aliases)
cctra rename <old> <new> # rename provider (updates alias values automatically)
cctra plugin add <name> <path>
cctra plugin ls / show / enable / disable / rm
cctra serve [--port N] # foreground HTTP serverConfiguration
Persisted at ~/.config/cctra/config.toml (TOML format, edited via CLI).
Plugin configs go in ~/.config/cctra/plugins/<name>/config.json.
Architecture
src/canonical/— protocol-agnostic internal typessrc/convert/— bidirectional protocol conversionssrc/server/— Bun.serve() routes, upstream forwardingsrc/plugin/— local-path plugin loader + author contractsrc/core/resolve.ts—provider/modeland global[aliases]table resolutionsrc/core/alias.ts— auto-alias decision (id globally unique → silentaliases[id] = "provider/id")
For design rationale, request lifecycle, and subsystem deep-dives, see ARCHITECTURE.md.
Credits
The vendor preset list (src/providers/presets.ts) — provider names and endpoint URLs — is derived from cc-switch (MIT, Copyright (c) 2025 Jason Young). Thanks to Jason and the cc-switch contributors for maintaining this comprehensive registry.
License
MIT — see LICENSE.
