Files
ttstd-infinite-canvas/backend/internal/service/channel_model_catalog.go

135 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strings"
"infinite-canvas/backend/internal/model"
)
type ChannelModelsRequest struct {
BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"`
APIFormat string `json:"apiFormat"`
Headers []OutboundHeader `json:"headers"`
}
type channelModelsPayload struct {
Data []channelModelItem `json:"data"`
Models []channelModelItem `json:"models"`
Error *providerError `json:"error"`
Code *int `json:"code"`
Msg string `json:"msg"`
}
type channelModelItem struct {
ID string `json:"id"`
Name string `json:"name"`
}
func (s *Service) FetchChannelModels(ctx context.Context, actor *model.User, input ChannelModelsRequest) ([]string, error) {
if actor == nil || strings.TrimSpace(actor.ID) == "" {
return nil, Unauthorized("请先登录")
}
baseURL := strings.TrimRight(strings.TrimSpace(input.BaseURL), "/")
apiKey := strings.TrimSpace(input.APIKey)
if baseURL == "" {
return nil, BadAuthRequest("请填写 Base URL")
}
if apiKey == "" {
return nil, BadAuthRequest("请填写 API Key")
}
apiFormat := strings.ToLower(strings.TrimSpace(input.APIFormat))
if apiFormat == "" {
apiFormat = "openai"
}
if apiFormat != "openai" && apiFormat != "gemini" {
return nil, BadAuthRequest("接口协议不支持拉取模型")
}
headers, err := NormalizeOutboundHeaders(input.Headers)
if err != nil {
return nil, err
}
target := apiURL(baseURL, "/models")
if apiFormat == "gemini" {
if !strings.HasSuffix(strings.ToLower(baseURL), "/v1beta") {
baseURL += "/v1beta"
}
target = baseURL + "/models"
}
if _, err := ValidateOutboundURL(target); err != nil {
return nil, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
return nil, BadAuthRequest("模型服务地址无效")
}
if apiFormat == "gemini" {
request.Header.Set("x-goog-api-key", apiKey)
} else {
request.Header.Set("Authorization", "Bearer "+apiKey)
}
ApplyOutboundHeaders(request, headers)
// 只代理固定的模型目录 GET;用户密钥仅用于本次请求,不写入数据库或日志。
data, _, err := doBinary(request)
if err != nil {
return nil, channelModelsUpstreamError(err)
}
var payload channelModelsPayload
if err := json.Unmarshal(data, &payload); err != nil {
return nil, &AuthError{Status: http.StatusBadGateway, Message: "模型服务返回的不是有效 JSON"}
}
if payload.Error != nil && strings.TrimSpace(payload.Error.Message) != "" {
return nil, &AuthError{Status: http.StatusBadGateway, Message: payload.Error.Message}
}
if payload.Code != nil && *payload.Code != 0 {
return nil, &AuthError{Status: http.StatusBadGateway, Message: firstNonEmpty(strings.TrimSpace(payload.Msg), "模型服务返回失败")}
}
items := payload.Data
if apiFormat == "gemini" {
items = payload.Models
}
seen := make(map[string]bool, len(items))
models := make([]string, 0, len(items))
for _, item := range items {
name := strings.TrimPrefix(strings.TrimSpace(firstNonEmpty(item.ID, item.Name)), "models/")
if name == "" || seen[name] {
continue
}
seen[name] = true
models = append(models, name)
}
sort.Strings(models)
return models, nil
}
func channelModelsUpstreamError(err error) error {
var authErr *AuthError
if errors.As(err, &authErr) {
return authErr
}
var httpErr providerHTTPError
if !errors.As(err, &httpErr) {
return &AuthError{Status: http.StatusBadGateway, Message: "连接模型服务失败:" + err.Error()}
}
switch httpErr.StatusCode {
case http.StatusUnauthorized, http.StatusForbidden:
return &AuthError{Status: http.StatusBadGateway, Message: "模型服务鉴权失败,请检查 API Key"}
case http.StatusNotFound:
return &AuthError{Status: http.StatusBadGateway, Message: "模型服务未提供 /models 接口"}
case http.StatusTooManyRequests:
return &AuthError{Status: http.StatusBadGateway, Message: "模型服务请求过于频繁或额度不足"}
default:
return &AuthError{Status: http.StatusBadGateway, Message: fmt.Sprintf("模型服务请求失败:HTTP %d", httpErr.StatusCode)}
}
}