17 Commits

Author SHA1 Message Date
npc0-hue cb0233f7b6 fix: 打印原文 2026-04-25 10:17:36 +08:00
npc0-hue 6c0c6ba1fe fix: client_ip 打印客户端ip 2026-04-25 10:11:09 +08:00
npc0-hue 3240e6e4e5 fix: 模型名称自动加us.anthropic之类的,方便维护 2026-04-24 18:09:06 +08:00
npc0-hue 8d823787b7 fix: 构建 Bedrock 请求详情打印 2026-04-24 17:24:45 +08:00
npc0-hue 84fc7bcb43 fix: 构建 Bedrock 请求详情打印 2026-04-24 16:52:49 +08:00
npc0-hue e08b1b079c fix: 构建 Bedrock URL处理 2026-04-24 16:38:41 +08:00
npc0-hue 3f6ef97148 fix: anthropic.* 前缀是 AWS Bedrock 专用模型 ID 格式(如 anthropic.claude-sonnet-4-6), 2026-04-24 14:42:15 +08:00
npc0-hue 33a34e4181 fix: 后端转发修改转发时长 2026-04-24 14:32:06 +08:00
npc0-hue 347e48cef6 fix: 添加DeepSeek支持 2026-04-24 12:12:28 +08:00
npc0-hue d2a7ade1b0 fix: 补充index 2026-04-24 11:04:57 +08:00
npc0-hue efc25217dc fix(bedrock): 代理改为反向代理模式,兼容不支持 CONNECT 的代理
- 取消 http.Transport.Proxy(要求代理支持 HTTP CONNECT 隧道)
- 改为直接向代理地址发 HTTP 请求(host:port),路径同 Bedrock 原生 API
- httpReq.Host 设为真实 Bedrock host,SigV4 仍针对真实 host 签名后复制头部
- 支持凭证内 bedrock_proxy_url 与全局 BEDROCK_PROXY 环境变量(优先凭证)
- config.Gaia 增加 BedrockProxy 字段,overrideGaiaFromEnv 从 BEDROCK_PROXY 读取
2026-04-24 10:58:58 +08:00
npc0-hue 618a355ec8 feat(bedrock): 全局 BEDROCK_PROXY 环境变量支持
凭证里 bedrock_proxy_url 优先,未配置则回落到 BEDROCK_PROXY env。
解决 admin-server 在受限地区(如中国)直连 bedrock-runtime 被 Anthropic 地区拦截的问题。
与 Dify Python 侧 BEDROCK_PROXY 含义一致,docker-compose 设置一次即可全局生效。
2026-04-24 10:48:36 +08:00
npc0-hue d474f15673 fix(routing): Claude 模型路由优先 aws,未开启再回落 anthropic
原来写死 anthropic 优先,导致只要 anthropic provider 配置过就直连
api.anthropic.com,在地区受限时报 geo-block 错误。
改为:哪个 provider 开启且包含该模型就用哪个(aws 先试)。
2026-04-24 10:15:47 +08:00
npc0-hue 3bad30bff1 feat(bedrock): 支持 bedrock_proxy_url 代理配置
从 Dify provider_credentials 的 encrypted_config 中读取 bedrock_proxy_url 字段,
若非空则将 HTTP 请求经该代理(host:port 或 http://host:port)转发到 AWS Bedrock,
不再强制直连 bedrock-runtime.{region}.amazonaws.com。
变更:
- ProviderCredentials 新增 BedrockProxyURL 字段
- ConfigKeyBedrockProxyURL 常量
- GetDifyProviderCredentials 提取 bedrock_proxy_url(明文,不解密)
- proxyBedrockRequest 根据 BedrockProxyURL 配置 http.Transport.Proxy
2026-04-24 09:59:56 +08:00
npc0-hue ea77171028 fix(aws): 修复 provider_credentials 表 LIKE 查询无法匹配 bedrock_claude
Dify 内部将 AWS Bedrock provider 存储为 'langgenius/bedrock_claude/bedrock_claude',
而非含 'aws' 关键字的名称,导致 LIKE '%aws%' 查询返回空结果。
新增 difyProviderLikePattern() 辅助方法:
- ProviderAWS → LIKE '%bedrock%'(兼容 bedrock_claude 和 bedrock 两种插件包)
- 其他 provider 保持原有 LIKE '%<name>%' 逻辑
2026-04-23 17:53:10 +08:00
npc0-hue bb1db4ca99 fix(aws): 前端 & 后端测试凭证支持 AWS Bedrock
- TestProviderCredentials: AWS 渠道返回 access_key_id(脱敏)/region/session_token 状态
- modelManagement/index.vue: 测试凭证弹窗适配 AWS 字段展示
- 模型列表提示区分 AWS/Anthropic/其他,附上常用 Bedrock 模型 ID 示例
2026-04-23 15:44:09 +08:00
npc0-hue 5618c89721 fix(aws): 打通 AWS Bedrock 转发链路
- model_provider_constants_extend.go: 新增 AWS 凭证配置 key 常量
  (ConfigKeyAWSAccessKeyID/SecretAccessKey/SessionToken/Region)
- GetDifyProviderCredentials: 解析 Dify bedrock provider 的 AWS 凭证字段
- GetAvailableModelsFromDify: AWS/Anthropic 在 credentials 之前提前返回,
  避免因 APIKey 为空触发误报
- ProxyRequest: 在获取凭证后新增 AWS 分支,直接调用 proxyBedrockRequest
  完成 SigV4 签名直连 Bedrock 原生 API(bedrock_extend.go 中已实现,此前未被调用)
2026-04-23 15:41:53 +08:00
13 changed files with 308 additions and 35 deletions
+36 -11
View File
@@ -116,6 +116,7 @@ func proxyWithAccountId(c *gin.Context, accountId string) {
zap.String("method", c.Request.Method),
zap.Int("body_len", len(body)),
zap.String("body_model", bodyModel),
zap.String("client_ip", c.ClientIP()),
)
// 余额前置检查:余额耗尽时直接拦截,不继续请求上游
@@ -126,6 +127,9 @@ func proxyWithAccountId(c *gin.Context, accountId string) {
if err = modelProviderService.ProxyRequest(
accountId, path, c.Request.Method, reqHeader, body, c.Writer); err != nil {
global.GVA_LOG.Info("Gaia代理请求body",
zap.String("body", string(body)),
)
global.GVA_LOG.Error("代理请求失败", zap.String("account_id", accountId), zap.String("path", path), zap.Error(err))
if !c.Writer.Written() {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"message": err.Error()}})
@@ -190,18 +194,39 @@ func (m *ModelProviderApi) TestProviderCredentials(c *gin.Context) {
return
}
// 隐藏API Key的大部分内容
maskedKey := ""
if len(creds.APIKey) > 8 {
maskedKey = creds.APIKey[:4] + "****" + creds.APIKey[len(creds.APIKey)-4:]
} else {
maskedKey = "****"
}
var result map[string]interface{}
result := map[string]interface{}{
"provider": providerName,
"has_api_key": creds.APIKey != "",
"api_key": maskedKey,
// AWS Bedrock:展示 access key 信息
if creds.AWSAccessKeyID != "" {
maskedKey := "****"
if len(creds.AWSAccessKeyID) > 8 {
maskedKey = creds.AWSAccessKeyID[:4] + "****" + creds.AWSAccessKeyID[len(creds.AWSAccessKeyID)-4:]
}
region := creds.AWSRegion
if region == "" {
region = "us-east-1(默认)"
}
result = map[string]interface{}{
"provider": providerName,
"has_api_key": true,
"api_key": maskedKey,
"aws_access_key_id": maskedKey,
"aws_region": region,
"has_session_token": creds.AWSSessionToken != "",
}
} else {
// 隐藏API Key的大部分内容
maskedKey := ""
if len(creds.APIKey) > 8 {
maskedKey = creds.APIKey[:4] + "****" + creds.APIKey[len(creds.APIKey)-4:]
} else {
maskedKey = "****"
}
result = map[string]interface{}{
"provider": providerName,
"has_api_key": creds.APIKey != "",
"api_key": maskedKey,
}
}
response.OkWithData(result, c)
+1 -1
View File
@@ -5,5 +5,5 @@ type Gaia struct {
LoginMaxErrorLimit int `mapstructure:"login_max_error_limit" json:"login_max_error_limit" yaml:"login_max_error_limit"`
SuperAdminAccountId string `mapstructure:"SUPER_ADMIN_ACCOUNT_ID" json:"SUPER_ADMIN_ACCOUNT_ID" yaml:"SUPER_ADMIN_ACCOUNT_ID"` // 超级管理员账号
SuperAdminTenantId string `mapstructure:"SUPER_ADMIN_TENANT_ID" json:"SUPER_ADMIN_TENANT_ID" yaml:"SUPER_ADMIN_TENANT_ID"` // 系统默认工作区
StoragePath string `mapstructure:"storage-path" json:"storage-path" yaml:"storage-path"` // Dify storage 目录路径,用于读取私钥
StoragePath string `mapstructure:"storage-path" json:"storage-path" yaml:"storage-path"` // Dify storage 目录路径,用于读取私钥
}
@@ -9,7 +9,8 @@ const (
ProviderAzure = "azure"
ProviderZhipuai = "zhipuai"
ProviderMinimax = "minimax"
ProviderAWS = "aws" // AWS Bedrock 渠道(用于转发 Claude 等 Anthropic 模型)
ProviderAWS = "aws" // AWS Bedrock 渠道(用于转发 Claude 等 Anthropic 模型)
ProviderDeepSeek = "deepseek" // DeepSeek 渠道
)
// DifyProviderTypeCustom Dify providers 表 provider_type 枚举
@@ -22,18 +23,26 @@ const (
ConfigKeyOpenaiAPIVersion = "openai_api_version"
ConfigKeyDashScopeAPIKey = "dashscope_api_key"
ConfigKeyAPIKey = "api_key"
// AWS Bedrock 凭证字段(Dify bedrock provider 的 encrypted_config 中使用)
ConfigKeyAWSAccessKeyID = "aws_access_key_id"
ConfigKeyAWSSecretAccessKey = "aws_secret_access_key"
ConfigKeyAWSSessionToken = "aws_session_token"
ConfigKeyAWSRegion = "aws_region"
ConfigKeyBedrockProxyURL = "bedrock_proxy_url" // 可选:HTTP 代理地址,格式 host:port 或 http://host:port
)
// SupportedProviders 列表展示的提供商顺序
var SupportedProviders = []string{ProviderOpenai, ProviderTongyi, ProviderGoogle, ProviderAnthropic, ProviderAWS, ProviderAzure, ProviderZhipuai, ProviderMinimax}
var SupportedProviders = []string{ProviderOpenai, ProviderTongyi, ProviderGoogle, ProviderAnthropic, ProviderAWS, ProviderAzure, ProviderZhipuai, ProviderMinimax, ProviderDeepSeek}
// DefaultChatCompletionsEndpoints 各提供商聊天接口默认完整 URL(兼容旧 ProxyChat
var DefaultChatCompletionsEndpoints = map[string]string{
ProviderOpenai: "https://api.openai.com/v1/chat/completions",
ProviderTongyi: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
ProviderGoogle: "https://generativelanguage.googleapis.com/v1beta/chat/completions",
ProviderZhipuai: "https://open.bigmodel.cn/api/paas/v4/chat/completions",
ProviderMinimax: "https://api.minimax.chat/v1/text/chatcompletion_v2",
ProviderOpenai: "https://api.openai.com/v1/chat/completions",
ProviderTongyi: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
ProviderGoogle: "https://generativelanguage.googleapis.com/v1beta/chat/completions",
ProviderZhipuai: "https://open.bigmodel.cn/api/paas/v4/chat/completions",
ProviderMinimax: "https://api.minimax.chat/v1/text/chatcompletion_v2",
ProviderDeepSeek: "https://api.deepseek.com/v1/chat/completions",
// Azure 需要动态构建 URL,不使用默认值
}
@@ -46,6 +55,7 @@ var DefaultAPIBase = map[string]string{
ProviderAWS: "https://bedrock-runtime.us-east-1.amazonaws.com",
ProviderZhipuai: "https://open.bigmodel.cn",
ProviderMinimax: "https://api.minimax.chat",
ProviderDeepSeek: "https://api.deepseek.com",
// Azure 的 base URL 来自 openai_api_base 配置,不设置默认值
}
@@ -122,4 +132,10 @@ var BuiltinModelPricing = map[string]ModelPricing{
// 命中分支见 service/gaia/model_provider.go 中的 isImageOrPerRequestPath 与 ProxyRequest 计费逻辑
"gpt-image-1": {Input: 0.04, Currency: "USD"},
"gpt-image-2": {Input: 0.05, Currency: "USD"},
// ──── DeepSeek 系列(USD / 百万 token ────
// deepseek-v4-pro:旗舰推理模型,定价参考官方 https://platform.deepseek.com/api-docs/pricing
"deepseek-v4-pro": {Input: 2.19 / 1000, Output: 8.19 / 1000, Unit: 0.001, Currency: "USD"},
// deepseek-v4-flash:高速轻量模型
"deepseek-v4-flash": {Input: 0.27 / 1000, Output: 1.10 / 1000, Unit: 0.001, Currency: "USD"},
}
@@ -11,6 +11,8 @@ type ProviderCredentials struct {
AWSSecretAccessKey string `json:"aws_secret_access_key,omitempty"`
AWSSessionToken string `json:"aws_session_token,omitempty"`
AWSRegion string `json:"aws_region,omitempty"`
// Bedrock 可选代理地址(host:port 或 http://host:port),非空时请求经该代理转发到 AWS
BedrockProxyURL string `json:"bedrock_proxy_url,omitempty"`
}
// ModelInfo 模型信息
+129 -3
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
@@ -64,6 +65,15 @@ func (s *ModelProviderService) proxyBedrockRequest(
delete(bodyObj, "model")
delete(bodyObj, "stream")
delete(bodyObj, "stream_options")
// OpenAI 兼容字段转换:max_completion_tokens → max_tokensBedrock/Anthropic 使用 max_tokens
if _, hasMaxTokens := bodyObj["max_tokens"]; !hasMaxTokens {
if v, ok := bodyObj["max_completion_tokens"]; ok {
bodyObj["max_tokens"] = v
delete(bodyObj, "max_completion_tokens")
}
} else {
delete(bodyObj, "max_completion_tokens")
}
// 注入 Bedrock 必需的 anthropic_version
if _, ok := bodyObj["anthropic_version"]; !ok {
bodyObj["anthropic_version"] = "bedrock-2023-05-31"
@@ -74,12 +84,31 @@ func (s *ModelProviderService) proxyBedrockRequest(
}
// 3) 构建 Bedrock URL
// 新一代 Claude 模型(3.5v2、3.7、Sonnet-4、Opus-4 等)要求通过跨区域推理配置文件调用,
// 模型 ID 需加地理前缀(us. / eu. / ap.),否则 Bedrock 返回 "on-demand throughput isn't supported" 错误。
// 若调用方已传入带前缀的 ID(如 us.anthropic.xxx)则直接使用,不重复添加。
invokeModelID := bedrockResolveModelID(modelID, region)
if invokeModelID != modelID {
global.GVA_LOG.Info("Bedrock 模型 ID 已映射为跨区域推理配置文件",
zap.String("original", modelID),
zap.String("resolved", invokeModelID),
zap.String("region", region),
)
}
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)
requestURL := fmt.Sprintf("https://%s/model/%s/%s", host, url.PathEscape(invokeModelID), op)
// 打印请求地址、参数和代理地址
global.GVA_LOG.Info("Bedrock 请求详情",
zap.String("request_url", requestURL),
zap.String("method", method),
zap.ByteString("body", rewritten),
zap.String("proxy_url", creds.BedrockProxyURL),
)
httpReq, err := http.NewRequest(method, requestURL, bytes.NewReader(rewritten))
if err != nil {
@@ -100,9 +129,19 @@ func (s *ModelProviderService) proxyBedrockRequest(
return fmt.Errorf("Bedrock SigV4 签名失败:%w", err)
}
// 5) 发起请求
// 5) 发起请求(若配置了 bedrock_proxy_url 则经 HTTP 代理转发)
startTime := time.Now()
client := &http.Client{Timeout: 5 * time.Minute}
transport := http.DefaultTransport
if creds.BedrockProxyURL != "" {
proxyAddr := creds.BedrockProxyURL
if !strings.HasPrefix(proxyAddr, "http://") && !strings.HasPrefix(proxyAddr, "https://") {
proxyAddr = "http://" + proxyAddr
}
if proxyURL, parseErr := url.Parse(proxyAddr); parseErr == nil {
transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
}
}
client := &http.Client{Timeout: 5 * time.Minute, Transport: transport}
resp, err := client.Do(httpReq)
if err != nil {
s.logBedrock(userID, modelID, "error", err.Error(), startTime, 0, 0)
@@ -256,6 +295,93 @@ func parseAnthropicUsage(data []byte) (input, output int) {
return 0, 0
}
// bedrockCrossRegionPrefixes 是需要跨区域推理配置文件的模型 ID 前缀列表(anthropic. 开头)。
// 来源:https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
// 规则:凡模型不在旧版 on-demand 列表中,均需加地理前缀才能调用。
var bedrockCrossRegionPrefixes = []string{
// Claude 3.5 v2
"anthropic.claude-3-5-sonnet-20241022-v2",
"anthropic.claude-3-5-haiku-20241022",
// Claude 3.7
"anthropic.claude-3-7-sonnet",
// Claude Sonnet 4 / Opus 4 / Haiku 4(及后续新版本)
"anthropic.claude-sonnet-4",
"anthropic.claude-opus-4",
"anthropic.claude-haiku-4",
}
// bedrockResolveModelID 将用户输入的模型 ID 解析为正确的 Bedrock 调用 ID。
//
// 支持以下输入格式(会自动规范化):
// - 完整带前缀:us.anthropic.claude-sonnet-4-6 → 直接使用
// - 带厂商前缀:anthropic.claude-sonnet-4-6 → 按需加 us./eu./ap.
// - 短横线名称:claude-sonnet-4-6 → 补 anthropic. 再按需加前缀
// - 空格+点号: "claude sonnet 4.6" → 规范化后同上
func bedrockResolveModelID(modelID, region string) string {
// Step 1: 规范化输入
// 小写、空格→横线、版本中的点→横线(如 4.6 → 4-6,但保留 : 用于版本后缀如 v2:0)
normalized := strings.ToLower(strings.TrimSpace(modelID))
normalized = strings.ReplaceAll(normalized, " ", "-")
// 仅将数字之间的 `.` 替换为 `-`(处理 "4.6" → "4-6"),保留 anthropic. 这样的厂商点
normalized = replaceVersionDots(normalized)
// Step 2: 补全 anthropic. 前缀(用户只填了 claude-xxx
if !strings.HasPrefix(normalized, "anthropic.") &&
!strings.HasPrefix(normalized, "us.") &&
!strings.HasPrefix(normalized, "eu.") &&
!strings.HasPrefix(normalized, "ap.") {
normalized = "anthropic." + normalized
}
// Step 3: 已经带地理前缀 → 直接使用
if strings.HasPrefix(normalized, "us.") || strings.HasPrefix(normalized, "eu.") || strings.HasPrefix(normalized, "ap.") {
return normalized
}
// Step 4: 判断是否属于需要跨区域推理配置文件的模型
needsCrossRegion := false
for _, prefix := range bedrockCrossRegionPrefixes {
if strings.HasPrefix(normalized, prefix) {
needsCrossRegion = true
break
}
}
if !needsCrossRegion {
return normalized
}
// Step 5: 根据 region 推导地理前缀
geoPrefix := "us" // 默认
switch {
case strings.HasPrefix(region, "us-"):
geoPrefix = "us"
case strings.HasPrefix(region, "eu-"):
geoPrefix = "eu"
case strings.HasPrefix(region, "ap-"):
geoPrefix = "ap"
}
return geoPrefix + "." + normalized
}
// replaceVersionDots 将版本号中数字之间的 `.` 替换为 `-`(如 4.6 → 4-6),
// 保留厂商命名空间中的点(如 anthropic. 开头不受影响,因为点后紧跟字母)。
func replaceVersionDots(s string) string {
var b strings.Builder
for i := 0; i < len(s); i++ {
if s[i] == '.' && i > 0 && i < len(s)-1 {
prev := s[i-1]
next := s[i+1]
// 仅当点号两侧都是数字时才替换为 -
if prev >= '0' && prev <= '9' && next >= '0' && next <= '9' {
b.WriteByte('-')
continue
}
}
b.WriteByte(s[i])
}
return b.String()
}
// 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{
+93 -12
View File
@@ -418,6 +418,14 @@ func (s *ModelProviderService) GetAvailableModelsFromDify(providerName string) (
if providerName == gaia.ProviderAzure {
return s.getAvailableModelsFromProviderModelCredentials(providerName)
}
// AWS Bedrock 没有统一的 /v1/models 接口,模型由前端手输;直接返回空列表
if providerName == gaia.ProviderAWS || providerName == gaia.ProviderAnthropic {
return nil, nil
}
// DeepSeek 模型由前端手输(避免拉取全量列表),直接返回空列表
if providerName == gaia.ProviderDeepSeek {
return nil, nil
}
creds, err := s.GetDifyProviderCredentials(providerName)
if err != nil || creds.APIKey == "" {
@@ -441,11 +449,6 @@ func (s *ModelProviderService) GetAvailableModelsFromDify(providerName string) (
base = gaia.DefaultAPIBase[gaia.ProviderGoogle]
}
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)
@@ -640,26 +643,33 @@ func (s *ModelProviderService) GetDifyProviderCredentials(providerName string) (
cacheKey := gaia.RedisKeyModelProviderCredentialsPrefix + providerName
if cached, err = global.GVA_Dify_REDIS.Get(context.Background(), cacheKey).Result(); err == nil {
if err = json.Unmarshal([]byte(cached), &creds); err == nil {
global.GVA_LOG.Info("GetDifyProviderCredentials 命中缓存",
zap.String("provider", providerName),
zap.String("aws_region", creds.AWSRegion),
zap.String("bedrock_proxy_url", creds.BedrockProxyURL),
)
return creds, nil
}
}
// 尝试方式1: 从 providers + provider_credentials 表查询
var row gaia.ProviderCredential
err = global.GVA_DB.Table("providers").
// 将短名转为 Dify 内部 provider_name 的 LIKE 模式(避免 aws 匹配不到 bedrock_claude
likePattern := s.difyProviderLikePattern(providerName)
err = global.GVA_DB.Debug().Table("providers").
Select("provider_credentials.encrypted_config, providers.tenant_id").
Joins("LEFT JOIN provider_credentials ON providers.credential_id = provider_credentials.id").
Where("providers.tenant_id = ? AND providers.provider_name LIKE ? AND providers.provider_type = ? AND providers.is_valid = ?",
tenantID, fmt.Sprintf("%%%s%%", providerName), gaia.DifyProviderTypeCustom, true).
tenantID, likePattern, gaia.DifyProviderTypeCustom, true).
Order("provider_credentials.updated_at DESC").
First(&row).Error
// 如果方式1 未找到记录,尝试方式2: 从 provider_model_credentials 表查询
if err != nil || row.EncryptedConfig == "" {
var pmcRow gaia.ProviderCredential
if pmcErr := global.GVA_DB.Table("provider_model_credentials").
if pmcErr := global.GVA_DB.Debug().Table("provider_model_credentials").
Select("encrypted_config, tenant_id, provider_name, updated_at").
Where("tenant_id = ? AND provider_name LIKE ?", tenantID, fmt.Sprintf("%%%s%%", providerName)).
Where("tenant_id = ? AND provider_name LIKE ?", tenantID, likePattern).
Order("updated_at DESC"). // 按 updated_at 倒序,取最新的凭证
First(&pmcRow).Error; pmcErr == nil && pmcRow.EncryptedConfig != "" {
row = pmcRow
@@ -674,6 +684,7 @@ func (s *ModelProviderService) GetDifyProviderCredentials(providerName string) (
// 兼容两种存储:1) 明文 JSON(如 {"openai_api_key":"...", "openai_api_base":"..."});2) Dify RSA+AES-EAX 加密后再 base64
var base, apiVersion string
var configMap map[string]interface{}
fmt.Println("row.EncryptedConfig", row.EncryptedConfig)
if err = json.Unmarshal([]byte(row.EncryptedConfig), &configMap); err == nil {
// 解密函数用于处理加密的值
if config, ok := configMap[gaia.ConfigKeyOpenaiAPIKey]; ok {
@@ -689,6 +700,35 @@ func (s *ModelProviderService) GetDifyProviderCredentials(providerName string) (
creds.APIKey, err = s.decryptConfig(config.(string), row.TenantID)
} else if config, ok = configMap[gaia.ConfigKeyAPIKey]; ok {
creds.APIKey, err = s.decryptConfig(config.(string), row.TenantID)
} else if _, hasAWSKey := configMap[gaia.ConfigKeyAWSAccessKeyID]; hasAWSKey {
// AWS Bedrock 凭证:解析 aws_access_key_id / aws_secret_access_key / aws_region
if v, ok2 := configMap[gaia.ConfigKeyAWSAccessKeyID].(string); ok2 && v != "" {
if creds.AWSAccessKeyID, err = s.decryptConfig(v, row.TenantID); err != nil {
return nil, fmt.Errorf("解密 aws_access_key_id 失败: %w", err)
}
}
if v, ok2 := configMap[gaia.ConfigKeyAWSSecretAccessKey].(string); ok2 && v != "" {
if creds.AWSSecretAccessKey, err = s.decryptConfig(v, row.TenantID); err != nil {
return nil, fmt.Errorf("解密 aws_secret_access_key 失败: %w", err)
}
}
if v, ok2 := configMap[gaia.ConfigKeyAWSSessionToken].(string); ok2 && v != "" {
if creds.AWSSessionToken, err = s.decryptConfig(v, row.TenantID); err != nil {
return nil, fmt.Errorf("解密 aws_session_token 失败: %w", err)
}
}
if v, ok2 := configMap[gaia.ConfigKeyAWSRegion].(string); ok2 && v != "" {
creds.AWSRegion = strings.TrimSpace(v)
}
// 可选:HTTP 代理地址(用于从受限地区中转 Bedrock 请求)。
// 支持 "host:port" 或 "http(s)://host:port";若值被加密则先解密,失败时按原文处理。
if v, ok2 := configMap[gaia.ConfigKeyBedrockProxyURL].(string); ok2 && strings.TrimSpace(v) != "" {
proxyVal := strings.TrimSpace(v)
if decrypted, decErr := s.decryptConfig(proxyVal, row.TenantID); decErr == nil && decrypted != "" {
proxyVal = strings.TrimSpace(decrypted)
}
creds.BedrockProxyURL = proxyVal
}
} else {
// 尝试从备选字段中查找
for _, key := range gaia.CredentialKeyFallback {
@@ -718,6 +758,7 @@ func (s *ModelProviderService) GetDifyProviderCredentials(providerName string) (
// 缓存凭证(1小时)
var cacheJSON []byte
if cacheJSON, err = json.Marshal(creds); err == nil {
fmt.Println("row.EncryptedConfig", string(cacheJSON))
global.GVA_Dify_REDIS.Set(context.Background(), cacheKey, cacheJSON, time.Hour)
}
@@ -989,9 +1030,14 @@ func (s *ModelProviderService) getProviderCandidatesByModel(modelName string) []
if strings.HasPrefix(modelLower, "gemini") || strings.Contains(modelLower, "google") {
return []string{gaia.ProviderGoogle}
}
if strings.HasPrefix(modelLower, "anthropic.") {
// anthropic.* 前缀是 AWS Bedrock 专用模型 ID 格式(如 anthropic.claude-sonnet-4-6),
// 仅走 Bedrock 渠道,不回落到 Anthropic 直连(直连不支持此格式且可能因地区受限)。
return []string{gaia.ProviderAWS}
}
if strings.Contains(modelLower, "claude") || strings.Contains(modelLower, "anthropic") {
// 顺序即优先级:anthropic 直连优先,未开启回落到 AWS Bedrock;都开则走 anthropic
return []string{gaia.ProviderAnthropic, gaia.ProviderAWS}
// 顺序即优先级:AWS Bedrock 优先(配置成本更高、可覆盖受限地区),未开启回落到 anthropic 直连
return []string{gaia.ProviderAWS, gaia.ProviderAnthropic}
}
// Kimi / Moonshot 系列经由 tongyi(百炼)渠道转发
if strings.HasPrefix(modelLower, "kimi") || strings.Contains(modelLower, "moonshot") {
@@ -1005,6 +1051,10 @@ func (s *ModelProviderService) getProviderCandidatesByModel(modelName string) []
if strings.HasPrefix(modelLower, "minimax") || strings.Contains(modelLower, "abab") {
return []string{gaia.ProviderTongyi, gaia.ProviderMinimax}
}
// DeepSeek 系列模型走 deepseek 渠道
if strings.HasPrefix(modelLower, "deepseek") {
return []string{gaia.ProviderDeepSeek}
}
return nil
}
@@ -1036,6 +1086,10 @@ func (s *ModelProviderService) getProviderByModel(modelName string) (string, err
if strings.HasPrefix(modelLower, "gemini") || strings.Contains(modelLower, "google") {
return gaia.ProviderGoogle, nil
}
if strings.HasPrefix(modelLower, "anthropic.") {
// anthropic.* 前缀是 AWS Bedrock 专用格式,直接归 aws 渠道
return gaia.ProviderAWS, nil
}
if strings.Contains(modelLower, "claude") || strings.Contains(modelLower, "anthropic") {
// 仅按名字推断时默认 anthropic;实际渠道(含 AWS Bedrock)由 resolveProviderByModel 决定
return gaia.ProviderAnthropic, nil
@@ -1055,6 +1109,10 @@ func (s *ModelProviderService) getProviderByModel(modelName string) (string, err
if strings.HasPrefix(modelLower, "minimax") || strings.Contains(modelLower, "abab") {
return gaia.ProviderTongyi, nil
}
// DeepSeek 系列走 deepseek 渠道
if strings.HasPrefix(modelLower, "deepseek") {
return gaia.ProviderDeepSeek, nil
}
return "", fmt.Errorf("无法识别模型 %s 的提供商", modelName)
}
@@ -1167,6 +1225,11 @@ func (s *ModelProviderService) ProxyRequest(
return err
}
// AWS Bedrock 直连:不走通用 HTTP 转发,改用 SigV4 签名的 Bedrock 原生 API
if providerName == gaia.ProviderAWS {
return s.proxyBedrockRequest(userID, path, method, reqHeader, body, writer, creds)
}
if base = s.getUpstreamBase(providerName, creds); base == "" {
return fmt.Errorf("提供商 %s 无可用上游地址", providerName)
}
@@ -1326,7 +1389,7 @@ func (s *ModelProviderService) ProxyRequest(
}
}
// 流式响应:按行扫描,顺带从最后一条含 usage 的 data 行中提取 token 数
// 流式响应:按行扫描,顺带从最后一条含 usage 的 data 行返回
if strings.Contains(resp.Header.Get("Content-Type"), "text/event-stream") {
if flusher, ok := writer.(http.Flusher); ok {
scanner := bufio.NewScanner(resp.Body)
@@ -1412,3 +1475,21 @@ func isImageOrPerRequestPath(path string) bool {
}
return false
}
// difyProviderLikePattern 将 Gaia 短名(openai/aws/...)转换为 Dify providers 表
// provider_name 字段的 LIKE 搜索模式。
// Dify 内部以 "langgenius/<plugin>/<plugin>" 格式存储提供商名,例如:
// - aws → langgenius/bedrock_claude/bedrock_claude 或 langgenius/bedrock/bedrock
// - openai → langgenius/openai/openai
// - anthropic → langgenius/anthropic/anthropic
//
// 对于 AWSDify 有两种插件包:bedrock_claude 和 bedrock;使用 bedrock 作为公共关键字可同时命中。
func (s *ModelProviderService) difyProviderLikePattern(providerName string) string {
switch providerName {
case gaia.ProviderAWS:
// langgenius/bedrock_claude/bedrock_claude 和 langgenius/bedrock/bedrock 都含 "bedrock"
return "%bedrock%"
default:
return fmt.Sprintf("%%%s%%", providerName)
}
}
@@ -151,7 +151,8 @@ const providerDisplayNames = {
aws: 'AWS Bedrock',
azure: 'Azure OpenAI',
zhipuai: '智谱 AI',
minimax: 'MiniMax'
minimax: 'MiniMax',
deepseek: 'DeepSeek'
}
const getProviderDisplayName = (providerName) => {
+3
View File
@@ -1283,8 +1283,11 @@ NGINX_SSL_PROTOCOLS=TLSv1.2 TLSv1.3
NGINX_WORKER_PROCESSES=auto
NGINX_CLIENT_MAX_BODY_SIZE=100M
NGINX_KEEPALIVE_TIMEOUT=65
NGINX_CLIENT_HEADER_TIMEOUT=1800s
NGINX_CLIENT_BODY_TIMEOUT=1800s
# Proxy settings
NGINX_PROXY_CONNECT_TIMEOUT=3600s
NGINX_PROXY_READ_TIMEOUT=3600s
NGINX_PROXY_SEND_TIMEOUT=3600s
+3
View File
@@ -419,6 +419,9 @@ services:
NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto}
NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M}
NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65}
NGINX_CLIENT_HEADER_TIMEOUT: ${NGINX_CLIENT_HEADER_TIMEOUT:-1800s}
NGINX_CLIENT_BODY_TIMEOUT: ${NGINX_CLIENT_BODY_TIMEOUT:-1800s}
NGINX_PROXY_CONNECT_TIMEOUT: ${NGINX_PROXY_CONNECT_TIMEOUT:-3600s}
NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s}
NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s}
NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false}
+6
View File
@@ -557,6 +557,9 @@ x-shared-env: &shared-api-worker-env
NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto}
NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M}
NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65}
NGINX_CLIENT_HEADER_TIMEOUT: ${NGINX_CLIENT_HEADER_TIMEOUT:-1800s}
NGINX_CLIENT_BODY_TIMEOUT: ${NGINX_CLIENT_BODY_TIMEOUT:-1800s}
NGINX_PROXY_CONNECT_TIMEOUT: ${NGINX_PROXY_CONNECT_TIMEOUT:-3600s}
NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s}
NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s}
NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false}
@@ -1170,6 +1173,9 @@ services:
NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto}
NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M}
NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65}
NGINX_CLIENT_HEADER_TIMEOUT: ${NGINX_CLIENT_HEADER_TIMEOUT:-1800s}
NGINX_CLIENT_BODY_TIMEOUT: ${NGINX_CLIENT_BODY_TIMEOUT:-1800s}
NGINX_PROXY_CONNECT_TIMEOUT: ${NGINX_PROXY_CONNECT_TIMEOUT:-3600s}
NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s}
NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s}
NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false}
+6
View File
@@ -554,6 +554,9 @@ x-shared-env: &shared-api-worker-env
NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto}
NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M}
NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65}
NGINX_CLIENT_HEADER_TIMEOUT: ${NGINX_CLIENT_HEADER_TIMEOUT:-1800s}
NGINX_CLIENT_BODY_TIMEOUT: ${NGINX_CLIENT_BODY_TIMEOUT:-1800s}
NGINX_PROXY_CONNECT_TIMEOUT: ${NGINX_PROXY_CONNECT_TIMEOUT:-3600s}
NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s}
NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s}
NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false}
@@ -1105,6 +1108,9 @@ services:
NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto}
NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M}
NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65}
NGINX_CLIENT_HEADER_TIMEOUT: ${NGINX_CLIENT_HEADER_TIMEOUT:-1800s}
NGINX_CLIENT_BODY_TIMEOUT: ${NGINX_CLIENT_BODY_TIMEOUT:-1800s}
NGINX_PROXY_CONNECT_TIMEOUT: ${NGINX_PROXY_CONNECT_TIMEOUT:-3600s}
NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s}
NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s}
NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false}
@@ -3,6 +3,9 @@
server {
listen ${NGINX_PORT};
server_name ${NGINX_SERVER_NAME};
keepalive_timeout 1800s;
client_header_timeout ${NGINX_CLIENT_HEADER_TIMEOUT};
client_body_timeout ${NGINX_CLIENT_BODY_TIMEOUT};
# 管理中心反向代理配置
location = /admin {
return 301 /admin/;
+1
View File
@@ -7,5 +7,6 @@ proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_connect_timeout ${NGINX_PROXY_CONNECT_TIMEOUT};
proxy_read_timeout ${NGINX_PROXY_READ_TIMEOUT};
proxy_send_timeout ${NGINX_PROXY_SEND_TIMEOUT};