upstream_response_model_test.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package service
  2. import (
  3. "strings"
  4. "testing"
  5. "github.com/gin-gonic/gin"
  6. "github.com/stretchr/testify/require"
  7. )
  8. func TestUpstreamResponseModelObserverTerminalWinsAndRecordsConflict(t *testing.T) {
  9. observer := &upstreamResponseModelObserver{}
  10. observer.ObserveOpenAI([]byte(`{"type":"response.created","response":{"model":"gpt-5.5"}}`), "response.created")
  11. observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"model":"gpt-5.4"}}`), "response.completed")
  12. require.Equal(t, "gpt-5.4", observer.Model())
  13. require.True(t, observer.Conflict())
  14. }
  15. func TestUpstreamResponseModelObserverSupportsAnthropicAndGeminiShapes(t *testing.T) {
  16. t.Run("anthropic", func(t *testing.T) {
  17. observer := &upstreamResponseModelObserver{}
  18. observer.ObserveAnthropic([]byte(`{"type":"message_start","message":{"model":"claude-sonnet-4-20250514"}}`))
  19. require.Equal(t, "claude-sonnet-4-20250514", observer.Model())
  20. })
  21. t.Run("gemini outer and nested", func(t *testing.T) {
  22. observer := &upstreamResponseModelObserver{}
  23. observer.ObserveGemini([]byte(`{"response":{"modelVersion":"gemini-2.5-pro"}}`))
  24. observer.ObserveGemini([]byte(`{"modelVersion":"gemini-2.5-pro-latest"}`))
  25. require.Equal(t, "gemini-2.5-pro-latest", observer.Model())
  26. require.True(t, observer.Conflict())
  27. })
  28. }
  29. func TestUpstreamResponseModelObservationAttemptReset(t *testing.T) {
  30. gin.SetMode(gin.TestMode)
  31. c, _ := gin.CreateTestContext(nil)
  32. first := beginUpstreamResponseModelObservation(c)
  33. first.Observe("failed-attempt-model", false)
  34. second := beginUpstreamResponseModelObservation(c)
  35. second.Observe("successful-attempt-model", false)
  36. require.Equal(t, "successful-attempt-model", observedUpstreamResponseModel(c))
  37. require.False(t, observedUpstreamResponseModelConflict(c))
  38. }
  39. func TestUpstreamModelMismatchThreeStateAndCaseInsensitiveComparison(t *testing.T) {
  40. require.Nil(t, upstreamModelMismatch("gpt-5.5", ""))
  41. matched := upstreamModelMismatch("gpt-5.5", "GPT-5.5")
  42. require.NotNil(t, matched)
  43. require.False(t, *matched)
  44. mismatched := upstreamModelMismatch("gpt-5.5", "gpt-5.4")
  45. require.NotNil(t, mismatched)
  46. require.True(t, *mismatched)
  47. }
  48. func TestUpstreamModelMismatchTreatsGrokBuildRuntimeIDsAsAliases(t *testing.T) {
  49. tests := []struct {
  50. name string
  51. sentModel string
  52. responseModel string
  53. }{
  54. {
  55. name: "issue 5634 grok 4.6",
  56. sentModel: "grok-4.6",
  57. responseModel: "grok-4.6-build",
  58. },
  59. {
  60. name: "grok 4.6 latest",
  61. sentModel: "grok-4.6-latest",
  62. responseModel: "grok-4.6-build",
  63. },
  64. {
  65. name: "issue 5647 grok 4.5 latest",
  66. sentModel: "grok-4.5-latest",
  67. responseModel: "grok-4.5-build",
  68. },
  69. {
  70. name: "grok 4.5 canonical",
  71. sentModel: "grok-4.5",
  72. responseModel: "GROK-4.5-BUILD",
  73. },
  74. }
  75. for _, tt := range tests {
  76. t.Run(tt.name, func(t *testing.T) {
  77. mismatch := upstreamModelMismatch(tt.sentModel, tt.responseModel)
  78. require.NotNil(t, mismatch)
  79. require.False(t, *mismatch)
  80. })
  81. }
  82. }
  83. func TestUpstreamModelMismatchDoesNotCollapseDifferentModels(t *testing.T) {
  84. tests := []struct {
  85. name string
  86. sentModel string
  87. responseModel string
  88. }{
  89. {
  90. name: "different grok versions",
  91. sentModel: "grok-4.5",
  92. responseModel: "grok-4.6-build",
  93. },
  94. {
  95. name: "unrelated build suffix",
  96. sentModel: "gpt-5.5",
  97. responseModel: "gpt-5.5-build",
  98. },
  99. {
  100. name: "different grok runtime",
  101. sentModel: "grok-build-0.1",
  102. responseModel: "grok-4.5-build",
  103. },
  104. }
  105. for _, tt := range tests {
  106. t.Run(tt.name, func(t *testing.T) {
  107. mismatch := upstreamModelMismatch(tt.sentModel, tt.responseModel)
  108. require.NotNil(t, mismatch)
  109. require.True(t, *mismatch)
  110. })
  111. }
  112. }
  113. func TestObserveOpenAISSEBodyIgnoresMalformedPayload(t *testing.T) {
  114. observer := &upstreamResponseModelObserver{}
  115. observeOpenAISSEBody(observer, "data: not-json\n\ndata: {\"type\":\"response.completed\",\"response\":{\"model\":\"gpt-5.4\"}}\n\n")
  116. require.Equal(t, "gpt-5.4", observer.Model())
  117. require.False(t, observer.Conflict())
  118. }
  119. func TestObserveAntigravityGeminiSSELineReadsWrapperModelWithoutUnwrap(t *testing.T) {
  120. tests := []struct {
  121. name string
  122. payload string
  123. want string
  124. }{
  125. {
  126. name: "top-level sibling",
  127. payload: `{"modelVersion":"gemini-3-pro","response":{"candidates":[]}}`,
  128. want: "gemini-3-pro",
  129. },
  130. {
  131. name: "single wrapper",
  132. payload: `{"response":{"modelVersion":"gemini-3-pro","candidates":[]}}`,
  133. want: "gemini-3-pro",
  134. },
  135. {
  136. name: "nested response after one wrapper",
  137. payload: `{"response":{"response":{"modelVersion":"gemini-3-pro","candidates":[]}}}`,
  138. want: "gemini-3-pro",
  139. },
  140. {
  141. name: "outer declaration takes precedence",
  142. payload: `{"modelVersion":"gemini-outer","response":{"modelVersion":"gemini-inner","candidates":[]}}`,
  143. want: "gemini-outer",
  144. },
  145. }
  146. for _, tt := range tests {
  147. t.Run(tt.name, func(t *testing.T) {
  148. gin.SetMode(gin.TestMode)
  149. c, _ := gin.CreateTestContext(nil)
  150. beginUpstreamResponseModelObservation(c)
  151. svc := &AntigravityGatewayService{}
  152. svc.observeAntigravityGeminiSSELine(c, "data: "+tt.payload)
  153. require.Equal(t, tt.want, observedUpstreamResponseModel(c))
  154. require.False(t, observedUpstreamResponseModelConflict(c))
  155. })
  156. }
  157. }
  158. func TestUpstreamResponseModelObserverRejectsMalformedJSONWithModelField(t *testing.T) {
  159. observer := &upstreamResponseModelObserver{}
  160. observer.ObserveOpenAI([]byte(`{"response":{"model":"gpt-5.4"}`), "response.completed")
  161. require.Empty(t, observer.Model())
  162. }
  163. func TestUpstreamResponseModelObserverBoundsUntrustedModelName(t *testing.T) {
  164. observer := &upstreamResponseModelObserver{}
  165. observer.Observe(" "+strings.Repeat("模", upstreamResponseModelMaxLength+1)+" ", false)
  166. require.Len(t, []rune(observer.Model()), upstreamResponseModelMaxLength)
  167. }
  168. func TestUpstreamResponseModelObserverServiceTierTerminalEventWins(t *testing.T) {
  169. observer := &upstreamResponseModelObserver{}
  170. // response.created echoes the requested tier and must not count as a declaration.
  171. observer.ObserveOpenAI([]byte(`{"type":"response.created","response":{"id":"resp_1","model":"gpt-5.6-sol","service_tier":"priority"}}`), "response.created")
  172. require.Equal(t, "", observer.ServiceTier())
  173. 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")
  174. require.Equal(t, "default", observer.ServiceTier())
  175. require.False(t, observer.Conflict(), "tier echo must not be reported as a model conflict")
  176. }
  177. func TestUpstreamResponseModelObserverServiceTierUntypedPayloads(t *testing.T) {
  178. t.Run("chat completions chunks agree", func(t *testing.T) {
  179. observer := &upstreamResponseModelObserver{}
  180. observer.ObserveOpenAI([]byte(`{"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-5.4","service_tier":"default","choices":[]}`), "")
  181. observer.ObserveOpenAI([]byte(`{"id":"chatcmpl-1","object":"chat.completion.chunk","model":"gpt-5.4","service_tier":"default","choices":[]}`), "")
  182. require.Equal(t, "default", observer.ServiceTier())
  183. })
  184. t.Run("disagreeing chunks are not trusted", func(t *testing.T) {
  185. observer := &upstreamResponseModelObserver{}
  186. observer.ObserveOpenAI([]byte(`{"model":"gpt-5.4","service_tier":"priority"}`), "")
  187. observer.ObserveOpenAI([]byte(`{"model":"gpt-5.4","service_tier":"default"}`), "")
  188. require.Equal(t, "", observer.ServiceTier())
  189. })
  190. t.Run("non-stream body normalises fast and drops auto", func(t *testing.T) {
  191. observer := &upstreamResponseModelObserver{}
  192. observer.ObserveOpenAI([]byte(`{"id":"resp_1","object":"response","model":"gpt-5.6-sol","service_tier":"fast"}`), "")
  193. require.Equal(t, "priority", observer.ServiceTier())
  194. auto := &upstreamResponseModelObserver{}
  195. auto.ObserveOpenAI([]byte(`{"id":"resp_2","object":"response","model":"gpt-5.6-sol","service_tier":"auto"}`), "")
  196. require.Equal(t, "", auto.ServiceTier())
  197. })
  198. t.Run("model-free deltas never declare a tier", func(t *testing.T) {
  199. observer := &upstreamResponseModelObserver{}
  200. observer.ObserveOpenAI([]byte(`{"type":"response.output_text.delta","delta":"hi","service_tier":"default"}`), "response.output_text.delta")
  201. require.Equal(t, "", observer.ServiceTier())
  202. })
  203. }
  204. func TestUpstreamResponseModelObserverServiceTierAnthropicSpeed(t *testing.T) {
  205. t.Run("message_start reports standard", func(t *testing.T) {
  206. observer := &upstreamResponseModelObserver{}
  207. observer.ObserveAnthropic([]byte(`{"type":"message_start","message":{"id":"msg_1","model":"claude-opus-5","usage":{"input_tokens":8,"output_tokens":1,"speed":"standard"}}}`))
  208. observer.ObserveAnthropic([]byte(`{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":12}}`))
  209. require.Equal(t, "standard", observer.ServiceTier())
  210. require.Equal(t, "claude-opus-5", observer.Model())
  211. })
  212. t.Run("non-stream body reports fast", func(t *testing.T) {
  213. observer := &upstreamResponseModelObserver{}
  214. observer.ObserveAnthropic([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-5","usage":{"input_tokens":8,"output_tokens":12,"speed":"fast"}}`))
  215. require.Equal(t, "fast", observer.ServiceTier())
  216. })
  217. t.Run("missing speed declares nothing", func(t *testing.T) {
  218. observer := &upstreamResponseModelObserver{}
  219. observer.ObserveAnthropic([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-5","usage":{"input_tokens":8,"output_tokens":12}}`))
  220. require.Equal(t, "", observer.ServiceTier())
  221. })
  222. }
  223. func TestObservedUpstreamResponseServiceTierFromContext(t *testing.T) {
  224. gin.SetMode(gin.TestMode)
  225. c, _ := gin.CreateTestContext(nil)
  226. require.Equal(t, "", observedUpstreamResponseServiceTier(c))
  227. observer := beginUpstreamResponseModelObservation(c)
  228. observer.ObserveOpenAI([]byte(`{"type":"response.completed","response":{"id":"resp_1","model":"gpt-5.6-sol","service_tier":"default"}}`), "response.completed")
  229. require.Equal(t, "default", observedUpstreamResponseServiceTier(c))
  230. }