Refresh README with polished overview and setup guide

This commit is contained in:
musistudio 2026-06-23 13:59:02 +08:00
parent c267bb293f
commit a8674287bc
26 changed files with 1455 additions and 219 deletions

406
README.md
View file

@ -1,23 +1,92 @@
# Claude Code Router Desktop
<h1 align="center">Claude Code Router Desktop</h1>
Electron desktop wrapper for Claude Code Router. The core gateway runtime is provided by the local `next-ai/gateway` project and installed as:
<p align="center">
<a href="README_zh.md"><img alt="Chinese README" src="https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat" /></a>
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
</p>
```json
"gateway": "file:../../next-ai/gateway"
```
<p align="center">
<img src="blog/images/sponsors/glm-en.jpg" alt="GLM CODING PLAN sponsor" />
</p>
At runtime this app starts two local services:
> **Sponsored by Z.ai.** This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN.
- CCR wrapper: `http://127.0.0.1:3456`
- next-ai core gateway: `http://127.0.0.1:3457`
> GLM CODING PLAN is a subscription service designed for AI coding, starting at just $10/month. It provides access to their flagship GLM-4.7 & (GLM-5 Only Available for Pro Users) model across 10+ popular AI coding tools (Claude Code, Cline, Roo Code, etc.), offering developers top-tier, fast, and stable coding experiences.
The wrapper also owns an internal backend service for local HTTP backend lifecycles and scoped SQLite stores.
> Get 10% OFF GLM CODING PLAN: https://z.ai/subscribe?ic=8JVLJQFSKB
The wrapper reads `~/.claude-code-router/config.json`, preserves the old CCR `Providers` / `Router` format, generates `~/.claude-code-router/gateway.config.json`, and routes Claude Code `POST /v1/messages` requests into the core gateway.
Claude Code Router Desktop is a local gateway and desktop control panel for routing agent requests from Claude Code, Codex, ZCode, and compatible clients to the model provider you actually want to use.
CCR runs on your machine, keeps provider configuration in your local config directory, and exposes a local gateway at `http://127.0.0.1:3456`.
## Why Use CCR
- Use one local endpoint for multiple agent tools instead of configuring every client separately.
- Route different workloads to different models, such as fast background work, reasoning tasks, long-context requests, image tasks, or web-search-capable models.
- Mix providers without changing your workflow. CCR supports OpenAI-compatible APIs, Anthropic Messages, Gemini Generate Content, OpenRouter, DeepSeek, SiliconFlow, Moonshot, Mistral, Z.AI, Bailian, and custom providers.
- Control cost and reliability with fallback routing, API key rotation, usage statistics, and request logs.
- Manage everything from a desktop UI instead of editing JSON by hand.
- Extend the gateway with plugins, proxy routes, local HTTP backends, and provider deeplinks.
## Features
- **Desktop dashboard**: start or stop the local gateway, inspect usage, configure the tray window, and manage runtime settings.
- **Provider management**: add provider presets or custom endpoints, test connectivity, manage credentials, and monitor supported account balances where available.
- **Routing rules**: set default, background, thinking, long-context, image, web-search, subagent, model-prefix, and conditional routing rules.
- **Agent profiles**: configure Claude Code, Codex, and ZCode profiles that point to the CCR gateway.
- **Gateway compatibility**: translate client requests through the local CCR wrapper and the core gateway runtime.
- **Proxy mode**: capture supported API traffic through a local proxy with optional system proxy integration and network capture.
- **Plugins**: install or load wrapper plugins, including routes for Claude Design and Cursor Proxy style integrations.
- **Virtual models**: expose aliases or composed model profiles for clients that expect a specific model name.
- **Provider deeplinks**: import provider configuration through `ccr://provider?...` links after user confirmation.
## Download And Install
1. Open the [GitHub Releases page](https://github.com/musistudio/claude-code-router/releases).
2. Download the package for your platform:
- macOS: `Claude Code Router_<version>.dmg` or `.zip`
- Windows: `Claude Code Router_<version>.exe`
- Linux: `Claude Code Router_<version>.AppImage`
3. Install and launch **Claude Code Router**.
4. On first launch, CCR creates its local configuration:
- macOS/Linux: `~/.claude-code-router/config.json`
- Windows: `%APPDATA%\Claude Code Router\config.json`
CCR starts two local services when the gateway is enabled:
- CCR wrapper gateway: `http://127.0.0.1:3456`
- Core gateway runtime: `http://127.0.0.1:3457`
## Quick Start
CCR can be configured entirely from the desktop UI. Use this setup order for a clean first run.
### 1. Add a provider
Open **Providers**, click **Add Provider**, then choose a built-in preset or create a custom provider. Fill in the provider name, endpoint, protocol, API key, and model list in the form. Use the connectivity check when available, then save the provider.
### 2. Configure routing
Open **Routing** and select which provider/model should handle the default route. Then fill optional routes for background work, thinking requests, long-context requests, image tasks, and web search if you want different models for those scenarios.
Use **Add Routing Rule** when you need more control, such as model-prefix routing, subagent routing, request conditions, or fallback behavior.
### 3. Start the gateway
Open **Server** and click **Start**. Enable auto start if you want CCR to start the local gateway whenever the desktop app opens.
### 4. Connect your agent tool
Open **Profiles** and choose the client you want to use. Configure the Claude Code, Codex, or ZCode profile from the form, select the target model, and apply the profile. For app-based profiles, use the profile action button to open the target app through CCR.
### 5. Monitor and adjust
Use **Dashboard** for usage and provider health, the tray window for quick token and account status, **Network Logs** for debugging provider behavior, and **Extensions** for plugin configuration.
## Provider Deeplink
Supplier websites can open CCR and import a model provider with a custom protocol link:
Provider websites can open CCR and import a model provider with a custom protocol link:
```text
ccr://provider?name=Example%20AI&base_url=https%3A%2F%2Fapi.example.com%2Fv1&api_key=sk-example&models=example-chat%2Cexample-coder&protocol=openai_chat_completions
@ -29,32 +98,18 @@ Supported query parameters:
- `base_url`: provider API base URL. Aliases: `baseUrl`, `api_base_url`, `url`, `endpoint`.
- `api_key`: optional provider API key. Aliases: `apiKey`, `apikey`, `key`, `token`.
- `models`: comma-separated or newline-separated model list. You can also repeat `model=...`.
- `protocol`: one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, or `gemini_generate_content`. Aliases such as `openai`, `responses`, `anthropic`, and `gemini` are accepted.
- `protocol`: one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, or `gemini_generate_content`.
For larger payloads, pass `payload` as URL-encoded JSON or base64url JSON with the same fields:
For larger payloads, pass `payload` as URL-encoded JSON or base64url JSON with the same fields. CCR always opens a confirmation dialog before writing a provider imported from an external link.
```json
{
"name": "Example AI",
"baseUrl": "https://api.example.com/v1",
"apiKey": "sk-example",
"models": ["example-chat", "example-coder"],
"protocol": "openai_chat_completions"
}
```
## Plugins
CCR always opens a confirmation dialog before writing a provider imported from an external link.
CCR has two plugin layers:
## Plugin Architecture
CCR now has two plugin layers:
- Core gateway plugins: keep using `providerPlugins` and `virtualModelProfiles`. They are passed through to `next-ai/gateway`.
- Core gateway plugins: use `providerPlugins` and `virtualModelProfiles`; these are passed through to the core gateway.
- Wrapper plugins: use top-level `plugins` to extend the Electron wrapper, register local HTTP backends, add gateway routes, and route proxy-mode traffic to plugin backends.
SQLite-backed local backend resources are managed by the wrapper's base backend service. They are not installed as a separate marketplace plugin.
Declarative proxy route to an existing backend:
Example wrapper plugin route:
```json
{
@ -78,176 +133,6 @@ Declarative proxy route to an existing backend:
}
```
Executable plugin module paths are resolved from `~/.claude-code-router`; absolute paths and package names are also supported.
Claude Design plugin routing:
The marketplace Claude Design plugin adapts Claude Design chat RPCs into CCR `/v1/messages` calls. Configure upstream APIs through CCR `Providers` as usual, then set Claude Design model routing from the Extensions page with the plugin's configure button. Routes created by the plugin are also shown on the Routing page as plugin-owned rows; they are read-only there and must be edited from the extension configuration dialog.
```json
{
"Providers": [
{
"name": "anthropic-main",
"type": "anthropic_messages",
"baseUrl": "https://api.anthropic.com",
"apiKey": "sk-ant-...",
"models": ["claude-sonnet-4-20250514"]
}
],
"plugins": [
{
"id": "claude-design",
"enabled": true,
"module": "./plugins/claude-design-plugin.cjs",
"config": {
"routing": {
"enabled": true,
"default": "anthropic-main,claude-sonnet-4-20250514",
"rules": [
{
"id": "design-opus",
"name": "Claude Design Opus",
"type": "model",
"model": "claude-opus-4-8",
"target": "anthropic-main,claude-sonnet-4-20250514",
"enabled": true
}
]
}
}
}
]
}
```
Cursor proxy plugin:
The marketplace Cursor Proxy plugin registers proxy-mode routes for all paths on `api*.cursor.sh`, forwards OpenAI/Anthropic/Gemini-compatible JSON LLM requests to the local CCR gateway, and uses the configured CCR API key automatically. For Cursor Agent traffic, it bridges the private protobuf `BidiAppend` + `AgentService/RunSSE` flow into CCR's `/v1/chat/completions` gateway and streams the gateway response back as Cursor `AgentServerMessage` events. It also attempts to decode Cursor native Connect JSON/protobuf LLM RPCs under `aiserver.v1.*` and `agent.v1.*`, preserving decoded system prompts, tools, tool choices, tool calls, and tool results when those fields are present in the native payload.
Cursor often sends Agent requests with `model: "default"` or another Cursor-local model name. Configure Cursor Proxy model routing from the Extensions page with the plugin's configure button, or set `config.routing` manually. Route targets use the same provider/model selector format as Claude Design plugin routing. The plugin rewrites Cursor's source model to the selected CCR target model before forwarding it to the gateway, so the core gateway does not need a model literally named `default`.
```json
{
"Providers": [
{
"name": "openrouter",
"type": "openai_chat_completions",
"baseUrl": "https://openrouter.ai/api/v1",
"apiKey": "sk-or-...",
"models": ["anthropic/claude-sonnet-4.5", "google/gemini-3-pro-preview"]
}
],
"proxy": {
"enabled": true,
"mode": "gateway"
},
"plugins": [
{
"id": "cursor-proxy",
"enabled": true,
"module": "./plugins/cursor-proxy-plugin.cjs",
"config": {
"routing": {
"enabled": true,
"default": "openrouter,anthropic/claude-sonnet-4.5",
"rules": [
{
"id": "cursor-default",
"name": "Cursor default",
"type": "model",
"model": "default",
"target": "openrouter,anthropic/claude-sonnet-4.5",
"enabled": true
}
]
}
}
}
]
}
```
Other unsupported native Cursor RPC traffic is passed through to Cursor by default; set `"fallbackToCursor": false` to fail unsupported requests instead. Set `paths` only if you intentionally want to restrict which Cursor paths the plugin captures.
`cursorBidiProto`, `cursorConnectJson`, and `cursorNativeProto` are enabled by default. Set them to `false` only when you want Cursor's private Agent protobuf, Connect JSON, or generic native LLM RPC traffic to pass through untouched. `bidiWaitMs`, `bidiSessionTtlMs`, and `gatewayTimeoutMs` can be tuned for slow clients or slow upstream providers.
Generic native RPC decoding is intentionally limited to Cursor methods that look like generation or streaming LLM calls. Metadata and status calls such as model pickers, repository sync, analytics, dashboards, and file sync are passed through to Cursor. If a new Cursor LLM method is not detected yet, add its method name or full RPC path to `cursorNativeLlmMethods`.
If Cursor sends an OpenAI-compatible `*/chat/completions` request that already includes `system` messages or `tools`, Cursor Proxy and the CCR gateway preserve them. Some Cursor custom-provider flows send only user messages; the proxy cannot recover system/tool context that is not present in the incoming request. For those flows, configure fallback context explicitly:
```json
{
"plugins": [
{
"id": "cursor-proxy",
"config": {
"systemPrompt": "You are Cursor in agent mode.",
"tools": [
{
"name": "read_file",
"description": "Read a file from the workspace.",
"input_schema": {
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
}
}
],
"toolChoice": "auto"
}
}
]
}
```
The plugin does not define a separate provider format. Configure upstream APIs through CCR's existing `Providers`, `Router`, `providerPlugins`, and `virtualModelProfiles`; Cursor Proxy only adapts Cursor-compatible request paths and forwards them to the local CCR gateway. Legacy `targetProvider`, `targetProviders`, and `targetModel` are still accepted and converted into a routing target when `routing.default` is not set, but `config.routing` is preferred:
```json
{
"Providers": [
{
"name": "anthropic-main",
"type": "anthropic_messages",
"baseUrl": "https://api.anthropic.com",
"apiKey": "sk-ant-...",
"models": ["claude-sonnet-4-20250514"]
}
],
"Router": {
"default": "anthropic-main,claude-sonnet-4-20250514"
},
"plugins": [
{
"id": "cursor-proxy",
"module": "./plugins/cursor-proxy-plugin.cjs",
"config": {
"routing": {
"enabled": true,
"default": "anthropic-main,claude-sonnet-4-20250514"
}
}
}
]
}
```
Local plugin directories can declare dependencies in `plugin.json`, `ccr-plugin.json`, `.ccr-plugin/plugin.json`, `.codex-plugin/plugin.json`, or under `ccr.dependencies` / `ccrPlugin.dependencies` in `package.json`:
```json
{
"id": "my-plugin",
"module": "./index.cjs",
"dependencies": [
"claude-design",
{ "id": "local-helper", "module": "../local-helper/index.cjs" }
]
}
```
Dependencies declared by ID are resolved from the marketplace; dependencies with `module`, `path`, or `modulePath` are installed from that local path.
Plugin modules export a function or object with `setup(ctx)`. The context supports:
- `ctx.registerGatewayRoute({ method, path, auth, handler })`
@ -257,9 +142,9 @@ Plugin modules export a function or object with `setup(ctx)`. The context suppor
- `ctx.registerCoreGatewayProviderPlugin(plugin)`
- `ctx.registerCoreGatewayVirtualModelProfile(profile)`
`ctx.registerHttpBackend` and `ctx.openSqliteStore` are backed by the wrapper's base backend service, so plugin modules do not need to ship or install a SQLite backend plugin.
Local plugin examples are available in [examples/plugins](examples/plugins).
## Scripts
## Development
```bash
npm install
@ -273,10 +158,117 @@ npm run build:app
`npm run build` packages the app for the current platform and writes installer artifacts to `release/`.
`npm run build:app` packages both macOS and Windows artifacts with `electron-builder --mac --win`. You can also run `npm run build:app:mac` or `npm run build:app:win` for a single platform. Cross-building Windows installers from macOS may require Wine; otherwise run the Windows build command on Windows.
`npm run build:app` packages macOS and Windows artifacts with `electron-builder --mac --win`. You can also run `npm run build:app:mac` or `npm run build:app:win` for a single platform. Linux AppImage packaging is configured in `electron-builder.json`.
## Online Updates
Packaged builds check GitHub Releases for updates through `electron-updater`. For local update feed testing, set `CCR_UPDATE_FEED_URL` to a generic electron-updater feed URL before starting the app. `CCR_UPDATE_ALLOW_PRERELEASE=1` enables prerelease updates.
Packaged builds check GitHub Releases for updates through `electron-updater`. The builder config publishes update metadata for `musistudio/claude-code-router`; attach the generated installer artifacts and `latest*.yml` files to each release. macOS builds include both `dmg` and `zip` targets because the updater requires the zip artifact.
## Further Reading
For local update feed testing, set `CCR_UPDATE_FEED_URL` to a generic electron-updater feed URL before starting the app. `CCR_UPDATE_ALLOW_PRERELEASE=1` enables prerelease updates.
- [Project Motivation and How It Works](blog/en/project-motivation-and-how-it-works.md)
- [Maybe We Can Do More with the Router](blog/en/maybe-we-can-do-more-with-the-route.md)
## Support & Sponsoring
If you find this project helpful, please consider sponsoring its development. Your support is greatly appreciated.
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)
[Paypal](https://paypal.me/musistudio1999)
<table>
<tr>
<td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td>
<td><img src="/blog/images/wechat.jpg" width="200" alt="WeChat Pay" /></td>
</tr>
</table>
### Our Sponsors
A huge thank you to all our sponsors for their generous support.
- [AIHubmix](https://aihubmix.com/)
- [BurnCloud](https://ai.burncloud.com)
- @Simon Leischnig
- [@duanshuaimin](https://github.com/duanshuaimin)
- [@vrgitadmin](https://github.com/vrgitadmin)
- @\*o
- [@ceilwoo](https://github.com/ceilwoo)
- @\*说
- @\*更
- @K\*g
- @R\*R
- [@bobleer](https://github.com/bobleer)
- @\*苗
- @\*划
- [@Clarence-pan](https://github.com/Clarence-pan)
- [@carter003](https://github.com/carter003)
- @S\*r
- @\*晖
- @\*敏
- @Z\*z
- @\*然
- [@cluic](https://github.com/cluic)
- @\*苗
- [@PromptExpert](https://github.com/PromptExpert)
- @\*应
- [@yusnake](https://github.com/yusnake)
- @\*飞
- @董\*
- @\*汀
- @\*涯
- @\*:-
- @\*\*磊
- @\*琢
- @\*成
- @Z\*o
- @\*琨
- [@congzhangzh](https://github.com/congzhangzh)
- @\*\_
- @Z\*m
- @\*鑫
- @c\*y
- @\*昕
- [@witsice](https://github.com/witsice)
- @b\*g
- @\*亿
- @\*辉
- @JACK
- @\*光
- @W\*l
- [@kesku](https://github.com/kesku)
- [@biguncle](https://github.com/biguncle)
- @二吉吉
- @a\*g
- @\*林
- @\*咸
- @\*明
- @S\*y
- @f\*o
- @\*智
- @F\*t
- @r\*c
- [@qierkang](http://github.com/qierkang)
- @\*军
- [@snrise-z](http://github.com/snrise-z)
- @\*王
- [@greatheart1000](http://github.com/greatheart1000)
- @\*王
- @zcutlip
- [@Peng-YM](http://github.com/Peng-YM)
- @\*更
- @\*.
- @F\*t
- @\*政
- @\*铭
- @\*叶
- @七\*o
- @\*青
- @\*\*晨
- @\*远
- @\*霄
- @\*\*吉
- @\*\*飞
- @\*\*驰
- @x\*g
(If your name is masked, please contact me via my homepage email to update it with your GitHub username.)

274
README_zh.md Normal file
View file

@ -0,0 +1,274 @@
<h1 align="center">Claude Code Router Desktop</h1>
<p align="center">
<a href="README.md"><img alt="English README" src="https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat" /></a>
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
</p>
<p align="center">
<img src="blog/images/sponsors/glm-zh.jpg" alt="GLM CODING PLAN 赞助" />
</p>
> **Z智谱赞助支持。** 本项目由 Z智谱提供赞助他们通过 GLM CODING PLAN 对本项目提供技术支持。
> GLM CODING PLAN 是专为 AI 编码打造的订阅套餐,每月最低仅需 20 元,即可在十余款主流 AI 编码工具如 Claude Code、Cline、Roo Code 中畅享智谱旗舰模型 GLM-4.7(受限于算力,目前仅限 Pro 用户开放),为开发者提供顶尖的编码体验。
> 智谱 AI 为本产品提供了特别优惠使用以下链接购买可以享受九折优惠https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII
Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。
CCR 在你的本机运行Provider 配置保存在本地配置目录,并默认暴露本地网关地址:`http://127.0.0.1:3456`
## 为什么使用 CCR
- 用一个本地入口连接多个 Agent 工具,不需要在每个客户端里重复配置 Provider。
- 不同任务使用不同模型,例如后台任务、推理任务、长上下文、图片任务或支持联网搜索的模型。
- 在不改变工作流的情况下混用不同 Provider。CCR 支持 OpenAI 兼容 API、Anthropic Messages、Gemini Generate Content、OpenRouter、DeepSeek、SiliconFlow、Moonshot、Mistral、Z.AI、百炼以及自定义 Provider。
- 通过 fallback 路由、API Key 轮换、用量统计和请求日志来控制成本和可靠性。
- 使用桌面 UI 管理配置,减少手写 JSON。
- 通过插件、代理路由、本地 HTTP 后端和 Provider deeplink 扩展网关能力。
## 功能和特性
- **桌面控制台**:启动或停止本地网关,查看用量,配置托盘窗口和运行时设置。
- **Provider 管理**:添加预设或自定义端点,检测连通性,管理凭据,并在可用时查看账号余额。
- **路由规则**配置默认、后台、thinking、长上下文、图片、Web Search、Subagent、模型前缀和条件路由。
- **Agent Profiles**:为 Claude Code、Codex 和 ZCode 配置指向 CCR 网关的 Profile。
- **网关兼容层**:通过本地 CCR wrapper 和 core gateway runtime 转换客户端请求。
- **代理模式**:通过本地代理捕获支持的 API 流量,可选系统代理和网络捕获。
- **插件系统**:安装或加载 wrapper 插件,包括 Claude Design、Cursor Proxy 这类集成路由。
- **虚拟模型**:为客户端暴露模型别名或组合模型配置,适配固定模型名场景。
- **Provider Deeplink**:通过 `ccr://provider?...` 链接导入 Provider 配置,写入前会弹出确认。
## 下载和安装
1. 打开 [GitHub Releases 页面](https://github.com/musistudio/claude-code-router/releases)。
2. 按系统下载对应安装包:
- macOS`Claude Code Router_<version>.dmg``.zip`
- Windows`Claude Code Router_<version>.exe`
- Linux`Claude Code Router_<version>.AppImage`
3. 安装并启动 **Claude Code Router**
4. 首次启动后CCR 会创建本地配置:
- macOS/Linux`~/.claude-code-router/config.json`
- Windows`%APPDATA%\Claude Code Router\config.json`
启用网关后CCR 会启动两个本地服务:
- CCR wrapper gateway`http://127.0.0.1:3456`
- Core gateway runtime`http://127.0.0.1:3457`
## 快速开始
CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序操作。
### 1. 添加 Provider
打开 **Providers**,点击 **Add Provider**,选择内置预设或创建自定义 Provider。按表单填写 Provider 名称、端点、协议、API Key 和模型列表。可用时先运行连通性检测,然后保存 Provider。
### 2. 设置路由
打开 **Routing**,先选择默认路由要使用的 provider/model。然后根据需要设置后台任务、Thinking、长上下文、图片任务和 Web Search 等场景的专用模型。
如果需要更细粒度控制,使用 **Add Routing Rule** 添加模型前缀、Subagent、请求条件或 fallback 规则。
### 3. 启动网关
打开 **Server**,点击 **Start** 启动本地网关。如果希望每次打开桌面应用时自动启动网关,可以启用 auto start。
### 4. 连接 Agent 工具
打开 **Profiles**,选择要使用的客户端。通过表单配置 Claude Code、Codex 或 ZCode Profile选择目标模型并应用配置。对于 App 类型的 Profile可以使用页面里的操作按钮通过 CCR 打开目标应用。
### 5. 日常查看和调整
使用 **Dashboard** 查看用量和 Provider 状态,使用托盘窗口快速查看 Token 和账号状态,使用 **Network Logs** 调试 Provider 行为,使用 **Extensions** 配置插件。
## Provider Deeplink
Provider 网站可以通过自定义协议打开 CCR 并导入模型服务配置:
```text
ccr://provider?name=Example%20AI&base_url=https%3A%2F%2Fapi.example.com%2Fv1&api_key=sk-example&models=example-chat%2Cexample-coder&protocol=openai_chat_completions
```
支持的 query 参数:
- `name`Provider 展示名称。
- `base_url`Provider API Base URL。别名`baseUrl``api_base_url``url``endpoint`
- `api_key`:可选 Provider API Key。别名`apiKey``apikey``key``token`
- `models`:逗号或换行分隔的模型列表,也可以重复传入 `model=...`
- `protocol``openai_chat_completions``openai_responses``anthropic_messages``gemini_generate_content`
更大的 payload 可以通过 URL 编码 JSON 或 base64url JSON 传入 `payload` 字段。CCR 在写入外部链接导入的 Provider 前,总会弹出确认窗口。
## 插件
CCR 有两层插件:
- Core gateway plugins使用 `providerPlugins``virtualModelProfiles`,会透传给 core gateway。
- Wrapper plugins使用顶层 `plugins` 扩展 Electron wrapper注册本地 HTTP 后端、添加 gateway route或把代理模式流量路由到插件后端。
Wrapper plugin route 示例:
```json
{
"plugins": [
{
"id": "local-admin-api",
"enabled": true,
"proxy": {
"routes": [
{
"id": "admin-api",
"host": "api.example.com",
"paths": ["/v1/admin"],
"upstream": "http://127.0.0.1:4510",
"stripPathPrefix": false
}
]
}
}
]
}
```
插件模块需要导出函数或包含 `setup(ctx)` 的对象。上下文支持:
- `ctx.registerGatewayRoute({ method, path, auth, handler })`
- `ctx.registerHttpBackend({ id, host, port, handler })`
- `ctx.registerProxyRoute({ host, paths, upstream, stripPathPrefix, rewritePathPrefix, headers })`
- `ctx.openSqliteStore({ filename, migrate })`
- `ctx.registerCoreGatewayProviderPlugin(plugin)`
- `ctx.registerCoreGatewayVirtualModelProfile(profile)`
本地插件示例见 [examples/plugins](examples/plugins)。
## 开发
```bash
npm install
npm run dev
npm run typecheck
npm run build:assets
npm run build:app
```
`npm run build:assets` 会把 Electron main process 和 renderer assets 编译到 `dist/`
`npm run build` 会为当前平台打包应用,并把安装包写入 `release/`
`npm run build:app` 会通过 `electron-builder --mac --win` 打包 macOS 和 Windows 产物。你也可以使用 `npm run build:app:mac``npm run build:app:win` 单独打包一个平台。Linux AppImage 打包配置在 `electron-builder.json` 中。
打包后的应用会通过 `electron-updater` 检查 GitHub Releases。测试本地更新源时可以在启动应用前设置 `CCR_UPDATE_FEED_URL` 为 generic electron-updater feed URL。`CCR_UPDATE_ALLOW_PRERELEASE=1` 可以启用 prerelease 更新。
## 深入阅读
- [项目动机和工作原理](blog/zh/项目初衷及原理.md)
- [也许我们可以用路由器做更多事情](blog/zh/或许我们能在Router中做更多事情.md)
## 支持与赞助
如果你觉得这个项目有帮助,欢迎赞助项目开发。非常感谢你的支持。
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)
[Paypal](https://paypal.me/musistudio1999)
<table>
<tr>
<td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td>
<td><img src="/blog/images/wechat.jpg" width="200" alt="WeChat Pay" /></td>
</tr>
</table>
### 我们的赞助商
非常感谢所有赞助商的慷慨支持。
- [AIHubmix](https://aihubmix.com/)
- [BurnCloud](https://ai.burncloud.com)
- @Simon Leischnig
- [@duanshuaimin](https://github.com/duanshuaimin)
- [@vrgitadmin](https://github.com/vrgitadmin)
- @\*o
- [@ceilwoo](https://github.com/ceilwoo)
- @\*说
- @\*更
- @K\*g
- @R\*R
- [@bobleer](https://github.com/bobleer)
- @\*苗
- @\*划
- [@Clarence-pan](https://github.com/Clarence-pan)
- [@carter003](https://github.com/carter003)
- @S\*r
- @\*晖
- @\*敏
- @Z\*z
- @\*然
- [@cluic](https://github.com/cluic)
- @\*苗
- [@PromptExpert](https://github.com/PromptExpert)
- @\*应
- [@yusnake](https://github.com/yusnake)
- @\*飞
- @董\*
- @\*汀
- @\*涯
- @\*:-
- @\*\*磊
- @\*琢
- @\*成
- @Z\*o
- @\*琨
- [@congzhangzh](https://github.com/congzhangzh)
- @\*\_
- @Z\*m
- @\*鑫
- @c\*y
- @\*昕
- [@witsice](https://github.com/witsice)
- @b\*g
- @\*亿
- @\*辉
- @JACK
- @\*光
- @W\*l
- [@kesku](https://github.com/kesku)
- [@biguncle](https://github.com/biguncle)
- @二吉吉
- @a\*g
- @\*林
- @\*咸
- @\*明
- @S\*y
- @f\*o
- @\*智
- @F\*t
- @r\*c
- [@qierkang](http://github.com/qierkang)
- @\*军
- [@snrise-z](http://github.com/snrise-z)
- @\*王
- [@greatheart1000](http://github.com/greatheart1000)
- @\*王
- @zcutlip
- [@Peng-YM](http://github.com/Peng-YM)
- @\*更
- @\*.
- @F\*t
- @\*政
- @\*铭
- @\*叶
- @七\*o
- @\*青
- @\*\*晨
- @\*远
- @\*霄
- @\*\*吉
- @\*\*飞
- @\*\*驰
- @x\*g
(如果你的名字被打码,请通过我的主页邮箱联系我更新为 GitHub 用户名。)

View file

@ -0,0 +1,105 @@
# Maybe We Can Do More with the Router
Since the release of `claude-code-router`, Ive received a lot of user feedback, and quite a few issues are still open. Most of them are related to support for different providers and the lack of tool usage from the deepseek model.
Originally, I created this project for personal use, mainly to access claude code at a lower cost. So, multi-provider support wasnt part of the initial design. But during troubleshooting, I discovered that even though most providers claim to be compatible with the OpenAI-style `/chat/completions` interface, there are many subtle differences. For example:
1. When Gemini's tool parameter type is string, the `format` field only supports `date` and `date-time`, and theres no tool call ID.
2. OpenRouter requires `cache_control` for caching.
3. The official DeepSeek API has a `max_output` of 8192, but Volcano Engines limit is even higher.
Aside from these, smaller providers often have quirks in their parameter handling. So I decided to create a new project, [musistudio/llms](https://github.com/musistudio/llms), to deal with these compatibility issues. It uses the OpenAI format as a base and introduces a generic Transformer interface for transforming both requests and responses.
Once a `Transformer` is implemented for each provider, it becomes possible to mix-and-match requests between them. For example, I implemented bidirectional conversion between Anthropic and OpenAI formats in `AnthropicTransformer`, which listens to the `/v1/messages` endpoint. Similarly, `GeminiTransformer` handles Gemini <-> OpenAI format conversions and listens to `/v1beta/models/:modelAndAction`.
When both requests and responses are transformed into a common format, they can interoperate seamlessly:
```
AnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer
```
```
GeminiResponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse
```
Using a middleware layer to smooth out differences may introduce some performance overhead, but the main goal here is to enable `claude-code-router` to support multiple providers.
As for the issue of DeepSeeks lackluster tool usage — I found that it stems from poor instruction adherence in long conversations. Initially, the model actively calls tools, but after several rounds, it starts responding with plain text instead. My first workaround was injecting a system prompt to remind the model to use tools proactively. But in long contexts, the model tends to forget this instruction.
After reading the DeepSeek documentation, I noticed it supports the `tool_choice` parameter, which can be set to `"required"` to force the model to use at least one tool. I tested this by enabling the parameter, and it significantly improved the models tool usage. We can remove the setting when it's no longer necessary. With the help of the `Transformer` interface in [musistudio/llms](https://github.com/musistudio/llms), we can modify the request before its sent and adjust the response after its received.
Inspired by the Plan Mode in `claude code`, I implemented a similar Tool Mode for DeepSeek:
```typescript
export class TooluseTransformer implements Transformer {
name = "tooluse";
transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {
if (request.tools?.length) {
request.messages.push({
role: "system",
content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.
Before invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \`ExitTool\` to exit tool mode — this is the only valid way to terminate tool mode.
Always prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,
});
request.tool_choice = "required";
request.tools.unshift({
type: "function",
function: {
name: "ExitTool",
description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.
IMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.
Examples:
1. Task: "Use a tool to summarize this document" — Do not use ExitTool if a summarization tool is available.
2. Task: "Whats the weather today?" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,
parameters: {
type: "object",
properties: {
response: {
type: "string",
description:
"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.",
},
},
required: ["response"],
},
},
});
}
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
if (
jsonResponse?.choices[0]?.message.tool_calls?.length &&
jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===
"ExitTool"
) {
const toolArguments = JSON.parse(toolCall.function.arguments || "{}");
jsonResponse.choices[0].message.content = toolArguments.response || "";
delete jsonResponse.choices[0].message.tool_calls;
}
// Handle non-streaming response if needed
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
// ...
}
return response;
}
}
```
This transformer ensures the model calls at least one tool. If no tools are appropriate or the task is finished, it can exit using `ExitTool`. Since this relies on the `tool_choice` parameter, it only works with models that support it.
In practice, this approach noticeably improves tool usage for DeepSeek. The tradeoff is that sometimes the model may invoke irrelevant or unnecessary tools, which could increase latency and token usage.
This update is just a small experiment — adding an `“agent”` to the router. Maybe there are more interesting things we can explore from here.

View file

@ -0,0 +1,154 @@
# Progressive Disclosure of Agent Tools from the Perspective of CLI Tool Style
It has been nearly two months since Anthropic released [Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills). In this release, Anthropic mentioned a term: Progressive Disclosure. What exactly is this? What problem does it solve?
Actually, in my Vibe Coding workflow, I rarely use MCP. The reason is that I find the implementation quality of MCP to be inconsistent. At its core, its about context injection (the essence of tools is also context injection), and Im not sure if prompts written by others might affect my workflow, so I simply avoid using it altogether. The current implementation of MCP essentially wraps all functionalities as tools exposed to the Agent (one functionality wrapped as one tool, given a detailed description, telling the agent when to call it and what the parameter format is). This has led to the current explosion of prompts.
It wasnt until Anthropic released Skills and I looked into it that I realized its essence is still prompt injection. If MCP provides a specification for injecting tools, then what Skills advocates is somewhat "unconventional." Skills provides a Markdown document to describe the purpose and best practices of the skill, along with some attached scripts (different from MCP).
![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F6f22d8913dbc6228e7f11a41e0b3c124d817b6d2-1650x929.jpg&w=3840&q=75)
Since these scripts run directly on the users local machine, there are significant security risks. If users cannot review the script code, it can easily lead to serious security issues such as data leakage or virus infections. Compared to MCP, which provides a standardized interface, Skills offer a series of script files. Different skills may have different types of script files—for example, some scripts are implemented in Node.js, while others use Python. To use these scripts, users also need to install the corresponding runtime and dependencies. This is why I describe it as "unconventional."
Is this really the best practice?
Regarding Progressive Disclosure, here is how Anthropic describes it:
> Progressive disclosure is the core design principle that makes Agent Skills flexible and scalable. Like a well-organized manual that starts with a table of contents, then specific chapters, and finally a detailed appendix, skills let Claude load information only as needed:
> ![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fa3bca2763d7892982a59c28aa4df7993aaae55ae-2292x673.jpg&w=3840&q=75)
> Agents with a filesystem and code execution tools dont need to read the entirety of a skill into their context window when working on a particular task. This means that the amount of context that can be bundled into a skill is effectively unbounded.
The following diagram shows how the context window changes when a skill is triggered by a users message.
![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F441b9f6cc0d2337913c1f41b05357f16f51f702e-1650x929.jpg&w=3840&q=75)
Do we really need to implement it this way?
In our daily use of CLI tools, most CLI tools come with a `--help` parameter for viewing the tool's usage and instructions. Isn't that essentially the tool's user manual? For example:
```shell
> npm --help
npm <command>
Usage:
npm install install all the dependencies in your project
npm install <foo> add the <foo> dependency to your project
npm test run this project's tests
npm run <foo> run the script named <foo>
npm <command> -h quick help on <command>
npm -l display usage info for all commands
npm help <term> search for help on <term>
npm help npm more involved overview
All commands:
access, adduser, audit, bugs, cache, ci, completion,
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
edit, exec, explain, explore, find-dupes, fund, get, help,
help-search, hook, init, install, install-ci-test,
install-test, link, ll, login, logout, ls, org, outdated,
owner, pack, ping, pkg, prefix, profile, prune, publish,
query, rebuild, repo, restart, root, run-script, sbom,
search, set, shrinkwrap, star, stars, start, stop, team,
test, token, uninstall, unpublish, unstar, update, version,
view, whoami
Specify configs in the ini-formatted file:
/Users/xxx/.npmrc
or on the command line via: npm <command> --key=value
More configuration info: npm help config
Configuration fields: npm help 7 config
```
This manual doesn't return every possible usage of every command either. It only lists which commands are available and what functions they can perform. For the specific usage of a command, you can still obtain it by using the `--help` parameter:
```shell
> npm install --help
Install a package
Usage:
npm install [<package-spec> ...]
Options:
[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]
[-E|--save-exact] [-g|--global]
[--install-strategy <hoisted|nested|shallow|linked>] [--legacy-bundling]
[--global-style] [--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]
[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]
[--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links]
[--no-fund] [--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root] [--install-links]
aliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall
Run "npm help install" for more info
```
Doesn't this resemble the definition of progressive disclosure mentioned above?
Can we implement an MCP in this style to achieve progressive disclosure of tools without needing skills? I used Codex to convert the official PDF Skill into an MCP, exposing only a single tool:
```json
{
"name": "mcp__pdf__pdf",
"description": "Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.You need to pass in the --help parameter to obtain the usage of this tool first.",
"input_schema": {
"type": "object",
"properties": {
"params": {
"$ref": "#/$defs/PdfCommandInput"
}
},
"required": [
"params"
],
"$defs": {
"PdfCommandInput": {
"additionalProperties": false,
"properties": {
"argv": {
"description": "Command tokens excluding the leading 'pdf' executable name",
"items": {
"type": "string"
},
"title": "Argv",
"type": "array"
},
"command": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional command string; if set, it will be split like a shell",
"title": "Command"
}
},
"title": "PdfCommandInput",
"type": "object"
}
},
"title": "pdfArguments"
}
}
```
Below is the change in the context window when using this MCP:
```json
User: Extract all the text from test.pdf and write it into result.txt
Agent: Tool(name="pdf", args=["--help"])
Tool: pdf - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale在使用之前你需要先传入--help参数以获取该工具的用法\n\nUsage:\n pdf [--help] <domain> <command> [options]\n\nDomains:\n forms Fill, extract, and validate PDF form fields.\n documents Create, merge, and split PDFs.\n text Extract text content from PDFs.\n tables Extract tables (heuristic, whitespace-based).\n images Render PDF pages to PNG images.\n\nRun `pdf <domain> --help` for domain help.\nRun `pdf <domain> <command> --help` for command help.
Agent: Tool(name="pdf", args=["text", "--help"])
Tool: text - Extract text content from PDFs.\n\nUsage:\n pdf text <command> [options]\n\nCommands:\n extract Extract text content from a PDF.\n\nRun `pdf text <command> --help` for details.
Agent: Tool(name="pdf", args=["text", "extract", "--help"])
Tool: usage: pdf text extract --pdf PDF_PATH [--pages PAGES] [--out OUTPUT_PATH]\n [--format {text,json}] [--overwrite]\n\nExtract text content from a PDF.\n\noptions:\n --pdf PDF_PATH Input PDF\n --pages PAGES Page selection like 1,3-5 or 'all' (default: all)\n --out OUTPUT_PATH Write extracted text to a file\n --format {text,json} Output format (default: text)\n --overwrite Overwrite output file if it exists\n
Agent: Tool(name="pdf", args=["text", "extract", "--pdf", "test.pdf", "--out", "result.txt", "--overwrite"])
Tool: {"result": {"stdout": "xx","data": {"pdf_path": "test.pdf","page_count": 6,"pages": [xxx],"format": "text","output_path": "result.txt"}}}
Agent: xxx
```
From the perspective of context changes, progressive disclosure has been fully realized. This MCP code is open-source (entirely written by Codex, serving only to validate the idea without any review): https://github.com/musistudio/pdf-skill-mcp
If you have any thoughts or ideas, Id also welcome the opportunity to discuss them with you. email: [m@musiiot.top](mailto://m@musiiot.top )

View file

@ -0,0 +1,103 @@
# Project Motivation and Principles
As early as the day after Claude Code was released (2025-02-25), I began and completed a reverse engineering attempt of the project. At that time, using Claude Code required registering for an Anthropic account, applying for a waitlist, and waiting for approval. However, due to well-known reasons, Anthropic blocks users from mainland China, making it impossible for me to use the service through normal means. Based on known information, I discovered the following:
1. Claude Code is installed via npm, so it's very likely developed with Node.js.
2. Node.js offers various debugging methods: simple `console.log` usage, launching with `--inspect` to hook into Chrome DevTools, or even debugging obfuscated code using `d8`.
My goal was to use Claude Code without an Anthropic account. I didnt need the full source code—just a way to intercept and reroute requests made by Claude Code to Anthropics models to my own custom endpoint. So I started the reverse engineering process:
1. First, install Claude Code:
```bash
npm install -g @anthropic-ai/claude-code
```
2. After installation, the project is located at `~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`(this may vary depending on your Node version manager and version).
3. Open the package.json to analyze the entry point:
```package.json
{
"name": "@anthropic-ai/claude-code",
"version": "1.0.24",
"main": "sdk.mjs",
"types": "sdk.d.ts",
"bin": {
"claude": "cli.js"
},
"engines": {
"node": ">=18.0.0"
},
"type": "module",
"author": "Boris Cherny <boris@anthropic.com>",
"license": "SEE LICENSE IN README.md",
"description": "Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.",
"homepage": "https://github.com/anthropics/claude-code",
"bugs": {
"url": "https://github.com/anthropics/claude-code/issues"
},
"scripts": {
"prepare": "node -e \"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\"",
"preinstall": "node scripts/preinstall.js"
},
"dependencies": {},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
}
}
```
The key entry is `"claude": "cli.js"`. Opening cli.js, you'll see the code is minified and obfuscated. But using WebStorms `Format File` feature, you can reformat it for better readability:
![webstorm-formate-file](../images/webstorm-formate-file.png)
Now you can begin understanding Claude Codes internal logic and prompt structure by reading the code. To dig deeper, you can insert console.log statements or launch in debug mode with Chrome DevTools using:
```bash
NODE_OPTIONS="--inspect-brk=9229" claude
```
This command starts Claude Code in debug mode and opens port 9229. Visit chrome://inspect/ in Chrome and click inspect to begin debugging:
![chrome-devtools](../images/chrome-inspect.png)
![chrome-devtools](../images/chrome-devtools.png)
By searching for the keyword api.anthropic.com, you can easily locate where Claude Code makes its API calls. From the surrounding code, it's clear that baseURL can be overridden with the `ANTHROPIC_BASE_URL` environment variable, and `apiKey` and `authToken` can be configured similarly:
![search](../images/search.png)
So far, weve discovered some key information:
1. Environment variables can override Claude Code's `baseURL` and `apiKey`.
2. Claude Code adheres to the Anthropic API specification.
Therefore, we need:
1. A service to convert OpenAI APIcompatible requests into Anthropic API format.
2. Set the environment variables before launching Claude Code to redirect requests to this service.
Thus, `claude-code-router` was born. This project uses `Express.js` to implement the `/v1/messages` endpoint. It leverages middlewares to transform request/response formats and supports request rewriting (useful for prompt tuning per model).
Back in February, the full DeepSeek model series had poor support for Function Calling, so I initially used `qwen-max`. It worked well—but without KV cache support, it consumed a large number of tokens and couldnt provide the native `Claude Code` experience.
So I experimented with a Router-based mode using a lightweight model to dispatch tasks. The architecture included four roles: `router`, `tool`, `think`, and `coder`. Each request passed through a free lightweight model that would decide whether the task involved reasoning, coding, or tool usage. Reasoning and coding tasks looped until a tool was invoked to apply changes. However, the lightweight model lacked the capability to route tasks accurately, and architectural issues prevented it from effectively driving Claude Code.
Everything changed at the end of May when the official Claude Code was launched, and `DeepSeek-R1` model (released 2025-05-28) added Function Call support. I redesigned the system. With the help of AI pair programming, I fixed earlier request/response transformation issues—especially the handling of models that return JSON instead of Function Call outputs.
This time, I used the `DeepSeek-V3` model. It performed better than expected: supporting most tool calls, handling task decomposition and stepwise planning, and—most importantly—costing less than one-tenth the price of Claude 3.5 Sonnet.
The official Claude Code organizes agents differently from the beta version, so I restructured my Router mode to include four roles: the default model, `background`, `think`, and `longContext`.
- The default model handles general tasks and acts as a fallback.
- The `background` model manages lightweight background tasks. According to Anthropic, Claude Haiku 3.5 is often used here, so I routed this to a local `ollama` service.
- The `think` model is responsible for reasoning and planning mode tasks. I use `DeepSeek-R1` here, though it doesnt support cost control, so `Think` and `UltraThink` behave identically.
- The `longContext` model handles long-context scenarios. The router uses `tiktoken` to calculate token lengths in real time, and if the context exceeds 32K, it switches to this model to compensate for DeepSeek's long-context limitations.
This describes the evolution and reasoning behind the project. By cleverly overriding environment variables, we can forward and modify requests without altering Claude Codes source—allowing us to benefit from official updates while using our own models and custom prompts.
This project offers a practical approach to running Claude Code under Anthropics regional restrictions, balancing `cost`, `performance`, and `customizability`. That said, the official `Max Plan` still offers the best experience if available.

BIN
blog/images/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
blog/images/claude-code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

67
blog/images/roadmap.svg Normal file
View file

@ -0,0 +1,67 @@
<svg viewBox="0 0 1200 420" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
.road { stroke: #7aa2ff; stroke-width: 6; fill: none; filter: drop-shadow(0 6px 18px rgba(122,162,255,0.25)); }
.dash { stroke: rgba(122,162,255,0.25); stroke-width: 6; fill: none; stroke-dasharray: 2 18; }
.node { filter: drop-shadow(0 3px 10px rgba(126,240,193,0.35)); }
.node-circle { fill: #7ef0c1; }
.node-core { fill: #181b22; stroke: white; stroke-width: 1.5; }
.label-bg { fill: rgba(24,27,34,0.8); stroke: rgba(255,255,255,0.12); rx: 12; }
.label-text { fill: #e8ecf1; font-weight: 700; font-size: 14px; font-family: Arial, sans-serif; }
.label-sub { fill: #9aa6b2; font-weight: 500; font-size: 12px; font-family: Arial, sans-serif; }
.spark { fill: none; stroke: #ffd36e; stroke-width: 1.6; stroke-linecap: round; }
</style>
</defs>
<!-- Background road with dash -->
<path class="dash" d="M60,330 C320,260 460,100 720,160 C930,205 990,260 1140,260"/>
<!-- Main road -->
<path class="road" d="M60,330 C320,260 460,100 720,160 C930,205 990,260 1140,260"/>
<!-- New Documentation Node -->
<g class="node" transform="translate(200,280)">
<circle class="node-circle" r="10"/>
<circle class="node-core" r="6"/>
</g>
<!-- New Documentation Label -->
<g transform="translate(80,120)">
<rect class="label-bg" width="260" height="92"/>
<text class="label-text" x="16" y="34">New Documentation</text>
<text class="label-sub" x="16" y="58">Clear structure, examples &amp; best practices</text>
</g>
<!-- Plugin Marketplace Node -->
<g class="node" transform="translate(640,150)">
<circle class="node-circle" r="10"/>
<circle class="node-core" r="6"/>
</g>
<!-- Plugin Marketplace Label -->
<g transform="translate(560,20)">
<rect class="label-bg" width="320" height="100"/>
<text class="label-text" x="16" y="34">Plugin Marketplace</text>
<text class="label-sub" x="16" y="58">Community submissions, ratings &amp; version constraints</text>
</g>
<!-- One More Thing Node -->
<g class="node" transform="translate(1080,255)">
<circle class="node-circle" r="10"/>
<circle class="node-core" r="6"/>
</g>
<!-- One More Thing Label -->
<g transform="translate(940,300)">
<rect class="label-bg" width="250" height="86"/>
<text class="label-text" x="16" y="34">One More Thing</text>
<text class="label-sub" x="16" y="58">🚀 Confidential project · Revealing soon</text>
</g>
<!-- Spark decorations -->
<g transform="translate(1125,290)">
<path class="spark" d="M0 0 L8 0 M4 -4 L4 4"/>
<path class="spark" d="M14 -2 L22 -2 M18 -6 L18 2"/>
<path class="spark" d="M-10 6 L-2 6 M-6 2 L-6 10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
blog/images/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
blog/images/statusline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
blog/images/ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,012 KiB

BIN
blog/images/wechat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View file

@ -0,0 +1,299 @@
# 从CLI工具风格看Agent工具渐进式披露
距离Anthropic发布[Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)也过去将近两个月的时间了其中Anthropic提到了一个术语渐进式披露(Progressive Disclosure),这到底是什么东西?解决了什么问题?
其实在我的Vibe Coding流程中我很少使用MCP。因为我觉得MCP实现质量层次不齐本质是上下文注入(工具的本质也是上下文注入)我不确定别人写的提示词会不会影响到我的工作流干脆直接不用。现在的MCP实现基本上就是把所有的功能全都包装成工具暴露给Agent(一个功能包装成一个工具给定详细的描述告诉agent在什么时候进行调用参数格式是什么),这就导致了现在的提示词爆炸。
直到Anthropic发布了Skills研究了一下发现本质仍然是提示词注入。如果说MCP是提供了一套注入工具的规范那么Skills所提倡的则是“离经叛道”。Skills给了一个Markdown文档用于描述该skill的用途和最佳用法附带提供了一些脚本(与MCP不同)。
![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F6f22d8913dbc6228e7f11a41e0b3c124d817b6d2-1650x929.jpg&w=3840&q=75)
由于这些脚本直接在用户本地运行存在极大的安全风险。如果用户不能对脚本代码进行review很容易造成数据泄露、感染病毒等严重安全性问题。相比于MCP提供一个标准化的接口Skill提供一系列的脚本文件不同的skill可能拥有不同类型的脚本文件比如有些脚本使用node.js实现有些脚本使用Python实现要使用这些脚本还需要用户安装对应的运行时和脚本所需要的依赖。这也是我说“离经叛道”的原因所在。
这真的是最佳实践吗?
关于渐进式披露Anthropic是这样描述的
> 渐进式披露是使代理技能灵活且可扩展的核心设计原则。就像一本组织良好的手册,从目录开始,然后是具体章节,最后是详细的附录一样,技能允许 Claude 仅在需要时加载信息:
> ![image](https://www.ant# 从CLI工具风格看Agent工具渐进式披露
距离Anthropic发布[Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)也过去将近两个月的时间了其中Anthropic提到了一个术语渐进式披露(Progressive Disclosure),这到底是什么东西?解决了什么问题?
其实在我的Vibe Coding流程中我很少使用MCP。因为我觉得MCP实现质量层次不齐本质是上下文注入(工具的本质也是上下文注入)我不确定别人写的提示词会不会影响到我的工作流干脆直接不用。现在的MCP实现基本上就是把所有的功能全都包装成工具暴露给Agent(一个功能包装成一个工具给定详细的描述告诉agent在什么时候进行调用参数格式是什么),这就导致了现在的提示词爆炸。
直到Anthropic发布了Skills研究了一下发现本质仍然是提示词注入。如果说MCP是提供了一套注入工具的规范那么Skills所提倡的则是“离经叛道”。Skills给了一个Markdown文档用于描述该skill的用途和最佳用法附带提供了一些脚本(与MCP不同)。
![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F6f22d8913dbc6228e7f11a41e0b3c124d817b6d2-1650x929.jpg&w=3840&q=75)
由于这些脚本直接在用户本地运行存在极大的安全风险。如果用户不能对脚本代码进行review很容易造成数据泄露、感染病毒等严重安全性问题。相比于MCP提供一个标准化的接口Skill提供一系列的脚本文件不同的skill可能拥有不同类型的脚本文件比如有些脚本使用node.js实现有些脚本使用Python实现要使用这些脚本还需要用户安装对应的运行时和脚本所需要的依赖。这也是我说“离经叛道”的原因所在。
这真的是最佳实践吗?
关于渐进式披露Anthropic是这样描述的
> 渐进式披露是使代理技能灵活且可扩展的核心设计原则。就像一本组织良好的手册,从目录开始,然后是具体章节,最后是详细的附录一样,技能允许 Claude 仅在需要时加载信息:
> ![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fa3bca2763d7892982a59c28aa4df7993aaae55ae-2292x673.jpg&w=3840&q=75)
> 拥有文件系统和代码执行工具的智能体在执行特定任务时,无需将技能的全部内容读取到上下文窗口中。这意味着技能中可以包含的上下文信息量实际上是无限的。
下图是使用Skill的上下文窗口变化
![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F441b9f6cc0d2337913c1f41b05357f16f51f702e-1650x929.jpg&w=3840&q=75)
我们真的需要这样去实现吗?
在我们平时使用CLI工具时一般的CLI工具都会带有一个`--help`参数,用于查看该工具的用法和说明,这不就是该工具的使用手册吗?比如:
```shell
> npm --help
npm <command>
Usage:
npm install install all the dependencies in your project
npm install <foo> add the <foo> dependency to your project
npm test run this project's tests
npm run <foo> run the script named <foo>
npm <command> -h quick help on <command>
npm -l display usage info for all commands
npm help <term> search for help on <term>
npm help npm more involved overview
All commands:
access, adduser, audit, bugs, cache, ci, completion,
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
edit, exec, explain, explore, find-dupes, fund, get, help,
help-search, hook, init, install, install-ci-test,
install-test, link, ll, login, logout, ls, org, outdated,
owner, pack, ping, pkg, prefix, profile, prune, publish,
query, rebuild, repo, restart, root, run-script, sbom,
search, set, shrinkwrap, star, stars, start, stop, team,
test, token, uninstall, unpublish, unstar, update, version,
view, whoami
Specify configs in the ini-formatted file:
/Users/xxx/.npmrc
or on the command line via: npm <command> --key=value
More configuration info: npm help config
Configuration fields: npm help 7 config
```
这份手册也不会返回所有的命令所有的用法,它只会返回它有哪些命令可以实现什么功能,对于命令的具体用法你仍然可以通过`--help`参数获得:
```shell
> npm install --help
Install a package
Usage:
npm install [<package-spec> ...]
Options:
[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]
[-E|--save-exact] [-g|--global]
[--install-strategy <hoisted|nested|shallow|linked>] [--legacy-bundling]
[--global-style] [--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]
[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]
[--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links]
[--no-fund] [--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root] [--install-links]
aliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall
Run "npm help install" for more info
```
这是不是很像上面的渐进式披露的定义?
我们是不是可以按照这种风格去实现一个MCP来实现无需skill的工具渐进式披露我使用Codex将官方的PDF Skill转换成了一个MCP只暴露一个工具
```json
{
"name": "mcp__pdf__pdf",
"description": "Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.You need to pass in the --help parameter to obtain the usage of this tool first.",
"input_schema": {
"type": "object",
"properties": {
"params": {
"$ref": "#/$defs/PdfCommandInput"
}
},
"required": [
"params"
],
"$defs": {
"PdfCommandInput": {
"additionalProperties": false,
"properties": {
"argv": {
"description": "Command tokens excluding the leading 'pdf' executable name",
"items": {
"type": "string"
},
"title": "Argv",
"type": "array"
},
"command": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional command string; if set, it will be split like a shell",
"title": "Command"
}
},
"title": "PdfCommandInput",
"type": "object"
}
},
"title": "pdfArguments"
}
}
```
下面是使用该MCP的上下文窗口变化
```json
User: Extract all the text from test.pdf and write it into result.txt
Agent: Tool(name="pdf", args=["--help"])
Tool: pdf - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale在使用之前你需要先传入--help参数以获取该工具的用法\n\nUsage:\n pdf [--help] <domain> <command> [options]\n\nDomains:\n forms Fill, extract, and validate PDF form fields.\n documents Create, merge, and split PDFs.\n text Extract text content from PDFs.\n tables Extract tables (heuristic, whitespace-based).\n images Render PDF pages to PNG images.\n\nRun `pdf <domain> --help` for domain help.\nRun `pdf <domain> <command> --help` for command help.
Agent: Tool(name="pdf", args=["text", "--help"])
Tool: text - Extract text content from PDFs.\n\nUsage:\n pdf text <command> [options]\n\nCommands:\n extract Extract text content from a PDF.\n\nRun `pdf text <command> --help` for details.
Agent: Tool(name="pdf", args=["text", "extract", "--help"])
Tool: usage: pdf text extract --pdf PDF_PATH [--pages PAGES] [--out OUTPUT_PATH]\n [--format {text,json}] [--overwrite]\n\nExtract text content from a PDF.\n\noptions:\n --pdf PDF_PATH Input PDF\n --pages PAGES Page selection like 1,3-5 or 'all' (default: all)\n --out OUTPUT_PATH Write extracted text to a file\n --format {text,json} Output format (default: text)\n --overwrite Overwrite output file if it exists\n
Agent: Tool(name="pdf", args=["text", "extract", "--pdf", "test.pdf", "--out", "result.txt", "--overwrite"])
Tool: {"result": {"stdout": "xx","data": {"pdf_path": "test.pdf","page_count": 6,"pages": [xxx],"format": "text","output_path": "result.txt"}}}
Agent: xxx
```
从上下文变化情况来看完全实现了渐进式披露该MCP代码开源(代码完全由codex编写只验证想法未做任何审查): https://github.com/musistudio/pdf-skill-mcp
如果你有什么想法也欢迎与我进行交流hropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fa3bca2763d7892982a59c28aa4df7993aaae55ae-2292x673.jpg&w=3840&q=75)
> 拥有文件系统和代码执行工具的智能体在执行特定任务时,无需将技能的全部内容读取到上下文窗口中。这意味着技能中可以包含的上下文信息量实际上是无限的。
下图是使用Skill的上下文窗口变化
![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F441b9f6cc0d2337913c1f41b05357f16f51f702e-1650x929.jpg&w=3840&q=75)
我们真的需要这样去实现吗?
在我们平时使用CLI工具时一般的CLI工具都会带有一个`--help`参数,用于查看该工具的用法和说明,这不就是该工具的使用手册吗?比如:
```shell
> npm --help
npm <command>
Usage:
npm install install all the dependencies in your project
npm install <foo> add the <foo> dependency to your project
npm test run this project's tests
npm run <foo> run the script named <foo>
npm <command> -h quick help on <command>
npm -l display usage info for all commands
npm help <term> search for help on <term>
npm help npm more involved overview
All commands:
access, adduser, audit, bugs, cache, ci, completion,
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
edit, exec, explain, explore, find-dupes, fund, get, help,
help-search, hook, init, install, install-ci-test,
install-test, link, ll, login, logout, ls, org, outdated,
owner, pack, ping, pkg, prefix, profile, prune, publish,
query, rebuild, repo, restart, root, run-script, sbom,
search, set, shrinkwrap, star, stars, start, stop, team,
test, token, uninstall, unpublish, unstar, update, version,
view, whoami
Specify configs in the ini-formatted file:
/Users/xxx/.npmrc
or on the command line via: npm <command> --key=value
More configuration info: npm help config
Configuration fields: npm help 7 config
```
这份手册也不会返回所有的命令所有的用法,它只会返回它有哪些命令可以实现什么功能,对于命令的具体用法你仍然可以通过`--help`参数获得:
```shell
> npm install --help
Install a package
Usage:
npm install [<package-spec> ...]
Options:
[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]
[-E|--save-exact] [-g|--global]
[--install-strategy <hoisted|nested|shallow|linked>] [--legacy-bundling]
[--global-style] [--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]
[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]
[--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links]
[--no-fund] [--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root] [--install-links]
aliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall
Run "npm help install" for more info
```
这是不是很像上面的渐进式披露的定义?
我们是不是可以按照这种风格去实现一个MCP来实现无需skill的工具渐进式披露我使用Codex将官方的PDF Skill转换成了一个MCP只暴露一个工具
```json
{
"name": "mcp__pdf__pdf",
"description": "Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.You need to pass in the --help parameter to obtain the usage of this tool first.",
"input_schema": {
"type": "object",
"properties": {
"params": {
"$ref": "#/$defs/PdfCommandInput"
}
},
"required": [
"params"
],
"$defs": {
"PdfCommandInput": {
"additionalProperties": false,
"properties": {
"argv": {
"description": "Command tokens excluding the leading 'pdf' executable name",
"items": {
"type": "string"
},
"title": "Argv",
"type": "array"
},
"command": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional command string; if set, it will be split like a shell",
"title": "Command"
}
},
"title": "PdfCommandInput",
"type": "object"
}
},
"title": "pdfArguments"
}
}
```
下面是使用该MCP的上下文窗口变化
```json
User: Extract all the text from test.pdf and write it into result.txt
Agent: Tool(name="pdf", args=["--help"])
Tool: pdf - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale在使用之前你需要先传入--help参数以获取该工具的用法\n\nUsage:\n pdf [--help] <domain> <command> [options]\n\nDomains:\n forms Fill, extract, and validate PDF form fields.\n documents Create, merge, and split PDFs.\n text Extract text content from PDFs.\n tables Extract tables (heuristic, whitespace-based).\n images Render PDF pages to PNG images.\n\nRun `pdf <domain> --help` for domain help.\nRun `pdf <domain> <command> --help` for command help.
Agent: Tool(name="pdf", args=["text", "--help"])
Tool: text - Extract text content from PDFs.\n\nUsage:\n pdf text <command> [options]\n\nCommands:\n extract Extract text content from a PDF.\n\nRun `pdf text <command> --help` for details.
Agent: Tool(name="pdf", args=["text", "extract", "--help"])
Tool: usage: pdf text extract --pdf PDF_PATH [--pages PAGES] [--out OUTPUT_PATH]\n [--format {text,json}] [--overwrite]\n\nExtract text content from a PDF.\n\noptions:\n --pdf PDF_PATH Input PDF\n --pages PAGES Page selection like 1,3-5 or 'all' (default: all)\n --out OUTPUT_PATH Write extracted text to a file\n --format {text,json} Output format (default: text)\n --overwrite Overwrite output file if it exists\n
Agent: Tool(name="pdf", args=["text", "extract", "--pdf", "test.pdf", "--out", "result.txt", "--overwrite"])
Tool: {"result": {"stdout": "xx","data": {"pdf_path": "test.pdf","page_count": 6,"pages": [xxx],"format": "text","output_path": "result.txt"}}}
Agent: xxx
```
从上下文变化情况来看完全实现了渐进式披露该MCP代码开源(代码完全由codex编写只验证想法未做任何审查): https://github.com/musistudio/pdf-skill-mcp
如果你有什么想法也欢迎与我进行交流 email: [m@musiiot.top](mailto://m@musiiot.top )

View file

@ -0,0 +1,95 @@
# 或许我们能在 Router 中做更多事情
自从`claude-code-router`发布以来,我收到了很多用户的反馈,至今还有不少的 issues 未处理。其中大多都是关于不同的供应商的支持和`deepseek`模型调用工具不积极的问题。
之前开发这个项目主要是为了我自己能以较低成本使用上`claude code`,所以一开始的设计并没有考虑到多供应商的情况。在实际的排查问题中,我发现尽管市面上所有的供应商几乎都宣称兼容`OpenAI`格式调用,即通过`/chat/compeletions`接口调用,但是其中的细节差异非常多。例如:
1. Gemini 的工具参数类型是 string 时,`format`参数只支持`date``date-time`,并且没有工具调用 ID。
2. OpenRouter 需要使用`cache_control`进行缓存。
3. DeepSeek 官方 API 的 `max_output` 为 8192而火山引擎的会更大。
除了这些问题之外,还有一些其他的小的供应商,他们或多或少参数都有点问题。于是,我打算开发一个新的项目[musistudio/llms](https://github.com/musistudio/llms)来处理这种不同服务商的兼容问题。该项目使用 OpenAI 格式为基础的通用格式,提供了一个`Transformer`接口,该接口用于处理转换请求和响应。当我们给不同的服务商都实现了`Transformer`后,我们可以实现不同服务商的混合调用。比如我在`AnthropicTransformer`中实现了`Anthropic`<->`OpenAI`格式的互相转换,并监听了`/v1/messages`端点,在`GeminiTransformer`中实现了`Gemini`<->`OpenAI`格式的互相转换,并监听了`/v1beta/models/:modelAndAction`端点,当他们的请求和响应都被转换成一个通用格式的时候,就可以实现他们的互相调用。
```
AnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer
```
```
GeminiReseponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse
```
虽然使用中间层抹平差异可能会带来一些性能问题,但是该项目最初的目的是为了让`claude-code-router`支持不同的供应商。
至于`deepseek`模型调用工具不积极的问题,我发现这是由于`deepseek`在长上下文中的指令遵循不佳导致的。现象就是刚开始模型会主动调用工具,但是在经过几轮对话后模型只会返回文本。一开始的解决方案是通过注入一个系统提示词告知模型需要积极去使用工具以解决用户的问题,但是后面测试发现在长上下文中模型会遗忘该指令。
查看`deepseek`文档后发现模型支持`tool_choice`参数,可以强制让模型最少调用 1 个工具,我尝试将该值设置为`required`,发现模型调用工具的积极性大大增加,现在我们只需要在合适的时候取消这个参数即可。借助[musistudio/llms](https://github.com/musistudio/llms)的`Transformer`可以让我们在发送请求前和收到响应后做点什么,所以我参考`claude code``Plan Mode`,实现了一个使用与`deepseek``Tool Mode`
```typescript
export class TooluseTransformer implements Transformer {
name = "tooluse";
transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {
if (request.tools?.length) {
request.messages.push({
role: "system",
content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.
Before invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \`ExitTool\` to exit tool mode — this is the only valid way to terminate tool mode.
Always prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,
});
request.tool_choice = "required";
request.tools.unshift({
type: "function",
function: {
name: "ExitTool",
description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.
IMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.
Examples:
1. Task: "Use a tool to summarize this document" — Do not use ExitTool if a summarization tool is available.
2. Task: "Whats the weather today?" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,
parameters: {
type: "object",
properties: {
response: {
type: "string",
description:
"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.",
},
},
required: ["response"],
},
},
});
}
return request;
}
async transformResponseOut(response: Response): Promise<Response> {
if (response.headers.get("Content-Type")?.includes("application/json")) {
const jsonResponse = await response.json();
if (
jsonResponse?.choices[0]?.message.tool_calls?.length &&
jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===
"ExitTool"
) {
const toolArguments = JSON.parse(toolCall.function.arguments || "{}");
jsonResponse.choices[0].message.content = toolArguments.response || "";
delete jsonResponse.choices[0].message.tool_calls;
}
// Handle non-streaming response if needed
return new Response(JSON.stringify(jsonResponse), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else if (response.headers.get("Content-Type")?.includes("stream")) {
// ...
}
return response;
}
}
```
该工具将始终让模型至少调用一个工具,如果没有合适的工具或者任务已完成可以调用`ExitTool`来退出工具模式,因为是依靠`tool_choice`参数实现的,所以仅适用于支持该参数的模型。经过测试,该工具能显著增加`deepseek`的工具调用次数,弊端是可能会有跟任务无关或者没有必要的工具调用导致增加任务执行事件和消耗的 `token` 数。
这次更新仅仅是在 Router 中实现一个`agent`的一次小探索,或许还能做更多其他有趣的事也说不定...

View file

@ -0,0 +1,96 @@
# 项目初衷及原理
早在 Claude Code 发布的第二天(2025-02-25),我就尝试并完成了对该项目的逆向。当时要使用 Claude Code 你需要注册一个 Anthropic 账号,然后申请 waitlist等待通过后才能使用。但是因为众所周知的原因Anthropic 屏蔽了中国区的用户,所以通过正常手段我无法使用,通过已知的信息,我发现:
1. Claude Code 使用 npm 进行安装,所以很大可能其使用 Node.js 进行开发。
2. Node.js 调试手段众多,可以简单使用`console.log`获取想要的信息,也可以使用`--inspect`将其接入`Chrome Devtools`,甚至你可以使用`d8`去调试某些加密混淆的代码。
由于我的目标是让我在没有 Anthropic 账号的情况下使用`Claude Code`,我并不需要获得完整的源代码,只需要将`Claude Code`请求 Anthropic 模型时将其转发到我自定义的接口即可。接下来我就开启了我的逆向过程:
1. 首先安装`Claude Code`
```bash
npm install -g @anthropic-ai/claude-code
```
2. 安装后该项目被放在了`~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`中,因为我使用了`nvm`作为我的 node 版本控制器,当前使用`node-v20.10.0`,所以该路径会因人而异。
3. 找到项目路径之后可通过 package.json 分析包入口,内容如下:
```package.json
{
"name": "@anthropic-ai/claude-code",
"version": "1.0.24",
"main": "sdk.mjs",
"types": "sdk.d.ts",
"bin": {
"claude": "cli.js"
},
"engines": {
"node": ">=18.0.0"
},
"type": "module",
"author": "Boris Cherny <boris@anthropic.com>",
"license": "SEE LICENSE IN README.md",
"description": "Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.",
"homepage": "https://github.com/anthropics/claude-code",
"bugs": {
"url": "https://github.com/anthropics/claude-code/issues"
},
"scripts": {
"prepare": "node -e \"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\"",
"preinstall": "node scripts/preinstall.js"
},
"dependencies": {},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
}
}
```
其中`"claude": "cli.js"`就是我们要找的入口,打开 cli.js发现代码被压缩混淆过了。没关系借助`webstorm``Formate File`功能可以重新格式化,让代码变得稍微好看一点。就像这样:
![webstorm-formate-file](../images/webstorm-formate-file.png)
现在,你可以通过阅读部分代码来了解`Claude Code`的内容工具原理与提示词。你也可以在关键地方使用`console.log`来获得更多信息,当然,也可以使用`Chrome Devtools`来进行断点调试,使用以下命令启动`Claude Code`:
```bash
NODE_OPTIONS="--inspect-brk=9229" claude
```
该命令会以调试模式启动`Claude Code`,并将调试的端口设置为`9229`。这时候通过 Chrome 访问`chrome://inspect/`即可看到当前的`Claude Code`进程,点击`inspect`即可进行调试。
![chrome-devtools](../images/chrome-inspect.png)
![chrome-devtools](../images/chrome-devtools.png)
通过搜索关键字符`api.anthropic.com`很容易能找到`Claude Code`用来发请求的地方,根据上下文的查看,很容易发现这里的`baseURL`可以通过环境变量`ANTHROPIC_BASE_URL`进行覆盖,`apiKey``authToken`也同理。
![search](../images/search.png)
到目前为止,我们获得关键信息:
1. 可以使用环境变量覆盖`Claude Code``BaseURL``apiKey`的配置
2. `Claude Code`使用[Anthropic API](https://docs.anthropic.com/en/api/overview)的规范
所以我们需要:
1. 实现一个服务用来将`OpenAI API`的规范转换成`Anthropic API`格式。
2. 启动`Claude Code`之前写入环境变量将`baseURL`指向到该服务。
于是,`claude-code-router`就诞生了,该项目使用`Express.js`作为 HTTP 服务,实现`/v1/messages`端点,使用`middlewares`处理请求/响应的格式转换以及请求重写功能(可以用来重写 Claude Code 的提示词以针对单个模型进行调优)。
在 2 月份由于`DeepSeek`全系列模型对`Function Call`的支持不佳导致无法直接使用`DeepSeek`模型,所以在当时我选择了`qwen-max`模型,一切表现的都很好,但是`qwen-max`不支持`KV Cache`,意味着我要消耗大量的 token但是却无法获取`Claude Code`原生的体验。
所以我又尝试了`Router`模式,即使用一个小模型对任务进行分发,一共分为四个模型:`router``tool``think``coder`,所有的请求先经过一个免费的小模型,由小模型去判断应该是进行思考还是编码还是调用工具,再进行任务的分发,如果是思考和编码任务将会进行循环调用,直到最终使用工具写入或修改文件。但是实践下来发现免费的小模型不足以很好的完成任务的分发,再加上整个 Agnet 的设计存在缺陷,导致并不能很好的驱动`Claude Code`
直到 5 月底,`Claude Code`被正式推出,这时`DeepSeek`全系列模型(R1 于 05-28)均支持`Function Call`,我开始重新设计该项目。在与 AI 的结对编程中我修复了之前的请求和响应转换问题,在某些场景下模型输出 JSON 响应而不是`Function Call`。这次直接使用`DeepSeek-v3`模型,它工作的比我想象中要好:能完成绝大多数工具调用,还支持用步骤规划解决任务,最关键的是`DeepSeek`的价格不到`claude Sonnet 3.5`的十分之一。正式发布的`Claude Code`对 Agent 的组织也不同于测试版,于是在分析了`Claude Code`的请求调用之后,我重新组织了`Router`模式:现在它还是四个模型:默认模型、`background``think``longContext`
- 默认模型作为最终的兜底和日常处理
- `background`是用来处理一些后台任务,据 Anthropic 官方说主要用`Claude Haiku 3.5`模型去处理一些小任务,如俳句生成和对话摘要,于是我将其路由到了本地的`ollama`服务。
- `think`模型用于让`Claude Code`进行思考或者在`Plan Mode`下使用,这里我使用的是`DeepSeek-R1`,由于其不支持推理成本控制,所以`Think``UltraThink`是一样的逻辑。
- `longContext`是用于处理长下上文的场景该项目会对每次请求使用tiktoken实时计算上下文长度如果上下文大于32K则使用该模型旨在弥补`DeepSeek`在长上下文处理不佳的情况。
以上就是该项目的发展历程以及我的一些思考,通过巧妙的使用环境变量覆盖的手段在不修改`Claude Code`源码的情况下完成请求的转发和修改,这就使得在可以得到 Anthropic 更新的同时使用自己的模型,自定义自己的提示词。该项目只是在 Anthropic 封禁中国区用户的情况下使用`Claude Code`并且达到成本和性能平衡的一种手段。如果可以的话还是官方的Max Plan体验最好。

View file

@ -195,6 +195,7 @@ function App() {
const [providerDeepLinkBusy, setProviderDeepLinkBusy] = useState(false);
const [providerDeepLinkError, setProviderDeepLinkError] = useState("");
const [providerDeepLinkIconLoading, setProviderDeepLinkIconLoading] = useState(false);
const [providerDeepLinkModelsLoading, setProviderDeepLinkModelsLoading] = useState(false);
const [proxyCertificateChecking, setProxyCertificateChecking] = useState(false);
const [proxyEnablePending, setProxyEnablePending] = useState(false);
const [providerProbeError, setProviderProbeError] = useState("");
@ -342,6 +343,7 @@ function App() {
setProviderDeepLinkRequest(request);
setProviderDeepLinkError("");
setProviderDeepLinkBusy(false);
setProviderDeepLinkModelsLoading(false);
setActiveView("providers");
};
@ -407,6 +409,7 @@ function App() {
const hasApiKey = Boolean(payload?.apiKey?.trim());
if (!request || !payload || (!hasApiKey && payload.models.length > 0) || !providerPresetsLoaded) {
setProviderDeepLinkModelsLoading(false);
return;
}
@ -414,6 +417,7 @@ function App() {
? probeProviderDeepLinkPayload(payload).then((probe) => mergeProviderModelLists(probe?.models ?? []))
: resolveProviderDeepLinkCatalogModels(payload);
setProviderDeepLinkModelsLoading(true);
void modelsPromise
.then((models) => {
if (providerDeepLinkCatalogModelsRequestId.current !== requestId || models.length === 0) {
@ -431,6 +435,14 @@ function App() {
}
};
});
})
.catch(() => {
// Model discovery is optional; importing performs the same resolution again.
})
.finally(() => {
if (providerDeepLinkCatalogModelsRequestId.current === requestId) {
setProviderDeepLinkModelsLoading(false);
}
});
}, [providerDeepLinkRequest?.id, providerDeepLinkRequest?.provider?.apiKey, providerDeepLinkRequest?.provider?.baseUrl, providerDeepLinkRequest?.provider?.name, providerPresetsLoaded]);
@ -1182,6 +1194,10 @@ function App() {
}
async function submitProviderDraft(): Promise<boolean> {
if (providerProbeLoading || providerConnectivityLoading) {
return false;
}
const probe = providerProbe;
const usesCatalog = Boolean(probe?.models.length);
@ -2895,6 +2911,7 @@ function App() {
}
},
onSubmit: confirmProviderDeepLinkImport,
modelsLoading: providerDeepLinkModelsLoading,
request: providerDeepLinkRequest
} : undefined}
providerDelete={providerDeleteItem ? {

View file

@ -2,7 +2,8 @@ import {
AddProfileDraft, AddProviderDraft, AppConfig, Button, Check, ChevronLeft,
ChevronRight, cn, GatewayProviderProbeResult, GatewayStatus, Gauge, getNextOnboardingStep,
isOnboardingProfileReady, isOnboardingProviderReady, Layers3, LucideIcon, motion, motionEase,
onboardingMascotSpriteUrl, OnboardingStepId, onboardingStepOrder, ProviderConnectivityCheckReport, reducedMotionTransition, useAppText, useReducedMotion,
LoaderCircle, onboardingMascotSpriteUrl, OnboardingStepId, onboardingStepOrder, ProviderConnectivityCheckReport, reducedMotionTransition, useAppText, useReducedMotion,
useState,
UserRound, X
} from "../shared";
import { AddProviderForm } from "./providers";
@ -104,6 +105,7 @@ export function OnboardingView({
}) {
const t = useAppText();
const shouldReduceMotion = useReducedMotion();
const [providerIconDetecting, setProviderIconDetecting] = useState(false);
const providerReady = isOnboardingProviderReady(config);
const profileReady = isOnboardingProfileReady(config);
const serviceReady = gatewayStatus.state === "running";
@ -112,8 +114,9 @@ export function OnboardingView({
const activeDetails = onboardingStepDetails[activeStep];
const previousStep = onboardingStepOrder[activeIndex - 1];
const nextStep = getNextOnboardingStep(activeStep, config);
const providerSubmitLoading = activeStep === "provider" && (providerProbeLoading || providerConnectivityLoading || providerIconDetecting);
const nextDisabled = activeStep === "provider"
? !(providerReady || canSubmitProvider)
? providerSubmitLoading || !(providerReady || canSubmitProvider)
: activeStep === "profile"
? !(profileReady || (providerReady && canSubmitProfile))
: !routeReady;
@ -133,6 +136,9 @@ export function OnboardingView({
}
if (activeStep === "provider") {
if (providerSubmitLoading) {
return;
}
if (canSubmitProvider) {
const saved = await onSubmitProvider();
if (saved) {
@ -208,6 +214,7 @@ export function OnboardingView({
mode={providerReady ? "edit" : "add"}
onCheck={onCheckProvider}
onChange={onChangeProvider}
onIconDetectingChange={setProviderIconDetecting}
probe={providerProbe}
probeLoading={providerProbeLoading}
providerPlugins={config.providerPlugins ?? []}
@ -260,9 +267,9 @@ export function OnboardingView({
<div className="mt-5 flex shrink-0 items-center justify-end gap-3 border-t border-border/60 pt-4">
<Button disabled={nextDisabled} onClick={() => void goToNextStep()} type="button">
{activeStep === "enter" ? <Check className="h-4 w-4" /> : null}
{activeStep === "enter" ? t("Let's start") : t("Next step")}
{activeStep !== "enter" ? <ChevronRight className="h-4 w-4" /> : null}
{providerSubmitLoading ? <LoaderCircle className="h-4 w-4 animate-spin" /> : activeStep === "enter" ? <Check className="h-4 w-4" /> : null}
{providerSubmitLoading ? t("Loading") : activeStep === "enter" ? t("Let's start") : t("Next step")}
{!providerSubmitLoading && activeStep !== "enter" ? <ChevronRight className="h-4 w-4" /> : null}
</Button>
</div>
</div>

View file

@ -420,6 +420,7 @@ export function ProviderDeepLinkDialog({
busy,
error,
iconLoading = false,
modelsLoading = false,
onClose,
onSubmit,
request
@ -427,6 +428,7 @@ export function ProviderDeepLinkDialog({
busy: boolean;
error: string;
iconLoading?: boolean;
modelsLoading?: boolean;
onClose: () => void;
onSubmit: () => Promise<void>;
request: ProviderDeepLinkRequest;
@ -438,6 +440,7 @@ export function ProviderDeepLinkDialog({
const providerPreset = provider ? resolveProviderDeepLinkPreset(provider) : undefined;
const providerIconUrl = provider ? providerDeepLinkDisplayIcon(provider) : "";
const modelPreview = provider?.models.slice(0, 8) ?? [];
const actionLoading = busy || Boolean(provider && (iconLoading || modelsLoading));
return (
<Dialog onOpenChange={(open) => !open && onClose()}>
@ -555,11 +558,11 @@ export function ProviderDeepLinkDialog({
{t("Cancel")}
</Button>
{provider || manifest ? (
<Button disabled={busy} onClick={() => void onSubmit()} type="button">
<AnimatedIconSwap iconKey={busy ? "busy" : "plus"}>
{busy ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
<Button disabled={actionLoading} onClick={() => void onSubmit()} type="button">
<AnimatedIconSwap iconKey={actionLoading ? "busy" : "plus"}>
{actionLoading ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
</AnimatedIconSwap>
{provider ? t("Import") : t("Fetch manifest")}
{actionLoading ? t("Loading") : provider ? t("Import") : t("Fetch manifest")}
</Button>
) : null}
</DialogFooter>
@ -984,6 +987,7 @@ export function AddProviderForm({
mode,
onCheck,
onChange,
onIconDetectingChange,
probe,
probeLoading,
providerPlugins = [],
@ -996,6 +1000,7 @@ export function AddProviderForm({
mode: "add" | "edit";
onCheck?: () => Promise<unknown>;
onChange: (patch: Partial<AddProviderDraft>, resetProbe?: boolean) => void;
onIconDetectingChange?: (detecting: boolean) => void;
probe?: GatewayProviderProbeResult;
probeLoading: boolean;
providerPlugins?: unknown[];
@ -1031,6 +1036,15 @@ export function AddProviderForm({
onChangeRef.current = onChange;
}, [onChange]);
useEffect(() => {
onIconDetectingChange?.(iconDetecting);
return () => {
if (iconDetecting) {
onIconDetectingChange?.(false);
}
};
}, [iconDetecting, onIconDetectingChange]);
useEffect(() => {
const requestId = iconDetectionRequestRef.current + 1;
iconDetectionRequestRef.current = requestId;
@ -1881,9 +1895,12 @@ export function AddProviderDialog({
const t = useAppText();
const [checkConfirmOpen, setCheckConfirmOpen] = useState(false);
const [checkConfirmBusy, setCheckConfirmBusy] = useState(false);
const [iconDetecting, setIconDetecting] = useState(false);
const [checkModelSelection, setCheckModelSelection] = useState<string[]>([]);
const [checkResult, setCheckResult] = useState<ProviderConnectivityCheckReport>();
const checkModels = mergeProviderModelLists(draft.selectedModels, splitLines(draft.modelsText));
const submitLoading = probeLoading || connectivityLoading || iconDetecting;
const submitDisabled = !canSubmit || submitLoading;
function openCheckConfirm() {
setCheckModelSelection(checkModels);
@ -1912,6 +1929,13 @@ export function AddProviderDialog({
setCheckResult(undefined);
}
function submit() {
if (submitDisabled) {
return;
}
void onSubmit();
}
return (
<>
<Dialog className="items-start" onOpenChange={(open) => !open && onClose()}>
@ -1934,6 +1958,7 @@ export function AddProviderDialog({
mode={mode}
onCheck={onCheck ? async () => openCheckConfirm() : undefined}
onChange={onChange}
onIconDetectingChange={setIconDetecting}
probe={probe}
probeLoading={probeLoading}
providerPlugins={providerPlugins}
@ -1945,9 +1970,11 @@ export function AddProviderDialog({
<Button onClick={onClose} type="button" variant="outline">
{t("Cancel")}
</Button>
<Button disabled={!canSubmit} onClick={() => void onSubmit()} type="button">
{mode === "edit" ? <Check className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
{mode === "edit" ? t("Save") : t("Add")}
<Button disabled={submitDisabled} onClick={submit} type="button">
<AnimatedIconSwap iconKey={submitLoading ? "loading" : mode}>
{submitLoading ? <LoaderCircle className="h-4 w-4 animate-spin" /> : mode === "edit" ? <Check className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
</AnimatedIconSwap>
{submitLoading ? t("Loading") : mode === "edit" ? t("Save") : t("Add")}
</Button>
</DialogFooter>
</DialogContent>