mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-01-30 06:12:06 +00:00
fix docs
This commit is contained in:
@@ -6,15 +6,29 @@ title: Basic Configuration
|
|||||||
|
|
||||||
CLI uses the same configuration file as Server: `~/.claude-code-router/config.json`
|
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
|
```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
|
```bash
|
||||||
ccr model
|
ccr model
|
||||||
@@ -26,16 +40,23 @@ This will guide you through:
|
|||||||
3. Select model
|
3. Select model
|
||||||
4. Set routing rules
|
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
|
```bash
|
||||||
# Open configuration file
|
ccr restart
|
||||||
nano ~/.claude-code-router/config.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Minimal Configuration Example
|
Or restart directly through the Web UI.
|
||||||
|
|
||||||
|
## Configuration File Location
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude-code-router/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minimal Configuration Example
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
@@ -129,14 +150,16 @@ Configuration is automatically backed up on each update:
|
|||||||
~/.claude-code-router/config.backup.{timestamp}.json
|
~/.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
|
```bash
|
||||||
ccr restart
|
ccr restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or restart directly through the Web UI by clicking the "Save and Restart" button.
|
||||||
|
|
||||||
## View Current Configuration
|
## View Current Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -26,27 +26,49 @@ npm install -g @musistudio/claude-code-router
|
|||||||
|
|
||||||
## Basic Usage
|
## 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
|
```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
|
```bash
|
||||||
ccr status
|
ccr code
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stop Service
|
This will launch Claude Code and route your requests through the configured provider.
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
```bash
|
```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
|
## 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.
|
Configure once, and both CLI and Server will use it.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,41 @@ sidebar_position: 3
|
|||||||
|
|
||||||
Get up and running with Claude Code Router in 5 minutes.
|
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
|
```bash
|
||||||
ccr start
|
ccr start
|
||||||
@@ -14,7 +48,7 @@ ccr start
|
|||||||
|
|
||||||
The router will start on `http://localhost:8080` by default.
|
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:
|
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.
|
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?
|
## What's Next?
|
||||||
|
|
||||||
- [Basic Configuration](/docs/config/basic) - Learn about configuration options
|
- [Basic Configuration](/docs/cli/config/basic) - Learn about configuration options
|
||||||
- [Routing](/docs/config/routing) - Configure smart routing rules
|
- [Routing](/docs/cli/config/routing) - Configure smart routing rules
|
||||||
- [CLI Commands](/docs/cli/start) - Explore all CLI commands
|
- [CLI Commands](/docs/category/cli-commands) - Explore all CLI commands
|
||||||
|
|||||||
@@ -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
|
## Project-Level Routing
|
||||||
|
|
||||||
Configure routing per project in `~/.claude/projects/<project-id>/claude-code-router.json`:
|
Configure routing per project in `~/.claude/projects/<project-id>/claude-code-router.json`:
|
||||||
|
|||||||
@@ -4,7 +4,152 @@ sidebar_position: 4
|
|||||||
|
|
||||||
# Transformers
|
# 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<Record<string, any>>;
|
||||||
|
|
||||||
|
// Convert provider request to unified format
|
||||||
|
transformRequestOut?: (
|
||||||
|
request: any,
|
||||||
|
context: TransformerContext
|
||||||
|
) => Promise<UnifiedChatRequest>;
|
||||||
|
|
||||||
|
// Convert provider response to unified format
|
||||||
|
transformResponseIn?: (
|
||||||
|
response: Response,
|
||||||
|
context?: TransformerContext
|
||||||
|
) => Promise<Response>;
|
||||||
|
|
||||||
|
// Convert unified response to provider format
|
||||||
|
transformResponseOut?: (
|
||||||
|
response: Response,
|
||||||
|
context: TransformerContext
|
||||||
|
) => Promise<Response>;
|
||||||
|
|
||||||
|
// 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<any>;
|
||||||
|
|
||||||
|
// 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
|
## 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
|
### deepseek
|
||||||
|
|
||||||
Specialized transformer for DeepSeek API:
|
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
|
### gemini
|
||||||
|
|
||||||
Transformer for Google Gemini API:
|
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
|
```json
|
||||||
{
|
{
|
||||||
"transformers": [
|
"transformers": [
|
||||||
{
|
{
|
||||||
"name": "groq",
|
"name": "maxtoken",
|
||||||
"providers": ["groq"]
|
"options": {
|
||||||
|
"max_tokens": 8192
|
||||||
|
},
|
||||||
|
"models": ["deepseek,deepseek-chat"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### openrouter
|
### customparams
|
||||||
|
|
||||||
Transformer for OpenRouter API:
|
Injects custom parameters into requests:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"transformers": [
|
"transformers": [
|
||||||
{
|
{
|
||||||
"name": "openrouter",
|
"name": "customparams",
|
||||||
"providers": ["openrouter"]
|
"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<UnifiedChatRequest> {
|
||||||
|
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<Response> {
|
||||||
|
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<any> {
|
||||||
|
// 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
|
## Applying Transformers
|
||||||
|
|
||||||
### Global Application
|
### Global Application (Provider Level)
|
||||||
|
|
||||||
Apply to all requests for a provider:
|
Apply to all requests for a provider:
|
||||||
|
|
||||||
@@ -104,7 +602,7 @@ Apply to all requests for a provider:
|
|||||||
|
|
||||||
### Model-Specific Application
|
### Model-Specific Application
|
||||||
|
|
||||||
Apply to specific models:
|
Apply to specific models only:
|
||||||
|
|
||||||
```json
|
```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
|
### Passing Options
|
||||||
|
|
||||||
Some transformers accept options:
|
Some transformers accept configuration options:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -132,45 +647,144 @@ Some transformers accept options:
|
|||||||
"options": {
|
"options": {
|
||||||
"max_tokens": 8192
|
"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
|
```javascript
|
||||||
module.exports = {
|
// Bad
|
||||||
name: 'my-transformer',
|
async transformRequestIn(request) {
|
||||||
transformRequest: async (req, config) => {
|
request.max_tokens = 4096;
|
||||||
// Modify request
|
return request;
|
||||||
return req;
|
}
|
||||||
},
|
|
||||||
transformResponse: async (res, config) => {
|
|
||||||
// Modify response
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Load in configuration:
|
// Good
|
||||||
|
async transformRequestIn(request) {
|
||||||
```json
|
return {
|
||||||
{
|
...request,
|
||||||
"transformers": [
|
max_tokens: request.max_tokens || 4096
|
||||||
{
|
};
|
||||||
"name": "my-transformer",
|
|
||||||
"path": "/path/to/transformer.js"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
## Next Steps
|
||||||
|
|
||||||
- [Advanced Topics](/docs/advanced/custom-router) - Advanced routing customization
|
- [Advanced Topics](/docs/server/advanced/custom-router) - Advanced routing customization
|
||||||
- [Agents](/docs/advanced/agents) - Extending with agents
|
- [Agents](/docs/server/advanced/agents) - Extending with agents
|
||||||
|
- [Core Package](/docs/server/intro) - Learn about @musistudio/llms
|
||||||
|
|||||||
@@ -15,17 +15,103 @@ Claude Code Router Server is a core service component responsible for routing Cl
|
|||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
┌─────────────┐ ┌─────────────────────────────┐ ┌──────────────┐
|
||||||
│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │
|
│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │
|
||||||
│ Client │ │ (Router + │ │ (OpenAI/ │
|
│ Client │ │ ┌─────────────────────┐ │ │ (OpenAI/ │
|
||||||
└─────────────┘ │ Transformer) │ │ Gemini/etc)│
|
└─────────────┘ │ │ @musistudio/llms │ │ │ Gemini/etc)│
|
||||||
└──────────────────┘ └──────────────┘
|
│ │ (Core Package) │ │ └──────────────┘
|
||||||
|
│ │ - Request Transform │ │
|
||||||
|
│ │ - Response Transform │ │
|
||||||
|
│ │ - Auth Handling │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ - Routing Logic │
|
||||||
|
│ - Agent System │
|
||||||
|
│ - Configuration │
|
||||||
|
└─────────────────────────────┘
|
||||||
│
|
│
|
||||||
├─ Web UI
|
├─ Web UI
|
||||||
├─ Config API
|
├─ Config API
|
||||||
└─ Logs 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<any>;
|
||||||
|
transformRequestOut?: (request: any, context: TransformerContext) => Promise<UnifiedChatRequest>;
|
||||||
|
transformResponseIn?: (response: Response, context?: TransformerContext) => Promise<Response>;
|
||||||
|
transformResponseOut?: (response: Response, context: TransformerContext) => Promise<Response>;
|
||||||
|
endPoint?: string;
|
||||||
|
name?: string;
|
||||||
|
auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise<any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
## Core Features
|
||||||
|
|
||||||
### 1. Request Routing
|
### 1. Request Routing
|
||||||
|
|||||||
@@ -81,35 +81,7 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
style: 'light',
|
style: 'light',
|
||||||
links: [
|
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
copyright: `Copyright © ${new Date().getFullYear()} Claude Code Router. Built with Docusaurus.`,
|
copyright: `Copyright © ${new Date().getFullYear()} Claude Code Router. Built with Docusaurus.`,
|
||||||
},
|
},
|
||||||
prism: {
|
prism: {
|
||||||
|
|||||||
@@ -27,15 +27,15 @@
|
|||||||
"message": "服务器 API 接口文档",
|
"message": "服务器 API 接口文档",
|
||||||
"description": "The generated-index page description for category 'API Reference' in sidebar 'tutorialSidebar'"
|
"description": "The generated-index page description for category 'API Reference' in sidebar 'tutorialSidebar'"
|
||||||
},
|
},
|
||||||
"sidebar.tutorialSidebar.category.Configuration": {
|
"sidebar.tutorialSidebar.category.server-configuration-category": {
|
||||||
"message": "配置",
|
"message": "配置",
|
||||||
"description": "The label for category 'Configuration' in sidebar 'tutorialSidebar'"
|
"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": "服务器配置",
|
"message": "服务器配置",
|
||||||
"description": "The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'"
|
"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": "服务器配置说明",
|
"message": "服务器配置说明",
|
||||||
"description": "The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'"
|
"description": "The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'"
|
||||||
},
|
},
|
||||||
@@ -74,5 +74,17 @@
|
|||||||
"sidebar.tutorialSidebar.category.Commands.link.generated-index.description": {
|
"sidebar.tutorialSidebar.category.Commands.link.generated-index.description": {
|
||||||
"message": "完整的命令参考",
|
"message": "完整的命令参考",
|
||||||
"description": "The generated-index page description for category 'Commands' in sidebar 'tutorialSidebar'"
|
"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'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/<project-id>/claude-code-router.json` 中为每个项目配置路由:
|
在 `~/.claude/projects/<project-id>/claude-code-router.json` 中为每个项目配置路由:
|
||||||
|
|||||||
@@ -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<Record<string, any>>;
|
||||||
|
|
||||||
|
// 将提供商请求转换为统一格式
|
||||||
|
transformRequestOut?: (
|
||||||
|
request: any,
|
||||||
|
context: TransformerContext
|
||||||
|
) => Promise<UnifiedChatRequest>;
|
||||||
|
|
||||||
|
// 将提供商响应转换为统一格式
|
||||||
|
transformResponseIn?: (
|
||||||
|
response: Response,
|
||||||
|
context?: TransformerContext
|
||||||
|
) => Promise<Response>;
|
||||||
|
|
||||||
|
// 将统一响应转换为提供商格式
|
||||||
|
transformResponseOut?: (
|
||||||
|
response: Response,
|
||||||
|
context: TransformerContext
|
||||||
|
) => Promise<Response>;
|
||||||
|
|
||||||
|
// 自定义端点路径(可选)
|
||||||
|
endPoint?: string;
|
||||||
|
|
||||||
|
// 转换器名称(用于自定义转换器)
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
// 自定义认证处理器(可选)
|
||||||
|
auth?: (
|
||||||
|
request: any,
|
||||||
|
provider: LLMProvider,
|
||||||
|
context: TransformerContext
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
|
// 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
|
```json
|
||||||
{
|
{
|
||||||
"transformer": {
|
"transformers": [
|
||||||
"use": ["anthropic"]
|
{
|
||||||
}
|
"name": "anthropic",
|
||||||
|
"providers": ["deepseek", "groq"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
如果只使用这一个转换器,它将直接透传请求和响应(您可以用来接入其他支持 Anthropic 端点的服务商)。
|
**功能:**
|
||||||
|
- 在 Anthropic 消息格式和 OpenAI 格式之间转换
|
||||||
|
- 处理工具调用和工具结果
|
||||||
|
- 支持思考/推理内容块
|
||||||
|
- 管理流式响应
|
||||||
|
|
||||||
### deepseek
|
### deepseek
|
||||||
|
|
||||||
@@ -29,169 +181,421 @@ sidebar_position: 4
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"transformer": {
|
"transformers": [
|
||||||
"use": ["deepseek"]
|
{
|
||||||
}
|
"name": "deepseek",
|
||||||
|
"providers": ["deepseek"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- DeepSeek 特定的推理格式
|
||||||
|
- 处理响应中的 `reasoning_content`
|
||||||
|
- 支持思考预算令牌
|
||||||
|
|
||||||
### gemini
|
### gemini
|
||||||
|
|
||||||
用于 Google Gemini API 的转换器:
|
用于 Google Gemini API 的转换器:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"transformer": {
|
"transformers": [
|
||||||
"use": ["gemini"]
|
{
|
||||||
}
|
"name": "gemini",
|
||||||
}
|
"providers": ["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"]
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### maxtoken
|
### maxtoken
|
||||||
|
|
||||||
设置特定的 `max_tokens` 值:
|
限制请求中的 max_tokens:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"transformer": {
|
"transformers": [
|
||||||
"use": [
|
{
|
||||||
["maxtoken", { "max_tokens": 65536 }]
|
"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<UnifiedChatRequest> {
|
||||||
|
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<Response> {
|
||||||
|
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
|
在 TypeScript/ES6 中创建转换器时使用:
|
||||||
{
|
|
||||||
"transformer": {
|
```typescript
|
||||||
"use": ["tooluse"]
|
export class MyTransformer implements Transformer {
|
||||||
|
static TransformerName = "my-transformer";
|
||||||
|
|
||||||
|
async transformRequestIn(request: UnifiedChatRequest): Promise<any> {
|
||||||
|
// 转换逻辑
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### reasoning
|
### 方法 2:实例名称(基于实例)
|
||||||
|
|
||||||
用于处理 `reasoning_content` 字段:
|
用于 JavaScript 转换器:
|
||||||
|
|
||||||
```json
|
```javascript
|
||||||
{
|
module.exports = class MyTransformer {
|
||||||
"transformer": {
|
constructor(options) {
|
||||||
"use": ["reasoning"]
|
this.name = 'my-transformer';
|
||||||
|
this.options = options;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### sampling
|
async transformRequestIn(request, provider, context) {
|
||||||
|
// 转换逻辑
|
||||||
用于处理采样信息字段,如 `temperature`、`top_p`、`top_k` 和 `repetition_penalty`:
|
return request;
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"transformer": {
|
|
||||||
"use": ["sampling"]
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
```
|
|
||||||
|
|
||||||
### enhancetool
|
|
||||||
|
|
||||||
对 LLM 返回的工具调用参数增加一层容错处理(注意:这会导致不再流式返回工具调用信息):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"transformer": {
|
|
||||||
"use": ["enhancetool"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### cleancache
|
|
||||||
|
|
||||||
清除请求中的 `cache_control` 字段:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"transformer": {
|
|
||||||
"use": ["cleancache"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### vertex-gemini
|
|
||||||
|
|
||||||
处理使用 Vertex 鉴权的 Gemini API:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"transformer": {
|
|
||||||
"use": ["vertex-gemini"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 应用转换器
|
## 应用转换器
|
||||||
|
|
||||||
### 全局应用
|
### 全局应用(提供商级别)
|
||||||
|
|
||||||
应用于提供商的所有请求:
|
为提供商的所有请求应用:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Providers": [
|
"Providers": [
|
||||||
{
|
{
|
||||||
"name": "deepseek",
|
"NAME": "deepseek",
|
||||||
"api_base_url": "https://api.deepseek.com/chat/completions",
|
"HOST": "https://api.deepseek.com",
|
||||||
"api_key": "your-api-key",
|
"APIKEY": "your-api-key",
|
||||||
"transformer": {
|
"transformers": ["anthropic"]
|
||||||
"use": ["deepseek"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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
|
```json
|
||||||
{
|
{
|
||||||
"transformers": [
|
"transformers": [
|
||||||
{
|
{
|
||||||
"path": "/path/to/transformer.js",
|
"name": "maxtoken",
|
||||||
"options": {
|
"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) - 高级路由自定义
|
- [高级主题](/docs/server/advanced/custom-router) - 高级路由自定义
|
||||||
- [Agent](/zh/docs/advanced/agents) - 使用 Agent 扩展功能
|
- [Agents](/docs/server/advanced/agents) - 使用 agents 扩展
|
||||||
|
- [核心包](/docs/server/intro) - 了解 @musistudio/llms
|
||||||
|
|||||||
@@ -11,17 +11,103 @@ Claude Code Router Server 是一个核心服务组件,负责将 Claude Code
|
|||||||
## 架构概述
|
## 架构概述
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
┌─────────────┐ ┌─────────────────────────────┐ ┌──────────────┐
|
||||||
│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │
|
│ Claude Code │────▶│ CCR Server │────▶│ LLM Provider │
|
||||||
│ Client │ │ (Router + │ │ (OpenAI/ │
|
│ Client │ │ ┌─────────────────────┐ │ │ (OpenAI/ │
|
||||||
└─────────────┘ │ Transformer) │ │ Gemini/etc)│
|
└─────────────┘ │ │ @musistudio/llms │ │ │ Gemini/etc)│
|
||||||
└──────────────────┘ └──────────────┘
|
│ │ (核心包) │ │ └──────────────┘
|
||||||
|
│ │ - 请求转换 │ │
|
||||||
|
│ │ - 响应转换 │ │
|
||||||
|
│ │ - 认证处理 │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ - 路由逻辑 │
|
||||||
|
│ - Agent 系统 │
|
||||||
|
│ - 配置管理 │
|
||||||
|
└─────────────────────────────┘
|
||||||
│
|
│
|
||||||
├─ Web UI
|
├─ Web UI
|
||||||
├─ Config API
|
├─ Config API
|
||||||
└─ Logs 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<any>;
|
||||||
|
transformRequestOut?: (request: any, context: TransformerContext) => Promise<UnifiedChatRequest>;
|
||||||
|
transformResponseIn?: (response: Response, context?: TransformerContext) => Promise<Response>;
|
||||||
|
transformResponseOut?: (response: Response, context: TransformerContext) => Promise<Response>;
|
||||||
|
endPoint?: string;
|
||||||
|
name?: string;
|
||||||
|
auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise<any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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. 请求路由
|
### 1. 请求路由
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
"description": "The alt text of navbar logo"
|
"description": "The alt text of navbar logo"
|
||||||
},
|
},
|
||||||
"item.label.Documentation": {
|
"item.label.Documentation": {
|
||||||
"message": "Documentation",
|
"message": "文档",
|
||||||
"description": "Navbar item with label Documentation"
|
"description": "Navbar item with label Documentation"
|
||||||
},
|
},
|
||||||
"item.label.Blog": {
|
"item.label.Blog": {
|
||||||
"message": "Blog",
|
"message": "博客",
|
||||||
"description": "Navbar item with label Blog"
|
"description": "Navbar item with label Blog"
|
||||||
},
|
},
|
||||||
"item.label.GitHub": {
|
"item.label.GitHub": {
|
||||||
|
|||||||
@@ -2,6 +2,54 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
|
|||||||
|
|
||||||
const sidebars: SidebarsConfig = {
|
const sidebars: SidebarsConfig = {
|
||||||
tutorialSidebar: [
|
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',
|
type: 'category',
|
||||||
label: 'Server',
|
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',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0",
|
"node": ">=20.0.0",
|
||||||
"pnpm": ">=8.0.0"
|
"pnpm": ">=8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,44 +50,147 @@ async function handleTransformerEndpoint(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process request transformer chain
|
try {
|
||||||
const { requestBody, config, bypass } = await processRequestTransformers(
|
// Process request transformer chain
|
||||||
body,
|
const { requestBody, config, bypass } = await processRequestTransformers(
|
||||||
provider,
|
body,
|
||||||
transformer,
|
provider,
|
||||||
req.headers,
|
transformer,
|
||||||
{
|
req.headers,
|
||||||
req,
|
{
|
||||||
}
|
req,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Send request to LLM provider
|
// Send request to LLM provider
|
||||||
const response = await sendRequestToProvider(
|
const response = await sendRequestToProvider(
|
||||||
requestBody,
|
requestBody,
|
||||||
config,
|
config,
|
||||||
provider,
|
provider,
|
||||||
fastify,
|
fastify,
|
||||||
bypass,
|
bypass,
|
||||||
transformer,
|
transformer,
|
||||||
{
|
{
|
||||||
req,
|
req,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process response transformer chain
|
// Process response transformer chain
|
||||||
const finalResponse = await processResponseTransformers(
|
const finalResponse = await processResponseTransformers(
|
||||||
requestBody,
|
requestBody,
|
||||||
response,
|
response,
|
||||||
provider,
|
provider,
|
||||||
transformer,
|
transformer,
|
||||||
bypass,
|
bypass,
|
||||||
{
|
{
|
||||||
req,
|
req,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Format and return response
|
// Format and return response
|
||||||
return formatResponse(finalResponse, reply, body);
|
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<any> {
|
||||||
|
const scenarioType = (req as any).scenarioType || 'default';
|
||||||
|
const fallbackConfig = fastify.configService.get<any>('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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ declare module "fastify" {
|
|||||||
interface FastifyRequest {
|
interface FastifyRequest {
|
||||||
provider?: string;
|
provider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
scenarioType?: string;
|
||||||
}
|
}
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
_server?: Server;
|
_server?: Server;
|
||||||
@@ -266,6 +267,7 @@ export { sessionUsageCache };
|
|||||||
export { router };
|
export { router };
|
||||||
export { calculateTokenCount };
|
export { calculateTokenCount };
|
||||||
export { searchProjectBySession };
|
export { searchProjectBySession };
|
||||||
|
export type { RouterScenarioType, RouterFallbackConfig } from "./utils/router";
|
||||||
export { ConfigService } from "./services/config";
|
export { ConfigService } from "./services/config";
|
||||||
export { ProviderService } from "./services/provider";
|
export { ProviderService } from "./services/provider";
|
||||||
export { TransformerService } from "./services/transformer";
|
export { TransformerService } from "./services/transformer";
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ const getUseModel = async (
|
|||||||
tokenCount: number,
|
tokenCount: number,
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
lastUsage?: Usage | undefined
|
lastUsage?: Usage | undefined
|
||||||
) => {
|
): Promise<{ model: string; scenarioType: RouterScenarioType }> => {
|
||||||
const projectSpecificRouter = await getProjectSpecificRouter(req, configService);
|
const projectSpecificRouter = await getProjectSpecificRouter(req, configService);
|
||||||
const providers = configService.get<any[]>("providers") || [];
|
const providers = configService.get<any[]>("providers") || [];
|
||||||
const Router = projectSpecificRouter || configService.get("Router");
|
const Router = projectSpecificRouter || configService.get("Router");
|
||||||
@@ -140,9 +140,9 @@ const getUseModel = async (
|
|||||||
(m: any) => m.toLowerCase() === model
|
(m: any) => m.toLowerCase() === model
|
||||||
);
|
);
|
||||||
if (finalProvider && finalModel) {
|
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
|
// if tokenCount is greater than the configured threshold, use the long context model
|
||||||
@@ -156,7 +156,7 @@ const getUseModel = async (
|
|||||||
req.log.info(
|
req.log.info(
|
||||||
`Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}`
|
`Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}`
|
||||||
);
|
);
|
||||||
return Router.longContext;
|
return { model: Router.longContext, scenarioType: 'longContext' };
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
req.body?.system?.length > 1 &&
|
req.body?.system?.length > 1 &&
|
||||||
@@ -170,7 +170,7 @@ const getUseModel = async (
|
|||||||
`<CCR-SUBAGENT-MODEL>${model[1]}</CCR-SUBAGENT-MODEL>`,
|
`<CCR-SUBAGENT-MODEL>${model[1]}</CCR-SUBAGENT-MODEL>`,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
return model[1];
|
return { model: model[1], scenarioType: 'default' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Use the background model for any Claude Haiku variant
|
// Use the background model for any Claude Haiku variant
|
||||||
@@ -181,7 +181,7 @@ const getUseModel = async (
|
|||||||
globalRouter?.background
|
globalRouter?.background
|
||||||
) {
|
) {
|
||||||
req.log.info(`Using background model for ${req.body.model}`);
|
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.
|
// The priority of websearch must be higher than thinking.
|
||||||
if (
|
if (
|
||||||
@@ -189,14 +189,14 @@ const getUseModel = async (
|
|||||||
req.body.tools.some((tool: any) => tool.type?.startsWith("web_search")) &&
|
req.body.tools.some((tool: any) => tool.type?.startsWith("web_search")) &&
|
||||||
Router?.webSearch
|
Router?.webSearch
|
||||||
) {
|
) {
|
||||||
return Router.webSearch;
|
return { model: Router.webSearch, scenarioType: 'webSearch' };
|
||||||
}
|
}
|
||||||
// if exits thinking, use the think model
|
// if exits thinking, use the think model
|
||||||
if (req.body.thinking && Router?.think) {
|
if (req.body.thinking && Router?.think) {
|
||||||
req.log.info(`Using think model for ${req.body.thinking}`);
|
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 {
|
export interface RouterContext {
|
||||||
@@ -205,6 +205,16 @@ export interface RouterContext {
|
|||||||
event?: any;
|
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) => {
|
export const router = async (req: any, _res: any, context: RouterContext) => {
|
||||||
const { configService, event } = context;
|
const { configService, event } = context;
|
||||||
// Parse sessionId from metadata.user_id
|
// Parse sessionId from metadata.user_id
|
||||||
@@ -247,9 +257,6 @@ export const router = async (req: any, _res: any, context: RouterContext) => {
|
|||||||
tokenizerConfig
|
tokenizerConfig
|
||||||
);
|
);
|
||||||
tokenCount = result.tokenCount;
|
tokenCount = result.tokenCount;
|
||||||
req.log.debug(
|
|
||||||
`Token count: ${tokenCount} (tokenizer: ${result.tokenizerUsed}, cached: ${result.cached})`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Legacy fallback
|
// Legacy fallback
|
||||||
tokenCount = calculateTokenCount(
|
tokenCount = calculateTokenCount(
|
||||||
@@ -273,13 +280,19 @@ export const router = async (req: any, _res: any, context: RouterContext) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!model) {
|
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;
|
req.body.model = model;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
req.log.error(`Error in router middleware: ${error.message}`);
|
req.log.error(`Error in router middleware: ${error.message}`);
|
||||||
const Router = configService.get("Router");
|
const Router = configService.get("Router");
|
||||||
req.body.model = Router?.default;
|
req.body.model = Router?.default;
|
||||||
|
req.scenarioType = 'default';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user