openai_compact_model_mapping_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "strings"
  9. "testing"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. "github.com/gin-gonic/gin"
  12. "github.com/stretchr/testify/require"
  13. "github.com/tidwall/gjson"
  14. )
  15. func TestOpenAIGatewayService_Forward_CompactOnlyModelMappingOverridesOAuthUpstreamModel(t *testing.T) {
  16. gin.SetMode(gin.TestMode)
  17. rec := httptest.NewRecorder()
  18. c, _ := gin.CreateTestContext(rec)
  19. body := []byte(`{"model":"gpt-5.4","stream":false,"instructions":"compact-test","input":"hello"}`)
  20. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", bytes.NewReader(body))
  21. c.Request.Header.Set("Content-Type", "application/json")
  22. upstream := &httpUpstreamRecorder{resp: &http.Response{
  23. StatusCode: http.StatusOK,
  24. Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid-compact-map"}},
  25. Body: io.NopCloser(strings.NewReader(`{"id":"resp_123","status":"completed","model":"gpt-5.4-openai-compact","output":[],"usage":{"input_tokens":1,"output_tokens":1}}`)),
  26. }}
  27. svc := &OpenAIGatewayService{httpUpstream: upstream}
  28. account := &Account{
  29. ID: 1,
  30. Name: "openai-oauth",
  31. Platform: PlatformOpenAI,
  32. Type: AccountTypeOAuth,
  33. Concurrency: 1,
  34. Credentials: map[string]any{
  35. "access_token": "oauth-token",
  36. "chatgpt_account_id": "chatgpt-acc",
  37. "model_mapping": map[string]any{"gpt-5.4": "gpt-5.3-codex"},
  38. "compact_model_mapping": map[string]any{"gpt-5.4": "gpt-5.4-openai-compact"},
  39. },
  40. Status: StatusActive,
  41. Schedulable: true,
  42. }
  43. result, err := svc.Forward(context.Background(), c, account, body)
  44. require.NoError(t, err)
  45. require.NotNil(t, result)
  46. require.Equal(t, "gpt-5.4", result.Model)
  47. require.Equal(t, "gpt-5.4-openai-compact", result.UpstreamModel)
  48. require.Equal(t, "gpt-5.4-openai-compact", gjson.GetBytes(upstream.lastBody, "model").String())
  49. opsModel, exists := c.Get(OpsUpstreamModelKey)
  50. require.True(t, exists)
  51. require.Equal(t, "gpt-5.4-openai-compact", opsModel)
  52. }
  53. func TestOpenAIGatewayService_Forward_APIKeyCompactSanitizesStatelessReplayAfterStoreWasDropped(t *testing.T) {
  54. gin.SetMode(gin.TestMode)
  55. rec := httptest.NewRecorder()
  56. c, _ := gin.CreateTestContext(rec)
  57. body := []byte(`{"model":"gpt-5.5","parallel_tool_calls":true,"input":[` +
  58. `{"type":"reasoning","id":"rs_server_only","summary":[]},` +
  59. `{"type":"item_reference","id":"rs_server_only"},` +
  60. `{"type":"message","role":"user","content":"compact this"}` +
  61. `]}`)
  62. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", bytes.NewReader(body))
  63. c.Request.Header.Set("Content-Type", "application/json")
  64. upstream := &httpUpstreamRecorder{resp: &http.Response{
  65. StatusCode: http.StatusOK,
  66. Header: http.Header{"Content-Type": []string{"application/json"}},
  67. Body: io.NopCloser(strings.NewReader(
  68. `{"id":"resp_compact","status":"completed","output":[{"type":"compaction","encrypted_content":"cipher"}],"usage":{"input_tokens":1,"output_tokens":1}}`,
  69. )),
  70. }}
  71. svc := &OpenAIGatewayService{
  72. httpUpstream: upstream,
  73. cfg: &config.Config{Security: config.SecurityConfig{
  74. URLAllowlist: config.URLAllowlistConfig{Enabled: false},
  75. }},
  76. }
  77. account := &Account{
  78. ID: 7, Name: "azure-openai", Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
  79. Credentials: map[string]any{"api_key": "test-key"}, Status: StatusActive, Schedulable: true,
  80. }
  81. result, err := svc.Forward(context.Background(), c, account, body)
  82. require.NoError(t, err)
  83. require.NotNil(t, result)
  84. require.False(t, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Exists())
  85. require.Equal(t, int64(1), gjson.GetBytes(upstream.lastBody, "input.#").Int())
  86. require.Equal(t, "message", gjson.GetBytes(upstream.lastBody, "input.0.type").String())
  87. }
  88. func TestOpenAIGatewayService_Forward_NormalizesCompactionTriggerAfterHistoryCleanup(t *testing.T) {
  89. gin.SetMode(gin.TestMode)
  90. rec := httptest.NewRecorder()
  91. c, _ := gin.CreateTestContext(rec)
  92. body := []byte(`{"model":"gpt-5.4","stream":false,"input":[{"type":"compaction_trigger"},{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{}"},{"type":"function_call_output","call_id":"call_1","output":"ok"},{"type":"message","role":"user","content":"visible"}]}`)
  93. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
  94. c.Request.Header.Set("Content-Type", "application/json")
  95. upstream := &httpUpstreamRecorder{resp: &http.Response{
  96. StatusCode: http.StatusOK,
  97. Header: http.Header{"Content-Type": []string{"application/json"}},
  98. Body: io.NopCloser(strings.NewReader(`{"id":"resp_trigger","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1}}`)),
  99. }}
  100. svc := &OpenAIGatewayService{httpUpstream: upstream}
  101. account := &Account{
  102. ID: 4, Name: "openai-oauth", Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 1,
  103. Credentials: map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"},
  104. Status: StatusActive, Schedulable: true,
  105. }
  106. result, err := svc.Forward(context.Background(), c, account, body)
  107. require.NoError(t, err)
  108. require.NotNil(t, result)
  109. items := gjson.GetBytes(upstream.lastBody, "input").Array()
  110. require.Len(t, items, 4)
  111. require.Equal(t, "function_call", items[0].Get("type").String())
  112. require.Equal(t, "function_call_output", items[1].Get("type").String())
  113. require.Equal(t, "message", items[2].Get("type").String())
  114. require.Equal(t, "compaction_trigger", items[3].Get("type").String())
  115. }
  116. func TestOpenAIGatewayService_Forward_NonCompactRequestIgnoresCompactOnlyModelMapping(t *testing.T) {
  117. gin.SetMode(gin.TestMode)
  118. rec := httptest.NewRecorder()
  119. c, _ := gin.CreateTestContext(rec)
  120. body := []byte(`{"model":"gpt-5.4","stream":false,"instructions":"normal-test","input":"hello"}`)
  121. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
  122. c.Request.Header.Set("Content-Type", "application/json")
  123. upstream := &httpUpstreamRecorder{resp: &http.Response{
  124. StatusCode: http.StatusOK,
  125. Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid-normal-map"}},
  126. Body: io.NopCloser(strings.NewReader(`{"id":"resp_124","status":"completed","model":"gpt-5.4","output":[],"usage":{"input_tokens":1,"output_tokens":1}}`)),
  127. }}
  128. svc := &OpenAIGatewayService{httpUpstream: upstream}
  129. account := &Account{
  130. ID: 2,
  131. Name: "openai-oauth",
  132. Platform: PlatformOpenAI,
  133. Type: AccountTypeOAuth,
  134. Concurrency: 1,
  135. Credentials: map[string]any{
  136. "access_token": "oauth-token",
  137. "chatgpt_account_id": "chatgpt-acc",
  138. "compact_model_mapping": map[string]any{"gpt-5.4": "gpt-5.4-openai-compact"},
  139. },
  140. Status: StatusActive,
  141. Schedulable: true,
  142. }
  143. result, err := svc.Forward(context.Background(), c, account, body)
  144. require.NoError(t, err)
  145. require.NotNil(t, result)
  146. require.Equal(t, "gpt-5.4", result.Model)
  147. require.Equal(t, "gpt-5.4", result.UpstreamModel)
  148. require.Equal(t, "gpt-5.4", gjson.GetBytes(upstream.lastBody, "model").String())
  149. }
  150. func TestOpenAIGatewayService_OAuthPassthrough_CompactOnlyModelMappingOverridesUpstreamModel(t *testing.T) {
  151. gin.SetMode(gin.TestMode)
  152. rec := httptest.NewRecorder()
  153. c, _ := gin.CreateTestContext(rec)
  154. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", bytes.NewReader(nil))
  155. c.Request.Header.Set("User-Agent", "codex_cli_rs/0.1.0")
  156. c.Request.Header.Set("Content-Type", "application/json")
  157. originalBody := []byte(`{"model":"gpt-5.4","stream":true,"store":true,"instructions":"compact-pass","input":[{"type":"text","text":"compact me"}]}`)
  158. upstream := &httpUpstreamRecorder{resp: &http.Response{
  159. StatusCode: http.StatusOK,
  160. Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid-compact-pass-map"}},
  161. Body: io.NopCloser(strings.NewReader(`{"id":"cmp_124","model":"gpt-5.4-openai-compact","usage":{"input_tokens":2,"output_tokens":3}}`)),
  162. }}
  163. svc := &OpenAIGatewayService{httpUpstream: upstream}
  164. account := &Account{
  165. ID: 3,
  166. Name: "openai-oauth-pass",
  167. Platform: PlatformOpenAI,
  168. Type: AccountTypeOAuth,
  169. Concurrency: 1,
  170. Credentials: map[string]any{
  171. "access_token": "oauth-token",
  172. "chatgpt_account_id": "chatgpt-acc",
  173. "compact_model_mapping": map[string]any{"gpt-5.4": "gpt-5.4-openai-compact"},
  174. },
  175. Extra: map[string]any{"openai_passthrough": true},
  176. Status: StatusActive,
  177. Schedulable: true,
  178. }
  179. result, err := svc.Forward(context.Background(), c, account, originalBody)
  180. require.NoError(t, err)
  181. require.NotNil(t, result)
  182. require.Equal(t, "gpt-5.4", result.Model)
  183. require.Equal(t, "gpt-5.4-openai-compact", result.UpstreamModel)
  184. require.Equal(t, "gpt-5.4-openai-compact", gjson.GetBytes(upstream.lastBody, "model").String())
  185. require.Equal(t, "gpt-5.4", gjson.GetBytes(rec.Body.Bytes(), "model").String())
  186. opsModel, exists := c.Get(OpsUpstreamModelKey)
  187. require.True(t, exists)
  188. require.Equal(t, "gpt-5.4-openai-compact", opsModel)
  189. }