Files
tbphp 91df89efed fix(bifrost): 恢复兼容渠道的工具降级转发 (#653)
* fix(execution): 保留跨协议工具调用约束

* fix(cpa): 修正工具白名单兼容边界

* fix(bifrost): 拒绝丢失压缩上下文

* fix(execution): 保持计数请求工具约束

* fix(bifrost): 恢复兼容渠道的工具降级转发
2026-09-14 22:21:59 +08:00

145 lines
6.5 KiB
Go

package bifrost
import (
"context"
"github.com/maximhq/bifrost/core/providers/anthropic"
"github.com/maximhq/bifrost/core/schemas"
"gpt-load/internal/channel"
"gpt-load/internal/dialect"
"gpt-load/internal/execution"
)
func finishConvertedPreparation(spec execution.AttemptSpec, providerKind channel.ProviderKind, prepared preparedAttempt) (preparedAttempt, *execution.AttemptResult) {
if spec.RouteMode != execution.RouteConverted ||
(spec.Operation != execution.OperationChatCompletion && spec.Operation != execution.OperationResponsesCreate) {
return prepared, nil
}
var toolConstraintsValid bool
var needsToolHistoryCheck bool
prepared, needsToolHistoryCheck, toolConstraintsValid = prepareConvertedToolConstraints(spec.ClientProtocol, providerKind, spec.UpstreamModel, prepared)
if !toolConstraintsValid {
failure := notSentConversionFailure(execution.ErrorCodeCriticalSemanticLoss, "conversion cannot preserve requested tools or tool choice")
return preparedAttempt{}, &failure
}
vertexChat := providerKind == channel.ProviderGoogleVertex && vertexUsesChatFallback(spec.UpstreamModel)
if providerKind == channel.ProviderAnthropic || providerKind == channel.ProviderGemini ||
providerKind == channel.ProviderAWSBedrock || (providerKind == channel.ProviderGoogleVertex && !vertexChat) {
preserveResponsesGlobalInstructions(prepared.responsesRequest)
}
chatFallback := providerKind == channel.ProviderOpenAICompatible || providerKind == channel.ProviderGroq ||
providerKind == channel.ProviderDeepSeek || vertexChat
if chatFallback {
dropsTools, newlyAllowed := chatFallbackToolCompatibility(prepared.responsesRequest)
needsToolHistoryCheck = needsToolHistoryCheck || newlyAllowed
// Compatible 保持 SDK 的尽力转换行为:不支持的工具及选择可被过滤,
// 不因此拒绝整个请求;普通函数白名单仍由前面的准备阶段适配。
if dropsTools && providerKind != channel.ProviderOpenAICompatible {
failure := notSentConversionFailure(execution.ErrorCodeCriticalSemanticLoss, "Chat conversion cannot preserve requested tools or tool choice")
return preparedAttempt{}, &failure
}
if providerKind == channel.ProviderDeepSeek && deepSeekConversionDisablesThinking(prepared.responsesRequest) {
failure := notSentConversionFailure(execution.ErrorCodeCriticalSemanticLoss, "DeepSeek conversion cannot preserve explicit thinking with this tool choice or history")
return preparedAttempt{}, &failure
}
}
if needsToolHistoryCheck && !convertedTargetPreservesToolHistory(providerKind, spec.UpstreamModel, prepared.responsesRequest) {
failure := notSentConversionFailure(execution.ErrorCodeCriticalSemanticLoss, "conversion cannot preserve requested tool history")
return preparedAttempt{}, &failure
}
wantSystems := dialect.CountMidConversationSystemMessages(spec.ClientProtocol, spec.Body)
if wantSystems == 0 {
return prepared, nil
}
preserved := true
switch providerKind {
case channel.ProviderGemini, channel.ProviderAWSBedrock:
// 这些现有转换器只能把中途系统指令前移或变成用户文本。
preserved = false
case channel.ProviderGoogleVertex:
// Vertex 的非 Claude/Gemini 模型使用 OpenAI wire,可以原位保留系统消息。
preserved = vertexChat
case channel.ProviderAnthropic:
// 仅对此输入形状复用 SDK 的 ModelCaps 和实际位置规则,避免复制模型表或近似工具分组。
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
defer ctx.Cancel()
var request *anthropic.AnthropicMessageRequest
var err error
if prepared.request != nil {
request, err = anthropic.ToAnthropicChatRequest(ctx, prepared.request)
} else {
request, err = anthropic.ToAnthropicResponsesRequest(ctx, prepared.responsesRequest)
}
keptSystems := 0
if err == nil && request != nil {
for _, message := range request.Messages {
if message.Role == anthropic.AnthropicMessageRoleSystem {
keptSystems++
}
}
}
preserved = keptSystems == wantSystems
}
if !preserved {
failure := notSentConversionFailure(execution.ErrorCodeCriticalSemanticLoss, "conversion cannot preserve mid-conversation system instructions")
return preparedAttempt{}, &failure
}
return prepared, nil
}
func vertexUsesChatFallback(model string) bool {
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
defer ctx.Cancel()
return !schemas.IsAnthropicModelFamily(ctx, model) && !schemas.IsGeminiModelFamily(ctx, model) &&
!schemas.IsGemmaModelFamily(ctx, model) && !schemas.IsAllDigitsASCII(model)
}
func preserveResponsesGlobalInstructions(request *schemas.BifrostResponsesRequest) {
if request == nil || request.Params == nil || request.Params.Instructions == nil || *request.Params.Instructions == "" {
return
}
// 这些 SDK 转换器优先使用 input 内的 system,可能覆盖并存的 instructions。
// 将全局指令放在输入前端,让同一转换器按顺序处理,并避免重复发送。
request.Input = append([]schemas.ResponsesMessage{{
Type: schemas.Ptr(schemas.ResponsesMessageTypeMessage),
Role: schemas.Ptr(schemas.ResponsesInputMessageRoleSystem),
Content: &schemas.ResponsesMessageContent{ContentStr: request.Params.Instructions},
}}, request.Input...)
request.Params.Instructions = nil
}
func deepSeekConversionDisablesThinking(request *schemas.BifrostResponsesRequest) bool {
if request == nil || request.Params == nil || request.Params.Reasoning == nil {
return false
}
reasoning := request.Params.Reasoning
enabled := (reasoning.Effort != nil && *reasoning.Effort != "" && *reasoning.Effort != "none") ||
(reasoning.MaxTokens != nil && *reasoning.MaxTokens != 0)
if !enabled {
return false
}
chat := request.ToChatRequest()
// 对齐锁定 SDK 的 requiresDeepSeekThinkingDisabled,仅阻断显式开启思考的冲突输入。
if choice := chat.Params.ToolChoice; choice != nil {
var kind schemas.ChatToolChoiceType
if choice.ChatToolChoiceStr != nil {
kind = schemas.ChatToolChoiceType(*choice.ChatToolChoiceStr)
} else if choice.ChatToolChoiceStruct != nil {
kind = choice.ChatToolChoiceStruct.Type
}
switch kind {
case schemas.ChatToolChoiceTypeRequired, schemas.ChatToolChoiceTypeAny, schemas.ChatToolChoiceTypeFunction,
schemas.ChatToolChoiceTypeCustom, schemas.ChatToolChoiceTypeAllowedTools:
return true
}
}
for _, message := range chat.Input {
if message.Role == schemas.ChatMessageRoleAssistant && message.ChatAssistantMessage != nil &&
len(message.ToolCalls) != 0 && message.Reasoning == nil {
return true
}
}
return false
}