Merge remote-tracking branch 'origin/dev/3.1' into dev/3.1

This commit is contained in:
musistudio 2026-07-02 11:46:30 +08:00
commit 32db838b0b
63 changed files with 1129 additions and 722 deletions

351
README.md
View file

@ -9,21 +9,17 @@
</p>
<p align="center">
<img src="blog/images/claude-code-router.png" alt="Claude Code Router Desktop screenshot" />
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop screenshot" />
</p>
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 the local gateway at `http://localhost:8080` by default.
## Why Use CCR
- Use one local endpoint for multiple agent tools instead of configuring every client separately.
- Route requests with default routing, conditional rules, fallback targets, and request rewrites instead of editing client configuration by hand.
- Mix providers without changing your workflow. CCR supports OpenAI-compatible APIs, Anthropic Messages, Gemini Generate Content, OpenRouter, DeepSeek, SiliconFlow, Moonshot, Kimi Code, 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
@ -33,9 +29,11 @@ CCR runs on your machine, keeps provider configuration in your local config dire
- **Agent Config**: configure Claude Code, Codex, and ZCode launch entries, models, scopes, and multi-instance app profiles.
- **Gateway compatibility**: translate supported client requests through the local CCR model gateway.
- **Proxy mode**: capture supported API traffic through a local proxy with optional system proxy integration and network capture.
- **Extensions**: install or load wrapper plugins, including routes for Claude Design and Cursor Proxy style integrations.
- **Fusion models**: combine a base model with vision, web search, or MCP tools into a reusable selectable model.
- **Provider deeplinks**: import provider configuration, manifests, and embeddable one-click provider buttons through `ccr://provider?...` links after user confirmation.
## Documentation
Read the full documentation at [ccrdesk.top](https://ccrdesk.top/).
## Download And Install
@ -64,7 +62,7 @@ Open **Providers**, click **Add Provider**, then choose a built-in preset or **O
### 2. Configure routing
Open **Routing** to choose the default route, add conditional rules, configure request rewrites, and set fallback behavior.
Open **Routing** to add conditional rules, configure request rewrites, and set fallback behavior.
Use **Add Routing Rule** for request conditions, model-prefix routing, or rule-level fallback targets.
@ -80,80 +78,6 @@ Open **Agent Config** and choose the client you want to use. Configure Claude Co
Use **Settings → Logs & Observability** to enable request logs and agent observability. Use **Logs** to confirm `request model`, `resolved provider`, `resolved model`, status, tokens, latency, and errors; use the tray window for quick token and account status.
## Provider Deeplink
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
```
Supported query parameters include:
- `name`: display name for the provider.
- `base_url`: provider API base URL; required for direct imports.
- `api_key`: optional provider API key.
- `models`: comma-separated or newline-separated model list. You can also repeat `models=...`.
- `protocol`: one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, or `gemini_generate_content`.
- `icon`: provider icon URL.
- `source`: provider website or config source.
- `manifest`: remote HTTPS manifest URL.
- `usage_url`, `fetch_usage`, `usage_method`, `usage_headers`, `usage_body`: optional account usage fetching configuration.
- `balance`, `balance_unit`, `subscription`, `subscription_limit`, `subscription_reset`, `subscription_unit`, `subscription_window`: optional usage field mappings.
For larger payloads, pass `payload` as URL-encoded JSON or base64url JSON with the same fields, or pass `manifest` to let CCR fetch a remote provider manifest. Manifest URLs must use HTTPS, return JSON, avoid local or private network hosts, and stay under 128 KB. Parameter names and protocol values must use the exact names above; aliases such as `baseUrl`, `apiKey`, `model`, `type`, or `openai` are not accepted.
CCR always opens a confirmation dialog before writing a provider imported from an external link.
## Extensions
CCR has two extension layers:
- Wrapper plugins: use top-level `plugins` to extend the Electron wrapper, register local HTTP routes, start local HTTP backends, route proxy-mode traffic to plugin backends, add built-in browser entries, and connect provider account meters.
- Core gateway plugins: use `providerPlugins` or `plugins[].coreGateway.providerPlugins` to extend upstream provider, auth, or core gateway behavior. `plugins[].coreGateway.virtualModelProfiles` can inject virtual model profiles.
Example 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
}
]
}
}
]
}
```
Plugin modules export a function or an object with `setup(ctx)` or `activate(ctx)`. The context supports:
- `ctx.registerGatewayRoute({ method, path, auth, handler })`
- `ctx.registerHttpBackend({ id, host, port, handler })`
- `ctx.registerProxyRoute({ host, paths, upstream, stripPathPrefix, rewritePathPrefix, headers })`
- `ctx.registerApp(app)`
- `ctx.openSqliteStore({ filename, migrate })`
- `ctx.registerProviderAccountConnector(connector)`
- `ctx.registerCoreGatewayProviderPlugin(plugin)`
- `ctx.registerCoreGatewayVirtualModelProfile(profile)`
Local plugin examples are available in [examples/plugins](examples/plugins).
## Further Reading
- [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)
## Acknowledgements
Codex support is powered by [musistudio/codexl](https://github.com/musistudio/codexl).
@ -206,177 +130,186 @@ Codex support is powered by [musistudio/codexl](https://github.com/musistudio/co
<p>A huge thank you to all our sponsors for their generous support.</p>
<table>
<table width="100%">
<tr>
<td align="center" width="260">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ"><strong>Z智谱</strong></a>
<td align="center" width="330">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="Zhipu icon" />
<br />
<strong>Z智谱</strong>
</a>
</td>
<td align="center" width="260">
<a href="https://aihubmix.com/"><strong>AIHubmix</strong></a>
<td align="center" width="330">
<a href="https://aihubmix.com/">
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&amp;sz=128" width="42" height="42" alt="AIHubmix icon" />
<br />
<strong>AIHubmix</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://ai.burncloud.com">
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud icon" />
<br />
<strong>BurnCloud</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://share.302.ai/ZGVF9w">
<img src="https://www.google.com/s2/favicons?domain=302.ai&amp;sz=128" width="42" height="42" alt="302.AI icon" />
<br />
<strong>302.AI</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="260">
<a href="https://ai.burncloud.com"><strong>BurnCloud</strong></a>
</td>
<td align="center" width="260">
<a href="https://share.302.ai/ZGVF9w"><strong>302.AI</strong></a>
<td align="center" width="330">
<a href="https://runapi.co/register?aff=IX1t">
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI icon" />
<br />
<strong>RunAPI</strong>
</a>
</td>
</tr>
</table>
<h4>Community Sponsors</h4>
<table>
<table width="100%">
<tr>
<td align="center" width="25%">@Simon Leischnig</td>
<td align="center" width="25%"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="25%"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="25%">@*o</td>
<td align="center" width="220">@Simon Leischnig</td>
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="220">@*o</td>
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center" width="220">@*说</td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center">@*说</td>
<td align="center">@*更</td>
<td align="center">@K*g</td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@K*g</td>
<td align="center" width="220">@R*R</td>
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220">@*划</td>
</tr>
<tr>
<td align="center">@R*R</td>
<td align="center"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center">@*苗</td>
<td align="center">@*划</td>
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center" width="220">@S*r</td>
<td align="center" width="220">@*晖</td>
<td align="center" width="220">@*敏</td>
<td align="center" width="220">@Z*z</td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center">@S*r</td>
<td align="center">@*晖</td>
<td align="center" width="220">@*然</td>
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center" width="220">@*应</td>
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
</tr>
<tr>
<td align="center">@*敏</td>
<td align="center">@Z*z</td>
<td align="center">@*然</td>
<td align="center"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*飞</td>
<td align="center" width="220">@董*</td>
<td align="center" width="220">@*汀</td>
<td align="center" width="220">@*涯</td>
<td align="center" width="220">@*:-</td>
<td align="center" width="220">@**磊</td>
</tr>
<tr>
<td align="center">@*苗</td>
<td align="center"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center">@*应</td>
<td align="center"><a href="https://github.com/yusnake">@yusnake</a></td>
<td align="center" width="220">@*琢</td>
<td align="center" width="220">@*成</td>
<td align="center" width="220">@Z*o</td>
<td align="center" width="220">@*琨</td>
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center" width="220">@*_</td>
</tr>
<tr>
<td align="center">@*飞</td>
<td align="center">@董*</td>
<td align="center">@*汀</td>
<td align="center">@*涯</td>
<td align="center" width="220">@Z*m</td>
<td align="center" width="220">@*鑫</td>
<td align="center" width="220">@c*y</td>
<td align="center" width="220">@*昕</td>
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center" width="220">@b*g</td>
</tr>
<tr>
<td align="center">@*:-</td>
<td align="center">@**磊</td>
<td align="center">@*琢</td>
<td align="center">@*成</td>
<td align="center" width="220">@*亿</td>
<td align="center" width="220">@*辉</td>
<td align="center" width="220">@JACK</td>
<td align="center" width="220">@*光</td>
<td align="center" width="220">@W*l</td>
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
</tr>
<tr>
<td align="center">@Z*o</td>
<td align="center">@*琨</td>
<td align="center"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center">@*_</td>
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center" width="220">@二吉吉</td>
<td align="center" width="220">@a*g</td>
<td align="center" width="220">@*林</td>
<td align="center" width="220">@*咸</td>
<td align="center" width="220">@*明</td>
</tr>
<tr>
<td align="center">@Z*m</td>
<td align="center">@*鑫</td>
<td align="center">@c*y</td>
<td align="center">@*昕</td>
<td align="center" width="220">@S*y</td>
<td align="center" width="220">@f*o</td>
<td align="center" width="220">@*智</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@r*c</td>
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center">@b*g</td>
<td align="center">@*亿</td>
<td align="center">@*辉</td>
<td align="center" width="220">@*军</td>
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220">@zcutlip</td>
</tr>
<tr>
<td align="center">@JACK</td>
<td align="center">@*光</td>
<td align="center">@W*l</td>
<td align="center"><a href="https://github.com/kesku">@kesku</a></td>
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@*.</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@*政</td>
<td align="center" width="220">@*铭</td>
</tr>
<tr>
<td align="center"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center">@二吉吉</td>
<td align="center">@a*g</td>
<td align="center">@*林</td>
<td align="center" width="220">@*叶</td>
<td align="center" width="220">@七*o</td>
<td align="center" width="220">@*青</td>
<td align="center" width="220">@**晨</td>
<td align="center" width="220">@*远</td>
<td align="center" width="220">@*霄</td>
</tr>
<tr>
<td align="center">@*咸</td>
<td align="center">@*明</td>
<td align="center">@S*y</td>
<td align="center">@f*o</td>
<td align="center" width="220">@**吉</td>
<td align="center" width="220">@**飞</td>
<td align="center" width="220">@**驰</td>
<td align="center" width="220">@x*g</td>
<td align="center" width="220">@**东</td>
<td align="center" width="220">@*落</td>
</tr>
<tr>
<td align="center">@*智</td>
<td align="center">@F*t</td>
<td align="center">@r*c</td>
<td align="center"><a href="https://github.com/qierkang">@qierkang</a></td>
<td align="center" width="220">@哆*k</td>
<td align="center" width="220">@*涛</td>
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center" width="220">@*呢</td>
<td align="center" width="220">@d*u</td>
<td align="center" width="220">@crizcraig</td>
</tr>
<tr>
<td align="center">@*军</td>
<td align="center"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center">@*王</td>
<td align="center"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">s*s</td>
<td align="center" width="220">*火</td>
<td align="center" width="220">*勤</td>
<td align="center" width="220">**锟</td>
<td align="center" width="220">*涛</td>
<td align="center" width="220">**明</td>
</tr>
<tr>
<td align="center">@*王</td>
<td align="center">@zcutlip</td>
<td align="center"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center">@*更</td>
</tr>
<tr>
<td align="center">@*.</td>
<td align="center">@F*t</td>
<td align="center">@*政</td>
<td align="center">@*铭</td>
</tr>
<tr>
<td align="center">@*叶</td>
<td align="center">@七*o</td>
<td align="center">@*青</td>
<td align="center">@**晨</td>
</tr>
<tr>
<td align="center">@*远</td>
<td align="center">@*霄</td>
<td align="center">@**吉</td>
<td align="center">@**飞</td>
</tr>
<tr>
<td align="center">@**驰</td>
<td align="center">@x*g</td>
<td align="center">@**东</td>
<td align="center">@*落</td>
</tr>
<tr>
<td align="center">@哆*k</td>
<td align="center">@*涛</td>
<td align="center"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center">@*呢</td>
</tr>
<tr>
<td align="center">@d*u</td>
<td align="center">@crizcraig</td>
<td align="center">s*s</td>
<td align="center">*火</td>
</tr>
<tr>
<td align="center">*勤</td>
<td align="center">**锟</td>
<td align="center">*涛</td>
<td align="center">**明</td>
</tr>
<tr>
<td align="center">*知</td>
<td align="center">*语</td>
<td align="center">*瓜</td>
<td align="center"></td>
<td align="center" width="220">*知</td>
<td align="center" width="220">*语</td>
<td align="center" width="220">*瓜</td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
</tr>
</table>

View file

@ -9,33 +9,30 @@
</p>
<p align="center">
<img src="blog/images/claude-code-router.png" alt="Claude Code Router Desktop 项目截图" />
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop 项目截图" />
</p>
Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。
CCR 在你的本机运行Provider 配置保存在本地配置目录,并默认暴露本地网关地址:`http://localhost:8080`
## 为什么使用 CCR
- 用一个本地入口连接多个 Agent 工具,不需要在每个客户端里重复配置 Provider。
- 不同任务使用不同模型,例如后台任务、推理任务、长上下文、图片任务或支持联网搜索的模型。
- 在不改变工作流的情况下混用不同 Provider。CCR 支持 OpenAI 兼容 API、Anthropic Messages、Gemini Generate Content、OpenRouter、DeepSeek、SiliconFlow、Moonshot、Kimi Code、Mistral、Z.AI、百炼以及自定义 Provider。
- 通过 fallback 路由、API Key 轮换、用量统计和请求日志来控制成本和可靠性。
- 使用桌面 UI 管理配置,减少手写 JSON。
- 通过插件、代理路由、本地 HTTP 后端和 Provider deeplink 扩展网关能力。
## 功能和特性
- **概览仪表盘**:查看系统状态、用量组件、账号余额、模型分布和分享卡片。
- **Provider 管理**:添加预设或自定义端点,探测协议支持,检测模型连通性,管理凭据,并在可用时查看账号余额。
- **路由规则**:配置默认路由、条件路由、模型前缀规则、失败降级和请求改写。
- **路由规则**:配置条件路由、模型前缀规则、失败降级和请求改写。
- **Agent配置**:为 Claude Code、Codex 和 ZCode 配置启动入口、模型、作用范围和多开 App 配置。
- **网关兼容层**:通过本地 CCR 模型网关转换支持的客户端请求。
- **代理模式**:通过本地代理捕获支持的 API 流量,可选系统代理和网络捕获。
- **扩展机制**:安装或加载 wrapper 插件,包括 Claude Design、Cursor Proxy 这类集成路由。
- **Fusion 组合模型**:把基础模型与视觉、联网搜索或 MCP 工具组合成新的可选模型。
- **Provider Deeplink**:通过 `ccr://provider?...` 链接导入 Provider 配置、manifest 和嵌入式一键导入按钮,写入前会弹出确认。
## 文档
完整文档见 [ccrdesk.top](https://ccrdesk.top/)。
## 下载和安装
@ -64,7 +61,7 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序
### 2. 设置路由
打开 **路由**选择默认路由,添加条件规则,配置请求改写和失败降级。
打开 **路由**,添加条件规则,配置请求改写和失败降级。
如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。
@ -80,80 +77,6 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序
**设置 → 日志与观测** 打开请求日志和 Agent 观测。使用 **日志** 确认 `request model``resolved provider``resolved model`、状态码、tokens、耗时和错误使用托盘窗口快速查看 Token 和账号状态。
## 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直链导入时必填。
- `api_key`:可选 Provider API Key。
- `models`:逗号或换行分隔的模型列表,也可以重复传入 `models=...`
- `protocol``openai_chat_completions``openai_responses``anthropic_messages``gemini_generate_content`
- `icon`Provider 图标 URL。
- `source`Provider 官网或配置来源。
- `manifest`:远程 HTTPS manifest URL。
- `usage_url``fetch_usage``usage_method``usage_headers``usage_body`:可选账号用量读取配置。
- `balance``balance_unit``subscription``subscription_limit``subscription_reset``subscription_unit``subscription_window`:可选用量字段映射。
更大的 payload 可以通过 URL 编码 JSON 或 base64url JSON 传入 `payload` 字段,也可以传入 `manifest` 让 CCR 拉取远程 Provider manifest。Manifest 必须使用 HTTPS返回 JSON不能指向本地或内网地址体积不能超过 128 KB。参数名和协议值必须使用上面的完整规范名不再接受 `baseUrl``apiKey``model``type``openai` 等别名。
CCR 在写入外部链接导入的 Provider 前,总会弹出确认窗口。
## 扩展机制
CCR 有两层扩展:
- Wrapper plugin使用顶层 `plugins` 扩展 Electron wrapper注册本地 HTTP 路由、启动本地后端、拦截代理流量、添加内置浏览器入口、连接 Provider 账号用量。
- Core gateway plugin使用 `providerPlugins``plugins[].coreGateway.providerPlugins` 扩展上游 Provider、认证方式或 core gateway 内部能力。`plugins[].coreGateway.virtualModelProfiles` 可以注入虚拟模型配置。
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)` / `activate(ctx)` 的对象。上下文支持:
- `ctx.registerGatewayRoute({ method, path, auth, handler })`
- `ctx.registerHttpBackend({ id, host, port, handler })`
- `ctx.registerProxyRoute({ host, paths, upstream, stripPathPrefix, rewritePathPrefix, headers })`
- `ctx.registerApp(app)`
- `ctx.openSqliteStore({ filename, migrate })`
- `ctx.registerProviderAccountConnector(connector)`
- `ctx.registerCoreGatewayProviderPlugin(plugin)`
- `ctx.registerCoreGatewayVirtualModelProfile(profile)`
本地插件示例见 [examples/plugins](examples/plugins)。
## 深入阅读
- [项目动机和工作原理](blog/zh/项目初衷及原理.md)
- [也许我们可以用路由器做更多事情](blog/zh/或许我们能在Router中做更多事情.md)
## 致谢
对 Codex 的支持来自于 [musistudio/codexl](https://github.com/musistudio/codexl) 这个项目。
@ -206,177 +129,186 @@ Wrapper plugin route 示例:
<p>非常感谢所有赞助商的慷慨支持。</p>
<table>
<table width="100%">
<tr>
<td align="center" width="260">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ"><strong>Z智谱</strong></a>
<td align="center" width="330">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="智谱图标" />
<br />
<strong>Z智谱</strong>
</a>
</td>
<td align="center" width="260">
<a href="https://aihubmix.com/"><strong>AIHubmix</strong></a>
<td align="center" width="330">
<a href="https://aihubmix.com/">
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&amp;sz=128" width="42" height="42" alt="AIHubmix 图标" />
<br />
<strong>AIHubmix</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://ai.burncloud.com">
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud 图标" />
<br />
<strong>BurnCloud</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://share.302.ai/ZGVF9w">
<img src="https://www.google.com/s2/favicons?domain=302.ai&amp;sz=128" width="42" height="42" alt="302.AI 图标" />
<br />
<strong>302.AI</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="260">
<a href="https://ai.burncloud.com"><strong>BurnCloud</strong></a>
</td>
<td align="center" width="260">
<a href="https://share.302.ai/ZGVF9w"><strong>302.AI</strong></a>
<td align="center" width="330">
<a href="https://runapi.co/register?aff=IX1t">
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI 图标" />
<br />
<strong>RunAPI</strong>
</a>
</td>
</tr>
</table>
<h4>社区赞助者</h4>
<table>
<table width="100%">
<tr>
<td align="center" width="25%">@Simon Leischnig</td>
<td align="center" width="25%"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="25%"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="25%">@*o</td>
<td align="center" width="220">@Simon Leischnig</td>
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="220">@*o</td>
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center" width="220">@*说</td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center">@*说</td>
<td align="center">@*更</td>
<td align="center">@K*g</td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@K*g</td>
<td align="center" width="220">@R*R</td>
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220">@*划</td>
</tr>
<tr>
<td align="center">@R*R</td>
<td align="center"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center">@*苗</td>
<td align="center">@*划</td>
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center" width="220">@S*r</td>
<td align="center" width="220">@*晖</td>
<td align="center" width="220">@*敏</td>
<td align="center" width="220">@Z*z</td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center">@S*r</td>
<td align="center">@*晖</td>
<td align="center" width="220">@*然</td>
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center" width="220">@*应</td>
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
</tr>
<tr>
<td align="center">@*敏</td>
<td align="center">@Z*z</td>
<td align="center">@*然</td>
<td align="center"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*飞</td>
<td align="center" width="220">@董*</td>
<td align="center" width="220">@*汀</td>
<td align="center" width="220">@*涯</td>
<td align="center" width="220">@*:-</td>
<td align="center" width="220">@**磊</td>
</tr>
<tr>
<td align="center">@*苗</td>
<td align="center"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center">@*应</td>
<td align="center"><a href="https://github.com/yusnake">@yusnake</a></td>
<td align="center" width="220">@*琢</td>
<td align="center" width="220">@*成</td>
<td align="center" width="220">@Z*o</td>
<td align="center" width="220">@*琨</td>
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center" width="220">@*_</td>
</tr>
<tr>
<td align="center">@*飞</td>
<td align="center">@董*</td>
<td align="center">@*汀</td>
<td align="center">@*涯</td>
<td align="center" width="220">@Z*m</td>
<td align="center" width="220">@*鑫</td>
<td align="center" width="220">@c*y</td>
<td align="center" width="220">@*昕</td>
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center" width="220">@b*g</td>
</tr>
<tr>
<td align="center">@*:-</td>
<td align="center">@**磊</td>
<td align="center">@*琢</td>
<td align="center">@*成</td>
<td align="center" width="220">@*亿</td>
<td align="center" width="220">@*辉</td>
<td align="center" width="220">@JACK</td>
<td align="center" width="220">@*光</td>
<td align="center" width="220">@W*l</td>
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
</tr>
<tr>
<td align="center">@Z*o</td>
<td align="center">@*琨</td>
<td align="center"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center">@*_</td>
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center" width="220">@二吉吉</td>
<td align="center" width="220">@a*g</td>
<td align="center" width="220">@*林</td>
<td align="center" width="220">@*咸</td>
<td align="center" width="220">@*明</td>
</tr>
<tr>
<td align="center">@Z*m</td>
<td align="center">@*鑫</td>
<td align="center">@c*y</td>
<td align="center">@*昕</td>
<td align="center" width="220">@S*y</td>
<td align="center" width="220">@f*o</td>
<td align="center" width="220">@*智</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@r*c</td>
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center">@b*g</td>
<td align="center">@*亿</td>
<td align="center">@*辉</td>
<td align="center" width="220">@*军</td>
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220">@zcutlip</td>
</tr>
<tr>
<td align="center">@JACK</td>
<td align="center">@*光</td>
<td align="center">@W*l</td>
<td align="center"><a href="https://github.com/kesku">@kesku</a></td>
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@*.</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@*政</td>
<td align="center" width="220">@*铭</td>
</tr>
<tr>
<td align="center"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center">@二吉吉</td>
<td align="center">@a*g</td>
<td align="center">@*林</td>
<td align="center" width="220">@*叶</td>
<td align="center" width="220">@七*o</td>
<td align="center" width="220">@*青</td>
<td align="center" width="220">@**晨</td>
<td align="center" width="220">@*远</td>
<td align="center" width="220">@*霄</td>
</tr>
<tr>
<td align="center">@*咸</td>
<td align="center">@*明</td>
<td align="center">@S*y</td>
<td align="center">@f*o</td>
<td align="center" width="220">@**吉</td>
<td align="center" width="220">@**飞</td>
<td align="center" width="220">@**驰</td>
<td align="center" width="220">@x*g</td>
<td align="center" width="220">@**东</td>
<td align="center" width="220">@*落</td>
</tr>
<tr>
<td align="center">@*智</td>
<td align="center">@F*t</td>
<td align="center">@r*c</td>
<td align="center"><a href="https://github.com/qierkang">@qierkang</a></td>
<td align="center" width="220">@哆*k</td>
<td align="center" width="220">@*涛</td>
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center" width="220">@*呢</td>
<td align="center" width="220">@d*u</td>
<td align="center" width="220">@crizcraig</td>
</tr>
<tr>
<td align="center">@*军</td>
<td align="center"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center">@*王</td>
<td align="center"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">s*s</td>
<td align="center" width="220">*火</td>
<td align="center" width="220">*勤</td>
<td align="center" width="220">**锟</td>
<td align="center" width="220">*涛</td>
<td align="center" width="220">**明</td>
</tr>
<tr>
<td align="center">@*王</td>
<td align="center">@zcutlip</td>
<td align="center"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center">@*更</td>
</tr>
<tr>
<td align="center">@*.</td>
<td align="center">@F*t</td>
<td align="center">@*政</td>
<td align="center">@*铭</td>
</tr>
<tr>
<td align="center">@*叶</td>
<td align="center">@七*o</td>
<td align="center">@*青</td>
<td align="center">@**晨</td>
</tr>
<tr>
<td align="center">@*远</td>
<td align="center">@*霄</td>
<td align="center">@**吉</td>
<td align="center">@**飞</td>
</tr>
<tr>
<td align="center">@**驰</td>
<td align="center">@x*g</td>
<td align="center">@**东</td>
<td align="center">@*落</td>
</tr>
<tr>
<td align="center">@哆*k</td>
<td align="center">@*涛</td>
<td align="center"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center">@*呢</td>
</tr>
<tr>
<td align="center">@d*u</td>
<td align="center">@crizcraig</td>
<td align="center">s*s</td>
<td align="center">*火</td>
</tr>
<tr>
<td align="center">*勤</td>
<td align="center">**锟</td>
<td align="center">*涛</td>
<td align="center">**明</td>
</tr>
<tr>
<td align="center">*知</td>
<td align="center">*语</td>
<td align="center">*瓜</td>
<td align="center"></td>
<td align="center" width="220">*知</td>
<td align="center" width="220">*语</td>
<td align="center" width="220">*瓜</td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
</tr>
</table>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -5,12 +5,6 @@ eyebrow: Detailed Configuration
lead: Manage API keys that clients use to access the CCR gateway, with expiration and local limits.
---
## Basic Concept
The API Keys page manages client access keys for CCR. These keys are different from provider `API key` values and provider credential pools: API Keys control who can call CCR; provider keys control how CCR calls upstream providers.
When API keys are configured, clients should send `Authorization: Bearer <key>` or `x-api-key: <key>`.
## List Fields
| Field | Capability |

View file

@ -5,10 +5,6 @@ eyebrow: Detailed Configuration
lead: Forward agent messages to instant-messaging platforms or hand off active work after desktop idle.
---
## What Bots Do
Bots forward agent messages to instant-messaging platforms and can hand off active work after the desktop has been idle.
## Common Modes
- **Forward agent messages**: mirror agent messages into IM.

View file

@ -5,7 +5,7 @@ eyebrow: Detailed Configuration
lead: Learn how CCR extensions are loaded, what they can register, and how to create, install, and debug your own extension.
---
## What Extensions Are
## Extension Types
CCR has two extension layers:
@ -177,7 +177,7 @@ The recommended flow is through the desktop UI:
4. Save the config.
5. Open **Server** and restart the gateway.
CCR stores runtime configuration in SQLite, so add extensions through the UI instead of editing the legacy JSON config file. The extension entry has this shape:
CCR stores runtime configuration in SQLite. Add extensions through the UI; the legacy JSON config file is kept here only as a reference. The extension entry has this shape:
```json
{
@ -270,7 +270,7 @@ Proxy route matching rules:
| Response is 401 | Routes require gateway API key by default; set `auth: "none"` for debug routes |
| Code changes do not apply | Wrapper plugins are not hot reloaded; restart the gateway or CCR |
| Port is already in use | Omit `port` in `registerHttpBackend` so CCR can allocate one automatically |
| Proxy route is not hit | Confirm proxy mode is enabled, the certificate is installed, and host matches the real request hostname |
| Proxy route misses requests | Confirm proxy mode is enabled, the certificate is installed, and host matches the real request hostname |
## Security Notes

View file

@ -2,58 +2,130 @@
title: Overview Dashboard
pageTitle: Overview Dashboard
eyebrow: Detailed Configuration
lead: Customize the CCR home dashboard for system status, account balance, requests, tokens, cost, model distribution, and share cards.
lead: Customize the CCR home dashboard to inspect system status, account balance, requests, tokens, cost, and model distribution.
---
## Basic Concept
## When To Use It
Overview is the CCR dashboard. It turns gateway status, provider accounts, request usage, and analysis results into draggable widgets. You can switch the time range, edit widgets, resize them, change styles, and generate shareable cards from selected data.
Overview depends on request logs, usage stats, and provider account usage. Some widgets require Request logs or Agent observability to be enabled in Settings. Account balance widgets require `Fetch usage` on providers.
## Top Controls
| Field | Capability |
| Scenario | What to inspect |
| --- | --- |
| Usage over time | Changes the statistics range used by the dashboard. |
| Today / 24h / 7d / 30d | Available time windows. Widgets recalculate requests, tokens, cost, and trends for the selected range. |
| Edit widgets | Enters layout editing mode. |
| Reset layout | Restores the default overview layout. |
| Done | Leaves editing mode and keeps the current widget configuration. |
| Check gateway health | System status, success rate, errors, average latency |
| Estimate recent spend | Requests, input / output / cache tokens, estimated cost |
| Compare upstream usage | Provider analysis, model distribution, client analysis |
| Watch account quota | Balance, subscription quota, remaining quota, account status |
| Report or share usage | AI Usage Wrapped, CCR Route Map, Model Leaderboard, Spend Receipt, and other share cards |
## Widget Editing
## Time Range
In editing mode, the left `Components` panel adds widgets, the middle `Preview` panel shows the current layout, and the right `Component properties` panel edits the selected widget.
The `Usage over time` control at the top drives every widget that depends on usage stats. After you switch ranges, requests, tokens, cost, trends, distribution, and share cards are recomputed for the selected window.
| Field | Capability |
| Option | Window |
| --- | --- |
| Components | List of widgets that can be added. |
| Preview | Current dashboard layout. Widgets can be dragged to reorder. |
| Component properties | Configuration for the selected widget. |
| Component category | Changes the widget category, such as status, account, metric, trend, activity, breakdown, analysis, or share card. |
| Data | Selects the data shown by the widget, such as requests, tokens, cost, account, client analysis, or provider analysis. |
| Widget size | Controls the widget's grid width and height. |
| Style | Changes visual style, such as cards, compact, bar, line, ring, and more. |
| Remove widget | Removes the selected widget from the overview. |
| `Today` | Current local date from 00:00 to now, bucketed hourly. |
| `24h` | Last 24 hours, bucketed hourly. |
| `7d` | Last 7 days, bucketed daily. |
| `30d` | Last 30 days, bucketed daily. |
## Widget Types
The account balance widget does not use this time range. It shows the latest snapshot returned by provider account connectors.
| Widget | Capability |
## Edit Layout
Click the pencil button in the upper-right corner to enter editing mode. Editing mode has three columns:
| Area | Purpose |
| --- | --- |
| Status component | Shows a system status timeline for recent gateway health. |
| Account component | Shows provider account balance, quota, or usage. Requires provider `Fetch usage`. |
| Metric component | Shows requests, total tokens, input tokens, output tokens, cache tokens, cache ratio, estimated cost, success rate, errors, or average latency. |
| Trend component | Shows usage trend over time. |
| Activity component | Shows token activity as a heatmap. |
| Breakdown component | Shows Token mix or Model distribution. |
| Analysis component | Shows Client Analysis or Provider Analysis. |
| Share card | Generates shareable PNG cards such as AI Usage Wrapped, CCR Route Map, Model Leaderboard, AI Fuel Cockpit, Token Calendar Poster, and Spend Receipt. |
| Components | Left palette. Click a template to add it to the dashboard. |
| Preview | Middle layout preview. Drag widgets to reorder them or click a widget to select it. |
| Component properties | Right property panel for changing type, data, size, style, or removing the selected widget. |
## Data Sources
Common operations:
| Data | Source |
1. Add a widget: click a template in `Components`.
2. Reorder widgets: drag them in `Preview`.
3. Resize a widget: select it, then drag the right, bottom, or bottom-right resize handle.
4. Change data: use `Component category` and `Data` in `Component properties`.
5. Change presentation: choose `Widget size` and `Style`.
6. Save the result: click `Done`; the layout is persisted in app configuration.
7. Restore defaults: click `Reset layout` while editing.
Removing a widget only removes that card from the overview layout. It does not delete request logs, providers, account connectors, or upstream configuration. If all widgets are removed, the page shows `No widgets configured`.
## Widget Catalog
Sizes are written as `width:height`, with both dimensions from `1` to `4`. The overview grid has up to 4 columns on desktop and collapses automatically on narrow screens.
| Widget | Data | Default size | Default style | Styles |
| --- | --- | --- | --- | --- |
| Status component | System status | `4:1` | Timeline | Timeline, Compact |
| Account component | All accounts or one account | `4:2` | Cards | Cards, Compact, Bars, Ring, Semicircle, Arc, Nested rings |
| Metric component | Requests, tokens, cost | `1:1` | Cards | Cards, Compact, Bar, Ring |
| Trend component | Usage over time | `3:2` | Composed | Composed, Area, Line, Bar |
| Activity component | Token activity | `4:2` | Heatmap | Heatmap |
| Breakdown component | Token distribution / Model distribution | Token distribution: `1:2`; Model distribution: `2:2` | Token distribution: Bars; Model distribution: Pie | Bars, Stacked, Donut, Pie |
| Analysis component | Client Analysis / Provider Analysis | `2:2` | Table | Table, Compact |
| Share card | AI Usage Wrapped, CCR Route Map, Model Leaderboard, AI Fuel Cockpit, Token Calendar Poster, Spend Receipt | `1:4` | Card | Card |
Size constraints:
| Rule | Reason |
| --- | --- |
| Requests, tokens, cost, success rate, latency | Request logs and usage stats. |
| Account balance, quota, status messages | Provider `Fetch usage` configuration. |
| Client analysis, provider analysis, model distribution | Client, provider, model, and token data from request logs. |
| Agent analysis data | Agent observability settings and agent execution traces. |
| Share cards have a minimum size of `1:4`. | PNG export uses a vertical poster ratio and needs enough height. |
| The account widget has a minimum size of `2:2` when showing All accounts with the Compact style. | Multi-account lists need readable space. |
| Legacy aliases are still accepted: `small` -> `1:1`, `medium` / `large` -> `2:2`, `wide` -> `3:2`, `full` -> `4:1` or `4:2`. | Backward compatibility for older config. |
## Metric Data
`metric` widgets use the `metric` field to choose the displayed value.
| `metric` | Meaning |
| --- | --- |
| `requests` | Request count |
| `total-tokens` | Total tokens |
| `input-tokens` | Input tokens |
| `output-tokens` | Output tokens |
| `cache-tokens` | Cache tokens |
| `cache-ratio` | Cache ratio |
| `estimated-cost` | Estimated cost, calculated from model pricing data |
| `success-rate` | Success rate |
| `errors` | Error count |
| `avg-latency` | Average latency |
## Account Widget
The account widget reads provider account / usage connectors. To show balance or remaining quota, first enable and test `Fetch usage` in provider configuration.
| Data selection | Behavior |
| --- | --- |
| `All accounts` | Shows every available account snapshot. |
| One account | Shows only one provider or credential snapshot. The internal config value is usually `provider` or `provider::credentialId`. |
If the account widget is empty, check:
1. Whether the provider has an account / usage connector configured.
2. Whether the `Fetch usage` test succeeds.
3. Whether the API key or account endpoint is still valid.
4. Whether the selected account was deleted or renamed.
## Share Cards
Share card widgets can export PNGs through the download button in the card header. The desktop app uses native export when available; browser environments fall back to frontend canvas export. The exported image size is `1080 x 1350`.
| Card | `type` | Content |
| --- | --- | --- |
| AI Usage Wrapped | `share-usage-wrapped` | Total tokens, requests, estimated cost, cache ratio, longest activity streak, top model, top provider, peak day. |
| CCR Route Map | `share-route-map` | Main client-to-provider/model route relationships, plus client, provider, and model counts. |
| Model Leaderboard | `share-model-leaderboard` | Models ranked by tokens. |
| AI Fuel Cockpit | `share-fuel-cockpit` | Up to 3 account quota gauges. Requires account / usage connectors. |
| Token Calendar Poster | `share-token-calendar` | Contribution-calendar style token activity poster. |
| Spend Receipt | `share-spend-receipt` | Estimated cost, requests, tokens, latency, and success rate for the selected range. |
## Data Sources And Troubleshooting
| Symptom | Likely cause | What to do |
| --- | --- | --- |
| Requests, tokens, or cost are 0 | No requests went through CCR in the selected range, or usage capture has not recorded data yet. | Try `24h` / `7d`, and confirm the client is actually using CCR. |
| Cost shows `$0.00` | The model has no pricing data, or usage is very small. | Check model catalog matching and provider model names; values under 0.01 USD are shown with extra decimals. |
| Success rate or errors look unexpected | The overview only aggregates request results captured by CCR. | Compare with records on the Logs page. |
| Account balance is empty | No account connector exists, or `Fetch usage` failed. | Test account / usage field mapping in provider configuration. |
| Distribution charts have no data | Request logs lack model, provider, or token information. | Confirm requests go through CCR and upstream responses include token usage. |
| PNG export fails | Canvas export is unavailable, the element has no size, or the save dialog was canceled. | Retry in the desktop app, and make sure the card is visible and not resized too small. |

View file

@ -5,11 +5,16 @@ eyebrow: Detailed Configuration
lead: Create reusable launch configurations for Claude Code, Codex, and ZCode, and open separate agent instances from different configs.
---
## What Agent Config Is
## Configuration Flow
Agent Config is the desktop app capability for managing Claude Code, Codex, and ZCode launch entries. It is not a provider or a routing rule; it is the full entry point for one agent launch: agent type, entry mode, model, effect scope, config file location, and optional Bot binding.
1. Add at least one usable provider and model in **Provider Config**, or create the Fusion model you want to use.
2. Open **Agent Config** and click **Add profile**.
3. Choose the agent type, name the config, then choose the effect scope and entry mode.
4. Select a model. The value is usually `Provider name/model name`, and Fusion models can be selected too.
5. If the entry mode includes App, optionally bind a Bot and choose whether to forward agent messages or enable handoff.
6. Save the config, then open it from the Agent Config card: the terminal button copies the CLI command, and the play button starts the App instance.
This page exists in Detailed Configuration to explain which config opens which agent instance, rather than provider, routing, or Fusion fields.
During trial, prefer **Only opened from CCR** and always open the agent from CCR. That keeps the config limited to CCR-launched instances and avoids changing the Claude Code, Codex, or ZCode setup you open directly from the system.
## Multi-Instance Mechanism
@ -26,20 +31,80 @@ This lets you create multiple configs for the same agent, such as "Claude Code -
## Common Options
| Option | Description |
| Option | Applies to | Description |
| --- | --- | --- |
| Agent | All | Claude Code, Codex, or ZCode. ZCode supports App only. |
| Config name | All | Identifies the config in CCR and can be used as the `ccr <config-name>` launch target. Names can contain spaces; copied commands are quoted automatically. |
| Enabled | All | Disabled configs are not exposed as active launch entries and are not applied as effective startup configs. |
| Effect scope | All | **Only opened from CCR** uses CCR-managed isolated config; **System default** writes the agent's default config. Only one enabled system-default config is allowed per agent. |
| Entry mode | Claude Code, Codex | `CLI & APP` exposes both CLI and App entry points; `CLI only` only generates a CLI command; `App only` only exposes the App entry point. |
| Model | All | Default model for the opened agent, either a provider model or Fusion model. For Claude Code, leaving it empty keeps the Claude Code default. |
| Bot | App entry | Bot forwarding only works for App mode opened from CCR. CLI does not forward Bot messages yet. |
| Environment variables | All | Extra environment variables injected into this config. Claude Code includes `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` by default so gateway model discovery is enabled. |
## Per-Agent Options
### Claude Code
| Option | What it does |
| --- | --- |
| Agent | Claude Code, Codex, or ZCode |
| Config name | Identifies the config in CCR and can be used as the `ccr <config-name>` launch target |
| Effect scope | **Only opened from CCR** uses CCR-managed isolated config; **System default** writes the agent's default config |
| Entry mode | `CLI & APP`, `CLI only`, or `App only`; ZCode supports App only |
| Model | Default model for the opened agent, either a provider model or Fusion model |
| Bot | App entry can bind a Bot for IM forwarding or handoff |
| Model override | Writes `ANTHROPIC_MODEL` for Claude Code. Leave it empty to keep Claude Code's own default model. |
| Small fast model | Writes `ANTHROPIC_SMALL_FAST_MODEL` for Claude Code lightweight tasks. Leave it empty to keep the Claude Code default. |
| Settings file | System-default mode uses the Claude Code default settings file; Only opened from CCR creates an isolated settings file under CCR's config directory, separated by Agent Config `id`. |
| Environment variables | Merged into the Claude Code settings `env`. CCR also writes the gateway endpoint, API key helper, and launch wrapper. |
| Bot | Applies only to the Claude App entry. Select a saved Bot, then choose message forwarding or handoff. |
After Claude Code CLI is opened from CCR, it uses CCR gateway model discovery. In Claude Code CLI, enter `/model` to view and switch the models exposed by CCR, including normal provider models and visible Fusion models.
Claude App is **zero-config**: when CCR opens Claude App from the desktop app, CCR automatically writes the Claude App gateway config, API key, model discovery list, and isolated user-data directory. No extra user action is required; opening Claude App from CCR automatically completes all necessary configuration. If Claude App is already running, restart it or reopen it from CCR when prompted.
Claude App and Claude Code CLI use different model-list adapters:
| Entry | Model list source | Notes |
| --- | --- | --- |
| Claude Code CLI | CCR gateway model discovery | Use `/model` in the CLI to view the list; selected requests still go through CCR providers, routing, and Fusion. |
| Claude App | CCR-generated Claude App inference models | Claude App needs Claude-compatible model names. CCR maps `Provider/model` and Fusion models into model entries Claude App can recognize, while display labels keep the real model meaning visible. |
### Codex
| Option | What it does |
| --- | --- |
| Provider ID | Writes Codex `model_provider`, defaulting to `claude-code-router`. Keep it stable and use only letters, numbers, dots, underscores, or hyphens. |
| Provider name | Display name shown in Codex, defaulting to `Claude Code Router`. |
| Codex model | Default Codex model. It can be a provider model or Fusion model; if left empty, CCR uses the first available default model. |
| Show all sessions | Lets Codex show all sessions. ZCode does not expose this option. |
| Config file | Defaults to `~/.codex/config.toml`. Only opened from CCR writes into CCR-managed isolated config directories. |
| Environment variables | Injected into Codex CLI or Codex App. Claude Code-specific model discovery variables are not passed to Codex. |
| Bot | Applies only to the Codex App entry. |
After saving, use the terminal button on the config card to copy the Codex CLI command, for example `ccr "Codex - Work"`. Use the play button to open Codex App. CCR generates `config.toml`, a model catalog file, and a middleware launcher so Codex CLI and Codex App use the same CCR model and provider information.
### ZCode
| Option | What it does |
| --- | --- |
| Provider ID | Writes the ZCode provider reference, defaulting to `claude-code-router`. |
| Provider name | Display name shown in ZCode, defaulting to `Claude Code Router`. |
| ZCode model | Default model when ZCode App opens. It can be a provider model or Fusion model. |
| Config file | Defaults to `~/.zcode/cli/config.json`; CCR also writes ZCode v2 config and model cache. |
| Environment variables | Injected into ZCode App and the middleware launcher. |
| Bot | Applies only to the ZCode App entry. |
ZCode supports App only, so its entry mode is fixed to `App only`. The `Show all sessions` option is hidden for ZCode.
## CLI And App Modes
| Mode | How to open | Best for | Key differences |
| --- | --- | --- | --- |
| CLI | Click the terminal button to copy the command, then run `ccr <config-name>` in a terminal | Working inside a project directory, shell workflows, scripting | Uses the config-specific wrapper or middleware launcher; usually stays in the terminal without opening a desktop window; Bot forwarding support is pending. |
| App | Click the play button in the CCR desktop app | Desktop windows, side-by-side instances, Bot forwarding, handoff | Uses a separate user-data directory per Agent Config; reopening the same config activates the existing window, while different configs can run in parallel. |
| CLI & APP | One config exposes both CLI and App entry points | Reusing the same model config in both terminal and desktop App workflows | Both entries share the config name, model, effect scope, and environment variables, but launch differently. |
## Agent Differences
### Claude Code
Claude Code config writes a settings file. With **Only opened from CCR**, CCR creates an isolated settings file under its own config directory and opens Claude Code through a separate launch wrapper.
Claude Code CLI config writes a settings file. With **Only opened from CCR**, CCR creates an isolated settings file under its own config directory and opens Claude Code through a separate launch wrapper.
When opening Claude App from the desktop app, CCR also prepares a separate user-data directory for that config. Different Agent Config entries use different directories, so multiple Claude App instances can run at the same time.

View file

@ -70,6 +70,10 @@ Choose a provider below to get started. CCR shows what will be added before savi
<span class="provider-import-icon-shell"><img src="../../../provider-icons/siliconflow.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">SiliconFlow</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-runapi" href="ccr://provider?name=RunAPI&amp;base_url=https%3A%2F%2Frunapi.co%2Fv1&amp;protocol=openai_responses" aria-label="Import RunAPI provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/runapi.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">RunAPI</span><span class="provider-import-meta">Responses / Chat Completions</span></span>
</a>
</div>
## Embeddable Button Component

View file

@ -5,20 +5,54 @@ eyebrow: Detailed Configuration
lead: Configure upstream model services, credentials, protocols, base URLs, and model lists.
---
## Basic Concept
## Import Local Agent Login
A provider is an upstream model service. One provider config describes the upstream address, protocol capabilities, model list, authentication, optional multi-key rotation, and optional account usage fetching.
When you add a provider, CCR scans for reusable local agent login state. If usable credentials are found, the add dialog shows the matching import entry. Importing creates a normal provider plus provider plugins, so CCR can reuse the local agent authorization without requiring a pasted API key.
Each provider needs at least a name, API endpoint, usable protocol, model list, and one valid credential. The add/edit dialog changes which fields are visible depending on preset providers, custom endpoints, local agent login import, and advanced settings.
### Claude Code
Preset providers fill common endpoints, protocols, models, and usage-fetching settings, so they are the recommended starting point. For custom providers, enter the endpoint manually. CCR uses the endpoint and API key to probe protocol compatibility and help identify whether the upstream works with OpenAI Chat, OpenAI Responses, Anthropic Messages, or Gemini Generate.
Claude Code import reads local Claude Code OAuth credentials. When a usable access token is available, CCR can import it as a `Claude Code API` provider.
After import:
1. The protocol is `anthropic_messages`.
2. The default model list includes `claude-sonnet-4-20250514`; you can later add or remove models in the provider model list.
3. CCR creates OAuth provider plugins that convert requests to use the Claude Code login state.
4. Account usage uses the Anthropic OAuth usage endpoint, so quota state can appear in the provider list, tray, and account panels.
If CCR only detects login traces but no usable access token, the import entry shows why it cannot be imported. Re-authenticate in Claude Code, then return to CCR and add the provider again.
### Codex
Codex import reads the local Codex auth file and model cache. When a Codex access token or refresh token is available, CCR can import it as a `Codex API` provider.
After import:
1. The protocol is `openai_responses`.
2. The API endpoint points to the Codex backend. The model list always includes at least `gpt-5-codex` and also merges models and display names from the local model cache.
3. CCR creates Codex OAuth provider plugins and refreshes access credentials when needed.
4. Account usage reads Codex quota, balance, and token-stat endpoints.
After import, select `Codex API/model-name` in routing or Agent Config. If the model cache is stale, open Codex first so it refreshes the model list, then return to CCR to import again or edit the models.
### ZCode
ZCode import reads provider API keys, API endpoints, and model lists from local ZCode config. It can be imported as a `ZCode API` provider only when CCR finds a usable provider key and Base URL.
After import:
1. The protocol is `anthropic_messages`.
2. Models come from local ZCode config first; if none are configured, CCR uses the ZCode runtime cache or default models.
3. CCR creates API-key provider plugins that use the key from local ZCode config for request authentication.
4. If the API endpoint matches a built-in CCR preset, account usage settings are reused from that preset.
If CCR detects ZCode login state but no usable provider API key, the import entry remains unavailable. Configure a usable model provider in ZCode first, then return to CCR and add the provider.
## Main Fields
| Field | Capability |
| --- | --- |
| Select preset provider | Applies a built-in provider template, including default endpoint, supported protocols, default models, icon, provider website, and sometimes account usage settings. Choose `Other / custom API endpoint` for any OpenAI, Anthropic, or Gemini compatible upstream. |
| Import local agent login | Appears while adding a provider if CCR finds usable Claude Code, Codex, or ZCode login state on this computer. Importing creates a provider and provider plugin that reuse the local login credential instead of a normal pasted API key. |
| Name | Internal CCR display name. It is also used by routing, model selectors, logs, and config references. Names must be unique. |
| API endpoint | Upstream API base URL. It controls where requests are sent, and is also used for protocol probing, model discovery, icon detection, and safety checks. Preset providers hide it by default while adding, but it can be overridden in Advanced settings. Custom providers must provide it. |
| API key | Default provider credential. When the credential pool is empty, model requests use this key. Protocol probing, model discovery, connection checks, and default usage fetching also use it. Only use a key issued for the selected endpoint. |
@ -27,13 +61,13 @@ Preset providers fill common endpoints, protocols, models, and usage-fetching se
| Custom models | Manually adds model IDs that discovery did not return. Use this when the provider lacks a `/models` endpoint or a new model is not in the catalog yet. |
| Check Connection | Sends real test requests with the current endpoint, API key, protocol, and selected models. It verifies key, model name, and protocol usability. |
| Models to check | Model selection inside the connection-check confirmation dialog. Use it to test only some models. |
| Check results | Shows whether each model is available, which protocol matched, and the upstream diagnostic message. Passing results do not automatically add models; the main model selection remains authoritative. |
| Check results | Shows whether each model is available, which protocol matched, and the upstream diagnostic message. Results are diagnostic. Add models through the main model selection when you want them saved. |
## Connectivity Checks
`Check Connection` sends real model requests for the models you select. It verifies whether the endpoint, API key, protocol, and model IDs are usable. The check limits generated output, but it can still create extra token usage or count against provider-side request limits.
If the provider bills by request, input tokens, or output tokens, select only the models you need to verify instead of checking every model at once. Check results are diagnostic only; they do not automatically change the model list or usage-fetching settings.
If the provider bills by request, input tokens, or output tokens, select only the models you need to verify. Checking every model at once can create unnecessary usage. Review the diagnostics, then adjust the model list or usage-fetching settings manually when needed.
## Credentials
@ -90,7 +124,7 @@ This mode tries provider-hosted CCR account endpoints such as `/.well-known/ccr/
### HTTP JSON Request
Use this mode when the provider has a balance or quota JSON endpoint that does not match CCR's standard account format.
Use this mode when the provider has a balance or quota endpoint that returns a custom JSON shape.
| Field | Capability |
| --- | --- |

View file

@ -9,9 +9,9 @@ lead: Choose the model for a request, then automatically retry or switch to fall
### Claude Code
The built-in Claude Code route detects requests from Claude Code and routes main requests to the Claude Code Agent Config model or the default route model.
The built-in Claude Code route detects requests from Claude Code and routes main requests to the Claude Code Agent Config model.
Claude Code **main requests** use the Claude Code Agent Config model by default; if it is unset, they use the Routing page default model. CCR also automatically removes the first `x-anthropic-billing-header` system message injected by Claude Code so that billing helper messages do not affect later routing decisions. Claude Code Subagent, Task, and Workflow-created agents can still choose different models through the tag mechanism below.
Claude Code **main requests** use the Claude Code Agent Config model. If that model is unset, the built-in route remains inactive. CCR also automatically removes the first `x-anthropic-billing-header` system message injected by Claude Code so that billing helper messages do not affect later routing decisions. Claude Code Subagent, Task, and Workflow-created agents can still choose different models through the tag mechanism below.
#### Subagent / Workflow Auto-Routing
@ -137,9 +137,9 @@ When a rule matches, its **On failure** setting is used. Requests that do not ma
After saving, the rule appears in the list. Use request logs, especially `request model`, `resolved provider`, `resolved model`, and route reason, to verify that it matched.
## What Fallback Does
## Fallback Handling
Fallback is the failure strategy after a model or upstream request fails. It does not pick the first model; it decides whether CCR should keep trying after the current target fails.
Fallback is the failure strategy after a model or upstream request fails. Routing picks the first model; Fallback decides whether CCR should keep trying after the current target fails.
The **Default on failure** control at the top of the Routing page is the global Fallback. Each rule also has **On failure**. When a rule matches, its rule-level Fallback overrides the global Fallback.
@ -176,7 +176,7 @@ Configure **Default on failure** at the top of the Routing page:
2. If you choose **Retry**, set `Retries`.
3. If you choose **Fallback targets**, add backup models in priority order.
Global Fallback applies to the default route and to rules that do not define their own Fallback.
Global Fallback applies to routing rules that do not define their own Fallback.
### Rule-Level Fallback

View file

@ -5,10 +5,6 @@ eyebrow: Detailed Configuration
lead: Configure the CCR gateway host, port, and Proxy mode for MITM interception and proxying into CCR.
---
## Basic Concept
The Server page controls how the local CCR gateway listens for requests. Normal model requests usually need only `Host` and `Port`.
## Main Fields
| Field | Capability |

View file

@ -5,10 +5,6 @@ eyebrow: Detailed Configuration
lead: Configure the CCR system tray icon, balance progress, and tray window widgets.
---
## Basic Concept
Tray settings live under Settings. They control the system tray icon and the tray window. The tray window can show providers, account balance, token trends, activity, metrics, and model share so you can check CCR status without opening the main window.
## Top Fields
| Field | Capability |

View file

@ -5,19 +5,6 @@ eyebrow: Product Documentation
lead: Learn what CCR is for, where its boundaries are, and how the docs are organized. Start with Quick Start when you want to configure it; use Detailed Configuration for fields, Bots, or Fusion.
---
## What CCR Can Do
**Claude Code Router (CCR) is a local model gateway.** It sits between agents such as Claude Code, Codex, and ZCode and upstream model services, then centralizes model management, API keys, routing rules, logs, observability, and Bot relay.
CCR is useful when:
- you do not want to maintain the same models and keys separately in every agent.
- you want different tasks to automatically use different models: fast models for lightweight background work, stronger models for complex tasks, and Fusion for image or web-search work.
- you need request logs showing which provider and model handled a request, whether it succeeded, how long it took, and roughly how much it cost.
- you want to forward long-running agent messages to Slack, Telegram, Feishu, WeCom, or other IM platforms.
CCR listens on the local default address `http://localhost:8080`. Once an agent points to this address, CCR can take over the request and forward it to upstream providers according to routing rules.
## Documentation Structure
The top navigation is split into four standalone pages:
@ -26,7 +13,7 @@ The top navigation is split into four standalone pages:
| --- | --- |
| [Documentation](./) | Product positioning, architecture overview, and reading path |
| [Quick Start](guides/) | From installation and provider setup to connecting an agent |
| [Detailed Configuration](configuration/providers/) | Overview dashboard, API keys, server, providers, routing, Agent Config, Fusion, Bots, tray, and config database location |
| [Detailed Configuration](configuration/overview/) | Overview dashboard, API keys, server, providers, routing, Agent Config, Fusion, Bots, tray, and config database location |
| [Q&A](troubleshooting/) | Request logs, observability panel, and common questions |
Bot platform guides are child pages under Detailed Configuration. Each platform has its own page so platform dashboard fields, callback URLs, signatures, and FAQs can be expanded independently.
@ -37,5 +24,5 @@ If this is your first time using CCR:
1. Start with [Quick Start](guides/) to connect a provider and Agent Config.
2. Use the app's request logs to confirm whether requests are passing through CCR.
3. Open [Detailed Configuration](configuration/providers/) for the overview dashboard, API keys, server, providers, vision, web search, MCP tools, tray, and IM relay.
3. Open [Detailed Configuration](configuration/overview/) for the overview dashboard, API keys, server, providers, vision, web search, MCP tools, tray, and IM relay.
4. Use [Q&A](troubleshooting/) for 401, 404, timeout, wrong-routing, or Bot delivery questions.

View file

@ -13,7 +13,7 @@ LINE 适合把 Agent 消息接入已有的 LINE 好友、群聊或 LINE Official
## 你会用到哪些字段
CCR 里 LINE 的认证方式叫 **Bot Token**但填的不是 Telegram 那种 token而是下面这两个 channel 字段:
CCR 里 LINE 的认证方式叫 **Bot Token**这里需要填写下面这两个 channel 字段:
| LINE 后台里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |

View file

@ -16,7 +16,7 @@ lead: 深入配置概览仪表盘、API 密钥、服务、供应商、路由、A
| 服务配置 | Host、Port、代理模式、系统代理、网络捕获和 CA 证书 |
| 供应商配置 | 上游服务、协议、基础 URL、模型列表和凭据 |
| 一键导入供应商 | Provider deeplink 协议、Manifest 导入、一键导入按钮和安全边界 |
| 路由配置 | 默认路由、条件规则、fallback 和请求改写 |
| 路由配置 | 条件规则、fallback 和请求改写 |
| 日志&观测 | 请求日志、Agent 执行追踪、工具调用和工具结果 |
| Fusion 组合模型 | 把基础模型与视觉、搜索、MCP 工具组合成新的可选模型 |
| Agent配置 | Agent 启动方式、模型、作用范围、多开和 Bot 绑定 |

View file

@ -5,12 +5,6 @@ eyebrow: 详细配置
lead: 管理客户端访问 CCR 网关时使用的 API Key并为每个 Key 设置过期时间和本地限额。
---
## 基本概念
“API 密钥”页面管理的是客户端访问 CCR 时使用的访问 Key。它不同于供应商配置里的 `API 密钥``凭据池`:这里的 Key 用来控制谁能调用 CCR供应商 Key 用来控制 CCR 调用上游供应商。
如果配置了 API Key客户端请求需要带 `Authorization: Bearer <key>``x-api-key: <key>`
## 列表字段
| 字段 | 代表的能力 |

View file

@ -5,10 +5,6 @@ eyebrow: 详细配置
lead: 通过 IM Bot 转发 Agent 消息,或在桌面空闲后把任务接力到手机。
---
## 能做什么
Bot 能把 Agent 消息转发到 IM 平台,也可以在桌面空闲后把任务接力到手机上继续看。
## 常见模式
- **转发 Agent 消息**:把消息同步到 IM。

View file

@ -5,7 +5,7 @@ eyebrow: 详细配置
lead: 了解 CCR 扩展如何加载、能注册哪些能力,并从零创建、安装和调试自己的扩展。
---
## 扩展是什么
## 扩展类型
CCR 的扩展分为两层:
@ -177,7 +177,7 @@ module.exports = {
4. 保存配置。
5. 打开 **Server** 页面,重启网关。
CCR 的运行配置存储在 SQLite 中,因此推荐通过 UI 添加扩展,而不是编辑旧版 JSON 配置文件。扩展条目的配置结构如下:
CCR 的运行配置存储在 SQLite 中。请通过 UI 添加扩展;旧版 JSON 配置文件仅用于参考。扩展条目的配置结构如下:
```json
{

View file

@ -9,7 +9,7 @@ lead: 把基础模型和能力模型组合成新的可选模型,让你已经
Fusion 的价值在于保留基础模型的手感同时补齐它缺少的能力。基础模型继续负责推理、写作和代码生成CCR 在需要时调用图像、搜索或 MCP 工具,把结果整理进上下文,再交给基础模型完成回答。
保存后的 Fusion 模型会像普通模型一样出现在路由和配置中。它不是额外的一次手动流程,而是一个可复用的新模型:把强文本模型升级成视觉模型,把稳定代码模型升级成可联网模型,也可以把 Agent 模型接入团队内部工具。
保存后的 Fusion 模型会像普通模型一样出现在路由和配置中。保存结果是一个可复用的新模型:把强文本模型升级成视觉模型,把稳定代码模型升级成可联网模型,也可以把 Agent 模型接入团队内部工具。
## 适用能力

View file

@ -2,58 +2,130 @@
title: 概览仪表盘
pageTitle: 概览仪表盘
eyebrow: 详细配置
lead: 自定义 CCR 首页组件查看系统状态、账户余额、请求量、tokens、成本、模型分布和可分享卡片
lead: 自定义 CCR 首页仪表盘,查看系统状态、账户余额、请求量、令牌、成本和模型分布
---
## 基本概念
## 适用场景
“概览”是 CCR 的仪表盘页面。它把网关运行状态、供应商账户、请求用量和分析结果做成可拖拽组件。你可以切换时间范围、编辑组件、调整大小、切换样式,也可以把部分数据生成分享卡片。
概览依赖请求日志、用量统计和供应商账户用量。某些组件需要先在“设置”里开启请求日志或 Agent 观测,账户余额组件需要在供应商配置中开启“获取用量”。
## 顶部控件
| 字段 | 代表的能力 |
| 场景 | 看什么 |
| --- | --- |
| 按时间查看用量 | 切换仪表盘使用的统计时间范围。 |
| 今天 / 24 小时 / 7 天 / 30 天 | 可选统计窗口。不同组件会根据这个窗口重新计算请求、tokens、成本和趋势。 |
| 编辑组件 | 进入布局编辑模式。 |
| 重置布局 | 恢复默认概览布局。 |
| 完成 | 退出布局编辑模式并保留当前组件配置。 |
| 检查网关是否正常 | 系统状态、成功率、错误数、平均延迟 |
| 估算今日或近期消耗 | 请求数、输入 / 输出 / 缓存令牌、估算成本 |
| 比较上游使用情况 | 供应商分析、模型分布、客户端分析 |
| 关注账户额度 | 账户余额、套餐额度、剩余额度、账户状态 |
| 汇报或分享用量 | AI 用量年报、CCR 路由图、模型排行榜、消费小票等分享卡片 |
## 组件区
## 时间范围
编辑模式下,左侧“组件”用于添加组件,中间“预览”展示当前布局,右侧“组件属性”编辑选中组件
顶部的 `按时间查看用量` 控制所有依赖用量统计的组件。切换后,请求、令牌、成本、趋势、分布和分享卡片会按新的窗口重新聚合
| 字段 | 代表的能力 |
| 选项 | 统计窗口 |
| --- | --- |
| 组件 | 可添加组件列表。 |
| 预览 | 当前仪表盘布局。组件可以拖拽排序。 |
| 组件属性 | 当前选中组件的配置区域。 |
| 组件类型 | 切换组件大类,例如状态、账户、指标、趋势、活跃度、构成、分析或分享卡片。 |
| 数据 | 选择组件展示的数据例如请求数、tokens、成本、账户、客户端分析或供应商分析。 |
| 组件大小 | 控制组件占用的网格宽高。 |
| 样式 | 切换组件展示样式,例如卡片、紧凑、柱状图、折线图、圆环等。 |
| 移除组件 | 从概览中移除当前组件。 |
| `今天` | 当前本地日期从 00:00 到现在,按小时分桶。 |
| `24 小时` | 最近 24 小时,按小时分桶。 |
| `7 天` | 最近 7 天,按天分桶。 |
| `30 天` | 最近 30 天,按天分桶。 |
## 组件类型
账户余额组件不按这个时间窗口重算,它展示供应商账户连接器最近一次获取到的快照。
| 组件 | 代表的能力 |
## 编辑布局
点击右上角铅笔按钮进入编辑模式。编辑模式分为三栏:
| 区域 | 作用 |
| --- | --- |
| 状态组件 | 展示系统状态时间线,帮助判断网关近期是否正常。 |
| 账户组件 | 展示供应商账户余额、套餐额度或用量。需要供应商开启“获取用量”。 |
| 指标组件 | 展示请求数、总 tokens、输入 tokens、输出 tokens、缓存 tokens、缓存比例、估算成本、成功率、错误数或平均延迟。 |
| 趋势组件 | 展示按时间聚合的用量趋势。 |
| 活跃度组件 | 展示 token 活跃度热力图。 |
| 构成组件 | 展示 Token 构成或模型分布。 |
| 分析组件 | 展示客户端分析或供应商分析。 |
| 分享卡片 | 生成适合分享的 PNG 卡片,例如 AI Usage Wrapped、CCR Route Map、Model Leaderboard、AI Fuel Cockpit、Token Calendar Poster 和 Spend Receipt。 |
| 组件 | 左侧组件库,点击任一组件即可添加到当前仪表盘。 |
| 预览 | 中间布局预览。可以拖拽组件排序,也可以点击组件选中它。 |
| 组件属性 | 右侧属性面板,用于修改类型、数据、尺寸、样式或移除组件。 |
## 数据来源
常用操作:
| 数据 | 来源 |
1. 添加组件:在左侧 `组件` 中点击组件模板。
2. 调整顺序:在 `预览` 中拖拽组件。
3. 调整尺寸:选中组件后拖动右侧、底部或右下角的缩放手柄。
4. 修改数据:在 `组件属性` 里切换 `组件类型``数据`
5. 修改展示:在 `组件大小``样式` 中选择合适的布局。
6. 保存结果:点击 `完成` 退出编辑模式;布局会保存在应用配置中。
7. 恢复默认:编辑模式下点击 `重置布局`
移除组件只会从概览布局中删除该卡片,不会删除请求日志、供应商、账户连接器或任何上游配置。如果所有组件都被移除,页面会显示 `未配置组件`
## 组件目录
尺寸使用 `宽:高` 表示,宽高范围都是 `1``4`。概览网格在桌面端最多 4 列,在窄屏上会自动折叠。
| 组件 | 可选数据 | 默认尺寸 | 默认样式 | 可选样式 |
| --- | --- | --- | --- | --- |
| 状态组件 | 系统状态 | `4:1` | 时间线 | 时间线、紧凑 |
| 账户组件 | 所有账户或指定账户 | `4:2` | 卡片 | 卡片、紧凑、横条、圆环、半圆、弧形、内外圆 |
| 指标组件 | 请求、Token、成本 | `1:1` | 卡片 | 卡片、紧凑、柱状图、圆环 |
| 趋势组件 | 按时间查看用量 | `3:2` | 组合图 | 组合图、面积图、折线图、柱状图 |
| 活跃度组件 | Token 活跃度 | `4:2` | 热力图 | 热力图 |
| 构成组件 | Token 分布 / 模型分布 | Token 分布:`1:2`;模型分布:`2:2` | Token 分布:横条;模型分布:饼图 | 横条、堆叠、环形图、饼图 |
| 分析组件 | 客户端分析 / 供应商分析 | `2:2` | 表格 | 表格、紧凑 |
| 分享卡片 | AI 用量年报、CCR 路由图、模型排行榜、AI 燃料仪表、Token 日历海报、消费小票 | `1:4` | 卡片 | 卡片 |
尺寸约束:
| 规则 | 原因 |
| --- | --- |
| 请求数、tokens、成本、成功率、延迟 | 请求日志和用量统计。 |
| 账户余额、套餐额度、状态消息 | 供应商“获取用量”配置。 |
| 客户端分析、供应商分析、模型分布 | 请求日志中的客户端、供应商、模型和 token 信息。 |
| Agent 分析相关数据 | 设置中的“Agent 观测”和 Agent 执行链路数据。 |
| 分享卡片最小高度是 `1:4`。 | 导出图片使用竖版海报比例,需要足够高度。 |
| 账户组件在展示所有账户且使用“紧凑”样式时,最小尺寸是 `2:2`。 | 多账户列表需要保留可读空间。 |
| 旧版尺寸别名仍可被解析:`small` -> `1:1``medium` / `large` -> `2:2``wide` -> `3:2``full` -> `4:1``4:2`。 | 用于兼容旧配置。 |
## 指标数据
`metric` 组件通过 `metric` 字段选择要展示的指标。
| `metric` | 含义 |
| --- | --- |
| `requests` | 请求数 |
| `total-tokens` | 总令牌 |
| `input-tokens` | 输入令牌 |
| `output-tokens` | 输出令牌 |
| `cache-tokens` | 缓存令牌 |
| `cache-ratio` | 缓存率 |
| `estimated-cost` | 估算成本,按模型价格信息计算 |
| `success-rate` | 成功率 |
| `errors` | 错误数 |
| `avg-latency` | 平均延迟 |
## 账户组件
账户组件读取供应商配置里的账户 / 用量连接器。要让它显示余额或剩余额度,需要先在供应商配置中启用并测试 `获取用量`
| 数据选择 | 行为 |
| --- | --- |
| `所有账户` | 展示所有可用账户快照。 |
| 指定账户 | 只展示某个供应商或某个凭据的账户快照。内部配置值格式通常是 `provider``provider::credentialId`。 |
如果账户组件为空,优先检查:
1. 供应商是否配置了账户 / 用量连接器。
2. `获取用量` 测试是否成功。
3. API Key 或账户接口是否仍有效。
4. 当前选择的指定账户是否已经被删除或重命名。
## 分享卡片
分享卡片组件可以通过右上角下载按钮导出 PNG。桌面 App 会优先使用原生导出能力,浏览器环境会退回到前端 canvas 导出。导出图片尺寸为 `1080 x 1350`
| 卡片 | `type` | 内容 |
| --- | --- | --- |
| AI 用量年报 | `share-usage-wrapped` | 总令牌、请求数、估算成本、缓存率、最长连续、最高频模型、最高频供应商、峰值日期。 |
| CCR 路由图 | `share-route-map` | 客户端到供应商 / 模型的主要路由关系,以及客户端、供应商、模型数量。 |
| 模型排行榜 | `share-model-leaderboard` | 按令牌排序的模型排行榜。 |
| AI 燃料仪表 | `share-fuel-cockpit` | 最多 3 个账户额度仪表,依赖账户 / 用量连接器。 |
| Token 日历海报 | `share-token-calendar` | 类似贡献日历的 Token 活跃度海报。 |
| 消费小票 | `share-spend-receipt` | 当前时间范围内的估算成本、请求、令牌、延迟和成功率小票。 |
## 数据来源与排障
| 现象 | 可能原因 | 处理方式 |
| --- | --- | --- |
| 请求、令牌或成本为 0 | 当前时间范围内没有经过 CCR 的请求,或用量捕获尚未记录。 | 切换到 `24h` / `7d`,确认客户端请求确实走 CCR。 |
| 成本显示为 `$0.00` | 模型没有价格信息,或用量过小。 | 检查模型目录和供应商模型名是否能匹配价格;低于 0.01 美元会显示更多小数。 |
| 成功率、错误数不符合预期 | 只统计 CCR 捕获到的请求结果。 | 对照日志页面中的请求记录。 |
| 账户余额为空 | 没有账户连接器,或 `获取用量` 失败。 | 到供应商配置中测试账户 / 用量字段映射。 |
| 分布图没有数据 | 请求日志里缺少模型、供应商或 token 信息。 | 确认请求经过 CCR并检查上游响应是否返回 token usage。 |
| PNG 导出失败 | 浏览器不支持 canvas 导出、元素尺寸为空,或用户取消了保存。 | 在桌面 App 中重试,确保卡片可见且没有被缩到过小。 |

View file

@ -5,11 +5,16 @@ eyebrow: 详细配置
lead: 为 Claude Code、Codex、ZCode 创建可复用的启动配置,并通过不同配置打开不同的 Agent 实例。
---
## Agent配置是什么
## 配置流程
Agent配置是桌面 App 中管理 Claude Code、Codex、ZCode 启动入口的能力。它不是供应商或路由规则,而是一次 Agent 启动所需的完整入口Agent 类型、打开方式、模型、作用范围、配置文件位置,以及可选的 Bot 绑定。
1. 先在 **供应商配置** 中添加至少一个可用供应商和模型,或先创建需要使用的 Fusion 模型。
2. 打开 **Agent配置**,点击 **添加配置**
3. 选择 Agent 类型,填写配置名称,并选择作用范围和入口模式。
4. 选择模型。模型值通常是 `供应商名称/模型名称`,也可以选择 Fusion 模型。
5. 如果入口模式包含 App可以绑定 Bot并选择是否转发 Agent 消息或开启接力。
6. 保存后,从 Agent配置卡片打开终端图标会复制 CLI 命令,播放图标会启动 App 实例。
因此详细配置里会有这个页面。它用于解释“用哪个配置打开哪个 Agent 实例”,而不是继续拆分供应商、路由或 Fusion 的字段。
试用阶段建议选择 **仅从 CCR 打开时生效**,并且总是从 CCR 打开 Agent。这样配置只影响 CCR 启动的实例,不会改掉你系统里原本直接打开的 Claude Code、Codex 或 ZCode
## 多开机制
@ -26,20 +31,80 @@ Agent配置是桌面 App 中管理 Claude Code、Codex、ZCode 启动入口的
## 常用选项
| 选项 | 说明 |
| 选项 | 适用范围 | 说明 |
| --- | --- | --- |
| Agent | 全部 | 选择 Claude Code、Codex 或 ZCode。ZCode 只支持 App。 |
| 配置名称 | 全部 | 用于在 CCR 中识别配置,也会作为 `ccr <配置名称>` 的打开目标。名称可以有空格,复制命令时 CCR 会自动加引号。 |
| 启用开关 | 全部 | 关闭后该配置不会出现在打开入口中,也不会被应用为有效启动配置。 |
| 作用范围 | 全部 | **仅从 CCR 打开时生效** 会使用 CCR 管理的独立配置;**系统默认** 会写入对应 Agent 的默认配置。同一个 Agent 同时只能有一个启用的系统默认配置。 |
| 入口模式 | Claude Code、Codex | `CLI & APP` 同时显示 CLI 和 App 打开入口;`CLI only` 只生成 CLI 命令;`App only` 只显示 App 打开入口。 |
| 模型 | 全部 | 该 Agent 打开后的默认模型,可以选择普通供应商模型或 Fusion 模型。Claude Code 留空表示保留 Claude Code 默认模型。 |
| Bot | App 入口 | 只有从 CCR 打开的 App 模式会转发 Bot 消息。CLI 当前不转发 Bot 消息。 |
| 环境变量 | 全部 | 为该配置注入额外环境变量。Claude Code 默认带 `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`,用于启用网关模型发现。 |
## 各 Agent 的配置项
### Claude Code
| 配置项 | 作用 |
| --- | --- |
| Agent | 选择 Claude Code、Codex 或 ZCode |
| 配置名称 | 用于在 CCR 中识别配置,也会作为 `ccr <配置名称>` 的打开目标 |
| 作用范围 | “仅从 CCR 打开时生效”会使用 CCR 管理的独立配置;“系统默认”会写入对应 Agent 的默认配置 |
| 入口模式 | `CLI & APP``CLI only``App only`ZCode 只支持 App |
| 模型 | 该 Agent 打开后的默认模型,可以选择普通供应商模型或 Fusion 模型 |
| Bot | App 入口可以绑定 Bot用于 IM 消息转发或接力 |
| 模型覆盖 | 写入 Claude Code 使用的 `ANTHROPIC_MODEL`。留空时不覆盖 Claude Code 自己的默认模型。 |
| 小模型 | 写入 `ANTHROPIC_SMALL_FAST_MODEL`,供 Claude Code 的轻量任务使用。留空时保留 Claude Code 默认值。 |
| 设置文件 | 系统默认模式使用 Claude Code 默认设置文件;仅从 CCR 打开时生效会在 CCR 配置目录下按 Agent配置 `id` 生成独立设置文件。 |
| 环境变量 | 会合并到 Claude Code 设置文件的 `env` 中。CCR 同时写入网关地址、API Key helper 和启动包装器。 |
| Bot | 只在 Claude App 入口生效,可选择已保存 Bot并配置转发 Agent 消息或接力。 |
Claude Code CLI 从 CCR 打开后,会通过 CCR 网关获取模型发现信息。进入 Claude Code CLI 后可以输入 `/model` 查看并切换 CCR 暴露的模型列表,包括普通供应商模型和可见的 Fusion 模型。
Claude App 是 **零配置zero-config**:从 CCR 桌面 App 打开 Claude App 时CCR 会自动写入 Claude App 网关配置、API Key、模型发现列表和独立用户数据目录。用户不需要增加额外操作直接使用 CCR 打开就会自动完成所有必要配置;如果 Claude App 已经打开,按提示重启或从 CCR 重新打开即可。
Claude App 和 Claude Code CLI 的模型列表适配方式不同:
| 入口 | 模型列表来源 | 说明 |
| --- | --- | --- |
| Claude Code CLI | CCR 网关模型发现 | CLI 内使用 `/model` 查看列表;选择后请求仍走 CCR 的供应商、路由和 Fusion。 |
| Claude App | CCR 生成的 Claude App inference models | Claude App 需要 Claude 兼容的模型名。CCR 会把 `供应商/模型` 和 Fusion 模型映射成 Claude App 可识别的模型项,并用显示名称保留真实模型含义。 |
### Codex
| 配置项 | 作用 |
| --- | --- |
| Provider ID | 写入 Codex 的 `model_provider`,默认是 `claude-code-router`。建议保持稳定,只使用字母、数字、点、下划线或短横线。 |
| Provider name | Codex 中展示的供应商名称,默认是 `Claude Code Router`。 |
| Codex model | 写入 Codex 默认模型。可以选择普通供应商模型或 Fusion 模型;留空时 CCR 使用可用模型中的默认值。 |
| Show all sessions | 让 Codex 显示所有会话。ZCode 不提供该项。 |
| 配置文件 | 默认是 `~/.codex/config.toml`。仅从 CCR 打开时生效会写入 CCR 管理的独立配置目录。 |
| 环境变量 | 注入 Codex CLI 或 Codex App。Claude Code 专用的模型发现变量不会传给 Codex。 |
| Bot | 只在 Codex App 入口生效。 |
保存后Codex CLI 使用配置卡片里的终端图标复制命令,例如 `ccr "Codex - Work"`。Codex App 使用播放图标打开。CCR 会生成 `config.toml`、模型目录文件和中间层启动器,让 Codex CLI 与 Codex App 都使用同一套 CCR 模型和供应商信息。
### ZCode
| 配置项 | 作用 |
| --- | --- |
| Provider ID | 写入 ZCode 供应商引用,默认是 `claude-code-router`。 |
| Provider name | ZCode 中展示的供应商名称,默认是 `Claude Code Router`。 |
| ZCode model | ZCode App 打开后的默认模型。可以选择普通供应商模型或 Fusion 模型。 |
| 配置文件 | 默认是 `~/.zcode/cli/config.json`CCR 还会写入 ZCode v2 配置和模型缓存。 |
| 环境变量 | 注入 ZCode App 和中间层启动器。 |
| Bot | 只在 ZCode App 入口生效。 |
ZCode 只支持 App 打开,因此入口模式固定为 `App only`,也不会显示 `Show all sessions`
## CLI 与 App 模式区别
| 模式 | 如何打开 | 适合场景 | 主要差异 |
| --- | --- | --- | --- |
| CLI | 点击终端图标复制命令,然后在终端运行 `ccr <配置名称>` | 在项目目录中运行 Agent、需要 shell 工作流、需要把命令放进脚本 | 使用对应配置的包装器或中间层启动;通常不启动桌面窗口;当前不转发 Bot 消息。 |
| App | 点击播放图标从 CCR 桌面 App 启动 | 需要桌面窗口、多实例并存、Bot 消息转发或接力 | 每个 Agent配置使用独立用户数据目录同一配置重复打开会激活已有窗口不同配置可以并行打开。 |
| CLI & APP | 同一个配置同时提供 CLI 和 App 入口 | 同一套模型配置既用于终端,也用于桌面 App | 两个入口共用配置名称、模型、作用范围和环境变量,但启动方式不同。 |
## 各 Agent 的差异
### Claude Code
Claude Code 配置会写入设置文件。选择“仅从 CCR 打开时生效”时CCR 会在自己的配置目录下为这个 Agent配置生成独立设置文件并通过独立启动包装器打开 Claude Code。
Claude Code CLI 配置会写入设置文件。选择“仅从 CCR 打开时生效”时CCR 会在自己的配置目录下为这个 Agent配置生成独立设置文件并通过独立启动包装器打开 Claude Code。
从桌面 App 打开 Claude App 时CCR 还会为该配置准备独立用户数据目录。不同 Agent配置使用不同目录因此可以同时打开多个 Claude App 实例。

View file

@ -70,6 +70,10 @@ lead: 快速添加常见模型供应商,确认无误后即可保存,减少
<span class="provider-import-icon-shell"><img src="../../provider-icons/siliconflow.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">硅基流动</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-runapi" href="ccr://provider?name=RunAPI&amp;base_url=https%3A%2F%2Frunapi.co%2Fv1&amp;protocol=openai_responses" aria-label="导入 RunAPI 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/runapi.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">RunAPI</span><span class="provider-import-meta">Responses / Chat Completions</span></span>
</a>
</div>
## 嵌入式按钮组件

View file

@ -5,20 +5,54 @@ eyebrow: 详细配置
lead: 配置 CCR 的上游模型服务,包括协议、基础 URL、模型列表和凭据。
---
## 基本概念
## 导入本机 Agent 登录态
供应商是 CCR 转发请求的上游模型服务。一个供应商配置同时描述了上游地址、协议能力、模型列表、认证方式、可选的多 Key 轮换,以及可选的账号用量读取能力
添加供应商时CCR 会扫描本机已有的 Agent 登录状态。检测到可复用凭据后,添加弹窗会显示对应导入入口。导入会创建一个普通供应商和配套 provider plugin让 CCR 复用本机 Agent 授权访问上游服务,不需要再手动粘贴常规 API Key
每个供应商至少需要名称、API 地址、可用协议、模型列表和一条可用凭据。添加或编辑供应商时,弹窗中的字段会根据“预设供应商 / 自定义 API 地址 / 本机 Agent 登录态导入 / 高级设置”动态显示。
### Claude Code
预设供应商会自动填入常见 API 地址、协议、模型和用量读取配置,适合优先使用。自定义供应商需要手动填写 API 地址CCR 会根据 API 地址和 API 密钥做协议探测,帮助判断当前上游是否兼容 OpenAI Chat、OpenAI Responses、Anthropic Messages 或 Gemini Generate。
Claude Code 导入会读取本机 Claude Code OAuth 凭据。检测到可用 access token 时,可以导入为 `Claude Code API` 供应商。
导入后:
1. 协议使用 `anthropic_messages`
2. 默认模型包含 `claude-sonnet-4-20250514`,后续可以在供应商模型列表中增减模型。
3. CCR 会创建 OAuth provider plugin把请求认证转换为 Claude Code 登录态。
4. 账号用量会使用 Anthropic OAuth 用量接口,适合在供应商列表、托盘或账号面板里查看额度状态。
如果只检测到登录痕迹但没有可用 access token导入入口会显示不可导入原因。此时先在 Claude Code 中重新登录,再回到 CCR 添加供应商。
### Codex
Codex 导入会读取本机 Codex 登录文件和模型缓存。检测到 Codex access token 或 refresh token 时,可以导入为 `Codex API` 供应商。
导入后:
1. 协议使用 `openai_responses`
2. API 地址指向 Codex 后端,默认模型至少包含 `gpt-5-codex`,也会合并本机模型缓存中的模型和显示名。
3. CCR 会创建 Codex OAuth provider plugin并在需要时刷新访问凭据。
4. 账号用量会读取 Codex 额度、余额和 token 统计接口。
导入后可以直接在路由或 Agent配置中选择 `Codex API/模型名`。如果模型缓存较旧,可以先打开 Codex 让它刷新模型列表,再回到 CCR 重新导入或编辑模型。
### ZCode
ZCode 导入会读取本机 ZCode 配置中的供应商 API Key、API 地址和模型列表。只有检测到可用供应商 Key 和 Base URL 时,才能导入为 `ZCode API` 供应商。
导入后:
1. 协议使用 `anthropic_messages`
2. 模型优先来自 ZCode 本机配置;没有配置时会使用 ZCode 运行缓存或默认模型。
3. CCR 会创建 API Key provider plugin把 ZCode 本机配置中的 Key 用于请求认证。
4. 如果 API 地址命中 CCR 内置预设,账号用量配置会复用对应预设。
如果只检测到 ZCode 登录态,但没有检测到可用供应商 API Key导入入口会显示不可导入。此时需要先在 ZCode 中配置可用模型供应商,再回到 CCR 添加供应商。
## 主字段
| 字段 | 代表的能力 |
| --- | --- |
| 选择 预设供应商 | 套用 CCR 内置供应商模板,包括默认 API 地址、可用协议、默认模型、图标、官网链接和部分供应商的用量读取配置。选择 `其他 / 自定义 API 地址` 时,可以接入任意兼容 OpenAI、Anthropic 或 Gemini 协议的上游服务。 |
| 导入本机 Agent 登录态 | 添加供应商时,如果 CCR 在本机发现 Claude Code、Codex 或 ZCode 的可用登录状态,会显示导入入口。导入后 CCR 会生成供应商和对应 provider plugin用本机已有登录凭据访问上游不需要手动粘贴常规 API Key。 |
| 名称 | CCR 内部显示名,也是路由、模型选择、日志和配置中识别供应商的名字。名称必须唯一,建议短且稳定。 |
| API 地址 | 上游 API Base URL。它决定请求实际发往哪里也用于协议探测、模型列表探测、图标探测和安全校验。预设供应商添加时默认隐藏该字段可在高级设置里覆盖。自定义供应商必须填写。 |
| API 密钥 | 默认供应商凭据。没有配置凭据池时,模型请求会使用这条 Key协议探测、模型探测、连通性检测和默认用量读取也会使用它。只填写由当前 API 地址对应供应商签发的 Key。 |
@ -46,7 +80,7 @@ lead: 配置 CCR 的上游模型服务,包括协议、基础 URL、模型列
| 添加 Key | 新增一条上游 API Key。 |
| 启用 | 控制单条凭据是否参与请求转发和用量读取。关闭后保留配置,但不会被选中。 |
| 名称 | 单条凭据的显示名。会出现在账号用量、日志和内部诊断里,建议写成可识别的用途或额度来源。 |
| API 密钥 | 该凭据实际发送给上游的 Key。配置了凭据池后CCR 会把启用的凭据展开成多个内部上游目标,并优先使用凭据池,而不是主表单的默认 `API 密钥`。 |
| API 密钥 | 该凭据实际发送给上游的 Key。配置了凭据池后CCR 会把启用的凭据展开成多个内部上游目标,并优先使用凭据池中的 Key。主表单的默认 `API 密钥` 只作为单 Key 配置使用。 |
| 移除 | 删除当前凭据行。 |
| Key 高级选项 | 展开单条凭据的调度和限额字段。 |
| 优先级 | 凭据优先级,数字越小越优先。未填写时按凭据行顺序作为优先级。 |
@ -90,7 +124,7 @@ lead: 配置 CCR 的上游模型服务,包括协议、基础 URL、模型列
### HTTP JSON 请求
该模式适合供应商已有自己的余额或额度接口,但返回格式不是 CCR 标准格式的情况。
该模式适合供应商已有自己的余额或额度接口,且返回自定义 JSON 格式的情况。
| 字段 | 代表的能力 |
| --- | --- |

View file

@ -9,9 +9,9 @@ lead: 设置请求如何选择模型,并在失败时通过 Fallback 自动重
### Claude Code
Claude Code 内置路由的作用是识别 Claude Code 发来的请求,并把主请求路由到 Claude Code Agent 配置或默认路由中的模型。
Claude Code 内置路由的作用是识别 Claude Code 发来的请求,并把主请求路由到 Claude Code Agent 配置中的模型。
Claude Code **主请求** 默认使用 Claude Code Agent 配置中的模型;如果未设置,则使用路由页的默认模型。CCR 也会自动删除 Claude Code 注入的第一条 `x-anthropic-billing-header` system 消息避免这类计费辅助消息影响后续路由判断。Claude Code 创建的 Subagent、Task 或 Workflow 内部 Agent 可以继续用下面的标签机制自动选择不同模型。
Claude Code **主请求** 使用 Claude Code Agent 配置中的模型;如果未设置,该内置路由不会生效。CCR 也会自动删除 Claude Code 注入的第一条 `x-anthropic-billing-header` system 消息避免这类计费辅助消息影响后续路由判断。Claude Code 创建的 Subagent、Task 或 Workflow 内部 Agent 可以继续用下面的标签机制自动选择不同模型。
#### Subagent / Workflow 自动路由
@ -137,9 +137,9 @@ Rewrite 的值也会按字面量解析,所以 `0.2` 会变成数字,`true`
保存后,规则会出现在列表中。请求日志里的 `request model``resolved provider``resolved model` 和路由原因可以用来确认规则是否命中。
## Fallback 是什么
## Fallback 处理
Fallback 是请求失败后的降级策略。它不负责第一次选模型,而是在当前模型或上游失败时决定是否继续尝试。
Fallback 处理请求失败后的降级。第一次选模型由路由完成当前模型或上游失败时Fallback 决定是否继续尝试。
路由页面顶部的 **默认失败处理** 是全局 Fallback。每条路由规则里的 **失败时** 是规则级 Fallback当某条规则命中时规则级配置会覆盖全局配置。
@ -176,7 +176,7 @@ Fallback 是请求失败后的降级策略。它不负责第一次选模型,
2. 如果选择 **继续重试**,填写 `Retries`
3. 如果选择 **失败降级目标**,按优先级添加备用模型。
全局 Fallback 会应用到默认路由,以及没有单独配置 Fallback 的规则。
全局 Fallback 会应用到没有单独配置 Fallback 的规则。
### 规则级失败降级

View file

@ -5,10 +5,6 @@ eyebrow: 详细配置
lead: 配置 CCR 网关监听地址、端口,以及通过代理模式进行 MITM 劫持并代理到 CCR 的能力。
---
## 基本概念
“服务”页面控制 CCR 本地网关如何监听请求。普通模型请求通常只需要配置 `Host``Port`
## 主字段
| 字段 | 代表的能力 |

View file

@ -5,10 +5,6 @@ eyebrow: 详细配置
lead: 配置 CCR 系统托盘图标、余额进度条和托盘窗口组件。
---
## 基本概念
“托盘”位于“设置”里用于配置系统托盘图标和托盘窗口。托盘窗口可以展示供应商、账户余额、token 趋势、活跃度、指标和模型占比等信息,适合在不打开主窗口时快速查看 CCR 状态。
## 顶部字段
| 字段 | 代表的能力 |

View file

@ -5,19 +5,6 @@ eyebrow: 产品文档
lead: 了解 CCR 的定位、能力边界和文档结构。需要动手配置时从顶部的「快速开始」开始需要查字段、Bot 或 Fusion 时,进入「详细配置」。
---
## CCR 能帮你做什么
**Claude Code RouterCCR是本地运行的模型网关。** 它位于 Claude Code、Codex、ZCode 等 Agent 和上游模型服务之间统一管理模型、API Key、路由规则、日志观测和 Bot 接力。
CCR 适合解决这些问题:
- 你不想在每个 Agent 里重复维护模型和 Key。
- 你希望不同任务自动走不同模型:轻量后台任务用快模型,复杂任务用强模型,看图或联网任务走 Fusion。
- 你需要在请求日志里看到请求实际去了哪个供应商、哪个模型、是否成功、延迟和成本大概是多少。
- 你想把长时间运行的 Agent 消息转发到 Slack、Telegram、飞书、企业微信等 IM 平台。
CCR 默认监听本机地址 `http://localhost:8080`。Agent 只要指向这个地址,请求就可以被 CCR 接管并按路由规则转发到上游供应商。
## 文档结构
顶部栏现在对应四个独立页面:
@ -26,7 +13,7 @@ CCR 默认监听本机地址 `http://localhost:8080`。Agent 只要指向这个
| --- | --- |
| [文档](./) | 产品定位、架构概览、阅读路径 |
| [快速开始](guides/) | 从安装、接供应商,到接入 Agent 的上手流程 |
| [详细配置](configuration/provider/) | 概览仪表盘、API 密钥、服务、供应商、路由、Agent配置、Fusion、Bot、托盘和配置数据库位置 |
| [详细配置](configuration/overview/) | 概览仪表盘、API 密钥、服务、供应商、路由、Agent配置、Fusion、Bot、托盘和配置数据库位置 |
| [Q&A](troubleshooting/) | 请求日志、观测面板和常见问题 |
Bot 平台教程是「详细配置」分类下的子页面,每个平台有独立页面,方便逐步补齐平台后台字段、回调 URL、签名和 FAQ。
@ -37,7 +24,7 @@ Bot 平台教程是「详细配置」分类下的子页面,每个平台有独
1. [快速开始](guides/) 覆盖供应商接入和 Agent配置。
2. App 的请求日志页面展示请求是否经过 CCR。
3. [详细配置](configuration/provider/) 覆盖概览仪表盘、API 密钥、服务、供应商、图像、联网搜索、MCP 工具、托盘和 IM 接力。
3. [详细配置](configuration/overview/) 覆盖概览仪表盘、API 密钥、服务、供应商、图像、联网搜索、MCP 工具、托盘和 IM 接力。
4. [Q&A](troubleshooting/) 覆盖 401、404、超时、路由不对或 Bot 收不到消息等常见问题。
这样文档不会挤在一个长页面里,后续也能按顶部分类逐步扩展。

View file

@ -13,9 +13,9 @@ A: 服务运行状态、Agent 启动方式、配置应用状态和作用范围
### Q: 请求命中了错误模型怎么办?
A: 请求日志会展示 `request model``resolved provider``resolved model`。路由配置页包含默认路由、规则顺序、匹配条件和 fallback。
A: 请求日志会展示 `request model``resolved provider``resolved model`。路由配置页包含规则顺序、匹配条件和 fallback。
### Q: 供应商返回 401 或 403 是什么原因
### Q: 供应商返回 401 或 403 怎么处理
A: 相关字段包括 API Key、凭据启用状态、基础 URL、协议和额外请求头。供应商页面提供模型连通性检查。

View file

@ -15,7 +15,7 @@ export const docsContent = {
navItems: [
{ label: "文档", href: "/", pageKey: "documentation" },
{ label: "快速开始", href: "/guides/", pageKey: "guides" },
{ label: "详细配置", href: "/configuration/provider/", pageKey: "configuration" },
{ label: "详细配置", href: "/configuration/overview/", pageKey: "configuration" },
{ label: "Q&A", href: "/troubleshooting/", pageKey: "troubleshooting" },
],
pages: {
@ -77,7 +77,7 @@ export const docsContent = {
"Bot 与 IM 接力 Agent",
"配置数据库位置",
],
active: "供应商配置",
active: "概览仪表盘",
},
],
expandableSidebarItems: ["Fusion 组合模型", "Bot 与 IM 接力 Agent"],
@ -163,7 +163,7 @@ export const docsContent = {
navItems: [
{ label: "Documentation", href: "/en/", pageKey: "documentation" },
{ label: "Quick Start", href: "/en/guides/", pageKey: "guides" },
{ label: "Detailed Configuration", href: "/en/configuration/providers/", pageKey: "configuration" },
{ label: "Detailed Configuration", href: "/en/configuration/overview/", pageKey: "configuration" },
{ label: "Q&A", href: "/en/troubleshooting/", pageKey: "troubleshooting" },
],
pages: {
@ -225,7 +225,7 @@ export const docsContent = {
"Bots And IM Agent Relay",
"Config Database Location",
],
active: "Provider Config",
active: "Overview Dashboard",
},
],
expandableSidebarItems: ["Fusion Models", "Bots And IM Agent Relay"],

View file

@ -1,5 +1,5 @@
---
const target = "/configuration/provider/";
const target = "/configuration/overview/";
---
<html lang="zh-CN">
@ -8,10 +8,10 @@ const target = "/configuration/provider/";
<meta http-equiv="refresh" content={`0;url=${target}`} />
<link rel="canonical" href={target} />
<script is:inline>
window.location.replace("/configuration/provider/");
window.location.replace("/configuration/overview/");
</script>
</head>
<body>
<a href={target}>供应商配置</a>
<a href={target}>概览仪表盘</a>
</body>
</html>

View file

@ -1,5 +1,5 @@
---
const target = "/en/configuration/providers/";
const target = "/en/configuration/overview/";
---
<html lang="en">
@ -8,10 +8,10 @@ const target = "/en/configuration/providers/";
<meta http-equiv="refresh" content={`0;url=${target}`} />
<link rel="canonical" href={target} />
<script is:inline>
window.location.replace("/en/configuration/providers/");
window.location.replace("/en/configuration/overview/");
</script>
</head>
<body>
<a href={target}>Provider Config</a>
<a href={target}>Overview Dashboard</a>
</body>
</html>

View file

@ -1300,6 +1300,12 @@ h1 {
--provider-brand-3: #d9e3f1;
}
.doc-markdown a.provider-import-button.provider-runapi {
--provider-brand: #070707;
--provider-brand-2: #747474;
--provider-brand-3: #f3f3f3;
}
.doc-markdown a.provider-import-button.provider-deepseek {
--provider-brand: #173aa8;
--provider-brand-2: #4e69ff;

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "claude-code-router",
"version": "3.0.4",
"version": "3.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-code-router",
"version": "3.0.4",
"version": "3.0.5",
"license": "MIT",
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.3",

View file

@ -1,6 +1,6 @@
{
"name": "claude-code-router",
"version": "3.0.4",
"version": "3.0.5",
"license": "MIT",
"description": "Local Claude Code Router gateway with CLI and web management UI.",
"repository": {

View file

@ -5,6 +5,7 @@ import path from "node:path";
import type { AppConfig, ProfileConfig } from "../shared/app";
import { botGatewayProfileEnv } from "./bot-gateway-env";
import { prepareClaudeAppCdpUserDataDir, reserveClaudeAppCdpPort, scheduleClaudeAppDesignCdp } from "./claude-app-cdp";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
import { resolveClaudeCodeSettingsFile } from "./profile-launch-core";
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "./windows-app-discovery";
@ -59,7 +60,8 @@ export async function launchClaudeAppProfile(configDir: string, profile: Profile
CLAUDE_USER_DATA_DIR: userDataDir,
CCR_CLAUDE_APP_USER_DATA_PATH: userDataDir,
CCR_PROFILE_SURFACE: "app",
ELECTRON_ENABLE_LOGGING: "1"
ELECTRON_ENABLE_LOGGING: "1",
...claudeCodeUtcTimezoneEnvOverride()
};
delete env.ELECTRON_RUN_AS_NODE;

View file

@ -0,0 +1,27 @@
const chinaTimeZones = new Set([
"asia/chongqing",
"asia/chungking",
"asia/harbin",
"asia/kashgar",
"asia/shanghai",
"asia/urumqi",
"china standard time",
"prc"
]);
export function claudeCodeUtcTimezoneEnvOverride(timeZone = currentTimeZone()): Record<string, string> {
return isChinaTimeZone(timeZone) ? { TZ: "UTC" } : {};
}
export function currentTimeZone(): string | undefined {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
return undefined;
}
}
export function isChinaTimeZone(timeZone: string | undefined): boolean {
const normalized = timeZone?.trim().toLowerCase();
return Boolean(normalized && chinaTimeZones.has(normalized));
}

View file

@ -18,8 +18,35 @@ const REQUEST_TIMEOUT_MS = numberEnv("CCR_CODEX_APP_REQUEST_TIMEOUT_MS", 10 * 60
const TURN_IDLE_TIMEOUT_MS = numberEnv("CCR_CODEX_CLAUDE_TURN_IDLE_TIMEOUT_MS", 10 * 60 * 1000);
const CONFIG_DIR = resolveConfigDir();
const LOG_PATH = process.env.CCR_CODEX_CLI_MIDDLEWARE_LOG || "";
const CLAUDE_CODE_CHINA_TIME_ZONES = new Set([
"asia/chongqing",
"asia/chungking",
"asia/harbin",
"asia/kashgar",
"asia/shanghai",
"asia/urumqi",
"china standard time",
"prc"
]);
let BOT_BRIDGE_INSTANCE = null;
function claudeCodeUtcTimezoneEnvOverride() {
return isClaudeCodeChinaTimeZone(currentTimeZone()) ? { TZ: "UTC" } : {};
}
function currentTimeZone() {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
return "";
}
}
function isClaudeCodeChinaTimeZone(timeZone) {
const normalized = String(timeZone || "").trim().toLowerCase();
return Boolean(normalized && CLAUDE_CODE_CHINA_TIME_ZONES.has(normalized));
}
function resolveConfigDir() {
const configured = nonEmptyEnv("CODEXL_HOME") || nonEmptyEnv("CCR_CONFIG_DIR");
if (configured) {
@ -70,7 +97,10 @@ async function runClaudeCodeCliWrapper(args) {
});
const injectRemoteStdin = boolEnv("CCR_REMOTE_SYNC_INJECT_STDIN");
const child = childProcess.spawn(realCli, args, {
env: withoutKeys(process.env, ["CCR_CLAUDE_CODE_WRAPPER", "CCR_REAL_CLAUDE_CODE_BIN"]),
env: {
...withoutKeys(process.env, ["CCR_CLAUDE_CODE_WRAPPER", "CCR_REAL_CLAUDE_CODE_BIN"]),
...claudeCodeUtcTimezoneEnvOverride()
},
stdio: [injectRemoteStdin ? "pipe" : "inherit", captureStdout ? "pipe" : "inherit", "inherit"]
});
if (injectRemoteStdin && child.stdin) {
@ -1815,6 +1845,7 @@ function claudeCommand(work) {
const env = withoutKeys({
...process.env,
...settingsEnv,
...claudeCodeUtcTimezoneEnvOverride(),
CODEX_SESSION_ID: work.threadId,
CODEX_THREAD_ID: work.threadId,
CODEX_TURN_ID: work.turnId

View file

@ -1190,10 +1190,6 @@ function parseRouter(value: unknown): Partial<RouterConfig> | undefined {
}
const router: Partial<RouterConfig> = {};
const defaultRoute = readString(value.default);
if (defaultRoute) {
router.default = defaultRoute;
}
const builtInRules = parseRouterBuiltInRules(value.builtInRules ?? value.builtinRules ?? value.agentRules);
if (builtInRules) {
router.builtInRules = builtInRules;

View file

@ -7,6 +7,7 @@ import { mistralProviderPreset } from "./mistral";
import { moonshotChinaProviderPreset, moonshotGlobalProviderPreset } from "./moonshot";
import { openaiProviderPreset } from "./openai";
import { openRouterProviderPreset } from "./openrouter";
import { runApiProviderPreset } from "./runapi";
import { siliconFlowProviderPreset } from "./siliconflow";
import { zaiGlobalCodingProviderPreset } from "./zai-global-coding";
import { zaiGlobalGeneralProviderPreset } from "./zai-global-general";
@ -38,7 +39,8 @@ export const providerPresets: ProviderPreset[] = [
moonshotChinaProviderPreset,
moonshotGlobalProviderPreset,
bailianProviderPreset,
siliconFlowProviderPreset
siliconFlowProviderPreset,
runApiProviderPreset
];
export function getProviderPresets(): ProviderPreset[] {

View file

@ -0,0 +1,15 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets";
export const runApiProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig,
aliases: ["runapi"],
endpoints: [
{
baseUrl: "https://runapi.co/v1",
protocols: ["openai_responses", "openai_chat_completions"]
}
],
id: "runapi",
name: "RunAPI",
websiteUrl: "https://runapi.co/register?aff=IX1t"
};

View file

@ -1,5 +1,6 @@
import path from "node:path";
import type { AppConfig, ProfileConfig, ProfileOpenSurface } from "../shared/app";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
import { resolveZcodeConfigFile } from "./zcode-profile-config";
export type ProfileLaunchPlan = {
@ -180,7 +181,8 @@ function buildClaudeCodeLaunchPlan(
command: launcher,
env: {
CLAUDE_CONFIG_DIR: path.dirname(settingsFile),
CCR_PROFILE_SURFACE: surface
CCR_PROFILE_SURFACE: surface,
...claudeCodeUtcTimezoneEnvOverride()
},
profile,
surface

View file

@ -6,6 +6,7 @@ import { assertAvailableGatewayModels, type AppConfig, type ProfileOpenCommandRe
import { botGatewayProfileEnv } from "./bot-gateway-env";
import { applyClaudeAppGatewayConfig, readClaudeAppGatewayApiKeyCandidates } from "./claude-app-gateway-service";
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "./claude-app-launch";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
import { launchCodexAppProfile, launchZcodeAppProfile, refreshCodexCompatibleAppProfileFiles } from "./codex-app-launch";
import { codexCliMiddlewareRuntimeScript } from "./codex-cli-middleware-runtime";
import { CONFIGDIR } from "./constants";
@ -76,7 +77,8 @@ export async function openProfileFromCcr(config: AppConfig, request: ProfileOpen
env: {
...process.env,
...plan.env,
...botGatewayProfileEnv(config, profile, surface)
...botGatewayProfileEnv(config, profile, surface),
...(profile.agent === "claude-code" ? claudeCodeUtcTimezoneEnvOverride() : {})
},
stdio: "ignore"
});
@ -512,11 +514,7 @@ function profileGatewayConfigWithToken(config: AppConfig, profile: ReturnType<ty
key: token,
name: `Profile: ${profile.name?.trim() || profile.id || profile.agent}`
}
],
Router: {
...config.Router,
...(profile.model.trim() ? { default: profile.model.trim() } : {})
}
]
};
}
@ -961,7 +959,8 @@ function startClaudeAppBotWorker(config: AppConfig, profile: ReturnType<typeof f
CCR_CODEX_WORKSPACE_NAME: profile.name || profile.id,
CCR_PROFILE_SURFACE: "app",
CODEXL_CODEX_WORKSPACE_NAME: profile.name || profile.id,
CODEXL_PROFILE_SURFACE: "app"
CODEXL_PROFILE_SURFACE: "app",
...claudeCodeUtcTimezoneEnvOverride()
};
delete env.ELECTRON_NO_ATTACH_CONSOLE;

View file

@ -5,6 +5,7 @@ import path from "node:path";
import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, enforceSingleEnabledGlobalProfilePerAgent, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ProfileApplyResult, type ProfileClientApplyStatus, type ProfileClientKind, type ProfileConfig } from "../shared/app";
import { replacePersistedApiKeys } from "./api-key-store";
import { botGatewayProfileEnv } from "./bot-gateway-env";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
import { writeCodexCompatibleAppModelCatalog } from "./codex-app-launch";
import { codexCliMiddlewareRuntimeScript } from "./codex-cli-middleware-runtime";
import { codexModelCatalogJson } from "./codex-model-catalog";
@ -99,6 +100,7 @@ function applyClaudeCodeProfile(config: AppConfig, profile: ProfileConfig, token
} else {
delete env.ANTHROPIC_SMALL_FAST_MODEL;
}
Object.assign(env, claudeCodeUtcTimezoneEnvOverride());
const helperResult = writeClaudeCodeApiKeyHelper(profile, token);
const wrapperResult = writeClaudeCodeWrapper(config, profile, helperResult.file);
@ -538,6 +540,7 @@ function claudeCodeWrapperShellScript(config: AppConfig, profile: ProfileConfig,
return [
"#!/bin/sh",
...envExports,
...shellEnvExports(claudeCodeUtcTimezoneEnvOverride()),
`: "\${CCR_PROFILE_SURFACE:=${surface}}"`,
"export CCR_PROFILE_SURFACE",
...botEnvExports,
@ -566,6 +569,7 @@ function claudeCodeWrapperCmdScript(config: AppConfig, profile: ProfileConfig, r
return [
"@echo off",
...envExports,
...cmdEnvExports(claudeCodeUtcTimezoneEnvOverride()),
`if not defined CCR_PROFILE_SURFACE ${cmdSetLine("CCR_PROFILE_SURFACE", surface)}`,
...botEnvExports,
cmdSetLine("CCR_CLAUDE_CODE_WRAPPER", "1"),
@ -857,6 +861,10 @@ function shellBotGatewayEnvExports(config: AppConfig, profile: ProfileConfig): s
];
}
function shellEnvExports(env: Record<string, string>): string[] {
return Object.entries(env).map(([key, value]) => `export ${key}=${shellQuote(value)}`);
}
function cmdBotGatewayEnvExports(config: AppConfig, profile: ProfileConfig): string[] {
return [
`if /I "%CCR_PROFILE_SURFACE%"=="app" (`,
@ -867,6 +875,10 @@ function cmdBotGatewayEnvExports(config: AppConfig, profile: ProfileConfig): str
];
}
function cmdEnvExports(env: Record<string, string>): string[] {
return Object.entries(env).map(([key, value]) => cmdSetLine(key, value));
}
function withoutBotGatewayEnv(values: Record<string, string>): Record<string, string> {
return Object.fromEntries(Object.entries(values).filter(([key]) => !isBotGatewayEnvKey(key)));
}
@ -1292,10 +1304,6 @@ function gatewayEndpoint(config: AppConfig): string {
}
function defaultClientModel(config: AppConfig): string {
const configuredDefault = normalizeClientModel(config.Router.default);
if (configuredDefault) {
return configuredDefault;
}
const preferred = config.Providers.find((provider) => provider.name === config.preferredProvider) ?? config.Providers[0];
if (preferred?.name && preferred.models[0]) {
return `${preferred.name}/${preferred.models[0]}`;

View file

@ -47,7 +47,11 @@ export async function getSystemProxyUrlForProtocol(protocol: "http" | "https" =
async function systemProxyUrlForRequest(url: URL): Promise<string | undefined> {
const cache = await readSystemProxy();
const server = proxyServerForRequest(cache.upstreamProxy, url.protocol === "https:" ? "https" : "http");
return server ? formatProxyUrl(server) : undefined;
if (server) return formatProxyUrl(server);
const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy ||
process.env.HTTP_PROXY || process.env.http_proxy;
return envProxy ? envProxy.trim() : undefined;
}
async function readSystemProxy(): Promise<SystemProxyCache> {
@ -187,12 +191,65 @@ function isHttpUrl(url: URL): boolean {
function shouldBypassProxy(url: URL): boolean {
const hostname = normalizeHostname(url.hostname);
return hostname === "localhost" ||
if (hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname.startsWith("127.") ||
hostname === "0.0.0.0" ||
hostname === "::1" ||
hostname === "0:0:0:0:0:0:0:1";
hostname === "0:0:0:0:0:0:0:1") {
return true;
}
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
if (!noProxy) return false;
const patterns = noProxy.split(",").map((s) => s.trim()).filter(Boolean);
for (const pattern of patterns) {
if (pattern === "*") return true;
if (pattern.startsWith(".")) {
if (hostname.endsWith(pattern.toLowerCase()) || hostname === pattern.slice(1).toLowerCase()) return true;
continue;
}
if (pattern.includes("/") && isPlainIp(hostname)) {
if (isInCidr(hostname, pattern)) return true;
continue;
}
if (hostname === pattern.toLowerCase()) return true;
}
return false;
}
function isPlainIp(s: string): boolean {
return /^\d+\.\d+\.\d+\.\d+$/.test(s);
}
function isInCidr(ip: string, cidr: string): boolean {
const [network, prefixStr] = cidr.split("/");
const prefix = parseInt(prefixStr, 10);
if (isNaN(prefix) || prefix < 0 || prefix > 32) return false;
const ipInt = ipToInt(ip);
const netInt = ipToInt(network);
if (ipInt === null || netInt === null) return false;
const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
return (ipInt & mask) === (netInt & mask);
}
function ipToInt(ip: string): number | null {
const parts = ip.split(".");
if (parts.length !== 4) return null;
let result = 0;
for (const part of parts) {
const num = parseInt(part, 10);
if (isNaN(num) || num < 0 || num > 255) return null;
result = (result << 8) | num;
}
return result >>> 0;
}
function normalizeHostname(hostname: string): string {

View file

@ -282,10 +282,6 @@ function gatewayEndpoint(config: AppConfig): string {
}
function defaultClientModel(config: AppConfig): string {
const configuredDefault = normalizeClientModel(config.Router.default);
if (configuredDefault) {
return configuredDefault;
}
const preferred = config.Providers.find((provider) => provider.name === config.preferredProvider) ?? config.Providers[0];
if (preferred?.name && preferred.models[0]) {
return `${preferred.name}/${preferred.models[0]}`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -22,7 +22,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
type="checkbox"
{...props}
/>
<Check className="pointer-events-none absolute left-0.5 top-0.5 h-3 w-3 text-primary-foreground opacity-0 transition-opacity peer-checked:opacity-100" />
<Check className="pointer-events-none absolute left-1/2 top-1/2 z-20 h-3 w-3 -translate-x-1/2 -translate-y-1/2 text-primary-foreground opacity-0 transition-opacity peer-checked:opacity-100" />
</span>
)
);

View file

@ -6,8 +6,8 @@ export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputEle
}
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
({ checked = false, className, disabled, onChange, onCheckedChange, ...props }, ref) => (
<span className={cn("relative inline-flex h-[30px] w-[54px] shrink-0", className)}>
({ checked = false, className, disabled, onChange, onCheckedChange, title, ...props }, ref) => (
<span className={cn("relative inline-flex h-[30px] w-[54px] shrink-0", className)} title={title}>
<input
aria-checked={checked}
checked={checked}
@ -19,6 +19,7 @@ const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
}}
ref={ref}
role="switch"
title={title}
type="checkbox"
{...props}
/>

View file

@ -4,11 +4,11 @@ import {
CardHeader, Check, CircleAlert, clampNumber, cn, createRouteModelOptions, createRoutingRewriteDraftRow,
Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle,
disclosureSpringTransition, Field, formatRouterRuleCondition, formatRouterRuleTarget, GatewayProviderConfig, Input,
Info, motion, normalizeRouterFallbackConfig, Pencil, Plus, Route, RouterFallbackConfig,
AppI18nContext, appCopy, ExternalLink, Info, motion, normalizeRouterFallbackConfig, Pencil, Plus, Route, RouterFallbackConfig,
RouterBuiltInAgentRuleId, RouterFallbackMode, routerConditionSourceOptions, routerFallbackModeOptions, RouterRule, routerRewriteOperationOptions, routerRuleOperatorOptions,
RouterBuiltInAgentRuleConfig,
RouteTargetControl, routingRuleRowMatchesQuery, Search, SelectControl, Toggle, translateOptions,
Trash2, uniqueStrings, useAppText, useMemo, useState, X
Trash2, uniqueStrings, useAppText, useContext, useMemo, useState, X
} from "../shared";
import { ROUTER_FALLBACK_MAX_RETRY_COUNT } from "../../../../shared/app";
export function RoutingView({
@ -100,6 +100,7 @@ export function RoutingView({
{visibleRules.map((row) => {
const rowSourceLabel = row.builtInAgent ? t(row.sourceLabel) : row.sourceLabel;
const rowTarget = row.target === "Profile model unset" ? t(row.target) : row.target;
const toggleDisabledReason = row.toggleDisabledReason ? t(row.toggleDisabledReason) : undefined;
return (
<AnimatedListItem
className="grid min-h-[58px] grid-cols-[minmax(160px,0.8fr)_minmax(220px,1fr)_minmax(240px,1.15fr)_84px_148px] items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/35"
@ -108,7 +109,7 @@ export function RoutingView({
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<div className="truncate text-[12px] font-semibold">{row.name || t("Unnamed")}</div>
{row.builtInAgent ? <BuiltInRouteInfoIcon description={builtInRouteDescription(row.builtInAgent, t)} /> : null}
{row.builtInAgent ? <BuiltInRouteInfoIcon agent={row.builtInAgent} /> : null}
{row.builtInAgent ? <Badge variant="outline">{t("Built-in")}</Badge> : row.readonly ? <Badge variant="outline">{t("Plugin")}</Badge> : null}
</div>
<div className="mt-0.5 truncate font-mono text-[11px] text-muted-foreground" title={`${rowSourceLabel}: ${row.ruleId}`}>
@ -129,17 +130,28 @@ export function RoutingView({
{row.builtInAgent ? null : rowTarget}
</div>
<div className="flex min-w-0 items-center gap-2">
<Toggle
checked={row.enabled}
disabled={row.readonly || row.toggleDisabled}
onChange={(enabled) => {
if (row.builtInAgent) {
updateBuiltInRule(row.builtInAgent, { enabled });
} else if (row.index !== undefined) {
updateRule(row.index, { enabled });
}
}}
/>
<span
aria-label={toggleDisabledReason}
className="group relative inline-flex rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
tabIndex={toggleDisabledReason ? 0 : undefined}
>
<Toggle
checked={row.enabled}
disabled={row.readonly || row.toggleDisabled}
onChange={(enabled) => {
if (row.builtInAgent) {
updateBuiltInRule(row.builtInAgent, { enabled });
} else if (row.index !== undefined) {
updateRule(row.index, { enabled });
}
}}
/>
{toggleDisabledReason ? (
<span className="pointer-events-none absolute right-full top-1/2 z-[80] mr-2 hidden w-[240px] -translate-y-1/2 rounded-md border border-border bg-popover px-2.5 py-2 text-left text-[11px] font-medium leading-4 text-popover-foreground shadow-card group-hover:block group-focus:block group-focus-within:block">
{toggleDisabledReason}
</span>
) : null}
</span>
</div>
<div className="flex items-center justify-end gap-1">
{!row.builtInAgent ? (
@ -185,22 +197,55 @@ export function RoutingView({
);
}
function BuiltInRouteInfoIcon({ description }: { description: string }) {
function BuiltInRouteInfoIcon({ agent }: { agent: RouterBuiltInAgentRuleId }) {
const t = useAppText();
const copy = useContext(AppI18nContext);
const description = builtInRouteDescription(agent, t);
const docsUrl = builtInRouteDocsUrl(agent, copy === appCopy.zh ? "zh" : "en");
return (
<span aria-label={description} className="group relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/30" tabIndex={0} title={description}>
<span aria-label={description} className="group relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/30" tabIndex={0}>
<Info className="h-3.5 w-3.5" aria-hidden="true" />
<span className="pointer-events-none absolute left-full top-1/2 z-[80] ml-2 hidden w-[260px] -translate-y-1/2 rounded-md border border-border bg-popover px-2.5 py-2 text-left text-[11px] font-medium leading-4 text-popover-foreground shadow-card group-hover:block group-focus:block">
{description}
<span className="absolute left-full top-1/2 z-[80] hidden w-[232px] -translate-y-1/2 pl-2 group-hover:block group-focus:block group-focus-within:block">
<span className="block rounded-md border border-border bg-popover px-2.5 py-2 text-left text-[11px] font-medium leading-4 text-popover-foreground shadow-card">
<span>{description}</span>
<a
className="ml-1 inline-flex items-center gap-1 text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
href={docsUrl}
onClick={(event) => {
event.preventDefault();
openExternalUrl(docsUrl);
}}
rel="noreferrer"
target="_blank"
>
{t("Docs")}
<ExternalLink className="h-3 w-3" />
</a>
</span>
</span>
</span>
);
}
function builtInRouteDescription(agent: RouterBuiltInAgentRuleId, t: (value: string) => string): string {
if (agent === "claude-code") {
return t("Routes Claude Code requests by matching the Claude user-agent and setting request.body.model to the Claude Code profile or default model.");
return agent === "claude-code"
? t("Identifies the Claude Code user-agent to provide deep Claude Code integration.")
: t("Identifies the Codex user-agent to provide deep Codex integration.");
}
function builtInRouteDocsUrl(agent: RouterBuiltInAgentRuleId, language: "en" | "zh"): string {
const path = language === "zh" ? "/configuration/routing" : "/en/configuration/routing";
const hash = agent === "claude-code" ? "claude-code" : "codex";
return `https://ccrdesk.top${path}#${hash}`;
}
function openExternalUrl(url: string) {
if (window.ccr?.openExternal) {
void window.ccr.openExternal(url).catch(() => undefined);
return;
}
return t("Routes Codex requests by matching the Codex user-agent and setting request.body.model to the Codex profile or default model.");
window.open(url, "_blank", "noopener,noreferrer");
}
function RouterFallbackControl({

View file

@ -529,8 +529,8 @@ export function KeyValueRowsControl({
);
}
export function Toggle({ checked, disabled = false, onChange }: { checked: boolean; disabled?: boolean; onChange: (checked: boolean) => void }) {
return <Switch checked={checked} disabled={disabled} onCheckedChange={onChange} />;
export function Toggle({ checked, disabled = false, onChange, title }: { checked: boolean; disabled?: boolean; onChange: (checked: boolean) => void; title?: string }) {
return <Switch checked={checked} disabled={disabled} onCheckedChange={onChange} title={title} />;
}
export type MetricTone = "amber" | "blue" | "indigo" | "rose" | "slate" | "teal";

View file

@ -644,8 +644,8 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Check trust": "检查信任",
"Choose where each agent uses CCR.": "选择每个 Agent 在哪里使用 CCR。",
"Built-in": "内置",
"Routes Claude Code requests by matching the Claude user-agent and setting request.body.model to the Claude Code profile or default model.": "通过匹配 Claude user-agent 路由 Claude Code 请求,并将 request.body.model 设置为 Claude Code 配置档案模型或默认模型。",
"Routes Codex requests by matching the Codex user-agent and setting request.body.model to the Codex profile or default model.": "通过匹配 Codex user-agent 路由 Codex 请求,并将 request.body.model 设置为 Codex 配置档案模型或默认模型。",
"Identifies the Claude Code user-agent to provide deep Claude Code integration.": "通过识别 Claude Code 的 user-agent实现对 Claude Code 的深度适配。",
"Identifies the Codex user-agent to provide deep Codex integration.": "通过识别 Codex 的 user-agent实现对 Codex 的深度适配。",
"Click Check Connection to verify connectivity with a real model request.": "点击检测连通性,用一次真实模型请求验证是否可用。",
"Click Add to create one": "点击添加创建一项",
"Click Install to add one": "点击安装添加一项",
@ -859,6 +859,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"None": "无",
"Not configured": "未配置",
"Not running": "未运行",
"Agent profiles are disabled.": "Agent 配置档案已关闭。",
"Open": "打开",
"Open Agent": "打开Agent",
"Open CA": "打开 CA",
@ -883,6 +884,10 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Phone Wi-Fi target": "手机 Wi-Fi 目标",
"Plugin": "插件",
"Profile model unset": "未设置配置档案模型",
"Enable a Claude Code profile before enabling this built-in route.": "请先启用一个 Claude Code 配置档案,再启用此内置路由。",
"Enable a Codex profile before enabling this built-in route.": "请先启用一个 Codex 配置档案,再启用此内置路由。",
"Set a model on the Claude Code profile before enabling this built-in route.": "请先为 Claude Code 配置档案设置模型,再启用此内置路由。",
"Set a model on the Codex profile before enabling this built-in route.": "请先为 Codex 配置档案设置模型,再启用此内置路由。",
"Plugin apps must be a JSON array.": "插件 App 必须是 JSON 数组。",
"Plugin config JSON": "插件配置 JSON",
"Plugin config must be a JSON object.": "插件配置必须是 JSON 对象。",
@ -1354,6 +1359,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Detecting icon": "正在检测图标",
"Detecting provider": "正在检测供应商",
"Disabled": "已禁用",
"Docs": "文档",
"Edit description": "编辑描述",
"Expand": "展开",
"Expand models": "展开模型",

View file

@ -49,6 +49,7 @@ import mistralProviderIconUrl from "@/assets/provider-icons/mistral.webp";
import moonshotProviderIconUrl from "@/assets/provider-icons/moonshot.ico";
import openaiProviderIconUrl from "@/assets/provider-icons/openai.png";
import openrouterProviderIconUrl from "@/assets/provider-icons/openrouter.ico";
import runapiProviderIconUrl from "@/assets/provider-icons/runapi.jpg";
import siliconflowProviderIconUrl from "@/assets/provider-icons/siliconflow.png";
import zaiGlobalCodingProviderIconUrl from "@/assets/provider-icons/zai-global-coding.svg";
import zaiGlobalGeneralProviderIconUrl from "@/assets/provider-icons/zai-global-general.svg";
@ -322,6 +323,7 @@ export const providerPresetIconUrls: Record<string, string> = {
"moonshot-global": moonshotProviderIconUrl,
openai: openaiProviderIconUrl,
openrouter: openrouterProviderIconUrl,
runapi: runapiProviderIconUrl,
siliconflow: siliconflowProviderIconUrl,
"zai-global-coding": zaiGlobalCodingProviderIconUrl,
"zai-global-general": zaiGlobalGeneralProviderIconUrl,

View file

@ -389,10 +389,6 @@ export function gatewayEndpointFromConfig(config: AppConfig): string {
}
export function defaultProfileClientModel(config: AppConfig): string {
const configuredDefault = normalizeProfileClientModel(config.Router.default);
if (configuredDefault) {
return configuredDefault;
}
const preferred = config.Providers.find((provider) => provider.name === config.preferredProvider) ?? config.Providers[0];
if (preferred?.name && preferred.models[0]) {
return `${preferred.name}/${preferred.models[0]}`;

View file

@ -1657,9 +1657,11 @@ export function providerDraftSafetyIssue(draft: AddProviderDraft, baseUrl = draf
export function providerProbeCandidates(draft: AddProviderDraft): ProviderProbeCandidate[] {
const preset = findProviderPreset(draft.presetId);
const protocols = providerProtocolOptions.map((option) => option.value);
if (preset) {
return preset.endpoints.map((endpoint) => ({
...endpoint,
protocols,
source: "preset"
}));
}
@ -1667,7 +1669,7 @@ export function providerProbeCandidates(draft: AddProviderDraft): ProviderProbeC
return [
{
baseUrl: draft.baseUrl.trim(),
protocols: providerProtocolOptions.map((option) => option.value),
protocols,
source: "custom"
}
];
@ -1765,17 +1767,38 @@ export function selectedProviderProtocolsFromCapabilities(
export function selectedProviderProtocolsForProbe(
selectedProtocols: GatewayProviderProtocol[],
probe: GatewayProviderProbeResult,
fallback: GatewayProviderProtocol
fallback: GatewayProviderProtocol,
presetId?: string
): GatewayProviderProtocol[] {
const selectable = providerSelectableProtocolsFromProbe(probe);
if (selectable.length === 0) {
return selectedProtocols.length > 0 ? uniqueProviderProtocols(selectedProtocols) : [fallback];
}
if (selectedProtocolsMatchPresetDefault(selectedProtocols, presetId)) {
return selectable;
}
const selected = uniqueProviderProtocols(selectedProtocols).filter((protocol) => selectable.includes(protocol));
return selected.length > 0 ? selected : selectable;
}
function selectedProtocolsMatchPresetDefault(
selectedProtocols: GatewayProviderProtocol[],
presetId: string | undefined
): boolean {
const preset = findProviderPreset(presetId);
if (!preset) {
return false;
}
const selected = uniqueProviderProtocols(selectedProtocols);
const defaults = uniqueProviderProtocols(preset.endpoints.flatMap((endpoint) => endpoint.protocols));
return defaults.length > 0 &&
selected.length === defaults.length &&
defaults.every((protocol, index) => selected[index] === protocol);
}
export function uniqueProviderProtocols(values: Array<GatewayProviderProtocol | string | undefined>): GatewayProviderProtocol[] {
const allowed = new Set(providerProtocolOptions.map((option) => option.value));
const seen = new Set<GatewayProviderProtocol>();
@ -1823,7 +1846,7 @@ export function mergeProviderCapabilities(...groups: GatewayProviderCapability[]
export function applyProviderProbeResult(draft: AddProviderDraft, probe: GatewayProviderProbeResult): AddProviderDraft {
const protocol = probe.detectedProtocol ?? draft.protocol;
const selectedProtocols = selectedProviderProtocolsForProbe(draft.selectedProtocols, probe, protocol);
const selectedProtocols = selectedProviderProtocolsForProbe(draft.selectedProtocols, probe, protocol, draft.presetId);
const modelDisplayNames = mergeModelDisplayNames(draft.modelDisplayNames, probe.modelDisplayNames);
if (probe.models.length === 0) {

View file

@ -386,10 +386,11 @@ export function normalizeRouterConfig(value: Partial<RouterConfig> | undefined):
const router = {
...fallbackConfig.Router,
...(value || {})
};
} as RouterConfig & { default?: unknown };
const { default: _legacyDefault, ...routerWithoutLegacyDefault } = router;
const rules = normalizeRouterRules((value as Record<string, unknown> | undefined)?.rules) ?? [];
return {
...router,
...routerWithoutLegacyDefault,
builtInRules: normalizeRouterBuiltInRules((value as Record<string, unknown> | undefined)?.builtInRules),
fallback: normalizeRouterFallbackConfig((value as Record<string, unknown> | undefined)?.fallback),
rules
@ -847,7 +848,7 @@ export function buildRoutingRuleRows(config: AppConfig): RoutingRuleRow[] {
export function buildBuiltInAgentRoutingRows(config: AppConfig): RoutingRuleRow[] {
return routerBuiltInAgentRuleIds.map((agent): RoutingRuleRow => {
const target = routerBuiltInAgentRouteTarget(config, agent);
const profileEnabled = Boolean(routerBuiltInAgentProfile(config, agent));
const toggleDisabledReason = routerBuiltInAgentRuleDisabledReason(config, agent);
return {
builtInAgent: agent,
condition: `request.header.user-agent contains ${routerBuiltInAgentUserAgentNeedle(agent)}`,
@ -859,7 +860,8 @@ export function buildBuiltInAgentRoutingRows(config: AppConfig): RoutingRuleRow[
ruleId: `builtin-agent-${agent}`,
sourceLabel: "Built-in",
target: target ? `set request.body.model = ${target}` : "Profile model unset",
toggleDisabled: !profileEnabled || !target,
toggleDisabled: Boolean(toggleDisabledReason),
toggleDisabledReason,
typeLabel: "Condition"
};
});
@ -885,7 +887,22 @@ export function routerBuiltInAgentProfile(config: AppConfig, agent: RouterBuiltI
}
export function routerBuiltInAgentRouteTarget(config: AppConfig, agent: RouterBuiltInAgentRuleId): string {
return routerBuiltInAgentProfile(config, agent)?.model.trim() || config.Router.default?.trim() || "";
return routerBuiltInAgentProfile(config, agent)?.model.trim() || "";
}
export function routerBuiltInAgentRuleDisabledReason(config: AppConfig, agent: RouterBuiltInAgentRuleId): string | undefined {
if (config.profile.enabled === false) {
return "Agent profiles are disabled.";
}
const agentName = routerBuiltInAgentRuleName(agent);
const profile = routerBuiltInAgentProfile(config, agent);
if (!profile) {
return `Enable a ${agentName} profile before enabling this built-in route.`;
}
if (!profile.model.trim()) {
return `Set a model on the ${agentName} profile before enabling this built-in route.`;
}
return undefined;
}
export function routerBuiltInAgentRuleName(agent: RouterBuiltInAgentRuleId): string {

View file

@ -733,6 +733,7 @@ export type RoutingRuleRow = {
sourceLabel: string;
target: string;
toggleDisabled?: boolean;
toggleDisabledReason?: string;
typeLabel: string;
};

View file

@ -8,6 +8,16 @@ declare module "*.ico" {
export default src;
}
declare module "*.jpg" {
const src: string;
export default src;
}
declare module "*.jpeg" {
const src: string;
export default src;
}
declare module "*.svg" {
const src: string;
export default src;

View file

@ -210,7 +210,7 @@ function resolveConfiguredRouteDecision(
}
}
return { fallback: router.fallback, model: normalizeRouteSelector(router.default) ?? explicitModel, reason: "default" };
return { fallback: router.fallback, model: explicitModel, reason: "default" };
}
function resolveBuiltInClaudeCodeSubagentRouteDecision(
@ -287,8 +287,7 @@ function resolveBuiltInAgentProfile(config: AppConfig, agent: RouterBuiltInAgent
}
function resolveBuiltInAgentRouteTarget(config: AppConfig, agent: RouterBuiltInAgentRuleId): string | undefined {
return normalizeRouteSelector(resolveBuiltInAgentProfile(config, agent)?.model) ??
normalizeRouteSelector(config.Router.default);
return normalizeRouteSelector(resolveBuiltInAgentProfile(config, agent)?.model);
}
function builtInAgentUserAgentNeedle(agent: RouterBuiltInAgentRuleId): string {

View file

@ -532,7 +532,6 @@ export type RouterBuiltInRulesConfig = Record<RouterBuiltInAgentRuleId, RouterBu
export type RouterConfig = {
builtInRules: RouterBuiltInRulesConfig;
default?: string;
fallback: RouterFallbackConfig;
rules: RouterRule[];
};

View file

@ -25,14 +25,13 @@ export type ClaudeAppGatewayInferenceModel = {
supports1m?: true;
};
export function inferClaudeAppGatewayTargetModel(config: Pick<AppConfig, "Router" | "profile">): string {
return config.Router.default?.trim() ||
inferGlobalClaudeProfileModel(config) ||
export function inferClaudeAppGatewayTargetModel(config: Pick<AppConfig, "profile">): string {
return inferGlobalClaudeProfileModel(config) ||
CLAUDE_APP_FALLBACK_MODEL;
}
export function buildClaudeAppGatewayModelRoutes(
config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">,
config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">,
options: ClaudeAppGatewayModelRouteOptions = {}
): ClaudeAppGatewayModelRoute[] {
const targetModels = claudeAppGatewayTargetModels(config);
@ -74,7 +73,7 @@ export function buildClaudeAppGatewayModelRoutes(
export function resolveClaudeAppGatewayRouteModel(
model: string,
config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">,
config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">,
options: ClaudeAppGatewayModelRouteOptions = {}
): string | undefined {
const normalized = model.trim().toLowerCase();
@ -99,7 +98,7 @@ export function resolveClaudeAppGatewayRouteModel(
}
export function buildClaudeAppGatewayInferenceModels(
config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">,
config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">,
options: ClaudeAppGatewayModelRouteOptions = {}
): ClaudeAppGatewayInferenceModel[] {
const routes = buildClaudeAppGatewayModelRoutes(config, options);
@ -129,7 +128,7 @@ function inferGlobalClaudeProfileModel(config: Pick<AppConfig, "profile">): stri
)?.model.trim() ?? "";
}
function claudeAppGatewayTargetModels(config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">): string[] {
function claudeAppGatewayTargetModels(config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">): string[] {
const baseEntries = config.Providers.flatMap((provider) => {
const providerName = provider.name?.trim();
if (!providerName || !Array.isArray(provider.models)) {

View file

@ -0,0 +1,17 @@
import assert from "node:assert/strict";
import test from "node:test";
import { claudeCodeUtcTimezoneEnvOverride, isChinaTimeZone } from "../../src/main/claude-environment.ts";
test("detects China time zones used by Claude Code", () => {
assert.equal(isChinaTimeZone("Asia/Shanghai"), true);
assert.equal(isChinaTimeZone("Asia/Urumqi"), true);
assert.equal(isChinaTimeZone("PRC"), true);
assert.equal(isChinaTimeZone("UTC"), false);
assert.equal(isChinaTimeZone("Asia/Singapore"), false);
});
test("overrides Claude Code timezone only for China time zones", () => {
assert.deepEqual(claudeCodeUtcTimezoneEnvOverride("Asia/Shanghai"), { TZ: "UTC" });
assert.deepEqual(claudeCodeUtcTimezoneEnvOverride("UTC"), {});
assert.deepEqual(claudeCodeUtcTimezoneEnvOverride("America/Los_Angeles"), {});
});

View file

@ -20,7 +20,6 @@ function createRouterPlugin(options = {}) {
"claude-code": { enabled: options.claudeCodeRuleEnabled ?? true },
codex: { enabled: options.codexRuleEnabled ?? true }
},
default: options.defaultModel ?? "",
fallback: { mode: "off", models: [], retryCount: 1 },
rules: []
},
@ -59,10 +58,9 @@ test("built-in Claude Code route matches user-agent case-insensitively", async (
assert.equal(result.decision.reason, "builtin:claude-code");
});
test("built-in Codex route can use Router.default when profile model is unset", async () => {
test("built-in Codex route stays inactive when profile model is unset", async () => {
const plugin = createRouterPlugin({
agent: "codex",
defaultModel: "Provider/gpt-5-codex"
agent: "codex"
});
const result = await plugin.routeRequest({
body: {
@ -76,8 +74,8 @@ test("built-in Codex route can use Router.default when profile model is unset",
url: "/v1/messages"
});
assert.equal(result.body.model, "Provider/gpt-5-codex");
assert.equal(result.decision.reason, "builtin:codex");
assert.equal(result.body.model, "gpt-5");
assert.equal(result.decision.reason, "default");
});
test("built-in agent route stays off after the user disables it", async () => {
@ -493,7 +491,7 @@ test("built-in Claude Code route removes only the first billing system array ite
test("non-Claude-Code routes keep billing system prompts unchanged", async () => {
const plugin = createRouterPlugin({
agent: "codex",
defaultModel: "Provider/gpt-5-codex"
profileModel: "Provider/gpt-5-codex"
});
const result = await plugin.routeRequest({
body: {