Repository navigation
支持 Anthropic API 拉取模型列表(新结构体和认证头处理) #1546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package controller | |
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "one-api/common" | ||
| "one-api/constant" | ||
|
|
@@ -41,6 +42,47 @@ type OpenAIModelsResponse struct { | |
| Success bool `json:"success"` | ||
| } | ||
|
|
||
| // AnthropicModel 定义Anthropic API返回的模型结构(仅保留必要字段,降低耦合与解析失败风险) | ||
| type AnthropicModel struct { | ||
| ID string `json:"id"` | ||
| } | ||
|
|
||
| // AnthropicModelsResponse 定义Anthropic API响应结构(仅保留必要字段) | ||
| type AnthropicModelsResponse struct { | ||
| Data []AnthropicModel `json:"data"` | ||
| } | ||
|
|
||
| // MultiKeyManageRequest represents the request for multi-key management operations | ||
| type MultiKeyManageRequest struct { | ||
| ChannelId int `json:"channel_id"` | ||
| Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status" | ||
| KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions | ||
| Page int `json:"page,omitempty"` // for get_key_status pagination | ||
| PageSize int `json:"page_size,omitempty"` // for get_key_status pagination | ||
| Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all | ||
| } | ||
|
|
||
| // MultiKeyStatusResponse represents the response for key status query | ||
| type MultiKeyStatusResponse struct { | ||
| Keys []KeyStatus `json:"keys"` | ||
| Total int `json:"total"` | ||
| Page int `json:"page"` | ||
| PageSize int `json:"page_size"` | ||
| TotalPages int `json:"total_pages"` | ||
| // Statistics | ||
| EnabledCount int `json:"enabled_count"` | ||
| ManualDisabledCount int `json:"manual_disabled_count"` | ||
| AutoDisabledCount int `json:"auto_disabled_count"` | ||
| } | ||
|
|
||
| type KeyStatus struct { | ||
| Index int `json:"index"` | ||
| Status int `json:"status"` // 1: enabled, 2: disabled | ||
| DisabledTime int64 `json:"disabled_time,omitempty"` | ||
| Reason string `json:"reason,omitempty"` | ||
| KeyPreview string `json:"key_preview"` // first 10 chars of key for identification | ||
| } | ||
|
|
||
| func parseStatusFilter(statusParam string) int { | ||
| switch strings.ToLower(statusParam) { | ||
| case "enabled", "1": | ||
|
|
@@ -162,6 +204,78 @@ func GetAllChannels(c *gin.Context) { | |
| return | ||
| } | ||
|
|
||
| func getChannelBaseURL(channelType int, customURL string) string { | ||
| if customURL != "" { | ||
| return customURL | ||
| } | ||
|
|
||
| if channelType >= 0 && channelType < len(constant.ChannelBaseURLs) { | ||
| return constant.ChannelBaseURLs[channelType] | ||
| } | ||
|
|
||
| return "" // 返回空字符串,让调用方处理 | ||
| } | ||
|
|
||
| // 构建API端点URL | ||
| func buildModelsURL(channelType int, baseURL string) string { | ||
| baseURL = strings.TrimRight(baseURL, "/") | ||
| switch channelType { | ||
| case constant.ChannelTypeGemini: | ||
| return fmt.Sprintf("%s/v1beta/openai/models", baseURL) | ||
| case constant.ChannelTypeAli: | ||
| return fmt.Sprintf("%s/compatible-mode/v1/models", baseURL) | ||
| default: | ||
| return fmt.Sprintf("%s/v1/models", baseURL) | ||
| } | ||
| } | ||
|
|
||
| // 获取认证头 | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix multi-key header injection and add Azure header handling Two issues:
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 |
||
| // 解析不同类型的模型响应 | ||
| func parseModelsResponse(channelType int, body []byte) ([]string, error) { | ||
| var ids []string | ||
|
|
||
| switch channelType { | ||
| case constant.ChannelTypeAnthropic: | ||
| var anthropicResult AnthropicModelsResponse | ||
| if err := json.Unmarshal(body, &anthropicResult); err != nil { | ||
| return nil, fmt.Errorf("解析Anthropic响应失败: %s", err.Error()) | ||
| } | ||
|
|
||
| for _, model := range anthropicResult.Data { | ||
| ids = append(ids, model.ID) | ||
| } | ||
|
|
||
| default: | ||
| var result OpenAIModelsResponse | ||
| if err := json.Unmarshal(body, &result); err != nil { | ||
| return nil, fmt.Errorf("解析OpenAI格式响应失败: %s", err.Error()) | ||
| } | ||
|
|
||
| for _, model := range result.Data { | ||
| id := model.ID | ||
| if channelType == constant.ChannelTypeGemini { | ||
| id = strings.TrimPrefix(id, "models/") | ||
| } | ||
| ids = append(ids, id) | ||
| } | ||
| } | ||
|
|
||
| return ids, nil | ||
| } | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| func FetchUpstreamModels(c *gin.Context) { | ||
| id, err := strconv.Atoi(c.Param("id")) | ||
| if err != nil { | ||
|
|
@@ -175,53 +289,42 @@ func FetchUpstreamModels(c *gin.Context) { | |
| return | ||
| } | ||
|
|
||
| baseURL := constant.ChannelBaseURLs[channel.Type] | ||
| if channel.GetBaseURL() != "" { | ||
| baseURL = channel.GetBaseURL() | ||
| // 安全获取基础URL | ||
| baseURL := getChannelBaseURL(channel.Type, channel.GetBaseURL()) | ||
| if baseURL == "" { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": fmt.Sprintf("不支持的渠道类型: %d", channel.Type), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| var url string | ||
| switch channel.Type { | ||
| case constant.ChannelTypeGemini: | ||
| // curl https://example.com/v1beta/models?key=$GEMINI_API_KEY | ||
| url = fmt.Sprintf("%s/v1beta/openai/models", baseURL) // Remove key in url since we need to use AuthHeader | ||
| case constant.ChannelTypeAli: | ||
| url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL) | ||
| default: | ||
| url = fmt.Sprintf("%s/v1/models", baseURL) | ||
| } | ||
| // 构建请求URL | ||
| url := buildModelsURL(channel.Type, baseURL) | ||
|
|
||
| // 获取响应体 - 根据渠道类型决定是否添加 AuthHeader | ||
| var body []byte | ||
| key := strings.Split(channel.Key, "\n")[0] | ||
| if channel.Type == constant.ChannelTypeGemini { | ||
| body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key)) // Use AuthHeader since Gemini now forces it | ||
| } else { | ||
| body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key)) | ||
| } | ||
| // 获取认证头 | ||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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✅ Addressed in commit 1465503 |
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": fmt.Sprintf("解析响应失败: %s", err.Error()), | ||
| "message": err.Error(), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| var ids []string | ||
| for _, model := range result.Data { | ||
| id := model.ID | ||
| if channel.Type == constant.ChannelTypeGemini { | ||
| id = strings.TrimPrefix(id, "models/") | ||
| } | ||
| ids = append(ids, id) | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "message": "", | ||
|
|
@@ -829,14 +932,20 @@ func FetchModels(c *gin.Context) { | |
| return | ||
| } | ||
|
|
||
| baseURL := req.BaseURL | ||
| // 获取基础URL(复用helper) | ||
| baseURL := getChannelBaseURL(req.Type, req.BaseURL) | ||
| if baseURL == "" { | ||
| baseURL = constant.ChannelBaseURLs[req.Type] | ||
| c.JSON(http.StatusBadRequest, gin.H{ | ||
| "success": false, | ||
| "message": "Unsupported channel type", | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| client := &http.Client{} | ||
| url := fmt.Sprintf("%s/v1/models", baseURL) | ||
| // 构建请求URL(复用现有逻辑) | ||
| url := buildModelsURL(req.Type, baseURL) | ||
|
|
||
| // 创建HTTP请求 | ||
| request, err := http.NewRequest("GET", url, nil) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
|
|
@@ -846,12 +955,21 @@ func FetchModels(c *gin.Context) { | |
| return | ||
| } | ||
|
|
||
| // remove line breaks and extra spaces. | ||
| key := strings.TrimSpace(req.Key) | ||
| // If the key contains a line break, only take the first part. | ||
| key = strings.Split(key, "\n")[0] | ||
| request.Header.Set("Authorization", "Bearer "+key) | ||
| // 设置认证头(可以复用现有的逻辑) | ||
| // 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{ | ||
|
|
@@ -860,33 +978,35 @@ func FetchModels(c *gin.Context) { | |
| }) | ||
| return | ||
| } | ||
| //check status code | ||
| defer response.Body.Close() | ||
|
|
||
| // 检查状态码 | ||
| if response.StatusCode != http.StatusOK { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
| "success": false, | ||
| "message": "Failed to fetch models", | ||
| "message": fmt.Sprintf("Failed to fetch models, status code: %d", response.StatusCode), | ||
| }) | ||
| return | ||
| } | ||
| defer response.Body.Close() | ||
|
|
||
| var result struct { | ||
| Data []struct { | ||
| ID string `json:"id"` | ||
| } `json:"data"` | ||
| } | ||
|
|
||
| if err := json.NewDecoder(response.Body).Decode(&result); err != nil { | ||
| // 读取响应体 | ||
| body, err := io.ReadAll(response.Body) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| var models []string | ||
| for _, model := range result.Data { | ||
| models = append(models, model.ID) | ||
| // 解析响应(复用现有逻辑) | ||
| models, err := parseModelsResponse(req.Type, body) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
|
|
@@ -1010,37 +1130,6 @@ func CopyChannel(c *gin.Context) { | |
| c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": gin.H{"id": clone.Id}}) | ||
| } | ||
|
|
||
| // MultiKeyManageRequest represents the request for multi-key management operations | ||
| type MultiKeyManageRequest struct { | ||
| ChannelId int `json:"channel_id"` | ||
| Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status" | ||
| KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions | ||
| Page int `json:"page,omitempty"` // for get_key_status pagination | ||
| PageSize int `json:"page_size,omitempty"` // for get_key_status pagination | ||
| Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all | ||
| } | ||
|
|
||
| // MultiKeyStatusResponse represents the response for key status query | ||
| type MultiKeyStatusResponse struct { | ||
| Keys []KeyStatus `json:"keys"` | ||
| Total int `json:"total"` | ||
| Page int `json:"page"` | ||
| PageSize int `json:"page_size"` | ||
| TotalPages int `json:"total_pages"` | ||
| // Statistics | ||
| EnabledCount int `json:"enabled_count"` | ||
| ManualDisabledCount int `json:"manual_disabled_count"` | ||
| AutoDisabledCount int `json:"auto_disabled_count"` | ||
| } | ||
|
|
||
| type KeyStatus struct { | ||
| Index int `json:"index"` | ||
| Status int `json:"status"` // 1: enabled, 2: disabled | ||
| DisabledTime int64 `json:"disabled_time,omitempty"` | ||
| Reason string `json:"reason,omitempty"` | ||
| KeyPreview string `json:"key_preview"` // first 10 chars of key for identification | ||
| } | ||
|
|
||
| // ManageMultiKeys handles multi-key management operations | ||
| func ManageMultiKeys(c *gin.Context) { | ||
| request := MultiKeyManageRequest{} | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.