@musistudio/claude-code-router 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,13 +29,11 @@ ccr code
29
29
 
30
30
  ```json
31
31
  {
32
- "OPENAI_API_KEY": "sk-xxx",
33
- "OPENAI_BASE_URL": "https://api.deepseek.com",
34
- "OPENAI_MODEL": "deepseek-chat",
35
32
  "Providers": [
36
33
  {
37
34
  "name": "openrouter",
38
- "api_base_url": "https://openrouter.ai/api/v1",
35
+ // IMPORTANT: api_base_url must be a complete (full) URL.
36
+ "api_base_url": "https://openrouter.ai/api/v1/chat/completions",
39
37
  "api_key": "sk-xxx",
40
38
  "models": [
41
39
  "google/gemini-2.5-pro-preview",
@@ -46,18 +44,48 @@ ccr code
46
44
  },
47
45
  {
48
46
  "name": "deepseek",
49
- "api_base_url": "https://api.deepseek.com",
47
+ // IMPORTANT: api_base_url must be a complete (full) URL.
48
+ "api_base_url": "https://api.deepseek.com/chat/completions",
50
49
  "api_key": "sk-xxx",
51
- "models": ["deepseek-reasoner"]
50
+ "models": ["deepseek-chat", "deepseek-reasoner"],
51
+ "transformer": {
52
+ "use": ["deepseek"],
53
+ "deepseek-chat": {
54
+ // Enhance tool usage for the deepseek-chat model using the ToolUse transformer.
55
+ "use": ["tooluse"]
56
+ }
57
+ }
52
58
  },
53
59
  {
54
60
  "name": "ollama",
55
- "api_base_url": "http://localhost:11434/v1",
61
+ // IMPORTANT: api_base_url must be a complete (full) URL.
62
+ "api_base_url": "http://localhost:11434/v1/chat/completions",
56
63
  "api_key": "ollama",
57
64
  "models": ["qwen2.5-coder:latest"]
65
+ },
66
+ {
67
+ "name": "gemini",
68
+ // IMPORTANT: api_base_url must be a complete (full) URL.
69
+ "api_base_url": "https://generativelanguage.googleapis.com/v1beta/models/",
70
+ "api_key": "sk-xxx",
71
+ "models": ["gemini-2.5-flash", "gemini-2.5-pro"],
72
+ "transformer": {
73
+ "use": ["gemini"]
74
+ }
75
+ },
76
+ {
77
+ "name": "volcengine",
78
+ // IMPORTANT: api_base_url must be a complete (full) URL.
79
+ "api_base_url": "https://ark.cn-beijing.volces.com/api/v3/chat/completions",
80
+ "api_key": "sk-xxx",
81
+ "models": ["deepseek-v3-250324", "deepseek-r1-250528"],
82
+ "transformer": {
83
+ "use": ["deepseek"]
84
+ }
58
85
  }
59
86
  ],
60
87
  "Router": {
88
+ "default": "deepseek,deepseek-chat", // IMPORTANT OPENAI_MODEL has been deprecated
61
89
  "background": "ollama,qwen2.5-coder:latest",
62
90
  "think": "deepseek,deepseek-reasoner",
63
91
  "longContext": "openrouter,google/gemini-2.5-pro-preview"
@@ -86,40 +114,12 @@ ccr code
86
114
 
87
115
  - [x] Support change models
88
116
  - [x] Github Actions
89
- - [ ] More robust plugin support
90
117
  - [ ] More detailed logs
91
118
 
92
- ## Plugins
93
- You can modify or enhance Claude Code’s functionality by installing plugins. The mechanism works by using middleware to modify request parameters — this allows you to rewrite prompts or add/remove tools.
94
-
95
- To use a plugin, place it in the ~/.claude-code-router/plugins/ directory and specify the plugin name in config.js using the `usePlugins` option.like this
96
- ```json
97
- // ~/.claud-code-router/config.json
98
- {
99
- ...,
100
- "usePlugins": ["notebook-tools-filter", "toolcall-improvement"]
101
- }
102
- ```
103
-
104
- Currently, the following plugins are available:
105
-
106
-
107
- - **notebook-tools-filter**
108
- This plugin filters out tool calls related to Jupyter notebooks (.ipynb files). You can use it if your work does not involve Jupyter.
109
-
110
-
111
- - **toolcall-improvement**
112
- If your LLM doesn’t handle tool usage well (for example, always returning code as plain text instead of modifying files — such as with deepseek-v3), you can use this plugin.
113
- This plugin simply adds the following system prompt. If you have a better prompt, you can modify it.
114
- ```markdown
115
- ## **Important Instruction:**
116
- You must use tools as frequently and accurately as possible to help the user solve their problem.
117
- Prioritize tool usage whenever it can enhance accuracy, efficiency, or the quality of the response.
118
- ```
119
-
120
-
121
119
  ## Github Actions
120
+
122
121
  You just need to install `Claude Code Actions` in your repository according to the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions). For `ANTHROPIC_API_KEY`, you can use any string. Then, modify your `.github/workflows/claude.yaml` file to include claude-code-router, like this:
122
+
123
123
  ```yaml
124
124
  name: Claude Code
125
125
 
@@ -151,7 +151,7 @@ jobs:
151
151
  uses: actions/checkout@v4
152
152
  with:
153
153
  fetch-depth: 1
154
-
154
+
155
155
  - name: Prepare Environment
156
156
  run: |
157
157
  curl -fsSL https://bun.sh/install | bash
@@ -165,7 +165,7 @@ jobs:
165
165
  }
166
166
  EOF
167
167
  shell: bash
168
-
168
+
169
169
  - name: Start Claude Code Router
170
170
  run: |
171
171
  nohup ~/.bun/bin/bunx @musistudio/claude-code-router@1.0.8 start &
@@ -179,6 +179,7 @@ jobs:
179
179
  with:
180
180
  anthropic_api_key: "test"
181
181
  ```
182
+
182
183
  You can modify the contents of `$HOME/.claude-code-router/config.json` as needed.
183
184
  GitHub Actions support allows you to trigger Claude Code at specific times, which opens up some interesting possibilities.
184
185
 
@@ -190,7 +191,6 @@ For example, between 00:30 and 08:30 Beijing Time, using the official DeepSeek A
190
191
 
191
192
  So maybe in the future, I’ll describe detailed tasks for Claude Code ahead of time and let it run during these discounted hours to reduce costs?
192
193
 
193
-
194
194
  ## Some tips:
195
195
 
196
196
  Now you can use deepseek-v3 models directly without using any plugins.
@@ -205,22 +205,36 @@ Some interesting points: Based on my testing, including a lot of context informa
205
205
 
206
206
  ## Some articles:
207
207
 
208
- 1. [Project Motivation and Principles](blog/en/project-motivation-and-how-it-works.md) ([中文版看这里](blog/zh/项目初衷及原理.md))
208
+ 1. [Project Motivation and Principles](blog/en/project-motivation-and-how-it-works.md) ([项目初衷及原理](blog/zh/项目初衷及原理.md))
209
+ 2. [Maybe We Can Do More with the Router](blog/en/maybe-we-can-do-more-with-the-route.md) ([或许我们能在 Router 中做更多事情](blog/zh/或许我们能在Router中做更多事情.md))
209
210
 
210
211
  ## Buy me a coffee
211
212
 
212
- If you find this project helpful, you can choose to sponsor the author with a cup of coffee. Please provide your GitHub information so I can add you to the sponsor list below.
213
+ If you find this project helpful, you can choose to sponsor the author with a cup of coffee. Please provide your GitHub information so I can add you to the sponsor list below.
213
214
 
214
215
  [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)
215
216
 
216
- | ![alipay](/blog/images/alipay.jpg) | ![wechat](/blog/images/wechat.jpg) |
217
- |----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
217
+ <table>
218
+ <tr>
219
+ <td><img src="/blog/images/alipay.jpg" width="200" /></td>
220
+ <td><img src="/blog/images/wechat.jpg" width="200" /></td>
221
+ </tr>
222
+ </table>
218
223
 
219
224
  ## Sponsors
220
225
 
221
226
  Thanks to the following sponsors:
222
227
 
223
- @Simon Leischnig (If you see this, feel free to contact me and I can update it with your GitHub information)
224
- [@duanshuaimin](https://github.com/duanshuaimin)
225
- [@vrgitadmin](https://github.com/vrgitadmin)
226
- @*o(看到后可通过主页邮箱联系我修改github用户名)
228
+ @Simon Leischnig (If you see this, feel free to contact me and I can update it with your GitHub information)
229
+ [@duanshuaimin](https://github.com/duanshuaimin)
230
+ [@vrgitadmin](https://github.com/vrgitadmin)
231
+ @\*o (可通过主页邮箱联系我修改 github 用户名)
232
+ @\*\*聪 (可通过主页邮箱联系我修改 github 用户名)
233
+ @\*说 (可通过主页邮箱联系我修改 github 用户名)
234
+ @\*更 (可通过主页邮箱联系我修改 github 用户名)
235
+ @\*更 (可通过主页邮箱联系我修改 github 用户名)
236
+ @K\*g (可通过主页邮箱联系我修改 github 用户名)
237
+ @R\*R (可通过主页邮箱联系我修改 github 用户名)
238
+ @[@bobleer](https://github.com/bobleer) (可通过主页邮箱联系我修改 github 用户名)
239
+ @\*苗 (可通过主页邮箱联系我修改 github 用户名)
240
+ @\*划 (可通过主页邮箱联系我修改 github 用户名)
package/app.log ADDED
File without changes
@@ -0,0 +1,105 @@
1
+ # Maybe We Can Do More with the Router
2
+
3
+ Since the release of `claude-code-router`, I’ve received a lot of user feedback, and quite a few issues are still open. Most of them are related to support for different providers and the lack of tool usage from the deepseek model.
4
+
5
+ Originally, I created this project for personal use, mainly to access claude code at a lower cost. So, multi-provider support wasn’t part of the initial design. But during troubleshooting, I discovered that even though most providers claim to be compatible with the OpenAI-style `/chat/completions` interface, there are many subtle differences. For example:
6
+
7
+ 1. When Gemini's tool parameter type is string, the `format` field only supports `date` and `date-time`, and there’s no tool call ID.
8
+
9
+ 2. OpenRouter requires `cache_control` for caching.
10
+
11
+ 3. The official DeepSeek API has a `max_output` of 8192, but Volcano Engine’s limit is even higher.
12
+
13
+ Aside from these, smaller providers often have quirks in their parameter handling. So I decided to create a new project, [musistudio/llms](https://github.com/musistudio/llms), to deal with these compatibility issues. It uses the OpenAI format as a base and introduces a generic Transformer interface for transforming both requests and responses.
14
+
15
+ Once a `Transformer` is implemented for each provider, it becomes possible to mix-and-match requests between them. For example, I implemented bidirectional conversion between Anthropic and OpenAI formats in `AnthropicTransformer`, which listens to the `/v1/messages` endpoint. Similarly, `GeminiTransformer` handles Gemini <-> OpenAI format conversions and listens to `/v1beta/models/:modelAndAction`.
16
+
17
+ When both requests and responses are transformed into a common format, they can interoperate seamlessly:
18
+
19
+ ```
20
+ AnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer
21
+ ```
22
+
23
+ ```
24
+ GeminiResponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse
25
+ ```
26
+
27
+ Using a middleware layer to smooth out differences may introduce some performance overhead, but the main goal here is to enable `claude-code-router` to support multiple providers.
28
+
29
+ As for the issue of DeepSeek’s lackluster tool usage — I found that it stems from poor instruction adherence in long conversations. Initially, the model actively calls tools, but after several rounds, it starts responding with plain text instead. My first workaround was injecting a system prompt to remind the model to use tools proactively. But in long contexts, the model tends to forget this instruction.
30
+
31
+ After reading the DeepSeek documentation, I noticed it supports the `tool_choice` parameter, which can be set to `"required"` to force the model to use at least one tool. I tested this by enabling the parameter, and it significantly improved the model’s tool usage. We can remove the setting when it's no longer necessary. With the help of the `Transformer` interface in [musistudio/llms](https://github.com/musistudio/llms), we can modify the request before it’s sent and adjust the response after it’s received.
32
+
33
+ Inspired by the Plan Mode in `claude code`, I implemented a similar Tool Mode for DeepSeek:
34
+
35
+ ```typescript
36
+ export class TooluseTransformer implements Transformer {
37
+ name = "tooluse";
38
+
39
+ transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {
40
+ if (request.tools?.length) {
41
+ request.messages.push({
42
+ role: "system",
43
+ content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.
44
+ Before invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \`ExitTool\` to exit tool mode — this is the only valid way to terminate tool mode.
45
+ Always prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,
46
+ });
47
+ request.tool_choice = "required";
48
+ request.tools.unshift({
49
+ type: "function",
50
+ function: {
51
+ name: "ExitTool",
52
+ description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.
53
+ IMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.
54
+ Examples:
55
+ 1. Task: "Use a tool to summarize this document" — Do not use ExitTool if a summarization tool is available.
56
+ 2. Task: "What’s the weather today?" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,
57
+ parameters: {
58
+ type: "object",
59
+ properties: {
60
+ response: {
61
+ type: "string",
62
+ description:
63
+ "Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.",
64
+ },
65
+ },
66
+ required: ["response"],
67
+ },
68
+ },
69
+ });
70
+ }
71
+ return request;
72
+ }
73
+
74
+ async transformResponseOut(response: Response): Promise<Response> {
75
+ if (response.headers.get("Content-Type")?.includes("application/json")) {
76
+ const jsonResponse = await response.json();
77
+ if (
78
+ jsonResponse?.choices[0]?.message.tool_calls?.length &&
79
+ jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===
80
+ "ExitTool"
81
+ ) {
82
+ const toolArguments = JSON.parse(toolCall.function.arguments || "{}");
83
+ jsonResponse.choices[0].message.content = toolArguments.response || "";
84
+ delete jsonResponse.choices[0].message.tool_calls;
85
+ }
86
+
87
+ // Handle non-streaming response if needed
88
+ return new Response(JSON.stringify(jsonResponse), {
89
+ status: response.status,
90
+ statusText: response.statusText,
91
+ headers: response.headers,
92
+ });
93
+ } else if (response.headers.get("Content-Type")?.includes("stream")) {
94
+ // ...
95
+ }
96
+ return response;
97
+ }
98
+ }
99
+ ```
100
+
101
+ This transformer ensures the model calls at least one tool. If no tools are appropriate or the task is finished, it can exit using `ExitTool`. Since this relies on the `tool_choice` parameter, it only works with models that support it.
102
+
103
+ In practice, this approach noticeably improves tool usage for DeepSeek. The tradeoff is that sometimes the model may invoke irrelevant or unnecessary tools, which could increase latency and token usage.
104
+
105
+ This update is just a small experiment — adding an `“agent”` to the router. Maybe there are more interesting things we can explore from here.
@@ -0,0 +1,95 @@
1
+ # 或许我们能在 Router 中做更多事情
2
+
3
+ 自从`claude-code-router`发布以来,我收到了很多用户的反馈,至今还有不少的 issues 未处理。其中大多都是关于不同的供应商的支持和`deepseek`模型调用工具不积极的问题。
4
+ 之前开发这个项目主要是为了我自己能以较低成本使用上`claude code`,所以一开始的设计并没有考虑到多供应商的情况。在实际的排查问题中,我发现尽管市面上所有的供应商几乎都宣称兼容`OpenAI`格式调用,即通过`/chat/compeletions`接口调用,但是其中的细节差异非常多。例如:
5
+
6
+ 1. Gemini 的工具参数类型是 string 时,`format`参数只支持`date`和`date-time`,并且没有工具调用 ID。
7
+
8
+ 2. OpenRouter 需要使用`cache_control`进行缓存。
9
+
10
+ 3. DeepSeek 官方 API 的 `max_output` 为 8192,而火山引擎的会更大。
11
+
12
+ 除了这些问题之外,还有一些其他的小的供应商,他们或多或少参数都有点问题。于是,我打算开发一个新的项目[musistudio/llms](https://github.com/musistudio/llms)来处理这种不同服务商的兼容问题。该项目使用 OpenAI 格式为基础的通用格式,提供了一个`Transformer`接口,该接口用于处理转换请求和响应。当我们给不同的服务商都实现了`Transformer`后,我们可以实现不同服务商的混合调用。比如我在`AnthropicTransformer`中实现了`Anthropic`<->`OpenAI`格式的互相转换,并监听了`/v1/messages`端点,在`GeminiTransformer`中实现了`Gemini`<->`OpenAI`格式的互相转换,并监听了`/v1beta/models/:modelAndAction`端点,当他们的请求和响应都被转换成一个通用格式的时候,就可以实现他们的互相调用。
13
+
14
+ ```
15
+ AnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer
16
+ ```
17
+
18
+ ```
19
+ GeminiReseponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse
20
+ ```
21
+
22
+ 虽然使用中间层抹平差异可能会带来一些性能问题,但是该项目最初的目的是为了让`claude-code-router`支持不同的供应商。
23
+
24
+ 至于`deepseek`模型调用工具不积极的问题,我发现这是由于`deepseek`在长上下文中的指令遵循不佳导致的。现象就是刚开始模型会主动调用工具,但是在经过几轮对话后模型只会返回文本。一开始的解决方案是通过注入一个系统提示词告知模型需要积极去使用工具以解决用户的问题,但是后面测试发现在长上下文中模型会遗忘该指令。
25
+ 查看`deepseek`文档后发现模型支持`tool_choice`参数,可以强制让模型最少调用 1 个工具,我尝试将该值设置为`required`,发现模型调用工具的积极性大大增加,现在我们只需要在合适的时候取消这个参数即可。借助[musistudio/llms](https://github.com/musistudio/llms)的`Transformer`可以让我们在发送请求前和收到响应后做点什么,所以我参考`claude code`的`Plan Mode`,实现了一个使用与`deepseek`的`Tool Mode`
26
+
27
+ ```typescript
28
+ export class TooluseTransformer implements Transformer {
29
+ name = "tooluse";
30
+
31
+ transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {
32
+ if (request.tools?.length) {
33
+ request.messages.push({
34
+ role: "system",
35
+ content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.
36
+ Before invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \`ExitTool\` to exit tool mode — this is the only valid way to terminate tool mode.
37
+ Always prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,
38
+ });
39
+ request.tool_choice = "required";
40
+ request.tools.unshift({
41
+ type: "function",
42
+ function: {
43
+ name: "ExitTool",
44
+ description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.
45
+ IMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.
46
+ Examples:
47
+ 1. Task: "Use a tool to summarize this document" — Do not use ExitTool if a summarization tool is available.
48
+ 2. Task: "What’s the weather today?" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,
49
+ parameters: {
50
+ type: "object",
51
+ properties: {
52
+ response: {
53
+ type: "string",
54
+ description:
55
+ "Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.",
56
+ },
57
+ },
58
+ required: ["response"],
59
+ },
60
+ },
61
+ });
62
+ }
63
+ return request;
64
+ }
65
+
66
+ async transformResponseOut(response: Response): Promise<Response> {
67
+ if (response.headers.get("Content-Type")?.includes("application/json")) {
68
+ const jsonResponse = await response.json();
69
+ if (
70
+ jsonResponse?.choices[0]?.message.tool_calls?.length &&
71
+ jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===
72
+ "ExitTool"
73
+ ) {
74
+ const toolArguments = JSON.parse(toolCall.function.arguments || "{}");
75
+ jsonResponse.choices[0].message.content = toolArguments.response || "";
76
+ delete jsonResponse.choices[0].message.tool_calls;
77
+ }
78
+
79
+ // Handle non-streaming response if needed
80
+ return new Response(JSON.stringify(jsonResponse), {
81
+ status: response.status,
82
+ statusText: response.statusText,
83
+ headers: response.headers,
84
+ });
85
+ } else if (response.headers.get("Content-Type")?.includes("stream")) {
86
+ // ...
87
+ }
88
+ return response;
89
+ }
90
+ }
91
+ ```
92
+
93
+ 该工具将始终让模型至少调用一个工具,如果没有合适的工具或者任务已完成可以调用`ExitTool`来退出工具模式,因为是依靠`tool_choice`参数实现的,所以仅适用于支持该参数的模型。经过测试,该工具能显著增加`deepseek`的工具调用次数,弊端是可能会有跟任务无关或者没有必要的工具调用导致增加任务执行事件和消耗的 `token` 数。
94
+
95
+ 这次更新仅仅是在 Router 中实现一个`agent`的一次小探索,或许还能做更多其他有趣的事也说不定...