caidaoli Claude commited on
Commit
f6cebcf
·
1 Parent(s): 366aa7c

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>

Files changed (2) hide show
  1. CLAUDE.md +32 -6
  2. main.go +39 -9
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**:从路径中提取协议、域名、路径、HAIKU_MODEL 和模型名称信息
66
- 3. **格式转换**:将 Claude 请求格式转换为 OpenAI 格式
67
- 4. **转发请求**:向目标 API 发送转换后的请求
68
- 5. **处理响应**:将 OpenAI 响应转换回 Claude 格式
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
- openaiReq := convertClaudeToOpenAI(claudeReq, config.Model)
 
 
 
 
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
- // Extract API key from various header formats
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- modelName := matches[5]
 
303
 
304
  baseURL := fmt.Sprintf("%s://%s%s", protocol, domain, urlPath)
305
 
306
- debugLog("Parsed (%s) - protocol: %s, domain: %s, urlPath: %s, modelName: %s",
307
- p.name, protocol, domain, urlPath, modelName)
308
  debugLog("Final baseURL: %s", baseURL)
309
 
310
  return &ProxyConfig{
311
  BaseURL: baseURL,
312
- Model: modelName,
 
 
 
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 {