chore: import Local-Cursor v1.1.8 source snapshot
Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage. Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
This commit is contained in:
136
internal/client/models_fetch.go
Normal file
136
internal/client/models_fetch.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cursor/internal/modelchannel"
|
||||
)
|
||||
|
||||
// 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)
|
||||
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 := 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
|
||||
}
|
||||
Reference in New Issue
Block a user