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

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

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

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

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

368 lines
18 KiB
Go

package bifrost
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"gpt-load/internal/channel"
"gpt-load/internal/execution"
"gpt-load/internal/protocol"
)
func TestConvertedMidConversationInstructionsAreNotWeakened(t *testing.T) {
t.Parallel()
for _, clientProtocol := range []protocol.Protocol{protocol.OpenAICompletions, protocol.OpenAIResponses} {
for _, test := range []struct {
name string
channel channel.ID
model string
roles []string
wantError bool
}{
{"unsupported model", channel.Anthropic, "claude-sonnet-4-6", []string{"user", "system", "assistant"}, true},
{"unsupported position", channel.Anthropic, "claude-opus-4-8", []string{"user", "assistant", "system", "user"}, true},
{"supported position", channel.Anthropic, "claude-opus-4-8", []string{"user", "system", "assistant"}, false},
{"ordinary user reminder", channel.Anthropic, "claude-sonnet-4-6", []string{"user", "assistant", "user"}, false},
{"Gemini system hoist", channel.Gemini, "gemini-2.5-pro", []string{"user", "assistant", "system", "user"}, true},
} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/%s/stream=%t", clientProtocol, test.name, stream), func(t *testing.T) {
var calls atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
calls.Add(1)
var received struct {
Messages []struct{ Role string } `json:"messages"`
}
if err := json.NewDecoder(request.Body).Decode(&received); err != nil {
t.Error(err)
}
if !test.wantError && test.name == "supported position" &&
(len(received.Messages) < 2 || received.Messages[1].Role != "system") {
t.Error("supported system instruction did not keep its position")
}
if stream {
writer.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(writer, anthropicResponsesStreamFixture)
} else {
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, anthropicResponsesConvertedFixture)
}
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{allowPrivateNetwork: true,
anthropicBaseURL: server.URL, geminiBaseURL: server.URL + "/v1beta"})
operation, path, field := execution.OperationChatCompletion, "/v1/chat/completions", "messages"
if clientProtocol == protocol.OpenAIResponses {
operation, path, field = execution.OperationResponsesCreate, "/v1/responses", "input"
}
messages := make([]map[string]any, 0, len(test.roles))
for _, role := range test.roles {
messages = append(messages, map[string]any{"role": role, "content": "<system-reminder>instruction</system-reminder>"})
}
body, err := json.Marshal(map[string]any{"model": "client-model", field: messages, "max_tokens": 32, "stream": stream})
if err != nil {
t.Fatal(err)
}
spec := convertedSpec(test.channel, clientProtocol, operation, path, body)
spec.UpstreamModel = test.model
var evidence *execution.ErrorEvidence
if stream {
result := runtime.ExecuteStream(t.Context(), spec, func(execution.StreamEvent) error { return nil })
evidence = result.Error
} else {
evidence = runtime.Execute(t.Context(), spec).Error
}
if test.wantError {
if calls.Load() != 0 || evidence == nil || evidence.Kind != execution.ErrorKindConversionUnsupported ||
evidence.Code != execution.ErrorCodeCriticalSemanticLoss {
t.Fatalf("lossy conversion: calls=%d error=%+v", calls.Load(), evidence)
}
} else if calls.Load() != 1 || evidence != nil {
t.Fatalf("supported conversion: calls=%d error=%+v", calls.Load(), evidence)
}
})
}
}
}
}
func TestConvertedInstructionsAfterRolelessResponsesHistory(t *testing.T) {
t.Parallel()
const search = `{"type":"web_search_call","id":"ws_history","status":"completed","action":{"type":"search","query":"synthetic"}}`
const custom = `{"type":"custom_tool_call","id":"ct_history","call_id":"call_history","name":"synthetic_tool","input":"synthetic input"}`
for _, test := range []struct {
name string
history string
role string
wantError bool
}{
{"search then system", search, "system", true},
{"search then developer", search, "developer", true},
{"custom then system", custom, "system", true},
{"custom then developer", custom, "developer", true},
{"ordinary user reminder", search, "user", false},
{"leading global instructions", `{"role":"system","content":"global instruction"}`, "developer", false},
} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/stream=%t", test.name, stream), func(t *testing.T) {
var calls atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
calls.Add(1)
body, err := io.ReadAll(request.Body)
if err != nil {
t.Error(err)
}
if test.wantError {
t.Errorf("lossy instruction reached upstream: %s", body)
} else if !strings.Contains(string(body), "caller reminder") {
t.Errorf("preserved instruction or user text is missing: %s", body)
}
writer.Header().Set("Content-Type", "application/json")
response := anthropicResponsesConvertedFixture
if stream {
writer.Header().Set("Content-Type", "text/event-stream")
response = anthropicResponsesStreamFixture
}
_, _ = io.WriteString(writer, response)
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{allowPrivateNetwork: true, anthropicBaseURL: server.URL})
body := []byte(fmt.Sprintf(`{"model":"client-model","store":false,"input":[%s,{"role":"%s","content":"<system-reminder>caller reminder</system-reminder>"},{"role":"user","content":"continue"}]}`, test.history, test.role))
spec := convertedSpec(channel.Anthropic, protocol.OpenAIResponses, execution.OperationResponsesCreate, "/v1/responses", body)
spec.UpstreamModel = "claude-sonnet-4-6"
var evidence *execution.ErrorEvidence
var dispatch execution.DispatchState
if stream {
result := runtime.ExecuteStream(t.Context(), spec, func(execution.StreamEvent) error { return nil })
evidence, dispatch = result.Error, result.DispatchState
} else {
result := runtime.Execute(t.Context(), spec)
evidence, dispatch = result.Error, result.DispatchState
}
if test.wantError {
if calls.Load() != 0 || dispatch != execution.DispatchNotSent || evidence == nil ||
evidence.Kind != execution.ErrorKindConversionUnsupported || evidence.Code != execution.ErrorCodeCriticalSemanticLoss {
t.Fatalf("lossy conversion: calls=%d dispatch=%s error=%+v", calls.Load(), dispatch, evidence)
}
} else if calls.Load() != 1 || evidence != nil {
t.Fatalf("preservable input was rejected: calls=%d error=%+v", calls.Load(), evidence)
}
})
}
}
}
func TestChatFallbackDoesNotDropRequestedTools(t *testing.T) {
t.Parallel()
for _, channelID := range []channel.ID{channel.OpenAICompatible, channel.Groq} {
for _, test := range []struct {
name string
tools string
choice string
wantError bool
}{
{"no tools automatic", `[]`, `"auto"`, false},
{"no tools disabled", `[]`, `"none"`, false},
{"no tools required", `[]`, `"required"`, true},
{"web search", `[{"type":"web_search_preview"}]`, `"auto"`, true},
{"function", `[{"type":"function","name":"lookup","parameters":{"type":"object"}}]`, `"required"`, false},
{"allowed tools", `[{"type":"function","name":"lookup","parameters":{"type":"object"}}]`, `{"type":"allowed_tools","mode":"required","tools":[{"type":"function","name":"lookup"}]}`, false},
} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/%s/stream=%t", channelID, test.name, stream), func(t *testing.T) {
var calls atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
calls.Add(1)
_, body, events := nativeFidelityFixture(protocol.OpenAICompletions)
if stream {
writer.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(writer, events)
} else {
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, body)
}
}))
defer server.Close()
runtime := newRuntimeForTest(t, testRuntimeOptions{allowPrivateNetwork: true})
runtime.baseURLs[channelID] = server.URL
body := []byte(fmt.Sprintf(`{"model":"client-model","input":"hello","store":false,"tools":%s,"tool_choice":%s}`, test.tools, test.choice))
spec := convertedSpec(channelID, protocol.OpenAIResponses, execution.OperationResponsesCreate, "/v1/responses", body)
var evidence *execution.ErrorEvidence
if stream {
evidence = runtime.ExecuteStream(t.Context(), spec, func(execution.StreamEvent) error { return nil }).Error
} else {
evidence = runtime.Execute(t.Context(), spec).Error
}
if test.wantError && channelID != channel.OpenAICompatible {
if calls.Load() != 0 || evidence == nil || evidence.Kind != execution.ErrorKindConversionUnsupported || evidence.Code != execution.ErrorCodeCriticalSemanticLoss {
t.Fatalf("tool requirement was lost: calls=%d error=%+v", calls.Load(), evidence)
}
} else if calls.Load() != 1 || evidence != nil {
t.Fatalf("function tool was rejected: calls=%d error=%+v", calls.Load(), evidence)
}
})
}
}
}
}
func TestDeepSeekConversionDoesNotDisableExplicitThinking(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
thinking string
mode string
history string
wantError bool
}{
{"forced tool with thinking", `{"thinkingBudget":4096}`, "ANY", "", true},
{"automatic tools with thinking", `{"thinkingLevel":"high"}`, "AUTO", "", false},
{"forced tool without explicit thinking", `{}`, "ANY", "", false},
{"missing replay with thinking", `{"thinkingLevel":"high"}`, "AUTO", `,{"role":"model","parts":[{"functionCall":{"name":"lookup","args":{}}}]},{"role":"user","parts":[{"functionResponse":{"name":"lookup","response":{"result":"ok"}}}]}`, true},
{"thinking disabled", `{"thinkingBudget":0}`, "ANY", "", false},
} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/stream=%t", test.name, stream), func(t *testing.T) {
var calls atomic.Int64
var wireBody []byte
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
wireBody, _ = io.ReadAll(request.Body)
calls.Add(1)
_, body, events := nativeFidelityFixture(protocol.OpenAICompletions)
if stream {
writer.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(writer, events)
} else {
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, body)
}
}))
defer server.Close()
runtime := newRuntimeForTest(t, testRuntimeOptions{allowPrivateNetwork: true})
runtime.baseURLs[channel.DeepSeek] = server.URL
body := []byte(fmt.Sprintf(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}%s],"generationConfig":{"thinkingConfig":%s},"tools":[{"functionDeclarations":[{"name":"lookup","parameters":{"type":"object"}}]}],"toolConfig":{"functionCallingConfig":{"mode":"%s"}}}`, test.history, test.thinking, test.mode))
spec := convertedSpec(channel.DeepSeek, protocol.Gemini, execution.OperationChatCompletion, "/v1beta/models/client-model:generateContent", body)
if stream {
spec.Path = "/v1beta/models/client-model:streamGenerateContent"
}
var evidence *execution.ErrorEvidence
if stream {
evidence = runtime.ExecuteStream(t.Context(), spec, func(execution.StreamEvent) error { return nil }).Error
} else {
evidence = runtime.Execute(t.Context(), spec).Error
}
if test.wantError {
if calls.Load() != 0 || evidence == nil || evidence.Kind != execution.ErrorKindConversionUnsupported || evidence.Code != execution.ErrorCodeCriticalSemanticLoss {
t.Fatalf("thinking was silently disabled: calls=%d error=%+v wire=%s", calls.Load(), evidence, wireBody)
}
} else if calls.Load() != 1 || evidence != nil {
t.Fatalf("valid thinking/tool combination was rejected: calls=%d error=%+v", calls.Load(), evidence)
} else if test.name == "automatic tools with thinking" {
wire := nativeFidelityObject(t, wireBody)
thinking, _ := wire["thinking"].(map[string]any)
if wire["tool_choice"] != "auto" || thinking["type"] == "disabled" {
t.Fatalf("automatic tools changed thinking or forced a tool: %s", wireBody)
}
}
})
}
}
}
func TestCloudConversionKeepsSupportedSystemRoles(t *testing.T) {
t.Parallel()
for _, test := range []struct {
channel channel.ID
model string
config string
credential string
wantError bool
}{
{channel.GoogleVertex, "claude-opus-4-8", `{"location":"us-central1"}`, `{"service_account_json":"{\"type\":\"service_account\",\"project_id\":\"test-project\",\"client_email\":\"svc@example.com\",\"private_key\":\"synthetic\"}"}`, true},
{channel.GoogleVertex, "gemini-2.5-pro", `{"location":"us-central1"}`, `{"service_account_json":"{\"type\":\"service_account\",\"project_id\":\"test-project\",\"client_email\":\"svc@example.com\",\"private_key\":\"synthetic\"}"}`, true},
{channel.GoogleVertex, "meta/llama-4", `{"location":"us-central1"}`, `{"service_account_json":"{\"type\":\"service_account\",\"project_id\":\"test-project\",\"client_email\":\"svc@example.com\",\"private_key\":\"synthetic\"}"}`, false},
{channel.AWSBedrock, "anthropic.claude-opus-4-8", `{"region":"us-east-1"}`, `{"api_key":"synthetic"}`, true},
{channel.AWSBedrock, "amazon.nova-pro-v1:0", `{"region":"us-east-1"}`, `{"api_key":"synthetic"}`, true},
} {
for _, clientProtocol := range []protocol.Protocol{protocol.OpenAICompletions, protocol.OpenAIResponses} {
t.Run(fmt.Sprintf("%s/%s/%s", test.channel, test.model, clientProtocol), func(t *testing.T) {
field, path, operation := "messages", "/v1/chat/completions", execution.OperationChatCompletion
if clientProtocol == protocol.OpenAIResponses {
field, path, operation = "input", "/v1/responses", execution.OperationResponsesCreate
}
body := []byte(fmt.Sprintf(`{"model":"client-model","%s":[{"role":"user","content":"hello"},{"role":"system","content":"instruction"},{"role":"assistant","content":"ok"},{"role":"user","content":"next"}]}`, field))
spec := convertedSpec(test.channel, clientProtocol, operation, path, body)
spec.TargetConfig = json.RawMessage(test.config)
spec.UpstreamModel = test.model
spec.Credential = execution.NewCredentialSnapshot(1, 1, 1, []byte(test.credential))
spec = freezeTestAttempt(spec)
runtime := newTestRuntime(t)
for _, stream := range []bool{false, true} {
_, failure := runtime.prepare(spec, stream)
if test.wantError {
if failure == nil || failure.Error == nil || failure.Error.Code != execution.ErrorCodeCriticalSemanticLoss || failure.DispatchState != execution.DispatchNotSent {
t.Fatalf("lossy cloud conversion was not rejected before dispatch: %+v", failure)
}
} else if failure != nil {
t.Fatalf("OpenAI wire can preserve system roles: %+v", failure.Error)
}
}
})
}
}
}
func TestConvertedResponsesKeepsAllGlobalInstructions(t *testing.T) {
t.Parallel()
for _, channelID := range []channel.ID{channel.Anthropic, channel.Gemini, channel.OpenAICompatible} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/stream=%t", channelID, stream), func(t *testing.T) {
var calls atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
calls.Add(1)
body, _ := io.ReadAll(request.Body)
wire := string(body)
first, second := strings.Index(wire, "FIRST_GLOBAL"), strings.Index(wire, "SECOND_GLOBAL")
if first < 0 || second < 0 || first > second || strings.Count(wire, "FIRST_GLOBAL") != 1 || strings.Count(wire, "SECOND_GLOBAL") != 1 {
t.Errorf("global instructions were dropped, duplicated or reordered: %s", body)
}
_, response, events := nativeFidelityFixture(protocol.OpenAICompletions)
if channelID == channel.Anthropic {
response, events = anthropicResponsesConvertedFixture, anthropicResponsesStreamFixture
} else if channelID == channel.Gemini {
response, events = geminiResponsesConvertedFixture, geminiResponsesStreamFixture
}
writer.Header().Set("Content-Type", "application/json")
if stream {
writer.Header().Set("Content-Type", "text/event-stream")
response = events
}
_, _ = io.WriteString(writer, response)
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{allowPrivateNetwork: true, anthropicBaseURL: server.URL, geminiBaseURL: server.URL + "/v1beta"})
spec := convertedSpec(channelID, protocol.OpenAIResponses, execution.OperationResponsesCreate, "/v1/responses", []byte(`{"model":"client-model","instructions":"FIRST_GLOBAL","input":[{"role":"system","content":"SECOND_GLOBAL"},{"role":"user","content":"hello"}],"store":false}`))
if channelID == channel.OpenAICompatible {
spec.TargetConfig = json.RawMessage(`{"base_url":"` + server.URL + `/v1"}`)
spec = freezeTestAttempt(spec)
}
var evidence *execution.ErrorEvidence
if stream {
evidence = runtime.ExecuteStream(t.Context(), spec, func(execution.StreamEvent) error { return nil }).Error
} else {
evidence = runtime.Execute(t.Context(), spec).Error
}
if calls.Load() != 1 || evidence != nil {
t.Fatalf("valid global instructions were rejected: calls=%d error=%+v", calls.Load(), evidence)
}
})
}
}
}