feat: 实现智能模型映射机制
Browse files主要功能:
- 新增 ModelMapping 结构体支持双模型配置
- 实现灵活的模型名称检测,基于关键词而非硬编码
- 包含 'haiku' 的模型映射到第一个模型位置
- 包含 'sonnet' 的模型映射到第二个模型位置
- 支持不区分大小写的模型匹配
- 未知模型类型优雅降级到 haiku 模型
- 更新文档说明智能映射机制和使用示例
现在支持的映射:
- claude-3-5-haiku-20241022 → qwen-3-32b
- claude-sonnet-4-20250514 → deepseek-v3-fast
- 任何包含 haiku/sonnet 的模型都能正确映射
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
CLAUDE.md
CHANGED
|
@@ -12,12 +12,37 @@
|
|
| 12 |
|
| 13 |
## 🎯 核心功能
|
| 14 |
|
| 15 |
-
- **URL 路径解析**:自动从 URL 中提取目标 API
|
|
|
|
| 16 |
- **格式转换**:Claude API ↔ OpenAI API 双向转换
|
| 17 |
- **流式响应**:支持流式和非流式响应
|
| 18 |
- **错误处理**:完整的错误处理和调试日志
|
| 19 |
- **CORS 支持**:支持跨域请求
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
## ⚙️ 环境配置
|
| 22 |
|
| 23 |
```bash
|
|
@@ -62,11 +87,12 @@ POST /https://api.groq.com/openai/v1/claude-3-haiku-20240307/llama3-70b-8192/v1/
|
|
| 62 |
## 🔄 工作流程
|
| 63 |
|
| 64 |
1. **接收请求**:Claude CLI 发送 Claude API 格式的请求
|
| 65 |
-
2. **解析 URL
|
| 66 |
-
3.
|
| 67 |
-
4.
|
| 68 |
-
5.
|
| 69 |
-
6.
|
|
|
|
| 70 |
|
| 71 |
## 🛠️ 开发和测试
|
| 72 |
|
|
|
|
| 12 |
|
| 13 |
## 🎯 核心功能
|
| 14 |
|
| 15 |
+
- **URL 路径解析**:自动从 URL 中提取目标 API 服务器和模型映射信息
|
| 16 |
+
- **模型映射**:将 Claude 模型名称映射到目标 API 的具体模型
|
| 17 |
- **格式转换**:Claude API ↔ OpenAI API 双向转换
|
| 18 |
- **流式响应**:支持流式和非流式响应
|
| 19 |
- **错误处理**:完整的错误处理和调试日志
|
| 20 |
- **CORS 支持**:支持跨域请求
|
| 21 |
|
| 22 |
+
## 🔄 模型映射机制
|
| 23 |
+
|
| 24 |
+
代理服务器支持将 Claude Code 使用的模型智能映射到目标 API 的具体模型:
|
| 25 |
+
|
| 26 |
+
- 包含 `haiku` 的模型 → URL 中的第一个模型(haiku_model)
|
| 27 |
+
- 包含 `sonnet` 的模型 → URL 中的第二个模型(sonnet_model)
|
| 28 |
+
- 其他模型 → 默认使用 haiku_model
|
| 29 |
+
|
| 30 |
+
### 示例映射
|
| 31 |
+
```
|
| 32 |
+
URL: /https://voapi.16931.com/v1/qwen-3-32b/deepseek-v3-fast/v1/messages
|
| 33 |
+
|
| 34 |
+
映射关系:
|
| 35 |
+
- claude-3-5-haiku-20241022 → qwen-3-32b (包含 haiku)
|
| 36 |
+
- claude-sonnet-4-20250514 → deepseek-v3-fast (包含 sonnet)
|
| 37 |
+
- claude-3-haiku-20240307 → qwen-3-32b (包含 haiku)
|
| 38 |
+
- claude-3.5-sonnet-20241022 → deepseek-v3-fast (包含 sonnet)
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 映射逻辑
|
| 42 |
+
- 模型名称检查不区分大小写
|
| 43 |
+
- 优先匹配 `haiku`,其次匹配 `sonnet`
|
| 44 |
+
- 未知模型类型默认使用 haiku_model
|
| 45 |
+
|
| 46 |
## ⚙️ 环境配置
|
| 47 |
|
| 48 |
```bash
|
|
|
|
| 87 |
## 🔄 工作流程
|
| 88 |
|
| 89 |
1. **接收请求**:Claude CLI 发送 Claude API 格式的请求
|
| 90 |
+
2. **解析 URL**:从路径中提取协议、域名、路径和模型映射信息
|
| 91 |
+
3. **模型映射**:将 Claude 模型名称映射到目标 API 的具体模型
|
| 92 |
+
4. **格式转换**:将 Claude 请求格式转换为 OpenAI 格式
|
| 93 |
+
5. **转发请求**:向目标 API 发送转换后的请求
|
| 94 |
+
6. **处理响应**:将 OpenAI 响应转换回 Claude 格式
|
| 95 |
+
7. **返回结果**:将转换后的响应返回给客户端
|
| 96 |
|
| 97 |
## 🛠️ 开发和测试
|
| 98 |
|
main.go
CHANGED
|
@@ -110,6 +110,11 @@ type ProxyConfig struct {
|
|
| 110 |
Model string
|
| 111 |
}
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
// Global debug flag to avoid repeated environment variable checks
|
| 114 |
var debugMode bool
|
| 115 |
|
|
@@ -216,7 +221,7 @@ func handleProxy(c *gin.Context) {
|
|
| 216 |
return
|
| 217 |
}
|
| 218 |
|
| 219 |
-
config, err := parseProxyConfig(path)
|
| 220 |
if err != nil {
|
| 221 |
c.JSON(400, gin.H{"error": "Invalid proxy configuration: " + err.Error()})
|
| 222 |
return
|
|
@@ -232,7 +237,11 @@ func handleProxy(c *gin.Context) {
|
|
| 232 |
// Debug: Print the Claude request structure
|
| 233 |
debugLogData("Claude Request Debug", claudeReq)
|
| 234 |
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
// Debug: Print all incoming headers
|
| 238 |
debugLogData("Incoming Headers Debug", c.Request.Header)
|
|
@@ -252,7 +261,24 @@ func handleProxy(c *gin.Context) {
|
|
| 252 |
}
|
| 253 |
}
|
| 254 |
|
| 255 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
func extractAPIKey(c *gin.Context) string {
|
| 257 |
// Check common API key headers
|
| 258 |
headers := []string{"x-api-key", "anthropic-api-key"}
|
|
@@ -272,7 +298,7 @@ func extractAPIKey(c *gin.Context) string {
|
|
| 272 |
return ""
|
| 273 |
}
|
| 274 |
|
| 275 |
-
func parseProxyConfig(path string) (*ProxyConfig, error) {
|
| 276 |
debugLog("Parsing path: %s", path)
|
| 277 |
|
| 278 |
// Remove query parameters from path before matching
|
|
@@ -299,22 +325,26 @@ func parseProxyConfig(path string) (*ProxyConfig, error) {
|
|
| 299 |
protocol := matches[1]
|
| 300 |
domain := matches[2]
|
| 301 |
urlPath := matches[3]
|
| 302 |
-
|
|
|
|
| 303 |
|
| 304 |
baseURL := fmt.Sprintf("%s://%s%s", protocol, domain, urlPath)
|
| 305 |
|
| 306 |
-
debugLog("Parsed (%s) - protocol: %s, domain: %s, urlPath: %s,
|
| 307 |
-
p.name, protocol, domain, urlPath,
|
| 308 |
debugLog("Final baseURL: %s", baseURL)
|
| 309 |
|
| 310 |
return &ProxyConfig{
|
| 311 |
BaseURL: baseURL,
|
| 312 |
-
Model:
|
|
|
|
|
|
|
|
|
|
| 313 |
}, nil
|
| 314 |
}
|
| 315 |
}
|
| 316 |
|
| 317 |
-
return nil, fmt.Errorf("invalid path format. Expected: /protocol/domain/path/haiku_model/model_name/v1/messages or /protocol://domain/path/haiku_model/model_name/v1/messages, got: %s", pathWithoutQuery)
|
| 318 |
}
|
| 319 |
|
| 320 |
func convertClaudeToOpenAI(claudeReq ClaudeRequest, model string) OpenAIRequest {
|
|
|
|
| 110 |
Model string
|
| 111 |
}
|
| 112 |
|
| 113 |
+
type ModelMapping struct {
|
| 114 |
+
HaikuModel string
|
| 115 |
+
SonnetModel string
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
// Global debug flag to avoid repeated environment variable checks
|
| 119 |
var debugMode bool
|
| 120 |
|
|
|
|
| 221 |
return
|
| 222 |
}
|
| 223 |
|
| 224 |
+
config, mapping, err := parseProxyConfig(path)
|
| 225 |
if err != nil {
|
| 226 |
c.JSON(400, gin.H{"error": "Invalid proxy configuration: " + err.Error()})
|
| 227 |
return
|
|
|
|
| 237 |
// Debug: Print the Claude request structure
|
| 238 |
debugLogData("Claude Request Debug", claudeReq)
|
| 239 |
|
| 240 |
+
// Map the Claude model to the target API model
|
| 241 |
+
targetModel := mapClaudeModel(claudeReq.Model, mapping)
|
| 242 |
+
config.Model = targetModel
|
| 243 |
+
|
| 244 |
+
openaiReq := convertClaudeToOpenAI(claudeReq, targetModel)
|
| 245 |
|
| 246 |
// Debug: Print all incoming headers
|
| 247 |
debugLogData("Incoming Headers Debug", c.Request.Header)
|
|
|
|
| 261 |
}
|
| 262 |
}
|
| 263 |
|
| 264 |
+
// Map Claude model names to target API models based on model type
|
| 265 |
+
func mapClaudeModel(claudeModel string, mapping *ModelMapping) string {
|
| 266 |
+
debugLog("Mapping Claude model: %s", claudeModel)
|
| 267 |
+
|
| 268 |
+
claudeModelLower := strings.ToLower(claudeModel)
|
| 269 |
+
|
| 270 |
+
if strings.Contains(claudeModelLower, "haiku") {
|
| 271 |
+
debugLog("Model contains 'haiku', using haiku model: %s", mapping.HaikuModel)
|
| 272 |
+
return mapping.HaikuModel
|
| 273 |
+
} else if strings.Contains(claudeModelLower, "sonnet") {
|
| 274 |
+
debugLog("Model contains 'sonnet', using sonnet model: %s", mapping.SonnetModel)
|
| 275 |
+
return mapping.SonnetModel
|
| 276 |
+
} else {
|
| 277 |
+
// Default to haiku model for unknown models
|
| 278 |
+
debugLog("Unknown model type, defaulting to haiku model: %s", mapping.HaikuModel)
|
| 279 |
+
return mapping.HaikuModel
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
func extractAPIKey(c *gin.Context) string {
|
| 283 |
// Check common API key headers
|
| 284 |
headers := []string{"x-api-key", "anthropic-api-key"}
|
|
|
|
| 298 |
return ""
|
| 299 |
}
|
| 300 |
|
| 301 |
+
func parseProxyConfig(path string) (*ProxyConfig, *ModelMapping, error) {
|
| 302 |
debugLog("Parsing path: %s", path)
|
| 303 |
|
| 304 |
// Remove query parameters from path before matching
|
|
|
|
| 325 |
protocol := matches[1]
|
| 326 |
domain := matches[2]
|
| 327 |
urlPath := matches[3]
|
| 328 |
+
haikuModel := matches[4] // Model for haiku requests
|
| 329 |
+
sonnetModel := matches[5] // Model for sonnet requests
|
| 330 |
|
| 331 |
baseURL := fmt.Sprintf("%s://%s%s", protocol, domain, urlPath)
|
| 332 |
|
| 333 |
+
debugLog("Parsed (%s) - protocol: %s, domain: %s, urlPath: %s, haikuModel: %s, sonnetModel: %s",
|
| 334 |
+
p.name, protocol, domain, urlPath, haikuModel, sonnetModel)
|
| 335 |
debugLog("Final baseURL: %s", baseURL)
|
| 336 |
|
| 337 |
return &ProxyConfig{
|
| 338 |
BaseURL: baseURL,
|
| 339 |
+
Model: "", // Will be determined by mapClaudeModel function
|
| 340 |
+
}, &ModelMapping{
|
| 341 |
+
HaikuModel: haikuModel,
|
| 342 |
+
SonnetModel: sonnetModel,
|
| 343 |
}, nil
|
| 344 |
}
|
| 345 |
}
|
| 346 |
|
| 347 |
+
return nil, nil, fmt.Errorf("invalid path format. Expected: /protocol/domain/path/haiku_model/model_name/v1/messages or /protocol://domain/path/haiku_model/model_name/v1/messages, got: %s", pathWithoutQuery)
|
| 348 |
}
|
| 349 |
|
| 350 |
func convertClaudeToOpenAI(claudeReq ClaudeRequest, model string) OpenAIRequest {
|