Skip to content

支持 Anthropic API 拉取模型列表(新结构体和认证头处理) - #1546

Closed
wans10 wants to merge 4 commits into
QuantumNous:alphafrom
wans10:alpha
Closed

支持 Anthropic API 拉取模型列表(新结构体和认证头处理)#1546
wans10 wants to merge 4 commits into
QuantumNous:alphafrom
wans10:alpha

Conversation

@wans10

@wans10 wans10 commented Aug 10, 2025

Copy link
Copy Markdown
Contributor

增加对 Anthropic API 的支持(新结构体和认证头处理)。
新增专用请求函数(GetAnthropicResponseBody)专门处理 Anthropic API 请求

Summary by CodeRabbit

  • New Features

    • Added support for fetching and displaying models from Anthropic.
    • Exposed multi-key management responses so multi-key status and controls are available.
  • Improvements

    • Unified model fetching across providers for consistent model lists (including Gemini ID normalization).
    • Improved reliability and error messaging when retrieving model lists from different channels.

重构 FetchUpstreamModels 和 FetchModels,通过辅助函数(getChannelBaseURL、buildModelsURL、getAuthHeaders、parseModelsResponse)提高了代码复用性和可维护性。
新 FetchModels 改进了错误处理(更明确的错误信息)和类型检查。
@coderabbitai

coderabbitai Bot commented Aug 10, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds Anthropic model-list support, multi-key management types, and a new Anthropic HTTP helper; updates FetchModels/FetchUpstreamModels to handle Anthropic responses and normalize Gemini IDs, and adds imports for body reading and timeouts. (49 words)

Changes

Cohort / File(s) Change Summary
Anthropic types & helper
controller/channel.go
Added AnthropicModel, AnthropicModelsResponse and exported helper GetAnthropicResponseBody(method, url, apiKey string) ([]byte, error) to perform Anthropic HTTP requests and read response bodies.
Model fetching logic
controller/channel.go
Extended FetchUpstreamModels and FetchModels to support Anthropic (use baseURL/v1/models and parse AnthropicModelsResponse), use the new helper, and trim models/ prefix from Gemini IDs; improved error messages and body reading.
Multi-key management types
controller/channel.go
Added exported types MultiKeyManageRequest, MultiKeyStatusResponse, and KeyStatus to represent multi-key management requests/responses.
Imports / misc
controller/channel.go
Added io/ioutil and time imports to support reading full response bodies and timeout configuration.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant AnthropicAPI
    participant OtherUpstream

    Client->>Controller: Request model list (FetchModels / FetchUpstreamModels)
    Controller->>Controller: determine channel type & base URL
    alt Anthropic
        Controller->>AnthropicAPI: GET /v1/models (via GetAnthropicResponseBody with x-api-key)
        AnthropicAPI-->>Controller: AnthropicModelsResponse JSON
        Controller->>Controller: parse AnthropicModelsResponse -> []IDs
    else Other (OpenAI/Gemini)
        Controller->>OtherUpstream: GET /models (with auth headers)
        OtherUpstream-->>Controller: JSON response
        Controller->>Controller: parse response, trim "models/" for Gemini IDs
    end
    Controller-->>Client: Normalized model ID list or error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

I hop through bytes with whiskered cheer,
Anthropic models now appear near.
Headers set and IDs made right,
Keys in rows, all tidy and bright.
🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b93071e and 1465503.

📒 Files selected for processing (1)
  • controller/channel.go (5 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
controller/channel.go (1)

957-979: Unify HTTP execution: reuse GetResponseBody (timeout, proxy, retries)

To keep behavior consistent with FetchUpstreamModels and centralize transport concerns, use GetResponseBody instead of manual http.Client. This also simplifies error handling.

Apply:

-// 创建HTTP请求
-request, err := http.NewRequest("GET", url, nil)
-...
-// 设置认证头(可以复用现有的逻辑)
-// Create a temporary channel object for header generation
-tempChannel := &model.Channel{
-  Type: req.Type,
-  Key:  strings.TrimSpace(strings.Split(req.Key, "\n")[0]),
-}
-headers := getAuthHeaders(tempChannel)
-for key, values := range headers {
-  for _, value := range values {
-    request.Header.Add(key, value)
-  }
-}
-// 发起请求
-client := &http.Client{}
-response, err := client.Do(request)
-...
-defer response.Body.Close()
-// 检查状态码
-if response.StatusCode != http.StatusOK {
-  c.JSON(http.StatusInternalServerError, gin.H{
-    "success": false,
-    "message": fmt.Sprintf("Failed to fetch models, status code: %d", response.StatusCode),
-  })
-  return
-}
-// 读取响应体
-body, err := io.ReadAll(response.Body)
+// 组装认证头并发起请求(复用 GetResponseBody)
+tempChannel := &model.Channel{
+  Type: req.Type,
+  Key:  strings.TrimSpace(strings.Split(req.Key, "\n")[0]),
+}
+headers := getAuthHeaders(tempChannel)
+body, err := GetResponseBody("GET", url, tempChannel, headers)
 if err != nil {
   c.JSON(http.StatusInternalServerError, gin.H{
     "success": false,
     "message": err.Error(),
   })
   return
 }
-// 解析响应(复用现有逻辑)
-models, err := parseModelsResponse(req.Type, body)
+// 解析响应(复用现有逻辑)
+models, err := parseModelsResponse(req.Type, body)
 if err != nil {
   c.JSON(http.StatusInternalServerError, gin.H{
     "success": false,
     "message": err.Error(),
   })
   return
 }

If you prefer to keep manual HTTP calls, at minimum add context, timeout, and limit error body size:

-request, err := http.NewRequest("GET", url, nil)
+request, err := http.NewRequestWithContext(c.Request.Context(), "GET", url, nil)
...
-client := &http.Client{}
+client := &http.Client{ Timeout: 15 * time.Second }
...
-if response.StatusCode != http.StatusOK {
-  c.JSON(http.StatusInternalServerError, gin.H{ ... })
-  return
-}
-// 读取响应体
-body, err := io.ReadAll(response.Body)
+// 尝试读取响应体(限制大小),即便非200也便于返回更清晰的错误
+limited := io.LimitReader(response.Body, 1<<20) // 1MB
+body, err := io.ReadAll(limited)
+if response.StatusCode != http.StatusOK {
+  // Truncate message to avoid huge payloads
+  msg := string(body)
+  if len(msg) > 2048 { msg = msg[:2048] + "..." }
+  c.JSON(http.StatusBadGateway, gin.H{
+    "success": false,
+    "message": fmt.Sprintf("Failed to fetch models (%d): %s", response.StatusCode, msg),
+  })
+  return
+}

Also applies to: 980-991, 992-999, 1001-1009, 1011-1019

🧹 Nitpick comments (5)
controller/channel.go (5)

45-51: Anthropic model struct LGTM (minor note on created_at type)

Fields align with Anthropic’s /v1/models payload. If you later need time operations, consider parsing created_at into time.Time via a custom type; current string is fine for transport.


213-224: Base URL resolver is fine (edge: unsupported types)

Returning empty string for unsupported types is OK since callers handle it. Optional: document behavior in the function comment to guide future callers.


237-250: Anthropic auth headers: add Accept; Content-Type on GET optional

Current headers work. Consider adding Accept: application/json. Content-Type isn’t required for GETs (not harmful). If you foresee beta features, allow passing anthropic-version via config to avoid hardcoding.

 case constant.ChannelTypeAnthropic:
   headers := make(http.Header)
   headers.Set("x-api-key", strings.TrimSpace(channel.Key))
   headers.Set("anthropic-version", "2023-06-01")
+  headers.Set("Accept", "application/json")
   headers.Set("Content-Type", "application/json")
   return headers

251-283: Response parsing unified nicely; add small robustness

Looks good for Anthropic and OpenAI-style. Optional: guard empty Data and return clearer message, and consider sorting IDs for deterministic output.

   for _, model := range result.Data {
     id := model.ID
     if channelType == constant.ChannelTypeGemini {
       id = strings.TrimPrefix(id, "models/")
     }
     ids = append(ids, id)
   }
+  // Optional: deterministic order
+  sort.Strings(ids)

297-305: Refactor in FetchUpstreamModels is clean and modular

Good reuse of base URL, URL builder, headers, and parser. Error messages are clear. One nit: consider adding a short Accept header in getAuthHeaders as suggested, and normalize base URL to avoid // paths (see buildModelsURL comment).

Also applies to: 307-309, 310-312, 313-319, 323-333

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f942361 and d84ec3c.

📒 Files selected for processing (1)
  • controller/channel.go (7 hunks)
🔇 Additional comments (3)
controller/channel.go (3)

6-6: Import usage is correct

io is used for io.ReadAll below. No issues.


53-60: AnthropicModelsResponse looks correct

Matches data/has_more/first_id/last_id usage. If pagination is planned, surface has_more & cursors to caller; currently you only return IDs.


61-91: No duplicate type declarations detected

I ran a project-wide search and confirmed that MultiKeyManageRequest, MultiKeyStatusResponse, and KeyStatus are only declared in controller/channel.go. There are no leftover duplicates elsewhere. All set!

Comment thread controller/channel.go Outdated
Comment thread controller/channel.go Outdated
使用已有的 getChannelBaseURL 函数,避免重复实现相同的逻辑

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (1)
controller/channel.go (1)

954-1016: Unify HTTP execution and harden client behavior (timeouts, error detail)

Two improvements recommended here:

  • Preferred: Reuse existing GetResponseBody to inherit global settings (timeouts, proxies, retries, metrics).
  • Alternative: If keeping manual client, at least add a timeout and include response body for non-200 to aid diagnostics.

Preferred (reuse GetResponseBody):

-// 创建HTTP请求
-request, err := http.NewRequest("GET", url, nil)
-if err != nil {
-  c.JSON(http.StatusInternalServerError, gin.H{
-    "success": false,
-    "message": err.Error(),
-  })
-  return
-}
-
-// 设置认证头(可以复用现有的逻辑)
-// Create a temporary channel object for header generation
-tempChannel := &model.Channel{
-  Type: req.Type,
-  Key:  strings.TrimSpace(strings.Split(req.Key, "\n")[0]),
-}
-headers := getAuthHeaders(tempChannel)
-for key, values := range headers {
-  for _, value := range values {
-    request.Header.Add(key, value)
-  }
-}
-
-// 发起请求
-client := &http.Client{}
-response, err := client.Do(request)
-if err != nil {
-  c.JSON(http.StatusInternalServerError, gin.H{
-    "success": false,
-    "message": err.Error(),
-  })
-  return
-}
-defer response.Body.Close()
-
-// 检查状态码
-if response.StatusCode != http.StatusOK {
-  c.JSON(http.StatusInternalServerError, gin.H{
-    "success": false,
-    "message": fmt.Sprintf("Failed to fetch models, status code: %d", response.StatusCode),
-  })
-  return
-}
-
-// 读取响应体
-body, err := io.ReadAll(response.Body)
-if err != nil {
-  c.JSON(http.StatusInternalServerError, gin.H{
-    "success": false,
-    "message": err.Error(),
-  })
-  return
-}
-
-// 解析响应(复用现有逻辑)
-models, err := parseModelsResponse(req.Type, body)
+// 使用统一的请求执行工具,继承全局超时/代理/重试等配置
+tempChannel := &model.Channel{
+  Type: req.Type,
+  Key:  strings.TrimSpace(strings.Split(req.Key, "\n")[0]),
+}
+headers := getAuthHeaders(tempChannel)
+body, err := GetResponseBody("GET", url, tempChannel, headers)
 if err != nil {
   c.JSON(http.StatusInternalServerError, gin.H{
     "success": false,
-    "message": err.Error(),
+    "message": fmt.Sprintf("Failed to fetch models: %v", err),
   })
   return
 }
+
+// 解析响应(复用现有逻辑)
+models, err := parseModelsResponse(req.Type, body)
 if err != nil {
   c.JSON(http.StatusInternalServerError, gin.H{
     "success": false,
     "message": err.Error(),
   })
   return
 }

Alternative (keep manual client; add timeout and error body):

- client := &http.Client{}
+ client := &http.Client{Timeout: 30 * time.Second}
 response, err := client.Do(request)
@@
-// 检查状态码
-if response.StatusCode != http.StatusOK {
-  c.JSON(http.StatusInternalServerError, gin.H{
-    "success": false,
-    "message": fmt.Sprintf("Failed to fetch models, status code: %d", response.StatusCode),
-  })
-  return
-}
-
-// 读取响应体
-body, err := io.ReadAll(response.Body)
+// 读取响应体(即便非200也读取,便于诊断)
+body, err := io.ReadAll(response.Body)
 if err != nil {
   c.JSON(http.StatusInternalServerError, gin.H{
     "success": false,
     "message": err.Error(),
   })
   return
 }
+// 检查状态码
+if response.StatusCode != http.StatusOK {
+  // 限制返回体长度,避免过长
+  msg := string(body)
+  if len(msg) > 800 {
+    msg = msg[:800] + "..."
+  }
+  c.JSON(http.StatusInternalServerError, gin.H{
+    "success": false,
+    "message": fmt.Sprintf("Failed to fetch models, status code: %d, body: %s", response.StatusCode, msg),
+  })
+  return
+}

Note: This alternative requires importing time:

import "time"
🧹 Nitpick comments (3)
controller/channel.go (3)

84-90: Clarify KeyStatus status values

Comments say 1/2 only, but code elsewhere uses 3 for auto-disabled. Align the comment to prevent confusion.

 type KeyStatus struct {
   Index        int    `json:"index"`
-  Status       int    `json:"status"` // 1: enabled, 2: disabled
+  Status       int    `json:"status"` // 1: enabled, 2: manual_disabled, 3: auto_disabled
   DisabledTime int64  `json:"disabled_time,omitempty"`
   Reason       string `json:"reason,omitempty"`
   KeyPreview   string `json:"key_preview"` // first 10 chars of key for identification
 }

213-224: Trim whitespace in custom base URL

Small hardening: trim spaces from customURL before use. This prevents subtle 400s/404s caused by leading/trailing spaces.

 func getChannelBaseURL(channelType int, customURL string) string {
-	if customURL != "" {
+	if customURL != "" {
+		customURL = strings.TrimSpace(customURL)
 		return customURL
 	}

252-284: Add unit tests for parseModelsResponse (OpenAI, Gemini, Anthropic)

Recommend tests covering:

  • Anthropic: minimal payload with id only, and a payload including extra fields with differing types (created_at as number/string) to ensure robustness.
  • OpenAI-style: standard list and an error payload.
  • Gemini: ids prefixed with models/ and trim logic.

I can scaffold table-driven tests for parseModelsResponse using canned JSON payloads. Want me to open a follow-up PR with tests?

Also applies to: 998-1016

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d84ec3c and 6be7ebd.

📒 Files selected for processing (1)
  • controller/channel.go (7 hunks)
🔇 Additional comments (3)
controller/channel.go (3)

225-237: Resolved: trailing-slash normalization prevents // paths

Good call adding TrimRight to avoid double slashes. This addresses prior feedback and makes URL building predictable.


298-333: FetchUpstreamModels: solid refactor and clearer errors

Good reuse of helpers (base URL, URL builder, auth headers, response parsing). Error messages are more actionable. LGTM.


941-949: Duplication removed: getChannelBaseURL reused

This addresses earlier duplication concerns in FetchModels. Good improvement for maintainability.

Comment thread controller/channel.go Outdated
Comment thread controller/channel.go Outdated
Comment thread controller/channel.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
controller/channel.go (1)

946-1004: Reuse shared HTTP helper to avoid duplication and ensure timeouts/proxy

Prefer the existing GetResponseBody (which uses service.GetHttpClient) to avoid raw http.Client without timeouts and to keep behavior consistent.

Apply:

-	// 创建HTTP请求
-	request, err := http.NewRequest("GET", url, nil)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"success": false,
-			"message": err.Error(),
-		})
-		return
-	}
-
-	// 设置认证头(可以复用现有的逻辑)
-	// Create a temporary channel object for header generation
-	tempChannel := &model.Channel{
-		Type: req.Type,
-		Key:  strings.TrimSpace(strings.Split(req.Key, "\n")[0]),
-	}
-	headers := getAuthHeaders(tempChannel)
-	for key, values := range headers {
-		for _, value := range values {
-			request.Header.Add(key, value)
-		}
-	}
-
-	// 发起请求
-	client := &http.Client{}
-	response, err := client.Do(request)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"success": false,
-			"message": err.Error(),
-		})
-		return
-	}
-	defer response.Body.Close()
-
-	// 检查状态码
-	if response.StatusCode != http.StatusOK {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"success": false,
-			"message": fmt.Sprintf("Failed to fetch models, status code: %d", response.StatusCode),
-		})
-		return
-	}
-
-	// 读取响应体
-	body, err := io.ReadAll(response.Body)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"success": false,
-			"message": err.Error(),
-		})
-		return
-	}
+	// Prepare headers (select a single key)
+	tempChannel := &model.Channel{
+		Type: req.Type,
+		Key:  strings.TrimSpace(strings.Split(req.Key, "\n")[0]),
+	}
+	headers := getAuthHeaders(tempChannel)
+
+	// 发起请求(复用统一 HTTP 客户端与错误处理)
+	body, err := GetResponseBody("GET", url, tempChannel, headers)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
 
 	// 解析响应(复用现有逻辑)
 	models, err := parseModelsResponse(req.Type, body)
 	if err != nil {
 		c.JSON(http.StatusInternalServerError, gin.H{
 			"success": false,
 			"message": err.Error(),
 		})
 		return
 	}
♻️ Duplicate comments (3)
controller/channel.go (3)

45-53: Minimal Anthropic structs: good choice

Decoding only IDs reduces coupling and future-proofs unmarshalling.


219-231: Normalize baseURL to avoid double slashes: implemented

Trimming trailing slash prevents accidental “//v1”. Good.


935-943: Reuse helper for base URL: good

FetchModels now delegates to getChannelBaseURL and handles unsupported types cleanly.

🧹 Nitpick comments (2)
controller/channel.go (2)

246-277: Response parsing is sound; consider minimizing OpenAI struct

Parsing Anthropic with minimal structs is great. For OpenAI, unmarshalling into the large OpenAIModel risks breakage if upstream shapes change (e.g., permission). Suggest decoding only ID field.

-		var result OpenAIModelsResponse
+		var result struct {
+			Data []struct {
+				ID string `json:"id"`
+			} `json:"data"`
+		}
 		if err := json.Unmarshal(body, &result); err != nil {
 			return nil, fmt.Errorf("解析OpenAI格式响应失败: %s", err.Error())
 		}
 
 		for _, model := range result.Data {
-			id := model.ID
+			id := model.ID
 			if channelType == constant.ChannelTypeGemini {
 				id = strings.TrimPrefix(id, "models/")
 			}
 			ids = append(ids, id)
 		}

984-990: Optional: return upstream error body for non-200 responses

If you keep manual HTTP handling, consider reading and surfacing the error body to aid debugging (truncate if large). Using GetResponseBody centralizes this.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6be7ebd and b93071e.

📒 Files selected for processing (1)
  • controller/channel.go (7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
controller/channel.go (3)
constant/channel.go (4)
  • ChannelBaseURLs (57-111)
  • ChannelTypeGemini (28-28)
  • ChannelTypeAli (21-21)
  • ChannelTypeAnthropic (18-18)
model/channel.go (1)
  • Channel (20-54)
controller/channel-billing.go (2)
  • GetAuthHeader (124-128)
  • GetResponseBody (130-154)
🔇 Additional comments (5)
controller/channel.go (5)

6-6: Import io is appropriate

Needed for reading response bodies; aligns with new FetchModels logic.


55-85: Multi-key types relocation looks good

Centralizes type declarations and avoids duplication. No functional issues spotted.


207-217: getChannelBaseURL helper is correct

Clear precedence: custom URL > mapped base URL; returns empty for unsupported types so callers can handle gracefully.


219-231: Verify Azure models endpoint needs special handling

If Azure is supported, its models endpoint typically differs (path and required api-version query). Current default returns “.../v1/models”, which may not work. Please confirm and add a ChannelTypeAzure case if needed.

Consider adding:

  • Dedicated case in buildModelsURL for Azure to construct “{endpoint}/openai/models?api-version=” or equivalent.
  • A constant for Azure API version.

Would you like me to draft this once you confirm the expected Azure format in constant.ChannelBaseURLs and supported API version?


292-302: Helper usage in FetchUpstreamModels: good

Using getChannelBaseURL + buildModelsURL clarifies flow; returning a descriptive error on unsupported type is helpful.

Comment thread controller/channel.go Outdated
Comment on lines +232 to +245
// 获取认证头
func getAuthHeaders(channel *model.Channel) http.Header {
switch channel.Type {
case constant.ChannelTypeAnthropic:
headers := make(http.Header)
headers.Set("x-api-key", strings.TrimSpace(channel.Key))
headers.Set("anthropic-version", "2023-06-01")
headers.Set("Content-Type", "application/json")
return headers
default:
return GetAuthHeader(channel.Key)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix multi-key header injection and add Azure header handling

Two issues:

  • Multi-key channels store keys separated by newlines; passing channel.Key directly can produce invalid header values.
  • Azure typically requires api-key header, not Bearer.

Apply:

 func getAuthHeaders(channel *model.Channel) http.Header {
-	switch channel.Type {
+	// Always select a single key to avoid header injection when multi-key is stored as newline-separated.
+	key := strings.TrimSpace(channel.Key)
+	if channel.ChannelInfo.IsMultiKey {
+		keys := channel.GetKeys()
+		if len(keys) > 0 {
+			key = strings.TrimSpace(keys[0])
+		}
+	}
+	switch channel.Type {
 	case constant.ChannelTypeAnthropic:
 		headers := make(http.Header)
-		headers.Set("x-api-key", strings.TrimSpace(channel.Key))
-		headers.Set("anthropic-version", "2023-06-01")
+		headers.Set("x-api-key", key)
+		headers.Set("anthropic-version", constant.AnthropicAPIVersion)
+		headers.Set("Accept", "application/json")
 		headers.Set("Content-Type", "application/json")
 		return headers
+	case constant.ChannelTypeAzure:
+		headers := make(http.Header)
+		headers.Set("api-key", key)
+		headers.Set("Accept", "application/json")
+		return headers
 	default:
-		return GetAuthHeader(channel.Key)
+		return GetAuthHeader(key)
 	}
 }

And add the constant below in constant/channel.go:

// Centralize Anthropic API version here for easier upgrades
const AnthropicAPIVersion = "2023-06-01"
🤖 Prompt for AI Agents
In controller/channel.go around lines 232 to 245, the current getAuthHeaders
passes channel.Key directly and always sets the Anthropic version literal;
update it to split channel.Key on newlines, trim and use only the first
non-empty key to avoid multi-key header injection, replace the hard-coded
Anthropic version string with constant.AnthropicAPIVersion, and add Azure
handling branch that sets "api-key" header (not "Authorization: Bearer") when
channel.Type indicates Azure; also add the new constant AnthropicAPIVersion =
"2023-06-01" to constant/channel.go as requested.

Comment thread controller/channel.go Outdated
Comment on lines +306 to +320
headers := getAuthHeaders(channel)

// 发起请求
body, err := GetResponseBody("GET", url, channel, headers)
if err != nil {
common.ApiError(c, err)
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": fmt.Sprintf("请求失败: %s", err.Error()),
})
return
}

var result OpenAIModelsResponse
if err = json.Unmarshal(body, &result); err != nil {
// 解析响应
ids, err := parseModelsResponse(channel.Type, body)
if err != nil {

@coderabbitai coderabbitai Bot Aug 11, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Headers for multi-key channels may be invalid in this path

getAuthHeaders takes channel as-is; without the earlier fix to select a single key, multi-key channels will set headers with embedded newlines and fail. Please adopt the getAuthHeaders change suggested above.

🤖 Prompt for AI Agents
In controller/channel.go around lines 306 to 320, the code calls
getAuthHeaders(channel) directly which will produce invalid headers for
multi-key channels (embedded newlines); before calling getAuthHeaders, select or
extract a single API key using the same helper/fix applied elsewhere (e.g., the
helper that picks one key from multi-key channels) and pass the sanitized
channel or the selected key to getAuthHeaders so headers are built from a
single-key channel and do not contain newlines.

✅ Addressed in commit 1465503

@wans10 wans10 closed this Aug 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant