fix(gateway): 修复协议查询参数与零输出边界 (#603)

This commit is contained in:
tbphp authored and GitHub committed 2026-09-09 11:18:20 +08:00
1 parent f0bd09b31a
commit 1898d8ee64
7 files changed
+569

No files matched your search

+18
View File
@@ -1,6 +1,8 @@
package dialect
import (
"bytes"
"encoding/json"
"fmt"
"gpt-load/internal/execution"
@@ -19,6 +21,22 @@ func (*Anthropic) Protocol() protocol.Protocol {
return protocol.Anthropic
}
// AnthropicRequestsZeroOutput 判断请求是否显式要求零输出,不把缺失、null 或字符串视为数值零。
func AnthropicRequestsZeroOutput(body []byte) bool {
var request struct {
MaxTokens json.RawMessage `json:"max_tokens"`
}
if err := json.Unmarshal(body, &request); err != nil {
return false
}
mantissa := request.MaxTokens
if index := bytes.IndexAny(mantissa, "eE"); index >= 0 {
mantissa = mantissa[:index]
}
// 按有效数字判断,避免极小的非零数值经浮点下溢被误判为零。
return bytes.IndexByte(mantissa, '0') >= 0 && len(bytes.Trim(mantissa, "-0.")) == 0
}
func (d *Anthropic) InspectRequest(req *ParsedRequest) (RequestMetadata, error) {
if req == nil {
return RequestMetadata{}, fmt.Errorf("parsed request is required")
+21
View File
@@ -19,6 +19,7 @@ import (
"github.com/maximhq/bifrost/core/schemas"
"gpt-load/internal/channel"
"gpt-load/internal/dialect"
"gpt-load/internal/execution"
"gpt-load/internal/protocol"
"gpt-load/internal/reasoning"
@@ -456,6 +457,16 @@ func (r *Runtime) prepare(spec execution.AttemptSpec, stream bool) (preparedAtte
return preparedAttempt{}, &failure
}
safeQuery := safeAttemptQuery(spec)
if mode == channel.RouteConverted {
// 源协议的调用标记和响应格式不能继承到其他协议的目标 URL。
switch spec.ClientProtocol {
case protocol.Anthropic:
safeQuery = removeRawQueryValue(safeQuery, "beta")
case protocol.Gemini:
safeQuery = removeRawQueryValue(safeQuery, "alt")
safeQuery = removeRawQueryValue(safeQuery, "$alt")
}
}
if mode == channel.RouteNative && spec.ClientProtocol == protocol.Gemini &&
(providerKind == channel.ProviderGemini || providerKind == channel.ProviderGoogleVertex ||
providerKind == channel.ProviderMultiProtocolGateway) {
@@ -659,6 +670,16 @@ func (r *Runtime) prepare(spec execution.AttemptSpec, stream bool) (preparedAtte
secrets: secrets,
}, nil
}
if spec.ClientProtocol == protocol.Anthropic && spec.Operation == execution.OperationChatCompletion &&
dialect.AnthropicRequestsZeroOutput(spec.Body) {
failure := notSentConversionFailure(
execution.ErrorCodeCriticalSemanticLoss,
"typed conversion cannot preserve Anthropic zero-output semantics",
)
failure.Error.OriginHint = execution.ErrorOriginInternal
failure.Error.ScopeHint = execution.ErrorScopeGroup
return preparedAttempt{}, &failure
}
if spec.Operation == execution.OperationListModels {
typedURL, upstreamProtocol, targetErr := convertedListModelsTarget(providerKind, customTargetBaseURL, safeQuery)
if targetErr != nil {
+185
View File
@@ -0,0 +1,185 @@
package bifrost
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"gpt-load/internal/channel"
"gpt-load/internal/execution"
"gpt-load/internal/protocol"
)
func TestRuntimeNormalizesConvertedProtocolQueries(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
clientProtocol protocol.Protocol
channelID channel.ID
rawQuery string
query url.Values
wantQuery string
}{
{
name: "Anthropic to Gemini", clientProtocol: protocol.Anthropic, channelID: channel.Gemini,
rawQuery: "beta=true&%62eta=false&trace=%2f&&cursor=next&api_key=removed",
wantQuery: "trace=%2f&&cursor=next",
},
{
name: "Gemini to Anthropic", clientProtocol: protocol.Gemini, channelID: channel.Anthropic,
rawQuery: "alt=json&$alt=proto&%24alt=sse&trace=%2F&trace=%2f&key=removed",
wantQuery: "trace=%2F&trace=%2f",
},
{
name: "Gemini query values to OpenAI", clientProtocol: protocol.Gemini, channelID: channel.OpenAI,
query: url.Values{"alt": {"json"}, "$alt": {"sse"}, "trace": {"kept"}, "key": {"removed"}},
wantQuery: "trace=kept",
},
{
name: "native Anthropic preserves beta", clientProtocol: protocol.Anthropic, channelID: channel.Anthropic,
rawQuery: "beta=true&trace=%2f&key=removed", wantQuery: "beta=true&trace=%2f",
},
{
name: "native Gemini retains existing format handling", clientProtocol: protocol.Gemini, channelID: channel.Gemini,
rawQuery: "alt=json&trace=%2f&key=removed", wantQuery: "trace=%2f",
},
} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/stream=%t", test.name, stream), func(t *testing.T) {
wantQuery := test.wantQuery
if test.channelID == channel.Gemini && stream {
wantQuery += "&alt=sse"
}
wantPath := "/v1/messages"
responseBody := anthropicResponsesConvertedFixture
if stream {
responseBody = anthropicResponsesStreamFixture
}
switch test.channelID {
case channel.Gemini:
wantPath = "/v1beta/models/upstream-model:generateContent"
responseBody = geminiResponsesConvertedFixture
if stream {
wantPath = "/v1beta/models/upstream-model:streamGenerateContent"
responseBody = geminiResponsesStreamFixture
}
case channel.OpenAI:
wantPath = "/v1/responses"
responseBody = openAIResponsesConvertedFixture
if stream {
responseBody = openAIResponsesStreamFixture
}
}
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != wantPath || request.URL.RawQuery != wantQuery {
t.Errorf("target = %s?%s, want %s?%s", request.URL.Path, request.URL.RawQuery, wantPath, wantQuery)
}
if request.Header.Get("Anthropic-Beta") != "test-feature" {
t.Error("query normalization changed the request header")
}
writer.Header().Set("Content-Type", "application/json")
if stream {
writer.Header().Set("Content-Type", "text/event-stream")
}
_, _ = io.WriteString(writer, responseBody)
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{
allowPrivateNetwork: true, openAIBaseURL: server.URL,
anthropicBaseURL: server.URL, geminiBaseURL: server.URL + "/v1beta",
})
path := "/v1/messages"
body := []byte(`{"model":"client-model","max_tokens":16,"messages":[{"role":"user","content":"hello"}]}`)
if test.clientProtocol == protocol.Gemini {
path = "/v1beta/models/client-model:generateContent"
if stream {
path = "/v1beta/models/client-model:streamGenerateContent"
}
body = []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`)
}
spec := convertedSpec(test.channelID, test.clientProtocol, execution.OperationChatCompletion, path, body)
spec.RawQuery = test.rawQuery
spec.Query = test.query
spec.Header.Set("Anthropic-Beta", "test-feature")
original := spec.Clone()
if stream {
result := runtime.ExecuteStream(context.Background(), spec, func(execution.StreamEvent) error { return nil })
if result.Error != nil || result.StatusCode != http.StatusOK {
t.Fatalf("stream result = %+v", result)
}
} else {
result := runtime.Execute(context.Background(), spec)
if result.Error != nil || result.StatusCode != http.StatusOK {
t.Fatalf("result = %+v", result)
}
}
if !reflect.DeepEqual(spec, original) {
t.Error("execution mutated the original attempt")
}
})
}
}
}
func TestConvertedCountTokensDropsAnthropicBetaQuery(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/v1beta/models/upstream-model:countTokens" || request.URL.RawQuery != "trace=%2f" {
t.Errorf("count tokens target = %s?%s", request.URL.Path, request.URL.RawQuery)
}
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, `{"totalTokens":7}`)
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{allowPrivateNetwork: true, geminiBaseURL: server.URL + "/v1beta"})
spec := convertedSpec(channel.Gemini, protocol.Anthropic, execution.OperationCountTokens,
"/v1/messages/count_tokens", []byte(`{"model":"client-model","messages":[{"role":"user","content":"hello"}]}`))
spec.RawQuery = "beta=true&trace=%2f"
result := runtime.Execute(context.Background(), spec)
if result.Error != nil || result.StatusCode != http.StatusOK {
t.Fatalf("result = %+v", result)
}
}
func TestConvertedProtocolQueriesDoNotBlockProviderPreparation(t *testing.T) {
t.Parallel()
for _, target := range []struct {
channelID channel.ID
config json.RawMessage
}{
{channel.Groq, json.RawMessage(`{}`)},
{channel.AzureOpenAI, json.RawMessage(`{"endpoint":"https://resource.openai.azure.com"}`)},
{channel.AWSBedrock, json.RawMessage(`{"region":"us-east-1"}`)},
} {
for _, clientProtocol := range []protocol.Protocol{protocol.Anthropic, protocol.Gemini} {
t.Run(fmt.Sprintf("%s/%s", target.channelID, clientProtocol), func(t *testing.T) {
runtime := newProtocolTestRuntime(t, testRuntimeOptions{})
path := "/v1/messages"
body := []byte(`{"model":"client-model","max_tokens":16,"messages":[{"role":"user","content":"hello"}]}`)
query := "beta=true"
if clientProtocol == protocol.Gemini {
path = "/v1beta/models/client-model:streamGenerateContent"
body = []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`)
query = "alt=sse&%24alt=json"
}
spec := convertedSpec(target.channelID, clientProtocol, execution.OperationChatCompletion, path, body)
spec.TargetConfig = target.config
spec = freezeTestAttempt(spec)
spec.RawQuery = query
if _, failure := runtime.prepare(spec, true); failure != nil {
t.Fatalf("prepare() error = %+v", failure.Error)
}
})
}
}
}
@@ -0,0 +1,120 @@
package bifrost
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"gpt-load/internal/channel"
"gpt-load/internal/execution"
"gpt-load/internal/protocol"
)
func TestRuntimeRejectsAnthropicZeroOutputBeforeTypedConversion(t *testing.T) {
t.Parallel()
for _, channelID := range []channel.ID{channel.Gemini, channel.OpenAI, channel.OpenAICompatible, channel.DeepSeek} {
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)
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusBadRequest)
_, _ = io.WriteString(writer, `{"error":{"message":"unexpected generation request"}}`)
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{
allowPrivateNetwork: true, geminiBaseURL: server.URL + "/v1beta", openAIBaseURL: server.URL,
})
runtime.baseURLs[channel.OpenAICompatible] = server.URL
runtime.baseURLs[channel.DeepSeek] = server.URL
spec := convertedSpec(channelID, protocol.Anthropic, execution.OperationChatCompletion,
"/v1/messages", []byte(`{"model":"client-model","max_tokens":0,"messages":[{"role":"user","content":"hello"}]}`))
var evidence *execution.ErrorEvidence
if stream {
events := 0
result := runtime.ExecuteStream(t.Context(), spec, func(execution.StreamEvent) error {
events++
return nil
})
if result.DispatchState != execution.DispatchNotSent || result.ResponseStarted || events != 0 {
t.Errorf("stream result = %+v, events = %d", result, events)
}
evidence = result.Error
} else {
result := runtime.Execute(t.Context(), spec)
if result.DispatchState != execution.DispatchNotSent || result.ResponseStarted {
t.Errorf("result = %+v", result)
}
evidence = result.Error
}
if evidence == nil || evidence.Kind != execution.ErrorKindConversionUnsupported ||
evidence.Code != execution.ErrorCodeCriticalSemanticLoss ||
evidence.ScopeHint != execution.ErrorScopeGroup || evidence.OriginHint != execution.ErrorOriginInternal {
t.Errorf("error = %+v", evidence)
}
if calls.Load() != 0 {
t.Errorf("upstream calls = %d, want 0", calls.Load())
}
})
}
}
}
func TestNativeAnthropicZeroOutputPreservesLimitAndResponse(t *testing.T) {
t.Parallel()
const responseBody = `{"id":"msg_1","type":"message","role":"assistant","model":"upstream-model","content":[],"stop_reason":"max_tokens","stop_sequence":null,"usage":{"input_tokens":4,"output_tokens":0}}`
for _, channelID := range []channel.ID{channel.Anthropic, channel.NewAPI} {
t.Run(string(channelID), func(t *testing.T) {
var calls atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
calls.Add(1)
var body map[string]json.RawMessage
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
t.Error(err)
}
if string(body["max_tokens"]) != "0" || request.URL.Path != "/v1/messages" {
t.Errorf("native path/limit = %s/%s", request.URL.Path, body["max_tokens"])
}
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, responseBody)
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{allowPrivateNetwork: true, anthropicBaseURL: server.URL})
runtime.baseURLs[channel.NewAPI] = server.URL
spec := convertedSpec(channelID, protocol.Anthropic, execution.OperationChatCompletion,
"/v1/messages", []byte(`{"model":"client-model","max_tokens":0,"messages":[{"role":"user","content":"hello"}]}`))
spec.ClientModel = spec.UpstreamModel
result := runtime.Execute(t.Context(), spec)
if result.Error != nil || result.StatusCode != http.StatusOK || calls.Load() != 1 || string(result.Body) != responseBody {
t.Fatalf("result = %+v, calls = %d, body = %s", result, calls.Load(), result.Body)
}
})
}
}
func TestCountTokensDoesNotApplyZeroOutputGuard(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/v1beta/models/upstream-model:countTokens" {
t.Errorf("path = %s", request.URL.Path)
}
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, `{"totalTokens":7}`)
}))
defer server.Close()
runtime := newProtocolTestRuntime(t, testRuntimeOptions{allowPrivateNetwork: true, geminiBaseURL: server.URL + "/v1beta"})
spec := convertedSpec(channel.Gemini, protocol.Anthropic, execution.OperationCountTokens,
"/v1/messages/count_tokens", []byte(`{"model":"client-model","max_tokens":0,"messages":[{"role":"user","content":"hello"}]}`))
result := runtime.Execute(t.Context(), spec)
if result.Error != nil || result.StatusCode != http.StatusOK {
t.Fatalf("count tokens result = %+v", result)
}
}
+86
View File
@@ -0,0 +1,86 @@
package gateway
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"gpt-load/internal/channel"
"gpt-load/internal/dialect"
"gpt-load/internal/platform/config"
"gpt-load/internal/protocol"
"gpt-load/internal/state"
"gpt-load/internal/testutil/fakeupstream"
)
func TestGatewayZeroOutputProtectionUsesGroupEffectiveParameters(t *testing.T) {
for _, test := range []struct {
name string
original int
override any
wantReject bool
}{
{name: "zero limit is protected", original: 0, wantReject: true},
{name: "group requests zero output", original: 16, override: 0, wantReject: true},
{name: "explicit group override permits generation", original: 0, override: 16},
{name: "ordinary generation", original: 16},
} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/stream=%t", test.name, stream), func(t *testing.T) {
fixture := "gemini/success.json"
if stream {
fixture = "gemini/stream.sse"
}
upstream := fakeupstream.New(fakeupstream.Step{Status: http.StatusOK, Fixture: fixture})
defer upstream.Close()
params, err := json.Marshal(map[string]string{"base_url": upstream.URL + "/v1beta"})
if err != nil {
t.Fatal(err)
}
var settings config.Settings
if test.override != nil {
settings = config.Settings{state.SettingParameterOverrides: []any{
map[string]any{"set": map[string]any{"max_tokens": test.override}},
}}
}
engine, registry := newDialectGatewayEngine(t, protocol.Anthropic, "public",
dialect.NewSet(dialect.NewAnthropic()), dialectGatewayGroup{
id: 1, name: "gemini", channelID: channel.Gemini, params: params,
apiKeys: []string{"upstream-key"}, settings: settings,
})
before := registry.Snapshot()
body := fmt.Sprintf(`{"model":"public","max_tokens":%d,"stream":%t,"messages":[{"role":"user","content":"hello"}]}`, test.original, stream)
request := httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(body))
request.Header.Set("Authorization", "Bearer gl-client")
response := httptest.NewRecorder()
engine.ServeHTTP(response, request)
if test.wantReject {
if response.Code != http.StatusUnprocessableEntity ||
!strings.Contains(response.Body.String(), `"code":"protocol_conversion_unsupported"`) ||
len(upstream.Requests()) != 0 || strings.Contains(response.Body.String(), "event: message_start") {
t.Fatalf("response = %d %s, upstream calls = %d", response.Code, response.Body.String(), len(upstream.Requests()))
}
if !reflect.DeepEqual(before, registry.Snapshot()) {
t.Fatal("local conversion rejection changed credential health")
}
return
}
if response.Code != http.StatusOK || len(upstream.Requests()) != 1 {
t.Fatalf("generation response = %d %s, upstream calls = %d", response.Code, response.Body.String(), len(upstream.Requests()))
}
var received struct {
GenerationConfig struct {
MaxOutputTokens int `json:"maxOutputTokens"`
} `json:"generationConfig"`
}
if err := json.Unmarshal(upstream.Requests()[0].Body, &received); err != nil || received.GenerationConfig.MaxOutputTokens != 16 {
t.Fatalf("effective output limit = %d, decode error = %v", received.GenerationConfig.MaxOutputTokens, err)
}
})
}
}
}
+9
View File
@@ -8,6 +8,7 @@ import (
"reflect"
"gpt-load/internal/channel"
"gpt-load/internal/dialect"
"gpt-load/internal/execution"
"gpt-load/internal/outboundproxy"
"gpt-load/internal/protocol"
@@ -159,6 +160,14 @@ func (registry *Registry) resolve(spec execution.AttemptSpec) (execution.Executo
"attempt route is not declared by channel",
)
}
if spec.RouteMode == execution.RouteConverted && spec.ClientProtocol == protocol.Anthropic &&
spec.Operation == execution.OperationChatCompletion && dialect.AnthropicRequestsZeroOutput(spec.Body) {
return nil, localAdapterEvidence(
execution.ErrorKindConversionUnsupported,
execution.ErrorCodeCriticalSemanticLoss,
"protocol conversion cannot preserve Anthropic zero-output semantics",
)
}
if spec.ResponsesStoreDowngraded && target.ResponsesStoreHandling(
protocol.OpenAIResponses,
execution.OperationResponsesCreate,
@@ -0,0 +1,130 @@
package provideradapter
import (
"encoding/json"
"fmt"
"testing"
"gpt-load/internal/channel"
"gpt-load/internal/execution"
"gpt-load/internal/protocol"
)
func TestRegistryRejectsConvertedAnthropicZeroOutputBeforeDispatch(t *testing.T) {
t.Parallel()
for _, target := range []struct {
channelID channel.ID
config json.RawMessage
}{
{channel.Gemini, nil},
{channel.OpenAI, nil},
{channel.OpenAICompatible, json.RawMessage(`{"base_url":"https://upstream.example/v1"}`)},
{channel.Codex, json.RawMessage(`{"base_url":"https://chatgpt.com/backend-api/codex"}`)},
} {
for _, stream := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/stream=%t", target.channelID, stream), func(t *testing.T) {
adapter := &recordingAdapter{}
registry, err := NewRegistry(channel.NewRegistry(), completeBindings(adapter, adapter))
if err != nil {
t.Fatal(err)
}
spec := execution.AttemptSpec{
ChannelID: string(target.channelID), TargetConfig: target.config,
ClientProtocol: protocol.Anthropic, Operation: execution.OperationChatCompletion,
RouteMode: execution.RouteConverted, UpstreamModel: "upstream-model",
Body: []byte(`{"model":"client-model","max_tokens":0,"messages":[{"role":"user","content":"hello"}]}`),
}
var evidence *execution.ErrorEvidence
if stream {
events := 0
result := registry.ExecuteStream(t.Context(), spec, func(execution.StreamEvent) error {
events++
return nil
})
if result.DispatchState != execution.DispatchNotSent || result.ResponseStarted || events != 0 {
t.Errorf("stream result = %+v, events = %d", result, events)
}
evidence = result.Error
} else {
result := registry.Execute(t.Context(), spec)
if result.DispatchState != execution.DispatchNotSent || result.ResponseStarted {
t.Errorf("result = %+v", result)
}
evidence = result.Error
}
if evidence == nil || evidence.Kind != execution.ErrorKindConversionUnsupported ||
evidence.Code != execution.ErrorCodeCriticalSemanticLoss ||
evidence.OriginHint != execution.ErrorOriginInternal || evidence.ScopeHint != execution.ErrorScopeGroup {
t.Errorf("conversion evidence = %+v", evidence)
}
if adapter.unaryCalls != 0 || adapter.streamCalls != 0 {
t.Errorf("zero-output request reached adapter: %+v", adapter)
}
})
}
}
}
func TestRegistryZeroOutputGuardUsesEffectiveRequestAndOperation(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
body string
protocol protocol.Protocol
operation execution.Operation
native bool
wantReject bool
}{
{name: "zero", body: `{"max_tokens":0}`, wantReject: true},
{name: "negative zero", body: `{"max_tokens":-0}`, wantReject: true},
{name: "decimal zero", body: `{"max_tokens":0.0}`, wantReject: true},
{name: "exponent zero", body: `{"max_tokens":0e12}`, wantReject: true},
{name: "small exponent zero", body: `{"max_tokens":0e-10000}`, wantReject: true},
{name: "effective positive limit", body: `{"max_tokens":16}`},
{name: "missing limit", body: `{}`},
{name: "null limit", body: `{"max_tokens":null}`},
{name: "string is not numeric zero", body: `{"max_tokens":"0"}`},
{name: "nonzero must not underflow to zero", body: `{"max_tokens":1e-10000}`},
{name: "nested limit", body: `{"metadata":{"max_tokens":0}}`},
{name: "last value zero", body: `{"max_tokens":16,"max_tokens":0}`, wantReject: true},
{name: "last value positive", body: `{"max_tokens":0,"max_tokens":16}`},
{name: "malformed body keeps existing validation", body: `{"max_tokens":0`},
{name: "native Anthropic", body: `{"max_tokens":0}`, native: true},
{name: "count tokens", body: `{"max_tokens":0}`, operation: execution.OperationCountTokens},
{name: "OpenAI zero retains existing behavior", body: `{"max_tokens":0}`, protocol: protocol.OpenAICompletions},
} {
t.Run(test.name, func(t *testing.T) {
adapter := &recordingAdapter{}
registry, err := NewRegistry(channel.NewRegistry(), completeBindings(adapter, adapter))
if err != nil {
t.Fatal(err)
}
clientProtocol := test.protocol
if clientProtocol == "" {
clientProtocol = protocol.Anthropic
}
operation := test.operation
if operation == "" {
operation = execution.OperationChatCompletion
}
spec := execution.AttemptSpec{
ChannelID: string(channel.Gemini), ClientProtocol: clientProtocol, Operation: operation,
RouteMode: execution.RouteConverted, UpstreamModel: "upstream-model", Body: []byte(test.body),
}
if test.native {
spec.ChannelID = string(channel.Anthropic)
spec.RouteMode = execution.RouteNative
}
result := registry.Execute(t.Context(), spec)
if test.wantReject {
if result.Error == nil || result.Error.Code != execution.ErrorCodeCriticalSemanticLoss || adapter.unaryCalls != 0 {
t.Fatalf("result = %+v, adapter calls = %d", result, adapter.unaryCalls)
}
} else if result.Error != nil || adapter.unaryCalls != 1 {
t.Fatalf("unrelated request changed: result = %+v, adapter calls = %d", result, adapter.unaryCalls)
}
})
}
}