| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- package service
- import (
- "bytes"
- "context"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- )
- func TestOpenAIGatewayService_Forward_CompactOnlyModelMappingOverridesOAuthUpstreamModel(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- body := []byte(`{"model":"gpt-5.4","stream":false,"instructions":"compact-test","input":"hello"}`)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid-compact-map"}},
- Body: io.NopCloser(strings.NewReader(`{"id":"resp_123","status":"completed","model":"gpt-5.4-openai-compact","output":[],"usage":{"input_tokens":1,"output_tokens":1}}`)),
- }}
- svc := &OpenAIGatewayService{httpUpstream: upstream}
- account := &Account{
- ID: 1,
- Name: "openai-oauth",
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "oauth-token",
- "chatgpt_account_id": "chatgpt-acc",
- "model_mapping": map[string]any{"gpt-5.4": "gpt-5.3-codex"},
- "compact_model_mapping": map[string]any{"gpt-5.4": "gpt-5.4-openai-compact"},
- },
- Status: StatusActive,
- Schedulable: true,
- }
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "gpt-5.4", result.Model)
- require.Equal(t, "gpt-5.4-openai-compact", result.UpstreamModel)
- require.Equal(t, "gpt-5.4-openai-compact", gjson.GetBytes(upstream.lastBody, "model").String())
- opsModel, exists := c.Get(OpsUpstreamModelKey)
- require.True(t, exists)
- require.Equal(t, "gpt-5.4-openai-compact", opsModel)
- }
- func TestOpenAIGatewayService_Forward_APIKeyCompactSanitizesStatelessReplayAfterStoreWasDropped(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- body := []byte(`{"model":"gpt-5.5","parallel_tool_calls":true,"input":[` +
- `{"type":"reasoning","id":"rs_server_only","summary":[]},` +
- `{"type":"item_reference","id":"rs_server_only"},` +
- `{"type":"message","role":"user","content":"compact this"}` +
- `]}`)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"id":"resp_compact","status":"completed","output":[{"type":"compaction","encrypted_content":"cipher"}],"usage":{"input_tokens":1,"output_tokens":1}}`,
- )),
- }}
- svc := &OpenAIGatewayService{
- httpUpstream: upstream,
- cfg: &config.Config{Security: config.SecurityConfig{
- URLAllowlist: config.URLAllowlistConfig{Enabled: false},
- }},
- }
- account := &Account{
- ID: 7, Name: "azure-openai", Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
- Credentials: map[string]any{"api_key": "test-key"}, Status: StatusActive, Schedulable: true,
- }
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.False(t, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Exists())
- require.Equal(t, int64(1), gjson.GetBytes(upstream.lastBody, "input.#").Int())
- require.Equal(t, "message", gjson.GetBytes(upstream.lastBody, "input.0.type").String())
- }
- func TestOpenAIGatewayService_Forward_NormalizesCompactionTriggerAfterHistoryCleanup(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- 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"}]}`)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(`{"id":"resp_trigger","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1}}`)),
- }}
- svc := &OpenAIGatewayService{httpUpstream: upstream}
- account := &Account{
- ID: 4, Name: "openai-oauth", Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 1,
- Credentials: map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"},
- Status: StatusActive, Schedulable: true,
- }
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- items := gjson.GetBytes(upstream.lastBody, "input").Array()
- require.Len(t, items, 4)
- require.Equal(t, "function_call", items[0].Get("type").String())
- require.Equal(t, "function_call_output", items[1].Get("type").String())
- require.Equal(t, "message", items[2].Get("type").String())
- require.Equal(t, "compaction_trigger", items[3].Get("type").String())
- }
- func TestOpenAIGatewayService_Forward_NonCompactRequestIgnoresCompactOnlyModelMapping(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- body := []byte(`{"model":"gpt-5.4","stream":false,"instructions":"normal-test","input":"hello"}`)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid-normal-map"}},
- Body: io.NopCloser(strings.NewReader(`{"id":"resp_124","status":"completed","model":"gpt-5.4","output":[],"usage":{"input_tokens":1,"output_tokens":1}}`)),
- }}
- svc := &OpenAIGatewayService{httpUpstream: upstream}
- account := &Account{
- ID: 2,
- Name: "openai-oauth",
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "oauth-token",
- "chatgpt_account_id": "chatgpt-acc",
- "compact_model_mapping": map[string]any{"gpt-5.4": "gpt-5.4-openai-compact"},
- },
- Status: StatusActive,
- Schedulable: true,
- }
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "gpt-5.4", result.Model)
- require.Equal(t, "gpt-5.4", result.UpstreamModel)
- require.Equal(t, "gpt-5.4", gjson.GetBytes(upstream.lastBody, "model").String())
- }
- func TestOpenAIGatewayService_OAuthPassthrough_CompactOnlyModelMappingOverridesUpstreamModel(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", bytes.NewReader(nil))
- c.Request.Header.Set("User-Agent", "codex_cli_rs/0.1.0")
- c.Request.Header.Set("Content-Type", "application/json")
- originalBody := []byte(`{"model":"gpt-5.4","stream":true,"store":true,"instructions":"compact-pass","input":[{"type":"text","text":"compact me"}]}`)
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid-compact-pass-map"}},
- Body: io.NopCloser(strings.NewReader(`{"id":"cmp_124","model":"gpt-5.4-openai-compact","usage":{"input_tokens":2,"output_tokens":3}}`)),
- }}
- svc := &OpenAIGatewayService{httpUpstream: upstream}
- account := &Account{
- ID: 3,
- Name: "openai-oauth-pass",
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Concurrency: 1,
- Credentials: map[string]any{
- "access_token": "oauth-token",
- "chatgpt_account_id": "chatgpt-acc",
- "compact_model_mapping": map[string]any{"gpt-5.4": "gpt-5.4-openai-compact"},
- },
- Extra: map[string]any{"openai_passthrough": true},
- Status: StatusActive,
- Schedulable: true,
- }
- result, err := svc.Forward(context.Background(), c, account, originalBody)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "gpt-5.4", result.Model)
- require.Equal(t, "gpt-5.4-openai-compact", result.UpstreamModel)
- require.Equal(t, "gpt-5.4-openai-compact", gjson.GetBytes(upstream.lastBody, "model").String())
- require.Equal(t, "gpt-5.4", gjson.GetBytes(rec.Body.Bytes(), "model").String())
- opsModel, exists := c.Get(OpsUpstreamModelKey)
- require.True(t, exists)
- require.Equal(t, "gpt-5.4-openai-compact", opsModel)
- }
|