¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档 д Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£ Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£ Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢ Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£ Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
140 lines
4.4 KiB
Go
140 lines
4.4 KiB
Go
package client
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"cursor/internal/logger"
|
||
"cursor/internal/modelchannel"
|
||
"cursor/internal/netproxy"
|
||
)
|
||
|
||
// RemoteModel 表示从模型服务 /models 端点拉取到的一个模型。
|
||
type RemoteModel struct {
|
||
ID string `json:"id"`
|
||
}
|
||
|
||
// RemoteModelsResult 表示一次模型列表拉取结果。
|
||
type RemoteModelsResult struct {
|
||
Models []RemoteModel `json:"models"`
|
||
}
|
||
|
||
const remoteModelsFetchTimeout = 30 * time.Second
|
||
|
||
// ListRemoteModels 通过 openai/anthropic 兼容的 /v1/models 端点拉取模型列表。
|
||
func (s *ProxyService) ListRemoteModels(adapterType string, baseURL string, apiKey string) (RemoteModelsResult, error) {
|
||
result := RemoteModelsResult{Models: []RemoteModel{}}
|
||
if s == nil {
|
||
return result, fmt.Errorf("服务未初始化")
|
||
}
|
||
adapterType = strings.ToLower(strings.TrimSpace(adapterType))
|
||
baseURL = strings.TrimSpace(baseURL)
|
||
apiKey = strings.TrimSpace(apiKey)
|
||
logger.RegisterSecrets(apiKey)
|
||
if adapterType != "openai" && adapterType != "anthropic" {
|
||
return result, fmt.Errorf("模型类型仅支持 openai 或 anthropic")
|
||
}
|
||
if baseURL == "" {
|
||
return result, fmt.Errorf("接口地址不能为空")
|
||
}
|
||
if apiKey == "" {
|
||
return result, fmt.Errorf("访问密钥不能为空")
|
||
}
|
||
normalizedBaseURL, err := modelchannel.NormalizeBaseURL(baseURL)
|
||
if err != nil {
|
||
return result, err
|
||
}
|
||
|
||
endpoint := buildModelsEndpoint(adapterType, normalizedBaseURL)
|
||
ctx, cancel := context.WithTimeout(context.Background(), remoteModelsFetchTimeout)
|
||
defer cancel()
|
||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||
if err != nil {
|
||
return result, fmt.Errorf("构建模型列表请求失败: %w", err)
|
||
}
|
||
if adapterType == "openai" {
|
||
request.Header.Set("Authorization", "Bearer "+apiKey)
|
||
} else {
|
||
request.Header.Set("x-api-key", apiKey)
|
||
request.Header.Set("anthropic-version", "2023-06-01")
|
||
}
|
||
request.Header.Set("Accept", "application/json")
|
||
|
||
response, err := netproxy.CredentialSafeClient(http.DefaultClient).Do(request)
|
||
if err != nil {
|
||
return result, fmt.Errorf("请求模型列表失败: %w", err)
|
||
}
|
||
defer response.Body.Close()
|
||
|
||
body, err := io.ReadAll(io.LimitReader(response.Body, 8<<20))
|
||
if err != nil {
|
||
return result, fmt.Errorf("读取模型列表响应失败: %w", err)
|
||
}
|
||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
return result, fmt.Errorf("模型列表接口返回 %d: %s", response.StatusCode, strings.TrimSpace(string(body)))
|
||
}
|
||
|
||
models, err := decodeRemoteModels(adapterType, body)
|
||
if err != nil {
|
||
return result, err
|
||
}
|
||
result.Models = models
|
||
return result, nil
|
||
}
|
||
|
||
// buildModelsEndpoint 把规范化后的 baseURL 解析为模型列表端点,
|
||
// 与聊天转发的 URL 解析保持同样的幂等口径:
|
||
// - baseURL 以 /v1 结尾:仅追加 /models,避免拼出 /v1/v1/models
|
||
// - openai baseURL 内嵌完整聊天端点(/v1/responses、/v1/chat/completions):仅剥离方法段,端点回退为 /v1/models
|
||
// - 其余情况(裸 host、官方 anthropic 地址):追加 /v1/models
|
||
func buildModelsEndpoint(adapterType string, normalizedBaseURL string) string {
|
||
base := strings.TrimRight(normalizedBaseURL, "/")
|
||
lower := strings.ToLower(base)
|
||
if adapterType == "openai" {
|
||
for _, endpoint := range []string{modelchannel.OpenAIEndpointChatCompletions, modelchannel.OpenAIEndpointResponses} {
|
||
methodPath := strings.TrimPrefix(strings.ToLower(endpoint), "/v1")
|
||
if methodPath != "" && strings.HasSuffix(lower, methodPath) {
|
||
return base[:len(base)-len(methodPath)] + "/models"
|
||
}
|
||
}
|
||
}
|
||
if strings.HasSuffix(lower, "/v1") {
|
||
return base + "/models"
|
||
}
|
||
return base + "/v1/models"
|
||
}
|
||
|
||
func decodeRemoteModels(adapterType string, body []byte) ([]RemoteModel, error) {
|
||
var payload struct {
|
||
Data []struct {
|
||
ID string `json:"id"`
|
||
} `json:"data"`
|
||
}
|
||
if err := json.Unmarshal(body, &payload); err != nil {
|
||
return nil, fmt.Errorf("解析模型列表响应失败: %w", err)
|
||
}
|
||
seen := make(map[string]struct{}, len(payload.Data))
|
||
models := make([]RemoteModel, 0, len(payload.Data))
|
||
for _, item := range payload.Data {
|
||
id := strings.TrimSpace(item.ID)
|
||
if id == "" {
|
||
continue
|
||
}
|
||
if _, exists := seen[id]; exists {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
models = append(models, RemoteModel{ID: id})
|
||
}
|
||
if len(models) == 0 {
|
||
return nil, fmt.Errorf("模型列表为空,请检查接口地址与密钥")
|
||
}
|
||
return models, nil
|
||
}
|