支持 Anthropic API 拉取模型列表(新结构体和认证头处理) - #1546
Conversation
重构 FetchUpstreamModels 和 FetchModels,通过辅助函数(getChannelBaseURL、buildModelsURL、getAuthHeaders、parseModelsResponse)提高了代码复用性和可维护性。 新 FetchModels 改进了错误处理(更明确的错误信息)和类型检查。
|
Caution Review failedThe pull request is closed. WalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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 optionalCurrent 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 robustnessLooks 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 modularGood 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
📒 Files selected for processing (1)
controller/channel.go(7 hunks)
🔇 Additional comments (3)
controller/channel.go (3)
6-6: Import usage is correctio is used for io.ReadAll below. No issues.
53-60: AnthropicModelsResponse looks correctMatches 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 detectedI ran a project-wide search and confirmed that
MultiKeyManageRequest,MultiKeyStatusResponse, andKeyStatusare only declared incontroller/channel.go. There are no leftover duplicates elsewhere. All set!
使用已有的 getChannelBaseURL 函数,避免重复实现相同的逻辑
There was a problem hiding this comment.
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 valuesComments 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 URLSmall 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
📒 Files selected for processing (1)
controller/channel.go(7 hunks)
🔇 Additional comments (3)
controller/channel.go (3)
225-237: Resolved: trailing-slash normalization prevents // pathsGood call adding TrimRight to avoid double slashes. This addresses prior feedback and makes URL building predictable.
298-333: FetchUpstreamModels: solid refactor and clearer errorsGood reuse of helpers (base URL, URL builder, auth headers, response parsing). Error messages are more actionable. LGTM.
941-949: Duplication removed: getChannelBaseURL reusedThis addresses earlier duplication concerns in FetchModels. Good improvement for maintainability.
There was a problem hiding this comment.
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/proxyPrefer 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 choiceDecoding only IDs reduces coupling and future-proofs unmarshalling.
219-231: Normalize baseURL to avoid double slashes: implementedTrimming trailing slash prevents accidental “//v1”. Good.
935-943: Reuse helper for base URL: goodFetchModels 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 structParsing 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 responsesIf 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
📒 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 appropriateNeeded for reading response bodies; aligns with new FetchModels logic.
55-85: Multi-key types relocation looks goodCentralizes type declarations and avoids duplication. No functional issues spotted.
207-217: getChannelBaseURL helper is correctClear precedence: custom URL > mapped base URL; returns empty for unsupported types so callers can handle gracefully.
219-231: Verify Azure models endpoint needs special handlingIf 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: goodUsing getChannelBaseURL + buildModelsURL clarifies flow; returning a descriptive error on unsupported type is helpful.
| // 获取认证头 | ||
| 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) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
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.
| 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 { |
There was a problem hiding this comment.
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
增加对 Anthropic API 的支持(新结构体和认证头处理)。
新增专用请求函数(GetAnthropicResponseBody)专门处理 Anthropic API 请求
Summary by CodeRabbit
New Features
Improvements