fix: 转发添加新模型和模型提供商

This commit is contained in:
npc0-hue
2026-04-23 14:49:29 +08:00
parent 0af791f56c
commit 3a02769e4a
5 changed files with 359 additions and 36 deletions
@@ -9,6 +9,7 @@ const (
ProviderAzure = "azure"
ProviderZhipuai = "zhipuai"
ProviderMinimax = "minimax"
ProviderAWS = "aws" // AWS Bedrock 渠道(用于转发 Claude 等 Anthropic 模型)
)
// DifyProviderTypeCustom Dify providers 表 provider_type 枚举
@@ -16,15 +17,15 @@ const DifyProviderTypeCustom = "custom"
// 凭证配置中的 key 名
const (
ConfigKeyOpenaiAPIKey = "openai_api_key"
ConfigKeyOpenaiAPIBase = "openai_api_base"
ConfigKeyOpenaiAPIVersion = "openai_api_version"
ConfigKeyDashScopeAPIKey = "dashscope_api_key"
ConfigKeyAPIKey = "api_key"
ConfigKeyOpenaiAPIKey = "openai_api_key"
ConfigKeyOpenaiAPIBase = "openai_api_base"
ConfigKeyOpenaiAPIVersion = "openai_api_version"
ConfigKeyDashScopeAPIKey = "dashscope_api_key"
ConfigKeyAPIKey = "api_key"
)
// SupportedProviders 列表展示的提供商顺序
var SupportedProviders = []string{ProviderOpenai, ProviderTongyi, ProviderGoogle, ProviderAnthropic, ProviderAzure, ProviderZhipuai, ProviderMinimax}
var SupportedProviders = []string{ProviderOpenai, ProviderTongyi, ProviderGoogle, ProviderAnthropic, ProviderAWS, ProviderAzure, ProviderZhipuai, ProviderMinimax}
// DefaultChatCompletionsEndpoints 各提供商聊天接口默认完整 URL(兼容旧 ProxyChat
var DefaultChatCompletionsEndpoints = map[string]string{
@@ -42,6 +43,7 @@ var DefaultAPIBase = map[string]string{
ProviderTongyi: "https://dashscope.aliyuncs.com/compatible-mode",
ProviderGoogle: "https://generativelanguage.googleapis.com",
ProviderAnthropic: "https://api.anthropic.com",
ProviderAWS: "https://bedrock-runtime.us-east-1.amazonaws.com",
ProviderZhipuai: "https://open.bigmodel.cn",
ProviderMinimax: "https://api.minimax.chat",
// Azure 的 base URL 来自 openai_api_base 配置,不设置默认值
@@ -73,27 +75,51 @@ const (
// - 输入/输出价格均为「每百万 token」,换算为每千 token 时除以 1000。
var BuiltinModelPricing = map[string]ModelPricing{
// ──── 通义千问 Qwen3 系列(RMB / 百万 token128K 档) ────
"qwen3-235b-a22b": {Input: 0.4 / 1000, Output: 1.6 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-30b-a3b": {Input: 0.11 / 1000, Output: 0.44 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-32b": {Input: 0.8 / 1000, Output: 3.2 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-14b": {Input: 0.3 / 1000, Output: 1.2 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-8b": {Input: 0.1 / 1000, Output: 0.4 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-4b": {Input: 0.04 / 1000, Output: 0.16 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-1.7b": {Input: 0.02 / 1000, Output: 0.08 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-0.6b": {Input: 0.01 / 1000, Output: 0.04 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-235b-a22b": {Input: 0.4 / 1000, Output: 1.6 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-30b-a3b": {Input: 0.11 / 1000, Output: 0.44 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-32b": {Input: 0.8 / 1000, Output: 3.2 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-14b": {Input: 0.3 / 1000, Output: 1.2 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-8b": {Input: 0.1 / 1000, Output: 0.4 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-4b": {Input: 0.04 / 1000, Output: 0.16 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-1.7b": {Input: 0.02 / 1000, Output: 0.08 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3-0.6b": {Input: 0.01 / 1000, Output: 0.04 / 1000, Unit: 0.001, Currency: "RMB"},
// ──── 通义千问 Qwen3.5 系列(RMB / 百万 token128K 档) ────
"qwen3.5-plus": {Input: 0.8 / 1000, Output: 4.8 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3.5-turbo": {Input: 0.3 / 1000, Output: 1.2 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3.5-plus": {Input: 0.8 / 1000, Output: 4.8 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen3.5-turbo": {Input: 0.3 / 1000, Output: 1.2 / 1000, Unit: 0.001, Currency: "RMB"},
// ──── 通义千问 Qwen2.5 系列(RMB / 百万 token ────
"qwen2.5-72b-instruct": {Input: 4.0 / 1000, Output: 12.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-32b-instruct": {Input: 3.5 / 1000, Output: 7.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-14b-instruct": {Input: 2.0 / 1000, Output: 6.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-7b-instruct": {Input: 1.0 / 1000, Output: 2.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-3b-instruct": {Input: 0.3 / 1000, Output: 0.6 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-plus": {Input: 0.8 / 1000, Output: 2.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-turbo": {Input: 0.3 / 1000, Output: 0.6 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-max": {Input: 40.0 / 1000, Output: 120.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-long": {Input: 0.5 / 1000, Output: 2.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-72b-instruct": {Input: 4.0 / 1000, Output: 12.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-32b-instruct": {Input: 3.5 / 1000, Output: 7.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-14b-instruct": {Input: 2.0 / 1000, Output: 6.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-7b-instruct": {Input: 1.0 / 1000, Output: 2.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen2.5-3b-instruct": {Input: 0.3 / 1000, Output: 0.6 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-plus": {Input: 0.8 / 1000, Output: 2.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-turbo": {Input: 0.3 / 1000, Output: 0.6 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-max": {Input: 40.0 / 1000, Output: 120.0 / 1000, Unit: 0.001, Currency: "RMB"},
"qwen-long": {Input: 0.5 / 1000, Output: 2.0 / 1000, Unit: 0.001, Currency: "RMB"},
// ──── 月之暗面 Kimi 系列(RMB / 百万 token ────
// Kimi 走 tongyi(百炼)渠道转发,命名沿用 kimi 前缀;前缀匹配会让 kimi2-k2.6-xxx 也命中 kimi2-k2.6
"kimi2-k2.6": {Input: 4.0 / 1000, Output: 16.0 / 1000, Unit: 0.001, Currency: "RMB"},
"moonshot-v1-8k": {Input: 12.0 / 1000, Output: 12.0 / 1000, Unit: 0.001, Currency: "RMB"},
"moonshot-v1-32k": {Input: 24.0 / 1000, Output: 24.0 / 1000, Unit: 0.001, Currency: "RMB"},
"moonshot-v1-128k": {Input: 60.0 / 1000, Output: 60.0 / 1000, Unit: 0.001, Currency: "RMB"},
// ──── Anthropic Claude 系列(USD / 百万 token ────
// Claude 4.6 / 4.7 系列(Sonnet 与 Opus);anthropic 直连与 AWS Bedrock 走同一份定价
"claude-sonnet-4-6": {Input: 3.0 / 1000, Output: 15.0 / 1000, Unit: 0.001, Currency: "USD"},
"claude-sonnet-4-7": {Input: 3.0 / 1000, Output: 15.0 / 1000, Unit: 0.001, Currency: "USD"},
"claude-opus-4-6": {Input: 15.0 / 1000, Output: 75.0 / 1000, Unit: 0.001, Currency: "USD"},
"claude-opus-4-7": {Input: 15.0 / 1000, Output: 75.0 / 1000, Unit: 0.001, Currency: "USD"},
// AWS Bedrock 上常用的模型 ID 形式(带 anthropic. 前缀与 -v1:0 后缀),单独列出避免前缀匹配漂移
"anthropic.claude-sonnet-4-6-v1:0": {Input: 3.0 / 1000, Output: 15.0 / 1000, Unit: 0.001, Currency: "USD"},
"anthropic.claude-sonnet-4-7-v1:0": {Input: 3.0 / 1000, Output: 15.0 / 1000, Unit: 0.001, Currency: "USD"},
"anthropic.claude-opus-4-6-v1:0": {Input: 15.0 / 1000, Output: 75.0 / 1000, Unit: 0.001, Currency: "USD"},
"anthropic.claude-opus-4-7-v1:0": {Input: 15.0 / 1000, Output: 75.0 / 1000, Unit: 0.001, Currency: "USD"},
// ──── OpenAI 图片生成(按次计费,Input 字段表示「每次请求的 USD 单价」) ────
// 命中分支见 service/gaia/model_provider.go 中的 isImageOrPerRequestPath 与 ProxyRequest 计费逻辑
"gpt-image-1": {Input: 0.04, Currency: "USD"},
"gpt-image-2": {Input: 0.05, Currency: "USD"},
}
@@ -5,6 +5,12 @@ type ProviderCredentials struct {
APIKey string `json:"api_key"`
Endpoint string `json:"endpoint,omitempty"`
APIVersion string `json:"api_version,omitempty"` // Azure OpenAI API 版本
// AWS Bedrock 直连用:access key + secret + region(不走 APIKey/Endpoint
AWSAccessKeyID string `json:"aws_access_key_id,omitempty"`
AWSSecretAccessKey string `json:"aws_secret_access_key,omitempty"`
AWSSessionToken string `json:"aws_session_token,omitempty"`
AWSRegion string `json:"aws_region,omitempty"`
}
// ModelInfo 模型信息
@@ -40,10 +46,10 @@ type OpenAIModelsListResponse struct {
type TongyiModelsListResponse struct {
Success bool `json:"success"`
Output struct {
Total int `json:"total"`
PageNo int `json:"page_no"`
PageSize int `json:"page_size"`
Models []TongyiModelItem `json:"models"`
Total int `json:"total"`
PageNo int `json:"page_no"`
PageSize int `json:"page_size"`
Models []TongyiModelItem `json:"models"`
} `json:"output"`
}
@@ -55,8 +61,8 @@ type TongyiModelItem struct {
// GeminiModelsListResponse Google Gemini GET /v1beta/models 返回:models[] + nextPageToken
type GeminiModelsListResponse struct {
Models []GeminiModelItem `json:"models"`
NextPageToken string `json:"nextPageToken"`
Models []GeminiModelItem `json:"models"`
NextPageToken string `json:"nextPageToken"`
}
// GeminiModelItem Gemini 模型单项,name 为 "models/gemini-xxx"baseModelId 用于请求
+273
View File
@@ -0,0 +1,273 @@
package gaia
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/credentials"
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
estream "github.com/aws/aws-sdk-go/private/protocol/eventstream"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
gaiaResponse "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/response"
"go.uber.org/zap"
)
// proxyBedrockRequest 直连 AWS Bedrock 原生 API 转发 Anthropic Messages 请求。
//
// 路径转换:v1/messages → model/{modelId}/invoke 或 model/{modelId}/invoke-with-response-stream
// 鉴权:SigV4service=bedrockregion 来自 Dify 凭证 aws_region 字段)
// 请求体改写:去掉 model 字段(Bedrock 走 URL 路径),注入 anthropic_version=bedrock-2023-05-31
// 响应:
// - 非流式:Bedrock 返回 Anthropic Messages JSON(含 usage.input_tokens/output_tokens),原样转发
// - 流式:vnd.amazon.eventstream 二进制帧,每帧 payload 是 {"bytes":"<base64>"}
// 解出后是 Anthropic SSE 事件 JSON,在此重组为标准 SSE 写回客户端
//
// 计费:成功后按 (input_tokens, output_tokens) 调 calcQuotaDelta 扣额。
func (s *ModelProviderService) proxyBedrockRequest(
userID, _ /* path */, method string, _ /* reqHeader */ http.Header, body []byte, writer io.Writer,
creds *gaiaResponse.ProviderCredentials,
) error {
// 1) 校验 AWS 凭证
if creds == nil || creds.AWSAccessKeyID == "" || creds.AWSSecretAccessKey == "" {
return fmt.Errorf("AWS Bedrock 凭证缺失(需要 aws_access_key_id / aws_secret_access_key")
}
region := creds.AWSRegion
if region == "" {
region = "us-east-1"
}
// 2) 解析 body:拿到 modelId 与 stream 标记,并改写为 Bedrock 期望的格式
if len(body) == 0 {
return fmt.Errorf("Bedrock 请求 body 不能为空")
}
var bodyObj map[string]interface{}
if err := json.Unmarshal(body, &bodyObj); err != nil {
return fmt.Errorf("解析 Bedrock 请求 body 失败:%w", err)
}
modelID, _ := bodyObj["model"].(string)
if modelID == "" {
return fmt.Errorf("Bedrock 请求 body 缺少 model 字段")
}
streaming := false
if v, ok := bodyObj["stream"].(bool); ok {
streaming = v
}
// 移除 modelBedrock 不需要);删除 stream(流式由 URL 决定)
delete(bodyObj, "model")
delete(bodyObj, "stream")
delete(bodyObj, "stream_options")
// 注入 Bedrock 必需的 anthropic_version
if _, ok := bodyObj["anthropic_version"]; !ok {
bodyObj["anthropic_version"] = "bedrock-2023-05-31"
}
rewritten, err := json.Marshal(bodyObj)
if err != nil {
return fmt.Errorf("重写 Bedrock 请求 body 失败:%w", err)
}
// 3) 构建 Bedrock URL
host := fmt.Sprintf("bedrock-runtime.%s.amazonaws.com", region)
op := "invoke"
if streaming {
op = "invoke-with-response-stream"
}
requestURL := fmt.Sprintf("https://%s/model/%s/%s", host, modelID, op)
httpReq, err := http.NewRequest(method, requestURL, bytes.NewReader(rewritten))
if err != nil {
return fmt.Errorf("构建 Bedrock 请求失败:%w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if streaming {
httpReq.Header.Set("Accept", "application/vnd.amazon.eventstream")
httpReq.Header.Set("X-Amzn-Bedrock-Accept", "application/json")
} else {
httpReq.Header.Set("Accept", "application/json")
}
// 4) SigV4 签名(service=bedrock
awsCreds := credentials.NewStaticCredentials(creds.AWSAccessKeyID, creds.AWSSecretAccessKey, creds.AWSSessionToken)
signer := v4.NewSigner(awsCreds)
if _, err = signer.Sign(httpReq, bytes.NewReader(rewritten), "bedrock", region, time.Now()); err != nil {
return fmt.Errorf("Bedrock SigV4 签名失败:%w", err)
}
// 5) 发起请求
startTime := time.Now()
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(httpReq)
if err != nil {
s.logBedrock(userID, modelID, "error", err.Error(), startTime, 0, 0)
return err
}
defer func() { _ = resp.Body.Close() }()
// 6) 写回响应头/状态码(流式改写 Content-Type 为 SSE
if w, ok := writer.(http.ResponseWriter); ok {
for k, v := range resp.Header {
lower := strings.ToLower(k)
if streaming && (lower == "content-type" || lower == "content-length") {
continue
}
for _, vv := range v {
w.Header().Add(k, vv)
}
}
if streaming {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
}
w.WriteHeader(resp.StatusCode)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
raw, _ := io.ReadAll(resp.Body)
_, _ = writer.Write(raw)
s.logBedrock(userID, modelID, "error",
fmt.Sprintf("bedrock %d: %s", resp.StatusCode, string(raw)), startTime, 0, 0)
return nil
}
// 7) 处理响应体
var inputTokens, outputTokens int
if streaming {
inputTokens, outputTokens, err = s.streamBedrockEventStream(resp.Body, writer)
if err != nil {
s.logBedrock(userID, modelID, "error", err.Error(), startTime, inputTokens, outputTokens)
return err
}
} else {
var buf bytes.Buffer
tee := io.TeeReader(resp.Body, &buf)
if _, err = io.Copy(writer, tee); err != nil {
s.logBedrock(userID, modelID, "error", err.Error(), startTime, 0, 0)
return err
}
inputTokens, outputTokens = parseAnthropicUsage(buf.Bytes())
}
// 8) 记录日志 + 计费扣款
s.logBedrock(userID, modelID, "success", "", startTime, inputTokens, outputTokens)
if inputTokens > 0 || outputTokens > 0 {
pricing, _ := s.fetchModelPricingFromDify(modelID)
delta := calcQuotaDelta(pricing, modelID, inputTokens, outputTokens)
deductAccountQuota(userID, delta)
}
return nil
}
// streamBedrockEventStream 解析 Bedrock 的 vnd.amazon.eventstream 二进制流,
// 把每个事件还原为 Anthropic SSEevent: <type>\ndata: <json>\n\n)写给客户端。
// 返回累计的 input/output token 数(用于计费)。
func (s *ModelProviderService) streamBedrockEventStream(r io.Reader, w io.Writer) (int, int, error) {
flusher, _ := w.(http.Flusher)
dec := estream.NewDecoder(r)
payloadBuf := make([]byte, 0, 32*1024)
var inputTokens, outputTokens int
for {
msg, err := dec.Decode(payloadBuf)
if err != nil {
if err == io.EOF {
return inputTokens, outputTokens, nil
}
return inputTokens, outputTokens, fmt.Errorf("eventstream decode 失败:%w", err)
}
// Bedrock 的事件 payload 形如 {"bytes":"<base64-encoded inner JSON>"}
var wrap struct {
Bytes string `json:"bytes"`
}
var inner []byte
if e := json.Unmarshal(msg.Payload, &wrap); e == nil && wrap.Bytes != "" {
if decoded, e2 := base64.StdEncoding.DecodeString(wrap.Bytes); e2 == nil {
inner = decoded
}
}
if len(inner) == 0 {
// 非包装格式(如错误/ping),直接用原 payload
inner = msg.Payload
}
// 解析事件类型和 usageAnthropic 在 message_start.message.usage 给 input_tokens
// message_delta.usage 给 output_tokens
var ev struct {
Type string `json:"type"`
Message struct {
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
} `json:"message"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
_ = json.Unmarshal(inner, &ev)
if ev.Message.Usage.InputTokens > 0 {
inputTokens = ev.Message.Usage.InputTokens
}
if ev.Message.Usage.OutputTokens > 0 {
outputTokens = ev.Message.Usage.OutputTokens
}
if ev.Usage.InputTokens > 0 {
inputTokens = ev.Usage.InputTokens
}
if ev.Usage.OutputTokens > 0 {
outputTokens = ev.Usage.OutputTokens
}
// 重组为 Anthropic SSE 写回
eventName := ev.Type
if eventName == "" {
eventName = "message"
}
sse := "event: " + eventName + "\ndata: " + string(inner) + "\n\n"
if _, err = w.Write([]byte(sse)); err != nil {
return inputTokens, outputTokens, err
}
if flusher != nil {
flusher.Flush()
}
}
}
// parseAnthropicUsage 从非流式 Anthropic Messages 响应 JSON 中提取 usage 字段。
func parseAnthropicUsage(data []byte) (input, output int) {
var obj struct {
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
if json.Unmarshal(data, &obj) == nil {
return obj.Usage.InputTokens, obj.Usage.OutputTokens
}
return 0, 0
}
// logBedrock 记录代理日志(与 ProxyRequest 中的 ModelProxyLog 行为一致)。
func (s *ModelProviderService) logBedrock(userID, modelID, status, errMsg string, startTime time.Time, in, out int) {
if err := global.GVA_DB.Create(&gaia.ModelProxyLog{
UserId: userID,
ProviderName: gaia.ProviderAWS,
ModelName: modelID,
RequestTokens: in,
ResponseTokens: out,
Status: status,
ErrorMessage: errMsg,
CreatedAt: startTime,
}).Error; err != nil {
global.GVA_LOG.Warn("logBedrock 写日志失败", zap.Error(err))
}
}
+16 -3
View File
@@ -443,6 +443,9 @@ func (s *ModelProviderService) GetAvailableModelsFromDify(providerName string) (
return s.fetchGeminiModels(client, base, creds.APIKey)
case gaia.ProviderAnthropic:
return nil, nil
case gaia.ProviderAWS:
// AWS Bedrock 没有统一的 OpenAI 兼容 /v1/models 接口,模型由前端 allow-create 手输
return nil, nil
default:
if creds.Endpoint != "" {
return s.fetchOpenAICompatibleModels(client, creds.Endpoint, creds.APIKey)
@@ -708,8 +711,8 @@ func (s *ModelProviderService) GetDifyProviderCredentials(providerName string) (
return nil, fmt.Errorf("解密凭证失败: %w", err)
}
}
if creds.APIKey == "" {
return nil, fmt.Errorf("未能从配置中提取API Key")
if creds.APIKey == "" && creds.AWSAccessKeyID == "" {
return nil, fmt.Errorf("未能从配置中提取API Key(也未找到 AWS 凭证)")
}
// 缓存凭证(1小时)
@@ -987,7 +990,12 @@ func (s *ModelProviderService) getProviderCandidatesByModel(modelName string) []
return []string{gaia.ProviderGoogle}
}
if strings.Contains(modelLower, "claude") || strings.Contains(modelLower, "anthropic") {
return []string{gaia.ProviderAnthropic}
// 顺序即优先级:anthropic 直连优先,未开启则回落到 AWS Bedrock;都开则走 anthropic
return []string{gaia.ProviderAnthropic, gaia.ProviderAWS}
}
// Kimi / Moonshot 系列经由 tongyi(百炼)渠道转发
if strings.HasPrefix(modelLower, "kimi") || strings.Contains(modelLower, "moonshot") {
return []string{gaia.ProviderTongyi}
}
// GLM/智谱 可能配置在 tongyi(统一入口)或 zhipuai 下,先试 tongyi
if strings.HasPrefix(modelLower, "glm") || strings.Contains(modelLower, "zhipu") || strings.Contains(modelLower, "chatglm") {
@@ -1029,8 +1037,13 @@ func (s *ModelProviderService) getProviderByModel(modelName string) (string, err
return gaia.ProviderGoogle, nil
}
if strings.Contains(modelLower, "claude") || strings.Contains(modelLower, "anthropic") {
// 仅按名字推断时默认 anthropic;实际渠道(含 AWS Bedrock)由 resolveProviderByModel 决定
return gaia.ProviderAnthropic, nil
}
// Kimi / Moonshot 默认走 tongyi(百炼)渠道
if strings.HasPrefix(modelLower, "kimi") || strings.Contains(modelLower, "moonshot") {
return gaia.ProviderTongyi, nil
}
if strings.Contains(modelLower, "azure") {
return gaia.ProviderAzure, nil
}
@@ -146,7 +146,12 @@ const providerList = ref([])
const providerDisplayNames = {
openai: 'OpenAI',
tongyi: '千问(通义)',
google: 'Google Gemini'
google: 'Google Gemini',
anthropic: 'Anthropic',
aws: 'AWS Bedrock',
azure: 'Azure OpenAI',
zhipuai: '智谱 AI',
minimax: 'MiniMax'
}
const getProviderDisplayName = (providerName) => {
@@ -395,7 +400,7 @@ onMounted(() => {
:deep(.el-select-dropdown) {
.el-select-dropdown__item {
padding: 8px 16px;
&.is-selected {
font-weight: 600;
color: #409eff;