diff --git a/dto/dalle.go b/dto/dalle.go index 44104d33..a1309b6c 100644 --- a/dto/dalle.go +++ b/dto/dalle.go @@ -14,6 +14,7 @@ type ImageRequest struct { ExtraFields json.RawMessage `json:"extra_fields,omitempty"` Background string `json:"background,omitempty"` Moderation string `json:"moderation,omitempty"` + OutputFormat string `json:"output_format,omitempty"` } type ImageResponse struct { diff --git a/main.go b/main.go index 95c6820d..c286650f 100644 --- a/main.go +++ b/main.go @@ -89,9 +89,22 @@ func main() { if common.MemoryCacheEnabled { common.SysLog("memory cache enabled") common.SysError(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency)) - model.InitChannelCache() - } - if common.MemoryCacheEnabled { + + // Add panic recovery and retry for InitChannelCache + func() { + defer func() { + if r := recover(); r != nil { + common.SysError(fmt.Sprintf("InitChannelCache panic: %v, retrying once", r)) + // Retry once + _, fixErr := model.FixAbility() + if fixErr != nil { + common.SysError(fmt.Sprintf("InitChannelCache failed: %s", fixErr.Error())) + } + } + }() + model.InitChannelCache() + }() + go model.SyncOptions(common.SyncFrequency) go model.SyncChannelCache(common.SyncFrequency) } diff --git a/model/ability.go b/model/ability.go index 52720307..38b0bd73 100644 --- a/model/ability.go +++ b/model/ability.go @@ -50,7 +50,7 @@ func getPriority(group string, model string, retry int) (int, error) { err := DB.Model(&Ability{}). Select("DISTINCT(priority)"). Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model). - Order("priority DESC"). // 按优先级降序排序 + Order("priority DESC"). // 按优先级降序排序 Pluck("priority", &priorities).Error // Pluck用于将查询的结果直接扫描到一个切片中 if err != nil { @@ -261,12 +261,28 @@ func FixAbility() (int, error) { common.SysError(fmt.Sprintf("Get channel ids from channel table failed: %s", err.Error())) return 0, err } - // Delete abilities of channels that are not in channel table - err = DB.Where("channel_id NOT IN (?)", channelIds).Delete(&Ability{}).Error - if err != nil { - common.SysError(fmt.Sprintf("Delete abilities of channels that are not in channel table failed: %s", err.Error())) - return 0, err + + // Delete abilities of channels that are not in channel table - in batches to avoid too many placeholders + if len(channelIds) > 0 { + // Process deletion in chunks to avoid "too many placeholders" error + for _, chunk := range lo.Chunk(channelIds, 100) { + err = DB.Where("channel_id NOT IN (?)", chunk).Delete(&Ability{}).Error + if err != nil { + common.SysError(fmt.Sprintf("Delete abilities of channels (batch) that are not in channel table failed: %s", err.Error())) + return 0, err + } + } + } else { + // If no channels exist, delete all abilities + err = DB.Delete(&Ability{}).Error + if err != nil { + common.SysError(fmt.Sprintf("Delete all abilities failed: %s", err.Error())) + return 0, err + } + common.SysLog("Delete all abilities successfully") + return 0, nil } + common.SysLog(fmt.Sprintf("Delete abilities of channels that are not in channel table successfully, ids: %v", channelIds)) count += len(channelIds) @@ -275,17 +291,26 @@ func FixAbility() (int, error) { err = DB.Table("abilities").Distinct("channel_id").Pluck("channel_id", &abilityChannelIds).Error if err != nil { common.SysError(fmt.Sprintf("Get channel ids from abilities table failed: %s", err.Error())) - return 0, err + return count, err } + var channels []Channel if len(abilityChannelIds) == 0 { err = DB.Find(&channels).Error } else { - err = DB.Where("id NOT IN (?)", abilityChannelIds).Find(&channels).Error - } - if err != nil { - return 0, err + // Process query in chunks to avoid "too many placeholders" error + err = nil + for _, chunk := range lo.Chunk(abilityChannelIds, 100) { + var channelsChunk []Channel + err = DB.Where("id NOT IN (?)", chunk).Find(&channelsChunk).Error + if err != nil { + common.SysError(fmt.Sprintf("Find channels not in abilities table failed: %s", err.Error())) + return count, err + } + channels = append(channels, channelsChunk...) + } } + for _, channel := range channels { err := channel.UpdateAbilities(nil) if err != nil { diff --git a/model/cache.go b/model/cache.go index 2d1c36bf..e2f83e22 100644 --- a/model/cache.go +++ b/model/cache.go @@ -16,6 +16,9 @@ var channelsIDM map[int]*Channel var channelSyncLock sync.RWMutex func InitChannelCache() { + if !common.MemoryCacheEnabled { + return + } newChannelId2channel := make(map[int]*Channel) var channels []*Channel DB.Where("status = ?", common.ChannelStatusEnabled).Find(&channels) @@ -84,11 +87,11 @@ func CacheGetRandomSatisfiedChannel(group string, model string, retry int) (*Cha if !common.MemoryCacheEnabled { return GetRandomSatisfiedChannel(group, model, retry) } - + channelSyncLock.RLock() channels := group2model2channels[group][model] channelSyncLock.RUnlock() - + if len(channels) == 0 { return nil, errors.New("channel not found") } diff --git a/model/channel.go b/model/channel.go index 41e5e371..ed7a0a7e 100644 --- a/model/channel.go +++ b/model/channel.go @@ -46,6 +46,17 @@ func (channel *Channel) GetModels() []string { return strings.Split(strings.Trim(channel.Models, ","), ",") } +func (channel *Channel) GetGroups() []string { + if channel.Group == "" { + return []string{} + } + groups := strings.Split(strings.Trim(channel.Group, ","), ",") + for i, group := range groups { + groups[i] = strings.TrimSpace(group) + } + return groups +} + func (channel *Channel) GetOtherInfo() map[string]interface{} { otherInfo := make(map[string]interface{}) if channel.OtherInfo != "" { diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 37196fd8..078155f6 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -11,6 +11,8 @@ var awsModelIDMap = map[string]string{ "claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0", "claude-3-5-haiku-20241022": "anthropic.claude-3-5-haiku-20241022-v1:0", "claude-3-7-sonnet-20250219": "anthropic.claude-3-7-sonnet-20250219-v1:0", + "claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0", + "claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0", } var awsModelCanCrossRegionMap = map[string]map[string]bool{ @@ -41,6 +43,16 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{ }, "anthropic.claude-3-7-sonnet-20250219-v1:0": { "us": true, + "ap": true, + "eu": true, + }, + "apac.anthropic.claude-sonnet-4-20250514-v1:0": { + "us": true, + "ap": true, + "eu": true, + }, + "anthropic.claude-opus-4-20250514-v1:0": { + "us": true, }, } diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go index 4b071712..8389b9f1 100644 --- a/relay/channel/claude/adaptor.go +++ b/relay/channel/claude/adaptor.go @@ -38,10 +38,10 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf } func (a *Adaptor) Init(info *relaycommon.RelayInfo) { - if strings.HasPrefix(info.UpstreamModelName, "claude-3") { - a.RequestMode = RequestModeMessage - } else { + if strings.HasPrefix(info.UpstreamModelName, "claude-2") || strings.HasPrefix(info.UpstreamModelName, "claude-instant") { a.RequestMode = RequestModeCompletion + } else { + a.RequestMode = RequestModeMessage } } diff --git a/relay/channel/claude/constants.go b/relay/channel/claude/constants.go index d7e0c8e3..e0e3c421 100644 --- a/relay/channel/claude/constants.go +++ b/relay/channel/claude/constants.go @@ -13,6 +13,10 @@ var ModelList = []string{ "claude-3-5-sonnet-20241022", "claude-3-7-sonnet-20250219", "claude-3-7-sonnet-20250219-thinking", + "claude-sonnet-4-20250514", + "claude-sonnet-4-20250514-thinking", + "claude-opus-4-20250514", + "claude-opus-4-20250514-thinking", } var ChannelName = "claude" diff --git a/relay/channel/gemini/dto.go b/relay/channel/gemini/dto.go index 5d5c1287..a0e38cb4 100644 --- a/relay/channel/gemini/dto.go +++ b/relay/channel/gemini/dto.go @@ -2,10 +2,10 @@ package gemini type GeminiChatRequest struct { Contents []GeminiChatContent `json:"contents"` - SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"` - GenerationConfig GeminiChatGenerationConfig `json:"generation_config,omitempty"` + SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"` + GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"` Tools []GeminiChatTool `json:"tools,omitempty"` - SystemInstructions *GeminiChatContent `json:"system_instruction,omitempty"` + SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"` } type GeminiThinkingConfig struct { @@ -54,6 +54,7 @@ type GeminiFileData struct { type GeminiPart struct { Text string `json:"text,omitempty"` + Thought bool `json:"thought,omitempty"` InlineData *GeminiInlineData `json:"inlineData,omitempty"` FunctionCall *FunctionCall `json:"functionCall,omitempty"` FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"` diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index ae9a3b7b..da0bc5fc 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -539,6 +539,8 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp if call := getResponseToolCall(&part); call != nil { toolCalls = append(toolCalls, *call) } + } else if part.Thought { + choice.Message.ReasoningContent = part.Text } else { if part.ExecutableCode != nil { texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```") @@ -556,7 +558,6 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp choice.Message.SetToolCalls(toolCalls) isToolCall = true } - choice.Message.SetStringContent(strings.Join(texts, "\n")) } @@ -596,6 +597,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C } var texts []string isTools := false + isThought := false if candidate.FinishReason != nil { // p := GeminiConvertFinishReason(*candidate.FinishReason) switch *candidate.FinishReason { @@ -620,6 +622,9 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C call.SetIndex(len(choice.Delta.ToolCalls)) choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call) } + } else if part.Thought { + isThought = true + texts = append(texts, part.Text) } else { if part.ExecutableCode != nil { texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```\n") @@ -632,7 +637,11 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C } } } - choice.Delta.SetContentString(strings.Join(texts, "\n")) + if isThought { + choice.Delta.SetReasoningContent(strings.Join(texts, "\n")) + } else { + choice.Delta.SetContentString(strings.Join(texts, "\n")) + } if isTools { choice.FinishReason = &constant.FinishReasonToolCalls } @@ -716,8 +725,11 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re if err != nil { return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil } + if common.DebugEnabled { + println(string(responseBody)) + } var geminiResponse GeminiChatResponse - err = json.Unmarshal(responseBody, &geminiResponse) + err = common.DecodeJson(responseBody, &geminiResponse) if err != nil { return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil } diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index 7daf9a61..d21a3e08 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -31,6 +31,8 @@ var claudeModelMap = map[string]string{ "claude-3-5-sonnet-20240620": "claude-3-5-sonnet@20240620", "claude-3-5-sonnet-20241022": "claude-3-5-sonnet-v2@20241022", "claude-3-7-sonnet-20250219": "claude-3-7-sonnet@20250219", + "claude-sonnet-4-20250514": "claude-sonnet-4@20250514", + "claude-opus-4-20250514": "claude-opus-4@20250514", } const anthropicVersion = "vertex-2023-10-16" diff --git a/relay/relay-mj.go b/relay/relay-mj.go index a7018456..9d0a2077 100644 --- a/relay/relay-mj.go +++ b/relay/relay-mj.go @@ -32,7 +32,23 @@ func RelayMidjourneyImage(c *gin.Context) { }) return } - resp, err := http.Get(midjourneyTask.ImageUrl) + var httpClient *http.Client + if channel, err := model.CacheGetChannel(midjourneyTask.ChannelId); err == nil { + if proxy, ok := channel.GetSetting()["proxy"]; ok { + if proxyURL, ok := proxy.(string); ok && proxyURL != "" { + if httpClient, err = service.NewProxyHttpClient(proxyURL); err != nil { + c.JSON(400, gin.H{ + "error": "proxy_url_invalid", + }) + return + } + } + } + } + if httpClient == nil { + httpClient = service.GetHttpClient() + } + resp, err := httpClient.Get(midjourneyTask.ImageUrl) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "http_get_image_failed", diff --git a/setting/operation_setting/cache_ratio.go b/setting/operation_setting/cache_ratio.go index dd29eac2..ec0c766d 100644 --- a/setting/operation_setting/cache_ratio.go +++ b/setting/operation_setting/cache_ratio.go @@ -36,6 +36,10 @@ var defaultCacheRatio = map[string]float64{ "claude-3-5-sonnet-20241022": 0.1, "claude-3-7-sonnet-20250219": 0.1, "claude-3-7-sonnet-20250219-thinking": 0.1, + "claude-sonnet-4-20250514": 0.1, + "claude-sonnet-4-20250514-thinking": 0.1, + "claude-opus-4-20250514": 0.1, + "claude-opus-4-20250514-thinking": 0.1, } var defaultCreateCacheRatio = map[string]float64{ @@ -47,6 +51,10 @@ var defaultCreateCacheRatio = map[string]float64{ "claude-3-5-sonnet-20241022": 1.25, "claude-3-7-sonnet-20250219": 1.25, "claude-3-7-sonnet-20250219-thinking": 1.25, + "claude-sonnet-4-20250514": 1.25, + "claude-sonnet-4-20250514-thinking": 1.25, + "claude-opus-4-20250514": 1.25, + "claude-opus-4-20250514-thinking": 1.25, } //var defaultCreateCacheRatio = map[string]float64{} diff --git a/setting/operation_setting/model-ratio.go b/setting/operation_setting/model-ratio.go index fdc1c950..700a7c4e 100644 --- a/setting/operation_setting/model-ratio.go +++ b/setting/operation_setting/model-ratio.go @@ -114,7 +114,9 @@ var defaultModelRatio = map[string]float64{ "claude-3-5-sonnet-20241022": 1.5, "claude-3-7-sonnet-20250219": 1.5, "claude-3-7-sonnet-20250219-thinking": 1.5, + "claude-sonnet-4-20250514": 1.5, "claude-3-opus-20240229": 7.5, // $15 / 1M tokens + "claude-opus-4-20250514": 7.5, "ERNIE-4.0-8K": 0.120 * RMB, "ERNIE-3.5-8K": 0.012 * RMB, "ERNIE-3.5-8K-0205": 0.024 * RMB, @@ -440,13 +442,15 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) { if name == "chatgpt-4o-latest" { return 3, true } - if strings.Contains(name, "claude-instant-1") { - return 3, true - } else if strings.Contains(name, "claude-2") { - return 3, true - } else if strings.Contains(name, "claude-3") { + + if strings.Contains(name, "claude-3") { return 5, true + } else if strings.Contains(name, "claude-sonnet-4") || strings.Contains(name, "claude-opus-4") { + return 5, true + } else if strings.Contains(name, "claude-instant-1") || strings.Contains(name, "claude-2") { + return 3, true } + if strings.HasPrefix(name, "gpt-3.5") { if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") { // https://openai.com/blog/new-embedding-models-and-api-updates diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 3425beea..9b1dd602 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -871,7 +871,16 @@ const ChannelsTable = () => { }; const refresh = async () => { - await loadChannels(activePage - 1, pageSize, idSort, enableTagMode); + if (searchKeyword === '' && searchGroup === '' && searchModel === '') { + await loadChannels(activePage - 1, pageSize, idSort, enableTagMode); + } else { + await searchChannels( + searchKeyword, + searchGroup, + searchModel, + enableTagMode, + ); + } }; useEffect(() => { @@ -979,8 +988,8 @@ const ChannelsTable = () => { enableTagMode, ) => { if (searchKeyword === '' && searchGroup === '' && searchModel === '') { - await loadChannels(0, pageSize, idSort, enableTagMode); - setActivePage(1); + await loadChannels(activePage - 1, pageSize, idSort, enableTagMode); + // setActivePage(1); return; } setSearching(true);