From 4c5a84e0284ae220cbfa7b0b462214e0dcf56997 Mon Sep 17 00:00:00 2001 From: musistudio Date: Thu, 1 Jan 2026 22:11:21 +0800 Subject: [PATCH] fix docs --- docs/docs/cli/config/basic.md | 45 +- docs/docs/cli/intro.md | 36 +- docs/docs/cli/quick-start.md | 54 +- docs/docs/server/config/routing.md | 109 +++ docs/docs/server/config/transformers.md | 692 +++++++++++++- docs/docs/server/intro.md | 96 +- docs/docusaurus.config.ts | 30 +- .../current.json | 18 +- .../current/server/config/routing.md | 109 +++ .../current/server/config/transformers.md | 879 ++++++++++++++---- .../current/server/intro.md | 96 +- .../docusaurus-theme-classic/navbar.json | 4 +- docs/sidebars.ts | 96 +- package.json | 2 +- packages/core/src/api/routes.ts | 173 +++- packages/core/src/server.ts | 2 + packages/core/src/utils/router.ts | 39 +- 17 files changed, 2092 insertions(+), 388 deletions(-) diff --git a/docs/docs/cli/config/basic.md b/docs/docs/cli/config/basic.md index 187f86a..5f17bee 100644 --- a/docs/docs/cli/config/basic.md +++ b/docs/docs/cli/config/basic.md @@ -6,15 +6,29 @@ title: Basic Configuration CLI uses the same configuration file as Server: `~/.claude-code-router/config.json` -## Configuration File Location +## Configuration Methods + +You can configure Claude Code Router in three ways: + +### Option 1: Edit Configuration File Directly + +Edit `~/.claude-code-router/config.json` with your favorite editor: ```bash -~/.claude-code-router/config.json +nano ~/.claude-code-router/config.json ``` -## Quick Configuration +### Option 2: Use Web UI -Use interactive command to configure: +Open the web interface and configure visually: + +```bash +ccr ui +``` + +### Option 3: Interactive Configuration + +Use the interactive command-line configuration: ```bash ccr model @@ -26,16 +40,23 @@ This will guide you through: 3. Select model 4. Set routing rules -## Manual Configuration +## Restart After Configuration Changes -### Edit Configuration File +After modifying the configuration file or making changes through the Web UI, you must restart the service: ```bash -# Open configuration file -nano ~/.claude-code-router/config.json +ccr restart ``` -### Minimal Configuration Example +Or restart directly through the Web UI. + +## Configuration File Location + +```bash +~/.claude-code-router/config.json +``` + +## Minimal Configuration Example ```json5 { @@ -129,14 +150,16 @@ Configuration is automatically backed up on each update: ~/.claude-code-router/config.backup.{timestamp}.json ``` -## Reload Configuration +## Apply Configuration Changes -Restart service after modifying configuration: +After modifying the configuration file or making changes through the Web UI, restart the service: ```bash ccr restart ``` +Or restart directly through the Web UI by clicking the "Save and Restart" button. + ## View Current Configuration ```bash diff --git a/docs/docs/cli/intro.md b/docs/docs/cli/intro.md index 78e41a1..7b1ffb3 100644 --- a/docs/docs/cli/intro.md +++ b/docs/docs/cli/intro.md @@ -26,27 +26,49 @@ npm install -g @musistudio/claude-code-router ## Basic Usage -### Start Service +### Configuration + +Before using Claude Code Router, you need to configure your providers. You can either: + +1. **Edit configuration file directly**: Edit `~/.claude-code-router/config.json` manually +2. **Use Web UI**: Run `ccr ui` to open the web interface and configure visually + +After making configuration changes, restart the service: ```bash -ccr start +ccr restart ``` -### View Status +Or restart directly through the Web UI. + +### Start Claude Code + +Once configured, you can start Claude Code with: ```bash -ccr status +ccr code ``` -### Stop Service +This will launch Claude Code and route your requests through the configured provider. + +### Service Management ```bash -ccr stop +ccr start # Start the router service +ccr status # View service status +ccr stop # Stop the router service +ccr restart # Restart the router service +``` + +### Web UI + +```bash +ccr ui # Open Web management interface ``` ## Configuration File -`ccr` uses the same configuration file as Server: `~/.claude-code-router/config.json` +`ccr` uses the configuration file at `~/.claude-code-router/config.json` Configure once, and both CLI and Server will use it. diff --git a/docs/docs/cli/quick-start.md b/docs/docs/cli/quick-start.md index 76ea0a0..d814d72 100644 --- a/docs/docs/cli/quick-start.md +++ b/docs/docs/cli/quick-start.md @@ -6,7 +6,41 @@ sidebar_position: 3 Get up and running with Claude Code Router in 5 minutes. -## 1. Start the Router +## 1. Configure the Router + +Before using Claude Code Router, you need to configure your LLM providers. You can either: + +### Option A: Edit Configuration File Directly + +Edit `~/.claude-code-router/config.json`: + +```json +{ + "HOST": "0.0.0.0", + "PORT": 8080, + "Providers": [ + { + "name": "openai", + "api_base_url": "https://api.openai.com/v1", + "api_key": "your-api-key-here", + "models": ["gpt-4", "gpt-3.5-turbo"] + } + ], + "Router": { + "default": "openai,gpt-4" + } +} +``` + +### Option B: Use Web UI + +```bash +ccr ui +``` + +This will open the web interface where you can configure providers visually. + +## 2. Start the Router ```bash ccr start @@ -14,7 +48,7 @@ ccr start The router will start on `http://localhost:8080` by default. -## 2. Use Claude Code +## 3. Use Claude Code Now you can use Claude Code normally: @@ -24,8 +58,18 @@ ccr code Your requests will be routed through Claude Code Router to your configured provider. +## Restart After Configuration Changes + +If you modify the configuration file or make changes through the Web UI, restart the service: + +```bash +ccr restart +``` + +Or restart directly through the Web UI. + ## What's Next? -- [Basic Configuration](/docs/config/basic) - Learn about configuration options -- [Routing](/docs/config/routing) - Configure smart routing rules -- [CLI Commands](/docs/cli/start) - Explore all CLI commands +- [Basic Configuration](/docs/cli/config/basic) - Learn about configuration options +- [Routing](/docs/cli/config/routing) - Configure smart routing rules +- [CLI Commands](/docs/category/cli-commands) - Explore all CLI commands diff --git a/docs/docs/server/config/routing.md b/docs/docs/server/config/routing.md index f6b5d48..58ffc66 100644 --- a/docs/docs/server/config/routing.md +++ b/docs/docs/server/config/routing.md @@ -81,6 +81,115 @@ Route image-related tasks: } ``` +## Fallback + +When a request fails, you can configure a list of backup models. The system will try each model in sequence until one succeeds: + +### Basic Configuration + +```json +{ + "Router": { + "default": "deepseek,deepseek-chat", + "background": "ollama,qwen2.5-coder:latest", + "think": "deepseek,deepseek-reasoner", + "longContext": "openrouter,google/gemini-2.5-pro-preview", + "longContextThreshold": 60000, + "webSearch": "gemini,gemini-2.5-flash" + }, + "fallback": { + "default": [ + "aihubmix,Z/glm-4.5", + "openrouter,anthropic/claude-sonnet-4" + ], + "background": [ + "ollama,qwen2.5-coder:latest" + ], + "think": [ + "openrouter,anthropic/claude-3.7-sonnet:thinking" + ], + "longContext": [ + "modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct" + ], + "webSearch": [ + "openrouter,anthropic/claude-sonnet-4" + ] + } +} +``` + +### How It Works + +1. **Trigger**: When a model request fails for a routing scenario (HTTP error response) +2. **Auto-switch**: The system automatically checks the fallback configuration for that scenario +3. **Sequential retry**: Tries each backup model in order +4. **Success**: Once a model responds successfully, returns immediately +5. **All failed**: If all backup models fail, returns the original error + +### Configuration Details + +- **Format**: Each backup model format is `provider,model` +- **Validation**: Backup models must exist in the `Providers` configuration +- **Flexibility**: Different scenarios can have different fallback lists +- **Optional**: If a scenario doesn't need fallback, omit it or use an empty array + +### Use Cases + +#### Scenario 1: Primary Model Quota Exhausted + +```json +{ + "Router": { + "default": "openrouter,anthropic/claude-sonnet-4" + }, + "fallback": { + "default": [ + "deepseek,deepseek-chat", + "aihubmix,Z/glm-4.5" + ] + } +} +``` + +Automatically switches to backup models when the primary model quota is exhausted. + +#### Scenario 2: Service Reliability + +```json +{ + "Router": { + "background": "volcengine,deepseek-v3-250324" + }, + "fallback": { + "background": [ + "modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct", + "dashscope,qwen3-coder-plus" + ] + } +} +``` + +Automatically switches to other providers when the primary service fails. + +### Log Monitoring + +The system logs detailed fallback process: + +``` +[warn] Request failed for default, trying 2 fallback models +[info] Trying fallback model: aihubmix,Z/glm-4.5 +[warn] Fallback model aihubmix,Z/glm-4.5 failed: API rate limit exceeded +[info] Trying fallback model: openrouter,anthropic/claude-sonnet-4 +[info] Fallback model openrouter,anthropic/claude-sonnet-4 succeeded +``` + +### Important Notes + +1. **Cost consideration**: Backup models may incur different costs, configure appropriately +2. **Performance differences**: Different models may have varying response speeds and quality +3. **Quota management**: Ensure backup models have sufficient quotas +4. **Testing**: Regularly test the availability of backup models + ## Project-Level Routing Configure routing per project in `~/.claude/projects//claude-code-router.json`: diff --git a/docs/docs/server/config/transformers.md b/docs/docs/server/config/transformers.md index 63806cf..4a17f0f 100644 --- a/docs/docs/server/config/transformers.md +++ b/docs/docs/server/config/transformers.md @@ -4,7 +4,152 @@ sidebar_position: 4 # Transformers -Transformers adapt API differences between providers. +Transformers are the core mechanism for adapting API differences between LLM providers. They convert requests and responses between different formats, handle authentication, and manage provider-specific features. + +## Understanding Transformers + +### What is a Transformer? + +A transformer is a plugin that: +- **Transforms requests** from the unified format to provider-specific format +- **Transforms responses** from provider format back to unified format +- **Handles authentication** for provider APIs +- **Modifies requests** to add or adjust parameters + +### Data Flow + +``` +┌─────────────────┐ +│ Incoming Request│ (Anthropic format from Claude Code) +└────────┬────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformRequestOut │ ← Parse incoming request to unified format +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ UnifiedChatRequest │ +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformRequestIn (optional) │ ← Modify unified request before sending +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ Provider API Call │ +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformResponseIn (optional) │ ← Convert provider response to unified format +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformResponseOut (optional)│ ← Convert unified response to Anthropic format +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────┐ +│ Outgoing Response│ (Anthropic format to Claude Code) +└─────────────────┘ +``` + +### Transformer Interface + +All transformers implement the following interface: + +```typescript +interface Transformer { + // Convert unified request to provider-specific format + transformRequestIn?: ( + request: UnifiedChatRequest, + provider: LLMProvider, + context: TransformerContext + ) => Promise>; + + // Convert provider request to unified format + transformRequestOut?: ( + request: any, + context: TransformerContext + ) => Promise; + + // Convert provider response to unified format + transformResponseIn?: ( + response: Response, + context?: TransformerContext + ) => Promise; + + // Convert unified response to provider format + transformResponseOut?: ( + response: Response, + context: TransformerContext + ) => Promise; + + // Custom endpoint path (optional) + endPoint?: string; + + // Transformer name (for custom transformers) + name?: string; + + // Custom authentication handler (optional) + auth?: ( + request: any, + provider: LLMProvider, + context: TransformerContext + ) => Promise; + + // Logger instance (auto-injected) + logger?: any; +} +``` + +### Key Types + +#### UnifiedChatRequest + +```typescript +interface UnifiedChatRequest { + messages: UnifiedMessage[]; + model: string; + max_tokens?: number; + temperature?: number; + stream?: boolean; + tools?: UnifiedTool[]; + tool_choice?: any; + reasoning?: { + effort?: ThinkLevel; // "none" | "low" | "medium" | "high" + max_tokens?: number; + enabled?: boolean; + }; +} +``` + +#### UnifiedMessage + +```typescript +interface UnifiedMessage { + role: "user" | "assistant" | "system" | "tool"; + content: string | null | MessageContent[]; + tool_calls?: Array<{ + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; + }>; + tool_call_id?: string; + thinking?: { + content: string; + signature?: string; + }; +} +``` ## Built-in Transformers @@ -23,6 +168,12 @@ Transforms requests to be compatible with Anthropic-style APIs: } ``` +**Features:** +- Converts Anthropic message format to/from OpenAI format +- Handles tool calls and tool results +- Supports thinking/reasoning content blocks +- Manages streaming responses + ### deepseek Specialized transformer for DeepSeek API: @@ -38,6 +189,11 @@ Specialized transformer for DeepSeek API: } ``` +**Features:** +- DeepSeek-specific reasoning format +- Handles `reasoning_content` in responses +- Supports thinking budget tokens + ### gemini Transformer for Google Gemini API: @@ -53,39 +209,381 @@ Transformer for Google Gemini API: } ``` -### groq +### maxtoken -Transformer for Groq API: +Limits max_tokens in requests: ```json { "transformers": [ { - "name": "groq", - "providers": ["groq"] + "name": "maxtoken", + "options": { + "max_tokens": 8192 + }, + "models": ["deepseek,deepseek-chat"] } ] } ``` -### openrouter +### customparams -Transformer for OpenRouter API: +Injects custom parameters into requests: ```json { "transformers": [ { - "name": "openrouter", - "providers": ["openrouter"] + "name": "customparams", + "options": { + "include_reasoning": true, + "custom_header": "value" + } } ] } ``` +## Creating Custom Transformers + +### Simple Transformer: Modifying Requests + +The simplest transformers just modify the request before it's sent to the provider. + +**Example: Add a custom header to all requests** + +```javascript +// custom-header-transformer.js +module.exports = class CustomHeaderTransformer { + name = 'custom-header'; + + constructor(options) { + this.headerName = options?.headerName || 'X-Custom-Header'; + this.headerValue = options?.headerValue || 'default-value'; + } + + async transformRequestIn(request, provider, context) { + // Add custom header (will be used by auth method) + request._customHeaders = { + [this.headerName]: this.headerValue + }; + return request; + } + + async auth(request, provider) { + const headers = { + 'authorization': `Bearer ${provider.apiKey}`, + ...request._customHeaders + }; + return { + body: request, + config: { headers } + }; + } +}; +``` + +**Usage in config:** + +```json +{ + "transformers": [ + { + "name": "custom-header", + "path": "/path/to/custom-header-transformer.js", + "options": { + "headerName": "X-My-Header", + "headerValue": "my-value" + } + } + ] +} +``` + +### Intermediate Transformer: Request/Response Conversion + +This example shows how to convert between different API formats. + +**Example: Mock API format transformer** + +```javascript +// mockapi-transformer.js +module.exports = class MockAPITransformer { + name = 'mockapi'; + endPoint = '/v1/chat'; // Custom endpoint + + // Convert from MockAPI format to unified format + async transformRequestOut(request, context) { + const messages = request.conversation.map(msg => ({ + role: msg.sender, + content: msg.text + })); + + return { + messages, + model: request.model_id, + max_tokens: request.max_tokens, + temperature: request.temp + }; + } + + // Convert from unified format to MockAPI format + async transformRequestIn(request, provider, context) { + return { + model_id: request.model, + conversation: request.messages.map(msg => ({ + sender: msg.role, + text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + })), + max_tokens: request.max_tokens || 4096, + temp: request.temperature || 0.7 + }; + } + + // Convert MockAPI response to unified format + async transformResponseIn(response, context) { + const data = await response.json(); + + const unifiedResponse = { + id: data.request_id, + object: 'chat.completion', + created: data.timestamp, + model: data.model, + choices: [{ + index: 0, + message: { + role: 'assistant', + content: data.reply.text + }, + finish_reason: data.stop_reason + }], + usage: { + prompt_tokens: data.tokens.input, + completion_tokens: data.tokens.output, + total_tokens: data.tokens.input + data.tokens.output + } + }; + + return new Response(JSON.stringify(unifiedResponse), { + status: response.status, + statusText: response.statusText, + headers: { 'Content-Type': 'application/json' } + }); + } +}; +``` + +### Advanced Transformer: Streaming Response Processing + +This example shows how to handle streaming responses. + +**Example: Add custom metadata to streaming responses** + +```javascript +// streaming-metadata-transformer.js +module.exports = class StreamingMetadataTransformer { + name = 'streaming-metadata'; + + constructor(options) { + this.metadata = options?.metadata || {}; + this.logger = null; // Will be injected by the system + } + + async transformResponseOut(response, context) { + const contentType = response.headers.get('Content-Type'); + + // Handle streaming response + if (contentType?.includes('text/event-stream')) { + return this.transformStream(response, context); + } + + // Handle non-streaming response + return response; + } + + async transformStream(response, context) { + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + + const transformedStream = new ReadableStream({ + start: async (controller) => { + const reader = response.body.getReader(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim() || !line.startsWith('data: ')) { + controller.enqueue(encoder.encode(line + '\n')); + continue; + } + + const data = line.slice(6).trim(); + if (data === '[DONE]') { + controller.enqueue(encoder.encode(line + '\n')); + continue; + } + + try { + const chunk = JSON.parse(data); + + // Add custom metadata + if (chunk.choices && chunk.choices[0]) { + chunk.choices[0].metadata = this.metadata; + } + + // Log for debugging + this.logger?.debug({ + chunk, + context: context.req.id + }, 'Transformed streaming chunk'); + + const modifiedLine = `data: ${JSON.stringify(chunk)}\n\n`; + controller.enqueue(encoder.encode(modifiedLine)); + } catch (parseError) { + // If parsing fails, pass through original line + controller.enqueue(encoder.encode(line + '\n')); + } + } + } + } catch (error) { + this.logger?.error({ error }, 'Stream transformation error'); + controller.error(error); + } finally { + controller.close(); + reader.releaseLock(); + } + } + }); + + return new Response(transformedStream, { + status: response.status, + statusText: response.statusText, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + } + }); + } +}; +``` + +### Real-World Example: Reasoning Content Transformer + +This is based on the actual `reasoning.transformer.ts` from the codebase. + +```typescript +// reasoning-transformer.ts +import { Transformer, TransformerOptions } from "@musistudio/llms"; + +export class ReasoningTransformer implements Transformer { + static TransformerName = "reasoning"; + enable: boolean; + + constructor(private readonly options?: TransformerOptions) { + this.enable = this.options?.enable ?? true; + } + + // Transform request to add reasoning parameters + async transformRequestIn(request: UnifiedChatRequest): Promise { + if (!this.enable) { + request.thinking = { + type: "disabled", + budget_tokens: -1, + }; + request.enable_thinking = false; + return request; + } + + if (request.reasoning) { + request.thinking = { + type: "enabled", + budget_tokens: request.reasoning.max_tokens, + }; + request.enable_thinking = true; + } + return request; + } + + // Transform response to convert reasoning_content to thinking format + async transformResponseOut(response: Response): Promise { + if (!this.enable) return response; + + const contentType = response.headers.get("Content-Type"); + + // Handle non-streaming response + if (contentType?.includes("application/json")) { + const jsonResponse = await response.json(); + if (jsonResponse.choices[0]?.message.reasoning_content) { + jsonResponse.thinking = { + content: jsonResponse.choices[0].message.reasoning_content + }; + } + return new Response(JSON.stringify(jsonResponse), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + + // Handle streaming response + if (contentType?.includes("stream")) { + // [Streaming transformation code here] + // See the full implementation in the codebase + } + + return response; + } +} +``` + +## Transformer Registration + +### Method 1: Static Name (Class-based) + +Use this when creating a transformer in TypeScript/ES6: + +```typescript +export class MyTransformer implements Transformer { + static TransformerName = "my-transformer"; + + async transformRequestIn(request: UnifiedChatRequest): Promise { + // Transformation logic + return request; + } +} +``` + +### Method 2: Instance Name (Instance-based) + +Use this for JavaScript transformers: + +```javascript +module.exports = class MyTransformer { + constructor(options) { + this.name = 'my-transformer'; + this.options = options; + } + + async transformRequestIn(request, provider, context) { + // Transformation logic + return request; + } +}; +``` + ## Applying Transformers -### Global Application +### Global Application (Provider Level) Apply to all requests for a provider: @@ -104,7 +602,7 @@ Apply to all requests for a provider: ### Model-Specific Application -Apply to specific models: +Apply to specific models only: ```json { @@ -120,9 +618,26 @@ Apply to specific models: } ``` +Note: The model format is `provider,model` (e.g., `deepseek,deepseek-chat`). + +### Global Transformers (All Providers) + +Apply transformers to all providers: + +```json +{ + "transformers": [ + { + "name": "custom-logger", + "path": "/path/to/custom-logger.js" + } + ] +} +``` + ### Passing Options -Some transformers accept options: +Some transformers accept configuration options: ```json { @@ -132,45 +647,144 @@ Some transformers accept options: "options": { "max_tokens": 8192 } + }, + { + "name": "customparams", + "options": { + "custom_param_1": "value1", + "custom_param_2": 42 + } } ] } ``` -## Custom Transformers +## Best Practices -Create custom transformer plugins: +### 1. Immutability -1. Create a transformer file: +Always create new objects rather than mutating existing ones: ```javascript -module.exports = { - name: 'my-transformer', - transformRequest: async (req, config) => { - // Modify request - return req; - }, - transformResponse: async (res, config) => { - // Modify response - return res; - } -}; -``` +// Bad +async transformRequestIn(request) { + request.max_tokens = 4096; + return request; +} -2. Load in configuration: - -```json -{ - "transformers": [ - { - "name": "my-transformer", - "path": "/path/to/transformer.js" - } - ] +// Good +async transformRequestIn(request) { + return { + ...request, + max_tokens: request.max_tokens || 4096 + }; } ``` +### 2. Error Handling + +Always handle errors gracefully: + +```javascript +async transformResponseIn(response) { + try { + const data = await response.json(); + // Process data + return new Response(JSON.stringify(processedData), { + status: response.status, + headers: response.headers + }); + } catch (error) { + this.logger?.error({ error }, 'Transformation failed'); + // Return original response if transformation fails + return response; + } +} +``` + +### 3. Logging + +Use the injected logger for debugging: + +```javascript +async transformRequestIn(request, provider, context) { + this.logger?.debug({ + model: request.model, + provider: provider.name + }, 'Transforming request'); + + // Your transformation logic + + return modifiedRequest; +} +``` + +### 4. Stream Handling + +When handling streams, always: +- Use a buffer to handle incomplete chunks +- Properly release the reader lock +- Handle errors in the stream +- Close the controller when done + +```javascript +const transformedStream = new ReadableStream({ + start: async (controller) => { + const reader = response.body.getReader(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // Process stream... + } + } catch (error) { + controller.error(error); + } finally { + controller.close(); + reader.releaseLock(); + } + } +}); +``` + +### 5. Context Usage + +The `context` parameter contains useful information: + +```javascript +async transformRequestIn(request, provider, context) { + // Access request ID + const requestId = context.req.id; + + // Access original request + const originalRequest = context.req.original; + + // Your transformation logic +} +``` + +## Testing Your Transformer + +### Manual Testing + +1. Add your transformer to the config +2. Start the server: `ccr restart` +3. Check logs: `tail -f ~/.claude-code-router/logs/ccr-*.log` +4. Make a test request +5. Verify the output + +### Debug Tips + +- Add logging to track transformation steps +- Test with both streaming and non-streaming requests +- Verify error handling with invalid inputs +- Check that original responses are returned on error + ## Next Steps -- [Advanced Topics](/docs/advanced/custom-router) - Advanced routing customization -- [Agents](/docs/advanced/agents) - Extending with agents +- [Advanced Topics](/docs/server/advanced/custom-router) - Advanced routing customization +- [Agents](/docs/server/advanced/agents) - Extending with agents +- [Core Package](/docs/server/intro) - Learn about @musistudio/llms diff --git a/docs/docs/server/intro.md b/docs/docs/server/intro.md index f1ab94e..7518d91 100644 --- a/docs/docs/server/intro.md +++ b/docs/docs/server/intro.md @@ -15,17 +15,103 @@ Claude Code Router Server is a core service component responsible for routing Cl ## Architecture Overview ``` -┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ -│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │ -│ Client │ │ (Router + │ │ (OpenAI/ │ -└─────────────┘ │ Transformer) │ │ Gemini/etc)│ - └──────────────────┘ └──────────────┘ +┌─────────────┐ ┌─────────────────────────────┐ ┌──────────────┐ +│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │ +│ Client │ │ ┌─────────────────────┐ │ │ (OpenAI/ │ +└─────────────┘ │ │ @musistudio/llms │ │ │ Gemini/etc)│ + │ │ (Core Package) │ │ └──────────────┘ + │ │ - Request Transform │ │ + │ │ - Response Transform │ │ + │ │ - Auth Handling │ │ + │ └─────────────────────┘ │ + │ │ + │ - Routing Logic │ + │ - Agent System │ + │ - Configuration │ + └─────────────────────────────┘ │ ├─ Web UI ├─ Config API └─ Logs API ``` +## Core Package: @musistudio/llms + +The server is built on top of **@musistudio/llms**, a universal LLM API transformation library that provides the core request/response transformation capabilities. + +### What is @musistudio/llms? + +`@musistudio/llms` is a standalone npm package (`@musistudio/llms`) that handles: + +- **API Format Conversion**: Transforms between different LLM provider APIs (Anthropic, OpenAI, Gemini, etc.) +- **Request/Response Transformation**: Converts requests and responses to a unified format +- **Authentication Handling**: Manages different authentication methods across providers +- **Streaming Support**: Handles streaming responses from different providers +- **Transformer System**: Provides an extensible architecture for adding new providers + +### Key Concepts + +#### 1. Unified Request/Response Format + +The core package defines a unified format (`UnifiedChatRequest`, `UnifiedChatResponse`) that abstracts away provider-specific differences: + +```typescript +interface UnifiedChatRequest { + messages: UnifiedMessage[]; + model: string; + max_tokens?: number; + temperature?: number; + stream?: boolean; + tools?: UnifiedTool[]; + tool_choice?: any; + reasoning?: { + effort?: ThinkLevel; + max_tokens?: number; + enabled?: boolean; + }; +} +``` + +#### 2. Transformer Interface + +All transformers implement a common interface: + +```typescript +interface Transformer { + transformRequestIn?: (request: UnifiedChatRequest, provider: LLMProvider, context: TransformerContext) => Promise; + transformRequestOut?: (request: any, context: TransformerContext) => Promise; + transformResponseIn?: (response: Response, context?: TransformerContext) => Promise; + transformResponseOut?: (response: Response, context: TransformerContext) => Promise; + endPoint?: string; + name?: string; + auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise; +} +``` + +#### 3. Built-in Transformers + +The core package includes transformers for: +- **anthropic**: Anthropic API format +- **openai**: OpenAI API format +- **gemini**: Google Gemini API format +- **deepseek**: DeepSeek API format +- **groq**: Groq API format +- **openrouter**: OpenRouter API format +- And more... + +### Integration with CCR Server + +The CCR server integrates `@musistudio/llms` through: + +1. **Transformer Service** (`packages/core/src/services/transformer.ts`): Manages transformer registration and instantiation +2. **Provider Configuration**: Maps provider configs to core package's LLMProvider interface +3. **Request Pipeline**: Applies transformers in sequence during request processing +4. **Custom Transformers**: Supports loading external transformer plugins + +### Version and Updates + +The current version of `@musistudio/llms` is `1.0.51`. It's published as an independent npm package and can be used standalone or as part of CCR Server. + ## Core Features ### 1. Request Routing diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 06bef0d..af177fc 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -81,35 +81,7 @@ const config: Config = { }, footer: { style: 'light', - links: [ - { - title: 'Docs', - items: [ - { - label: 'Tutorial', - to: '/docs/intro', - }, - ], - }, - { - title: 'Community', - items: [ - { - label: 'GitHub', - href: 'https://github.com/musistudio/claude-code-router', - }, - ], - }, - { - title: 'More', - items: [ - { - label: 'Blog', - to: '/blog', - }, - ], - }, - ], + links: [], copyright: `Copyright © ${new Date().getFullYear()} Claude Code Router. Built with Docusaurus.`, }, prism: { diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json index 3bebc6f..fd4a06d 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json @@ -27,15 +27,15 @@ "message": "服务器 API 接口文档", "description": "The generated-index page description for category 'API Reference' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.Configuration": { + "sidebar.tutorialSidebar.category.server-configuration-category": { "message": "配置", "description": "The label for category 'Configuration' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.Configuration.link.generated-index.title": { + "sidebar.tutorialSidebar.category.server-configuration-category.link.generated-index.title": { "message": "服务器配置", "description": "The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'" }, - "sidebar.tutorialSidebar.category.Configuration.link.generated-index.description": { + "sidebar.tutorialSidebar.category.server-configuration-category.link.generated-index.description": { "message": "服务器配置说明", "description": "The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'" }, @@ -74,5 +74,17 @@ "sidebar.tutorialSidebar.category.Commands.link.generated-index.description": { "message": "完整的命令参考", "description": "The generated-index page description for category 'Commands' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.cli-configuration-category": { + "message": "配置", + "description": "The label for category 'Configuration' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.cli-configuration-category.link.generated-index.title": { + "message": "CLI 配置", + "description": "The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'" + }, + "sidebar.tutorialSidebar.category.cli-configuration-category.link.generated-index.description": { + "message": "CLI 配置指南", + "description": "The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'" } } diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/routing.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/routing.md index e5280ed..31326df 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/routing.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/routing.md @@ -82,6 +82,115 @@ sidebar_position: 3 } ``` +## 故障转移(Fallback) + +当请求失败时,可以配置备用模型列表。系统会按顺序尝试每个模型,直到请求成功: + +### 基本配置 + +```json +{ + "Router": { + "default": "deepseek,deepseek-chat", + "background": "ollama,qwen2.5-coder:latest", + "think": "deepseek,deepseek-reasoner", + "longContext": "openrouter,google/gemini-2.5-pro-preview", + "longContextThreshold": 60000, + "webSearch": "gemini,gemini-2.5-flash" + }, + "fallback": { + "default": [ + "aihubmix,Z/glm-4.5", + "openrouter,anthropic/claude-sonnet-4" + ], + "background": [ + "ollama,qwen2.5-coder:latest" + ], + "think": [ + "openrouter,anthropic/claude-3.7-sonnet:thinking" + ], + "longContext": [ + "modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct" + ], + "webSearch": [ + "openrouter,anthropic/claude-sonnet-4" + ] + } +} +``` + +### 工作原理 + +1. **触发条件**:当某个路由场景的模型请求失败时(HTTP 错误响应) +2. **自动切换**:系统自动检查该场景的 fallback 配置 +3. **顺序尝试**:按照列表顺序依次尝试每个备用模型 +4. **成功返回**:一旦某个模型成功响应,立即返回结果 +5. **全部失败**:如果所有备用模型都失败,返回原始错误 + +### 配置说明 + +- **格式**:每个备用模型格式为 `provider,model` +- **验证**:备用模型必须在 `Providers` 配置中存在 +- **灵活性**:可以为不同场景配置不同的备用列表 +- **可选性**:如果某个场景不需要备用,可以不配置或使用空数组 + +### 使用场景 + +#### 场景一:主模型配额不足 + +```json +{ + "Router": { + "default": "openrouter,anthropic/claude-sonnet-4" + }, + "fallback": { + "default": [ + "deepseek,deepseek-chat", + "aihubmix,Z/glm-4.5" + ] + } +} +``` + +当主模型配额用完时,自动切换到备用模型。 + +#### 场景二:服务稳定性保障 + +```json +{ + "Router": { + "background": "volcengine,deepseek-v3-250324" + }, + "fallback": { + "background": [ + "modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct", + "dashscope,qwen3-coder-plus" + ] + } +} +``` + +当主服务商出现故障时,自动切换到其他服务商。 + +### 日志监控 + +系统会记录详细的 fallback 过程: + +``` +[warn] Request failed for default, trying 2 fallback models +[info] Trying fallback model: aihubmix,Z/glm-4.5 +[warn] Fallback model aihubmix,Z/glm-4.5 failed: API rate limit exceeded +[info] Trying fallback model: openrouter,anthropic/claude-sonnet-4 +[info] Fallback model openrouter,anthropic/claude-sonnet-4 succeeded +``` + +### 注意事项 + +1. **成本考虑**:备用模型可能产生不同的费用,请合理配置 +2. **性能差异**:不同模型的响应速度和质量可能有差异 +3. **配额管理**:确保备用模型有足够的配额 +4. **测试验证**:定期测试备用模型的可用性 + ## 项目级路由 在 `~/.claude/projects//claude-code-router.json` 中为每个项目配置路由: diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/transformers.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/transformers.md index 0f08fc1..c861eb4 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/transformers.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/transformers.md @@ -5,7 +5,152 @@ sidebar_position: 4 # 转换器 -转换器用于适配不同提供商之间的 API 差异。 +转换器是适配不同 LLM 提供商 API 差异的核心机制。它们在不同格式之间转换请求和响应,处理认证,并管理提供商特定的功能。 + +## 理解转换器 + +### 什么是转换器? + +转换器是一个插件,它可以: +- **转换请求**:从统一格式转换为提供商特定格式 +- **转换响应**:从提供商格式转换回统一格式 +- **处理认证**:为提供商 API 处理认证 +- **修改请求**:添加或调整参数 + +### 数据流 + +``` +┌─────────────────┐ +│ 传入请求 │ (来自 Claude Code 的 Anthropic 格式) +└────────┬────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformRequestOut │ ← 将传入请求解析为统一格式 +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ UnifiedChatRequest │ +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformRequestIn (可选) │ ← 在发送前修改统一请求 +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ 提供商 API 调用 │ +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformResponseIn (可选) │ ← 将提供商响应转换为统一格式 +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ transformResponseOut (可选) │ ← 将统一响应转换为 Anthropic 格式 +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────┐ +│ 传出响应 │ (返回给 Claude Code 的 Anthropic 格式) +└─────────────────┘ +``` + +### 转换器接口 + +所有转换器都实现以下接口: + +```typescript +interface Transformer { + // 将统一请求转换为提供商特定格式 + transformRequestIn?: ( + request: UnifiedChatRequest, + provider: LLMProvider, + context: TransformerContext + ) => Promise>; + + // 将提供商请求转换为统一格式 + transformRequestOut?: ( + request: any, + context: TransformerContext + ) => Promise; + + // 将提供商响应转换为统一格式 + transformResponseIn?: ( + response: Response, + context?: TransformerContext + ) => Promise; + + // 将统一响应转换为提供商格式 + transformResponseOut?: ( + response: Response, + context: TransformerContext + ) => Promise; + + // 自定义端点路径(可选) + endPoint?: string; + + // 转换器名称(用于自定义转换器) + name?: string; + + // 自定义认证处理器(可选) + auth?: ( + request: any, + provider: LLMProvider, + context: TransformerContext + ) => Promise; + + // Logger 实例(自动注入) + logger?: any; +} +``` + +### 关键类型 + +#### UnifiedChatRequest + +```typescript +interface UnifiedChatRequest { + messages: UnifiedMessage[]; + model: string; + max_tokens?: number; + temperature?: number; + stream?: boolean; + tools?: UnifiedTool[]; + tool_choice?: any; + reasoning?: { + effort?: ThinkLevel; // "none" | "low" | "medium" | "high" + max_tokens?: number; + enabled?: boolean; + }; +} +``` + +#### UnifiedMessage + +```typescript +interface UnifiedMessage { + role: "user" | "assistant" | "system" | "tool"; + content: string | null | MessageContent[]; + tool_calls?: Array<{ + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; + }>; + tool_call_id?: string; + thinking?: { + content: string; + signature?: string; + }; +} +``` ## 内置转换器 @@ -15,13 +160,20 @@ sidebar_position: 4 ```json { - "transformer": { - "use": ["anthropic"] - } + "transformers": [ + { + "name": "anthropic", + "providers": ["deepseek", "groq"] + } + ] } ``` -如果只使用这一个转换器,它将直接透传请求和响应(您可以用来接入其他支持 Anthropic 端点的服务商)。 +**功能:** +- 在 Anthropic 消息格式和 OpenAI 格式之间转换 +- 处理工具调用和工具结果 +- 支持思考/推理内容块 +- 管理流式响应 ### deepseek @@ -29,169 +181,421 @@ sidebar_position: 4 ```json { - "transformer": { - "use": ["deepseek"] - } + "transformers": [ + { + "name": "deepseek", + "providers": ["deepseek"] + } + ] } ``` +**功能:** +- DeepSeek 特定的推理格式 +- 处理响应中的 `reasoning_content` +- 支持思考预算令牌 + ### gemini 用于 Google Gemini API 的转换器: ```json { - "transformer": { - "use": ["gemini"] - } -} -``` - -### groq - -用于 Groq API 的转换器: - -```json -{ - "transformer": { - "use": ["groq"] - } -} -``` - -### openrouter - -用于 OpenRouter API 的转换器: - -```json -{ - "transformer": { - "use": ["openrouter"] - } -} -``` - -OpenRouter 转换器还支持 `provider` 路由参数,以指定 OpenRouter 应使用哪些底层提供商: - -```json -{ - "transformer": { - "use": ["openrouter"], - "moonshotai/kimi-k2": { - "use": [ - ["openrouter", { - "provider": { - "only": ["moonshotai/fp8"] - } - }] - ] + "transformers": [ + { + "name": "gemini", + "providers": ["gemini"] } - } + ] } ``` ### maxtoken -设置特定的 `max_tokens` 值: +限制请求中的 max_tokens: ```json { - "transformer": { - "use": [ - ["maxtoken", { "max_tokens": 65536 }] - ] + "transformers": [ + { + "name": "maxtoken", + "options": { + "max_tokens": 8192 + }, + "models": ["deepseek,deepseek-chat"] + } + ] +} +``` + +### customparams + +向请求中注入自定义参数: + +```json +{ + "transformers": [ + { + "name": "customparams", + "options": { + "include_reasoning": true, + "custom_header": "value" + } + } + ] +} +``` + +## 创建自定义转换器 + +### 简单转换器:修改请求 + +最简单的转换器只修改发送到提供商之前的请求。 + +**示例:为所有请求添加自定义头** + +```javascript +// custom-header-transformer.js +module.exports = class CustomHeaderTransformer { + name = 'custom-header'; + + constructor(options) { + this.headerName = options?.headerName || 'X-Custom-Header'; + this.headerValue = options?.headerValue || 'default-value'; + } + + async transformRequestIn(request, provider, context) { + // 添加自定义头(将被 auth 方法使用) + request._customHeaders = { + [this.headerName]: this.headerValue + }; + return request; + } + + async auth(request, provider) { + const headers = { + 'authorization': `Bearer ${provider.apiKey}`, + ...request._customHeaders + }; + return { + body: request, + config: { headers } + }; + } +}; +``` + +**在配置中使用:** + +```json +{ + "transformers": [ + { + "name": "custom-header", + "path": "/path/to/custom-header-transformer.js", + "options": { + "headerName": "X-My-Header", + "headerValue": "my-value" + } + } + ] +} +``` + +### 中级转换器:请求/响应转换 + +此示例展示如何在不同 API 格式之间转换。 + +**示例:Mock API 格式转换器** + +```javascript +// mockapi-transformer.js +module.exports = class MockAPITransformer { + name = 'mockapi'; + endPoint = '/v1/chat'; // 自定义端点 + + // 从 MockAPI 格式转换为统一格式 + async transformRequestOut(request, context) { + const messages = request.conversation.map(msg => ({ + role: msg.sender, + content: msg.text + })); + + return { + messages, + model: request.model_id, + max_tokens: request.max_tokens, + temperature: request.temp + }; + } + + // 从统一格式转换为 MockAPI 格式 + async transformRequestIn(request, provider, context) { + return { + model_id: request.model, + conversation: request.messages.map(msg => ({ + sender: msg.role, + text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + })), + max_tokens: request.max_tokens || 4096, + temp: request.temperature || 0.7 + }; + } + + // 将 MockAPI 响应转换为统一格式 + async transformResponseIn(response, context) { + const data = await response.json(); + + const unifiedResponse = { + id: data.request_id, + object: 'chat.completion', + created: data.timestamp, + model: data.model, + choices: [{ + index: 0, + message: { + role: 'assistant', + content: data.reply.text + }, + finish_reason: data.stop_reason + }], + usage: { + prompt_tokens: data.tokens.input, + completion_tokens: data.tokens.output, + total_tokens: data.tokens.input + data.tokens.output + } + }; + + return new Response(JSON.stringify(unifiedResponse), { + status: response.status, + statusText: response.statusText, + headers: { 'Content-Type': 'application/json' } + }); + } +}; +``` + +### 高级转换器:流式响应处理 + +此示例展示如何处理流式响应。 + +**示例:向流式响应添加自定义元数据** + +```javascript +// streaming-metadata-transformer.js +module.exports = class StreamingMetadataTransformer { + name = 'streaming-metadata'; + + constructor(options) { + this.metadata = options?.metadata || {}; + this.logger = null; // 将由系统注入 + } + + async transformResponseOut(response, context) { + const contentType = response.headers.get('Content-Type'); + + // 处理流式响应 + if (contentType?.includes('text/event-stream')) { + return this.transformStream(response, context); + } + + // 处理非流式响应 + return response; + } + + async transformStream(response, context) { + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + + const transformedStream = new ReadableStream({ + start: async (controller) => { + const reader = response.body.getReader(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim() || !line.startsWith('data: ')) { + controller.enqueue(encoder.encode(line + '\n')); + continue; + } + + const data = line.slice(6).trim(); + if (data === '[DONE]') { + controller.enqueue(encoder.encode(line + '\n')); + continue; + } + + try { + const chunk = JSON.parse(data); + + // 添加自定义元数据 + if (chunk.choices && chunk.choices[0]) { + chunk.choices[0].metadata = this.metadata; + } + + // 记录日志以便调试 + this.logger?.debug({ + chunk, + context: context.req.id + }, '转换流式数据块'); + + const modifiedLine = `data: ${JSON.stringify(chunk)}\n\n`; + controller.enqueue(encoder.encode(modifiedLine)); + } catch (parseError) { + // 如果解析失败,透传原始行 + controller.enqueue(encoder.encode(line + '\n')); + } + } + } + } catch (error) { + this.logger?.error({ error }, '流式转换错误'); + controller.error(error); + } finally { + controller.close(); + reader.releaseLock(); + } + } + }); + + return new Response(transformedStream, { + status: response.status, + statusText: response.statusText, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + } + }); + } +}; +``` + +### 真实示例:推理内容转换器 + +这是基于代码库中实际的 `reasoning.transformer.ts`。 + +```typescript +// reasoning-transformer.ts +import { Transformer, TransformerOptions } from "@musistudio/llms"; + +export class ReasoningTransformer implements Transformer { + static TransformerName = "reasoning"; + enable: boolean; + + constructor(private readonly options?: TransformerOptions) { + this.enable = this.options?.enable ?? true; + } + + // 转换请求以添加推理参数 + async transformRequestIn(request: UnifiedChatRequest): Promise { + if (!this.enable) { + request.thinking = { + type: "disabled", + budget_tokens: -1, + }; + request.enable_thinking = false; + return request; + } + + if (request.reasoning) { + request.thinking = { + type: "enabled", + budget_tokens: request.reasoning.max_tokens, + }; + request.enable_thinking = true; + } + return request; + } + + // 转换响应以将 reasoning_content 转换为 thinking 格式 + async transformResponseOut(response: Response): Promise { + if (!this.enable) return response; + + const contentType = response.headers.get("Content-Type"); + + // 处理非流式响应 + if (contentType?.includes("application/json")) { + const jsonResponse = await response.json(); + if (jsonResponse.choices[0]?.message.reasoning_content) { + jsonResponse.thinking = { + content: jsonResponse.choices[0].message.reasoning_content + }; + } + return new Response(JSON.stringify(jsonResponse), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + + // 处理流式响应 + if (contentType?.includes("stream")) { + // [流式转换代码在这里] + // 参见代码库中的完整实现 + } + + return response; } } ``` -### tooluse +## 转换器注册 -通过 `tool_choice` 参数优化某些模型的工具使用: +### 方法 1:静态名称(基于类) -```json -{ - "transformer": { - "use": ["tooluse"] +在 TypeScript/ES6 中创建转换器时使用: + +```typescript +export class MyTransformer implements Transformer { + static TransformerName = "my-transformer"; + + async transformRequestIn(request: UnifiedChatRequest): Promise { + // 转换逻辑 + return request; } } ``` -### reasoning +### 方法 2:实例名称(基于实例) -用于处理 `reasoning_content` 字段: +用于 JavaScript 转换器: -```json -{ - "transformer": { - "use": ["reasoning"] +```javascript +module.exports = class MyTransformer { + constructor(options) { + this.name = 'my-transformer'; + this.options = options; } -} -``` -### sampling - -用于处理采样信息字段,如 `temperature`、`top_p`、`top_k` 和 `repetition_penalty`: - -```json -{ - "transformer": { - "use": ["sampling"] + async transformRequestIn(request, provider, context) { + // 转换逻辑 + return request; } -} -``` - -### enhancetool - -对 LLM 返回的工具调用参数增加一层容错处理(注意:这会导致不再流式返回工具调用信息): - -```json -{ - "transformer": { - "use": ["enhancetool"] - } -} -``` - -### cleancache - -清除请求中的 `cache_control` 字段: - -```json -{ - "transformer": { - "use": ["cleancache"] - } -} -``` - -### vertex-gemini - -处理使用 Vertex 鉴权的 Gemini API: - -```json -{ - "transformer": { - "use": ["vertex-gemini"] - } -} +}; ``` ## 应用转换器 -### 全局应用 +### 全局应用(提供商级别) -应用于提供商的所有请求: +为提供商的所有请求应用: ```json { "Providers": [ { - "name": "deepseek", - "api_base_url": "https://api.deepseek.com/chat/completions", - "api_key": "your-api-key", - "transformer": { - "use": ["deepseek"] - } + "NAME": "deepseek", + "HOST": "https://api.deepseek.com", + "APIKEY": "your-api-key", + "transformers": ["anthropic"] } ] } @@ -199,84 +603,189 @@ OpenRouter 转换器还支持 `provider` 路由参数,以指定 OpenRouter 应 ### 模型特定应用 -应用于特定模型: - -```json -{ - "name": "deepseek", - "transformer": { - "use": ["deepseek"], - "deepseek-chat": { - "use": ["tooluse"] - } - } -} -``` - -### 传递选项 - -某些转换器接受选项: - -```json -{ - "transformer": { - "use": [ - ["maxtoken", { "max_tokens": 8192 }] - ] - } -} -``` - -## 自定义转换器 - -创建自定义转换器插件: - -1. 创建转换器文件: - -```javascript -module.exports = { - name: 'my-transformer', - transformRequest: async (req, config) => { - // 修改请求 - return req; - }, - transformResponse: async (res, config) => { - // 修改响应 - return res; - } -}; -``` - -2. 在配置中加载: +仅应用于特定模型: ```json { "transformers": [ { - "path": "/path/to/transformer.js", + "name": "maxtoken", "options": { - "key": "value" + "max_tokens": 8192 + }, + "models": ["deepseek,deepseek-chat"] + } + ] +} +``` + +注意:模型格式为 `provider,model`(例如 `deepseek,deepseek-chat`)。 + +### 全局转换器(所有提供商) + +将转换器应用于所有提供商: + +```json +{ + "transformers": [ + { + "name": "custom-logger", + "path": "/path/to/custom-logger.js" + } + ] +} +``` + +### 传递选项 + +某些转换器接受配置选项: + +```json +{ + "transformers": [ + { + "name": "maxtoken", + "options": { + "max_tokens": 8192 + } + }, + { + "name": "customparams", + "options": { + "custom_param_1": "value1", + "custom_param_2": 42 } } ] } ``` -## 实验性转换器 +## 最佳实践 -### gemini-cli(实验性) +### 1. 不可变性 -通过 Gemini CLI 对 Gemini 的非官方支持。 +始终创建新对象而不是修改现有对象: -### qwen-cli(实验性) +```javascript +// 不好的做法 +async transformRequestIn(request) { + request.max_tokens = 4096; + return request; +} -通过 Qwen CLI 对 qwen3-coder-plus 的非官方支持。 +// 好的做法 +async transformRequestIn(request) { + return { + ...request, + max_tokens: request.max_tokens || 4096 + }; +} +``` -### rovo-cli(实验性) +### 2. 错误处理 -通过 Atlassian Rovo Dev CLI 对 GPT-5 的非官方支持。 +始终优雅地处理错误: + +```javascript +async transformResponseIn(response) { + try { + const data = await response.json(); + // 处理数据 + return new Response(JSON.stringify(processedData), { + status: response.status, + headers: response.headers + }); + } catch (error) { + this.logger?.error({ error }, '转换失败'); + // 如果转换失败,返回原始响应 + return response; + } +} +``` + +### 3. 日志记录 + +使用注入的 logger 进行调试: + +```javascript +async transformRequestIn(request, provider, context) { + this.logger?.debug({ + model: request.model, + provider: provider.name + }, '转换请求'); + + // 转换逻辑 + + return modifiedRequest; +} +``` + +### 4. 流处理 + +处理流式响应时,始终: +- 使用缓冲区处理不完整的数据块 +- 正确释放 reader 锁 +- 处理流中的错误 +- 完成时关闭 controller + +```javascript +const transformedStream = new ReadableStream({ + start: async (controller) => { + const reader = response.body.getReader(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // 处理流... + } + } catch (error) { + controller.error(error); + } finally { + controller.close(); + reader.releaseLock(); + } + } +}); +``` + +### 5. 上下文使用 + +`context` 参数包含有用信息: + +```javascript +async transformRequestIn(request, provider, context) { + // 访问请求 ID + const requestId = context.req.id; + + // 访问原始请求 + const originalRequest = context.req.original; + + // 转换逻辑 +} +``` + +## 测试转换器 + +### 手动测试 + +1. 将转换器添加到配置 +2. 启动服务器:`ccr restart` +3. 检查日志:`tail -f ~/.claude-code-router/logs/ccr-*.log` +4. 发出测试请求 +5. 验证输出 + +### 调试技巧 + +- 添加日志记录以跟踪转换步骤 +- 使用流式和非流式请求进行测试 +- 使用无效输入验证错误处理 +- 检查错误时是否返回原始响应 ## 下一步 -- [高级主题](/zh/docs/advanced/custom-router) - 高级路由自定义 -- [Agent](/zh/docs/advanced/agents) - 使用 Agent 扩展功能 +- [高级主题](/docs/server/advanced/custom-router) - 高级路由自定义 +- [Agents](/docs/server/advanced/agents) - 使用 agents 扩展 +- [核心包](/docs/server/intro) - 了解 @musistudio/llms diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/intro.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/intro.md index ddb3ff3..438c4fa 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/intro.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/intro.md @@ -11,17 +11,103 @@ Claude Code Router Server 是一个核心服务组件,负责将 Claude Code ## 架构概述 ``` -┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ -│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │ -│ Client │ │ (Router + │ │ (OpenAI/ │ -└─────────────┘ │ Transformer) │ │ Gemini/etc)│ - └──────────────────┘ └──────────────┘ +┌─────────────┐ ┌─────────────────────────────┐ ┌──────────────┐ +│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │ +│ Client │ │ ┌─────────────────────┐ │ │ (OpenAI/ │ +└─────────────┘ │ │ @musistudio/llms │ │ │ Gemini/etc)│ + │ │ (核心包) │ │ └──────────────┘ + │ │ - 请求转换 │ │ + │ │ - 响应转换 │ │ + │ │ - 认证处理 │ │ + │ └─────────────────────┘ │ + │ │ + │ - 路由逻辑 │ + │ - Agent 系统 │ + │ - 配置管理 │ + └─────────────────────────────┘ │ ├─ Web UI ├─ Config API └─ Logs API ``` +## 核心包:@musistudio/llms + +服务器构建于 **@musistudio/llms** 之上,这是一个通用的 LLM API 转换库,提供了核心的请求/响应转换能力。 + +### 什么是 @musistudio/llms? + +`@musistudio/llms` 是一个独立的 npm 包(`@musistudio/llms`),负责处理: + +- **API 格式转换**:在不同的 LLM 提供商 API 之间转换(Anthropic、OpenAI、Gemini 等) +- **请求/响应转换**:将请求和响应转换为统一格式 +- **认证处理**:管理不同提供商的认证方法 +- **流式响应支持**:处理来自不同提供商的流式响应 +- **转换器系统**:提供可扩展的架构来添加新的提供商 + +### 核心概念 + +#### 1. 统一请求/响应格式 + +核心包定义了统一格式(`UnifiedChatRequest`、`UnifiedChatResponse`),抽象了提供商特定的差异: + +```typescript +interface UnifiedChatRequest { + messages: UnifiedMessage[]; + model: string; + max_tokens?: number; + temperature?: number; + stream?: boolean; + tools?: UnifiedTool[]; + tool_choice?: any; + reasoning?: { + effort?: ThinkLevel; + max_tokens?: number; + enabled?: boolean; + }; +} +``` + +#### 2. 转换器接口 + +所有转换器都实现一个通用接口: + +```typescript +interface Transformer { + transformRequestIn?: (request: UnifiedChatRequest, provider: LLMProvider, context: TransformerContext) => Promise; + transformRequestOut?: (request: any, context: TransformerContext) => Promise; + transformResponseIn?: (response: Response, context?: TransformerContext) => Promise; + transformResponseOut?: (response: Response, context: TransformerContext) => Promise; + endPoint?: string; + name?: string; + auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise; +} +``` + +#### 3. 内置转换器 + +核心包包含以下转换器: +- **anthropic**:Anthropic API 格式 +- **openai**:OpenAI API 格式 +- **gemini**:Google Gemini API 格式 +- **deepseek**:DeepSeek API 格式 +- **groq**:Groq API 格式 +- **openrouter**:OpenRouter API 格式 +- 等等... + +### 与 CCR Server 的集成 + +CCR server 通过以下方式集成 `@musistudio/llms`: + +1. **转换器服务**(`packages/core/src/services/transformer.ts`):管理转换器的注册和实例化 +2. **提供商配置**:将提供商配置映射到核心包的 LLMProvider 接口 +3. **请求管道**:在请求处理过程中按顺序应用转换器 +4. **自定义转换器**:支持加载外部转换器插件 + +### 版本和更新 + +`@musistudio/llms` 的当前版本是 `1.0.51`。它作为独立的 npm 包发布,可以独立使用或作为 CCR Server 的一部分使用。 + ## 核心功能 ### 1. 请求路由 diff --git a/docs/i18n/zh-CN/docusaurus-theme-classic/navbar.json b/docs/i18n/zh-CN/docusaurus-theme-classic/navbar.json index 547f110..1519c01 100644 --- a/docs/i18n/zh-CN/docusaurus-theme-classic/navbar.json +++ b/docs/i18n/zh-CN/docusaurus-theme-classic/navbar.json @@ -8,11 +8,11 @@ "description": "The alt text of navbar logo" }, "item.label.Documentation": { - "message": "Documentation", + "message": "文档", "description": "Navbar item with label Documentation" }, "item.label.Blog": { - "message": "Blog", + "message": "博客", "description": "Navbar item with label Blog" }, "item.label.GitHub": { diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 9fa17f9..18d4b4e 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -2,6 +2,54 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { tutorialSidebar: [ + { + type: 'category', + label: 'CLI', + link: { + type: 'generated-index', + title: 'Claude Code Router CLI', + description: 'Command-line tool usage guide', + slug: 'category/cli', + }, + items: [ + 'cli/intro', + 'cli/installation', + 'cli/quick-start', + { + type: 'category', + label: 'Commands', + link: { + type: 'generated-index', + title: 'CLI Commands', + description: 'Complete command reference', + slug: 'category/cli-commands', + }, + items: [ + 'cli/commands/start', + 'cli/commands/model', + 'cli/commands/status', + 'cli/commands/statusline', + 'cli/commands/preset', + 'cli/commands/other', + ], + }, + { + type: 'category', + label: 'Configuration', + key: 'cli-configuration-category', + link: { + type: 'generated-index', + title: 'CLI Configuration', + description: 'CLI configuration guide', + slug: 'category/cli-config', + }, + items: [ + 'cli/config/basic', + 'cli/config/project-level', + ], + }, + ], + }, { type: 'category', label: 'Server', @@ -63,54 +111,6 @@ const sidebars: SidebarsConfig = { }, ], }, - { - type: 'category', - label: 'CLI', - link: { - type: 'generated-index', - title: 'Claude Code Router CLI', - description: 'Command-line tool usage guide', - slug: 'category/cli', - }, - items: [ - 'cli/intro', - 'cli/installation', - 'cli/quick-start', - { - type: 'category', - label: 'Commands', - link: { - type: 'generated-index', - title: 'CLI Commands', - description: 'Complete command reference', - slug: 'category/cli-commands', - }, - items: [ - 'cli/commands/start', - 'cli/commands/model', - 'cli/commands/status', - 'cli/commands/statusline', - 'cli/commands/preset', - 'cli/commands/other', - ], - }, - { - type: 'category', - label: 'Configuration', - key: 'cli-configuration-category', - link: { - type: 'generated-index', - title: 'CLI Configuration', - description: 'CLI configuration guide', - slug: 'category/cli-config', - }, - items: [ - 'cli/config/basic', - 'cli/config/project-level', - ], - }, - ], - }, ], }; diff --git a/package.json b/package.json index 2e3f8b8..b7d3b00 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ } }, "engines": { - "node": ">=18.0.0", + "node": ">=20.0.0", "pnpm": ">=8.0.0" } } diff --git a/packages/core/src/api/routes.ts b/packages/core/src/api/routes.ts index 5ef76a9..bfe24aa 100644 --- a/packages/core/src/api/routes.ts +++ b/packages/core/src/api/routes.ts @@ -50,44 +50,147 @@ async function handleTransformerEndpoint( ); } - // Process request transformer chain - const { requestBody, config, bypass } = await processRequestTransformers( - body, - provider, - transformer, - req.headers, - { - req, - } - ); + try { + // Process request transformer chain + const { requestBody, config, bypass } = await processRequestTransformers( + body, + provider, + transformer, + req.headers, + { + req, + } + ); - // Send request to LLM provider - const response = await sendRequestToProvider( - requestBody, - config, - provider, - fastify, - bypass, - transformer, - { - req, - } - ); + // Send request to LLM provider + const response = await sendRequestToProvider( + requestBody, + config, + provider, + fastify, + bypass, + transformer, + { + req, + } + ); - // Process response transformer chain - const finalResponse = await processResponseTransformers( - requestBody, - response, - provider, - transformer, - bypass, - { - req, - } - ); + // Process response transformer chain + const finalResponse = await processResponseTransformers( + requestBody, + response, + provider, + transformer, + bypass, + { + req, + } + ); - // Format and return response - return formatResponse(finalResponse, reply, body); + // Format and return response + return formatResponse(finalResponse, reply, body); + } catch (error: any) { + // Handle fallback if error occurs + if (error.code === 'provider_response_error') { + const fallbackResult = await handleFallback(req, reply, fastify, transformer, error); + if (fallbackResult) { + return fallbackResult; + } + } + throw error; + } +} + +/** + * Handle fallback logic when request fails + * Tries each fallback model in sequence until one succeeds + */ +async function handleFallback( + req: FastifyRequest, + reply: FastifyReply, + fastify: FastifyInstance, + transformer: any, + error: any +): Promise { + const scenarioType = (req as any).scenarioType || 'default'; + const fallbackConfig = fastify.configService.get('fallback'); + + if (!fallbackConfig || !fallbackConfig[scenarioType]) { + return null; + } + + const fallbackList = fallbackConfig[scenarioType] as string[]; + if (!Array.isArray(fallbackList) || fallbackList.length === 0) { + return null; + } + + req.log.warn(`Request failed for ${(req as any).scenarioType}, trying ${fallbackList.length} fallback models`); + + // Try each fallback model in sequence + for (const fallbackModel of fallbackList) { + try { + req.log.info(`Trying fallback model: ${fallbackModel}`); + + // Update request with fallback model + const newBody = { ...(req.body as any) }; + const [fallbackProvider, ...fallbackModelName] = fallbackModel.split(','); + newBody.model = fallbackModelName.join(','); + + // Create new request object with updated provider and body + const newReq = { + ...req, + provider: fallbackProvider, + body: newBody, + }; + + const provider = fastify.providerService.getProvider(fallbackProvider); + if (!provider) { + req.log.warn(`Fallback provider '${fallbackProvider}' not found, skipping`); + continue; + } + + // Process request transformer chain + const { requestBody, config, bypass } = await processRequestTransformers( + newBody, + provider, + transformer, + req.headers, + { req: newReq } + ); + + // Send request to LLM provider + const response = await sendRequestToProvider( + requestBody, + config, + provider, + fastify, + bypass, + transformer, + { req: newReq } + ); + + // Process response transformer chain + const finalResponse = await processResponseTransformers( + requestBody, + response, + provider, + transformer, + bypass, + { req: newReq } + ); + + req.log.info(`Fallback model ${fallbackModel} succeeded`); + + // Format and return response + return formatResponse(finalResponse, reply, newBody); + } catch (fallbackError: any) { + req.log.warn(`Fallback model ${fallbackModel} failed: ${fallbackError.message}`); + continue; + } + } + + req.log.error(`All fallback models failed for yichu ${scenarioType}`); + return null; } /** diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 9dc34bc..c988c2e 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -39,6 +39,7 @@ declare module "fastify" { interface FastifyRequest { provider?: string; model?: string; + scenarioType?: string; } interface FastifyInstance { _server?: Server; @@ -266,6 +267,7 @@ export { sessionUsageCache }; export { router }; export { calculateTokenCount }; export { searchProjectBySession }; +export type { RouterScenarioType, RouterFallbackConfig } from "./utils/router"; export { ConfigService } from "./services/config"; export { ProviderService } from "./services/provider"; export { TransformerService } from "./services/transformer"; diff --git a/packages/core/src/utils/router.ts b/packages/core/src/utils/router.ts index 1789e8a..f3d4fa0 100644 --- a/packages/core/src/utils/router.ts +++ b/packages/core/src/utils/router.ts @@ -126,7 +126,7 @@ const getUseModel = async ( tokenCount: number, configService: ConfigService, lastUsage?: Usage | undefined -) => { +): Promise<{ model: string; scenarioType: RouterScenarioType }> => { const projectSpecificRouter = await getProjectSpecificRouter(req, configService); const providers = configService.get("providers") || []; const Router = projectSpecificRouter || configService.get("Router"); @@ -140,9 +140,9 @@ const getUseModel = async ( (m: any) => m.toLowerCase() === model ); if (finalProvider && finalModel) { - return `${finalProvider.name},${finalModel}`; + return { model: `${finalProvider.name},${finalModel}`, scenarioType: 'default' }; } - return req.body.model; + return { model: req.body.model, scenarioType: 'default' }; } // if tokenCount is greater than the configured threshold, use the long context model @@ -156,7 +156,7 @@ const getUseModel = async ( req.log.info( `Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}` ); - return Router.longContext; + return { model: Router.longContext, scenarioType: 'longContext' }; } if ( req.body?.system?.length > 1 && @@ -170,7 +170,7 @@ const getUseModel = async ( `${model[1]}`, "" ); - return model[1]; + return { model: model[1], scenarioType: 'default' }; } } // Use the background model for any Claude Haiku variant @@ -181,7 +181,7 @@ const getUseModel = async ( globalRouter?.background ) { req.log.info(`Using background model for ${req.body.model}`); - return globalRouter.background; + return { model: globalRouter.background, scenarioType: 'background' }; } // The priority of websearch must be higher than thinking. if ( @@ -189,14 +189,14 @@ const getUseModel = async ( req.body.tools.some((tool: any) => tool.type?.startsWith("web_search")) && Router?.webSearch ) { - return Router.webSearch; + return { model: Router.webSearch, scenarioType: 'webSearch' }; } // if exits thinking, use the think model if (req.body.thinking && Router?.think) { req.log.info(`Using think model for ${req.body.thinking}`); - return Router.think; + return { model: Router.think, scenarioType: 'think' }; } - return Router?.default; + return { model: Router?.default, scenarioType: 'default' }; }; export interface RouterContext { @@ -205,6 +205,16 @@ export interface RouterContext { event?: any; } +export type RouterScenarioType = 'default' | 'background' | 'think' | 'longContext' | 'webSearch'; + +export interface RouterFallbackConfig { + default?: string[]; + background?: string[]; + think?: string[]; + longContext?: string[]; + webSearch?: string[]; +} + export const router = async (req: any, _res: any, context: RouterContext) => { const { configService, event } = context; // Parse sessionId from metadata.user_id @@ -247,9 +257,6 @@ export const router = async (req: any, _res: any, context: RouterContext) => { tokenizerConfig ); tokenCount = result.tokenCount; - req.log.debug( - `Token count: ${tokenCount} (tokenizer: ${result.tokenizerUsed}, cached: ${result.cached})` - ); } else { // Legacy fallback tokenCount = calculateTokenCount( @@ -273,13 +280,19 @@ export const router = async (req: any, _res: any, context: RouterContext) => { } } if (!model) { - model = await getUseModel(req, tokenCount, configService, lastMessageUsage); + const result = await getUseModel(req, tokenCount, configService, lastMessageUsage); + model = result.model; + req.scenarioType = result.scenarioType; + } else { + // Custom router doesn't provide scenario type, default to 'default' + req.scenarioType = 'default'; } req.body.model = model; } catch (error: any) { req.log.error(`Error in router middleware: ${error.message}`); const Router = configService.get("Router"); req.body.model = Router?.default; + req.scenarioType = 'default'; } return; };