| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- package service
- import (
- "strings"
- "testing"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- func TestUpstreamResponseModelObserverTerminalWinsAndRecordsConflict(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveOpenAI([]byte(`{"type":"response.created","response":{"model":"gpt-5.5"}}`), "response.created")
- observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"model":"gpt-5.4"}}`), "response.completed")
- require.Equal(t, "gpt-5.4", observer.Model())
- require.True(t, observer.Conflict())
- }
- func TestUpstreamResponseModelObserverSupportsAnthropicAndGeminiShapes(t *testing.T) {
- t.Run("anthropic", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveAnthropic([]byte(`{"type":"message_start","message":{"model":"claude-sonnet-4-20250514"}}`))
- require.Equal(t, "claude-sonnet-4-20250514", observer.Model())
- })
- t.Run("gemini outer and nested", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveGemini([]byte(`{"response":{"modelVersion":"gemini-2.5-pro"}}`))
- observer.ObserveGemini([]byte(`{"modelVersion":"gemini-2.5-pro-latest"}`))
- require.Equal(t, "gemini-2.5-pro-latest", observer.Model())
- require.True(t, observer.Conflict())
- })
- }
- func TestUpstreamResponseModelObservationAttemptReset(t *testing.T) {
- gin.SetMode(gin.TestMode)
- c, _ := gin.CreateTestContext(nil)
- first := beginUpstreamResponseModelObservation(c)
- first.Observe("failed-attempt-model", false)
- second := beginUpstreamResponseModelObservation(c)
- second.Observe("successful-attempt-model", false)
- require.Equal(t, "successful-attempt-model", observedUpstreamResponseModel(c))
- require.False(t, observedUpstreamResponseModelConflict(c))
- }
- func TestUpstreamModelMismatchThreeStateAndCaseInsensitiveComparison(t *testing.T) {
- require.Nil(t, upstreamModelMismatch("gpt-5.5", ""))
- matched := upstreamModelMismatch("gpt-5.5", "GPT-5.5")
- require.NotNil(t, matched)
- require.False(t, *matched)
- mismatched := upstreamModelMismatch("gpt-5.5", "gpt-5.4")
- require.NotNil(t, mismatched)
- require.True(t, *mismatched)
- }
- func TestUpstreamModelMismatchTreatsGrokBuildRuntimeIDsAsAliases(t *testing.T) {
- tests := []struct {
- name string
- sentModel string
- responseModel string
- }{
- {
- name: "issue 5634 grok 4.6",
- sentModel: "grok-4.6",
- responseModel: "grok-4.6-build",
- },
- {
- name: "grok 4.6 latest",
- sentModel: "grok-4.6-latest",
- responseModel: "grok-4.6-build",
- },
- {
- name: "issue 5647 grok 4.5 latest",
- sentModel: "grok-4.5-latest",
- responseModel: "grok-4.5-build",
- },
- {
- name: "grok 4.5 canonical",
- sentModel: "grok-4.5",
- responseModel: "GROK-4.5-BUILD",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- mismatch := upstreamModelMismatch(tt.sentModel, tt.responseModel)
- require.NotNil(t, mismatch)
- require.False(t, *mismatch)
- })
- }
- }
- func TestUpstreamModelMismatchDoesNotCollapseDifferentModels(t *testing.T) {
- tests := []struct {
- name string
- sentModel string
- responseModel string
- }{
- {
- name: "different grok versions",
- sentModel: "grok-4.5",
- responseModel: "grok-4.6-build",
- },
- {
- name: "unrelated build suffix",
- sentModel: "gpt-5.5",
- responseModel: "gpt-5.5-build",
- },
- {
- name: "different grok runtime",
- sentModel: "grok-build-0.1",
- responseModel: "grok-4.5-build",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- mismatch := upstreamModelMismatch(tt.sentModel, tt.responseModel)
- require.NotNil(t, mismatch)
- require.True(t, *mismatch)
- })
- }
- }
- func TestObserveOpenAISSEBodyIgnoresMalformedPayload(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observeOpenAISSEBody(observer, "data: not-json\n\ndata: {\"type\":\"response.completed\",\"response\":{\"model\":\"gpt-5.4\"}}\n\n")
- require.Equal(t, "gpt-5.4", observer.Model())
- require.False(t, observer.Conflict())
- }
- func TestObserveAntigravityGeminiSSELineReadsWrapperModelWithoutUnwrap(t *testing.T) {
- tests := []struct {
- name string
- payload string
- want string
- }{
- {
- name: "top-level sibling",
- payload: `{"modelVersion":"gemini-3-pro","response":{"candidates":[]}}`,
- want: "gemini-3-pro",
- },
- {
- name: "single wrapper",
- payload: `{"response":{"modelVersion":"gemini-3-pro","candidates":[]}}`,
- want: "gemini-3-pro",
- },
- {
- name: "nested response after one wrapper",
- payload: `{"response":{"response":{"modelVersion":"gemini-3-pro","candidates":[]}}}`,
- want: "gemini-3-pro",
- },
- {
- name: "outer declaration takes precedence",
- payload: `{"modelVersion":"gemini-outer","response":{"modelVersion":"gemini-inner","candidates":[]}}`,
- want: "gemini-outer",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- gin.SetMode(gin.TestMode)
- c, _ := gin.CreateTestContext(nil)
- beginUpstreamResponseModelObservation(c)
- svc := &AntigravityGatewayService{}
- svc.observeAntigravityGeminiSSELine(c, "data: "+tt.payload)
- require.Equal(t, tt.want, observedUpstreamResponseModel(c))
- require.False(t, observedUpstreamResponseModelConflict(c))
- })
- }
- }
- func TestUpstreamResponseModelObserverRejectsMalformedJSONWithModelField(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveOpenAI([]byte(`{"response":{"model":"gpt-5.4"}`), "response.completed")
- require.Empty(t, observer.Model())
- }
- func TestUpstreamResponseModelObserverBoundsUntrustedModelName(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.Observe(" "+strings.Repeat("模", upstreamResponseModelMaxLength+1)+" ", false)
- require.Len(t, []rune(observer.Model()), upstreamResponseModelMaxLength)
- }
- func TestUpstreamResponseModelObserverServiceTierTerminalEventWins(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- // response.created echoes the requested tier and must not count as a declaration.
- observer.ObserveOpenAI([]byte(`{"type":"response.created","response":{"id":"resp_1","model":"gpt-5.6-sol","service_tier":"priority"}}`), "response.created")
- require.Equal(t, "", observer.ServiceTier())
- observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"id":"resp_1","model":"gpt-5.6-sol","service_tier":"default","usage":{"input_tokens":1,"output_tokens":2}}}`), "response.completed")
- require.Equal(t, "default", observer.ServiceTier())
- require.False(t, observer.Conflict(), "tier echo must not be reported as a model conflict")
- }
- func TestUpstreamResponseModelObserverServiceTierUntypedPayloads(t *testing.T) {
- t.Run("chat completions chunks agree", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveOpenAI([]byte(`{"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-5.4","service_tier":"default","choices":[]}`), "")
- observer.ObserveOpenAI([]byte(`{"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-5.4","service_tier":"default","choices":[]}`), "")
- require.Equal(t, "default", observer.ServiceTier())
- })
- t.Run("disagreeing chunks are not trusted", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveOpenAI([]byte(`{"model":"gpt-5.4","service_tier":"priority"}`), "")
- observer.ObserveOpenAI([]byte(`{"model":"gpt-5.4","service_tier":"default"}`), "")
- require.Equal(t, "", observer.ServiceTier())
- })
- t.Run("non-stream body normalises fast and drops auto", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveOpenAI([]byte(`{"id":"resp_1","object":"response","model":"gpt-5.6-sol","service_tier":"fast"}`), "")
- require.Equal(t, "priority", observer.ServiceTier())
- auto := &upstreamResponseModelObserver{}
- auto.ObserveOpenAI([]byte(`{"id":"resp_2","object":"response","model":"gpt-5.6-sol","service_tier":"auto"}`), "")
- require.Equal(t, "", auto.ServiceTier())
- })
- t.Run("model-free deltas never declare a tier", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveOpenAI([]byte(`{"type":"response.output_text.delta","delta":"hi","service_tier":"default"}`), "response.output_text.delta")
- require.Equal(t, "", observer.ServiceTier())
- })
- }
- func TestUpstreamResponseModelObserverServiceTierAnthropicSpeed(t *testing.T) {
- t.Run("message_start reports standard", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveAnthropic([]byte(`{"type":"message_start","message":{"id":"msg_1","model":"claude-opus-5","usage":{"input_tokens":8,"output_tokens":1,"speed":"standard"}}}`))
- observer.ObserveAnthropic([]byte(`{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":12}}`))
- require.Equal(t, "standard", observer.ServiceTier())
- require.Equal(t, "claude-opus-5", observer.Model())
- })
- t.Run("non-stream body reports fast", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveAnthropic([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-5","usage":{"input_tokens":8,"output_tokens":12,"speed":"fast"}}`))
- require.Equal(t, "fast", observer.ServiceTier())
- })
- t.Run("missing speed declares nothing", func(t *testing.T) {
- observer := &upstreamResponseModelObserver{}
- observer.ObserveAnthropic([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-5","usage":{"input_tokens":8,"output_tokens":12}}`))
- require.Equal(t, "", observer.ServiceTier())
- })
- }
- func TestObservedUpstreamResponseServiceTierFromContext(t *testing.T) {
- gin.SetMode(gin.TestMode)
- c, _ := gin.CreateTestContext(nil)
- require.Equal(t, "", observedUpstreamResponseServiceTier(c))
- observer := beginUpstreamResponseModelObservation(c)
- observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"id":"resp_1","model":"gpt-5.6-sol","service_tier":"default"}}`), "response.completed")
- require.Equal(t, "default", observedUpstreamResponseServiceTier(c))
- }
|