| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- package service
- import (
- "bytes"
- "context"
- "encoding/base64"
- "encoding/json"
- "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 TestAccountTestServiceOpenAICompactAgentIdentityUsesFreshAssertion(t *testing.T) {
- gin.SetMode(gin.TestMode)
- key, privateKey := newTestAgentIdentityKey(t)
- account := Account{
- ID: 21,
- Name: "agent-identity",
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "task_id": key.taskID,
- "chatgpt_account_id": "account-agent-test",
- "chatgpt_account_is_fedramp": true,
- },
- }
- repo := &snapshotUpdateAccountRepo{stubOpenAIAccountRepo: stubOpenAIAccountRepo{accounts: []Account{account}}}
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(`{"id":"compact-agent","status":"completed","output":[{"type":"compaction","id":"cmp_agent_fresh","encrypted_content":"blob"}]}`)),
- }}
- svc := &AccountTestService{accountRepo: repo, httpUpstream: upstream}
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/21/test", bytes.NewReader(nil))
- require.NoError(t, svc.TestAccountConnection(c, account.ID, "gpt-5.4", "", AccountTestModeCompact))
- require.Equal(t, "AgentAssertion", strings.SplitN(upstream.lastReq.Header.Get("Authorization"), " ", 2)[0])
- require.Equal(t, "account-agent-test", upstream.lastReq.Header.Get("chatgpt-account-id"))
- require.Equal(t, "true", upstream.lastReq.Header.Get("x-openai-fedramp"))
- require.NotContains(t, upstream.lastReq.Header.Get("Authorization"), privateKey)
- }
- func TestAccountTestServiceOpenAICompactAgentIdentityRecoversInvalidTaskOnce(t *testing.T) {
- gin.SetMode(gin.TestMode)
- key, privateKey := newTestAgentIdentityKey(t)
- account := &Account{
- ID: 22,
- Name: "agent-identity-recovery",
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "task_id": "task-compact-old",
- "chatgpt_account_id": "account-agent-compact-recovery",
- },
- }
- repo := &accountTestAgentIdentityRepo{account: account}
- registerCalls := 0
- registerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- registerCalls++
- _, _ = io.WriteString(w, `{"task_id":"task-compact-new"}`)
- }))
- defer registerServer.Close()
- oldBase := openAIAgentIdentityAuthAPIBaseURL
- openAIAgentIdentityAuthAPIBaseURL = registerServer.URL
- t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = oldBase })
- upstream := &httpUpstreamRecorder{responses: []*http.Response{
- {StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))},
- {StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"id":"compact-agent","status":"completed","output":[{"type":"compaction","id":"cmp_agent","encrypted_content":"blob"}]}`))},
- }}
- invalidator := &agentIdentityWSInvalidationRecorder{}
- svc := &AccountTestService{accountRepo: repo, httpUpstream: upstream, agentIdentityWS: invalidator}
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/22/test", bytes.NewReader(nil))
- require.NoError(t, svc.TestAccountConnection(c, account.ID, "gpt-5.4", "", AccountTestModeCompact))
- require.Equal(t, 1, registerCalls)
- require.Len(t, upstream.requests, 2)
- require.Equal(t, "task-compact-new", account.GetCredential("task_id"))
- require.Equal(t, 0, repo.setErrorCalls)
- require.Equal(t, []int64{account.ID}, invalidator.accountIDs)
- }
- func TestOpenAIAgentIdentityPassthroughKeepsSessionAndPromptCacheHeaders(t *testing.T) {
- gin.SetMode(gin.TestMode)
- key, privateKey := newTestAgentIdentityKey(t)
- account := &Account{
- ID: 24,
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "task_id": key.taskID,
- "chatgpt_account_id": "account-agent-passthrough",
- },
- }
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- body := []byte(`{"model":"gpt-5.4","instructions":"Reply OK","input":[],"stream":true,"prompt_cache_key":"cache-agent"}`)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
- c.Request.Header.Set("session_id", "client-session")
- c.Request.Header.Set("conversation_id", "client-conversation")
- c.Request.Header.Set("Authorization", "Bearer inbound-must-not-forward")
- svc := &OpenAIGatewayService{}
- req, err := svc.buildUpstreamRequestOpenAIPassthrough(context.Background(), c, account, body, "")
- require.NoError(t, err)
- require.Equal(t, "AgentAssertion", strings.SplitN(req.Header.Get("Authorization"), " ", 2)[0])
- require.Equal(t, "account-agent-passthrough", req.Header.Get("chatgpt-account-id"))
- require.NotEqual(t, "client-session", req.Header.Get("session_id"))
- require.NotEqual(t, "client-conversation", req.Header.Get("conversation_id"))
- require.Equal(t, isolateOpenAIUpstreamSessionID(0, account, "client-session"), req.Header.Get("session_id"))
- require.Equal(t, isolateOpenAIUpstreamSessionID(0, account, "client-conversation"), req.Header.Get("conversation_id"))
- requestBody, err := io.ReadAll(req.Body)
- require.NoError(t, err)
- require.Contains(t, string(requestBody), `"prompt_cache_key":"cache-agent"`)
- // Authentication mode must not affect session isolation or prompt-cache
- // behavior. Compare the same request with the existing OAuth path instead
- // of pinning this test to an implementation-specific hash.
- oauthAccount := &Account{
- ID: 26,
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "chatgpt_account_id": "account-agent-passthrough",
- },
- }
- oauthRecorder := httptest.NewRecorder()
- oauthContext, _ := gin.CreateTestContext(oauthRecorder)
- oauthContext.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
- oauthContext.Request.Header.Set("session_id", "client-session")
- oauthContext.Request.Header.Set("conversation_id", "client-conversation")
- oauthReq, err := svc.buildUpstreamRequestOpenAIPassthrough(context.Background(), oauthContext, oauthAccount, body, "oauth-token")
- require.NoError(t, err)
- require.Equal(t, oauthReq.Header.Get("session_id"), req.Header.Get("session_id"))
- require.Equal(t, oauthReq.Header.Get("conversation_id"), req.Header.Get("conversation_id"))
- }
- func TestOpenAIAgentIdentityErrorRedactionDoesNotLeakCredentialValues(t *testing.T) {
- key, privateKey := newTestAgentIdentityKey(t)
- account := &Account{
- ID: 25,
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "task_id": key.taskID,
- "access_token": key.runtimeID + "-oauth-value",
- },
- }
- svc := &OpenAIGatewayService{}
- oauthValue := account.GetCredential("access_token")
- redacted := svc.redactAgentIdentitySensitiveBody(context.Background(), account, []byte(`{"message":"runtime-test task-test `+oauthValue+` AgentAssertion abc123"}`))
- require.NotContains(t, string(redacted), key.runtimeID)
- require.NotContains(t, string(redacted), key.taskID)
- require.NotContains(t, string(redacted), oauthValue)
- require.NotContains(t, string(redacted), "AgentAssertion abc123")
- require.Contains(t, string(redacted), "[redacted]")
- }
- func TestOpenAIAuthenticationHeadersPreserveOAuthPATAndAPIKeyBearerModes(t *testing.T) {
- svc := &OpenAIGatewayService{}
- tests := []struct {
- name string
- account *Account
- token string
- }{
- {name: "oauth", account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}, token: "oauth-runtime-token"},
- {name: "personal access token", account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Credentials: map[string]any{"auth_mode": OpenAIAuthModePersonalAccessToken}}, token: "pat-runtime-token"},
- {name: "api key", account: &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, token: "api-key-runtime-token"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- headers, err := svc.buildOpenAIAuthenticationHeaders(context.Background(), tt.account, tt.token)
- require.NoError(t, err)
- require.Equal(t, "Bearer "+tt.token, headers.Get("Authorization"))
- })
- }
- }
- func TestOpenAIWSAgentIdentityRecoveryRequiresTaskInvalidBody(t *testing.T) {
- require.False(t, isAgentIdentityTaskInvalidWSDialError(&openAIWSDialError{
- StatusCode: http.StatusUnauthorized,
- ResponseBody: []byte(`{"error":{"code":"invalid_signature"}}`),
- }))
- require.True(t, isAgentIdentityTaskInvalidWSDialError(&openAIWSDialError{
- StatusCode: http.StatusUnauthorized,
- ResponseBody: []byte(`{"error":{"code":"invalid_task_id"}}`),
- }))
- }
- func TestValidateOpenAIWSBearerTokenAllowsAgentIdentityWithoutStoredToken(t *testing.T) {
- t.Run("Given Agent Identity When a WS path receives no bearer token Then dial-time assertion auth is allowed", func(t *testing.T) {
- account := &Account{
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- },
- }
- require.NoError(t, validateOpenAIWSBearerToken(account, ""))
- })
- t.Run("Given bearer credentials When a WS path receives no token Then the request is rejected", func(t *testing.T) {
- accounts := []*Account{
- {Platform: PlatformOpenAI, Type: AccountTypeOAuth},
- {Platform: PlatformOpenAI, Type: AccountTypeOAuth, Credentials: map[string]any{"auth_mode": OpenAIAuthModePersonalAccessToken}},
- {Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
- }
- for _, account := range accounts {
- require.EqualError(t, validateOpenAIWSBearerToken(account, ""), "token is empty")
- }
- })
- }
- func TestOpenAIWSConnPoolHeadersFactoryRunsAtDialAndStalePrewarmIsDiscarded(t *testing.T) {
- cfg := &config.Config{}
- cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1
- pool := newOpenAIWSConnPool(cfg)
- defer pool.Close()
- pool.setClientDialerForTest(&openAIWSFakeDialer{})
- accountID := int64(22)
- ap := pool.getOrCreateAccountPool(accountID)
- factoryCalls := 0
- latestHeader := ""
- req := openAIWSAcquireRequest{
- Account: &Account{ID: accountID, Platform: PlatformOpenAI, Type: AccountTypeOAuth},
- WSURL: "wss://example.com/v1/responses",
- HeadersFactory: func(_ context.Context, headers http.Header) (http.Header, error) {
- factoryCalls++
- latestHeader = "AgentAssertion dial-" + string(rune('0'+factoryCalls))
- if headers == nil {
- headers = make(http.Header)
- }
- headers.Set("Authorization", latestHeader)
- return headers, nil
- },
- }
- ap.mu.Lock()
- ap.lastAcquire = &req
- generation := ap.generation
- ap.mu.Unlock()
- pool.prewarmConns(accountID, req, 1, generation)
- require.Equal(t, 1, factoryCalls, "prewarm must generate authorization inside the actual dial")
- require.Equal(t, "AgentAssertion dial-1", latestHeader)
- pool.ClearAccount(accountID)
- ap.mu.Lock()
- require.Empty(t, ap.conns, "credential recovery must remove pooled connections")
- require.Nil(t, ap.lastAcquire, "credential recovery must discard delayed acquire state")
- require.Equal(t, generation+1, ap.generation)
- ap.mu.Unlock()
- // A prewarm captured before ClearAccount must not be admitted after recovery.
- pool.prewarmConns(accountID, req, 1, generation)
- ap.mu.Lock()
- require.Empty(t, ap.conns)
- ap.mu.Unlock()
- }
- func TestOpenAIAgentIdentityTaskInvalidRetriesExactlyOnce(t *testing.T) {
- gin.SetMode(gin.TestMode)
- key, privateKey := newTestAgentIdentityKey(t)
- account := &Account{
- ID: 23,
- Name: "agent-identity",
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "task_id": "task-old",
- "chatgpt_account_id": "account-agent-retry",
- },
- }
- repo := &agentIdentityForwardRepo{account: account}
- registerCalls := 0
- registerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- registerCalls++
- _, _ = io.WriteString(w, `{"task_id":"task-new"}`)
- }))
- defer registerServer.Close()
- oldBase := openAIAgentIdentityAuthAPIBaseURL
- openAIAgentIdentityAuthAPIBaseURL = registerServer.URL
- t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = oldBase })
- successBody := `{"id":"resp-agent-retry","object":"response","model":"gpt-5.4","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`
- upstream := &httpUpstreamRecorder{responses: []*http.Response{
- {StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))},
- {StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(successBody))},
- }}
- require.True(t, isAgentIdentityTaskInvalidHTTPResponse(http.StatusUnauthorized, []byte(`{"error":{"code":"invalid_task_id"}}`)))
- svc := &OpenAIGatewayService{cfg: &config.Config{}, accountRepo: repo, httpUpstream: upstream}
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5.4","instructions":"Reply OK","input":[],"stream":false}`))
- _, err := svc.Forward(context.Background(), c, account, []byte(`{"model":"gpt-5.4","instructions":"Reply OK","input":[],"stream":false}`))
- require.NoError(t, err)
- require.Equal(t, 1, registerCalls)
- require.Len(t, upstream.requests, 2)
- require.NotEqual(t, upstream.requests[0].Header.Get("Authorization"), upstream.requests[1].Header.Get("Authorization"))
- require.Equal(t, "task-new", decodeAgentAssertionTask(t, upstream.requests[1].Header.Get("Authorization")))
- // Two consecutive invalid responses still produce only one retry for this
- // request; the recovery path must not loop indefinitely.
- upstream.responses = []*http.Response{
- {StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))},
- {StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))},
- }
- rec2 := httptest.NewRecorder()
- c2, _ := gin.CreateTestContext(rec2)
- c2.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5.4","instructions":"Reply OK","input":[],"stream":false}`))
- _, err = svc.Forward(context.Background(), c2, account, []byte(`{"model":"gpt-5.4","instructions":"Reply OK","input":[],"stream":false}`))
- require.Error(t, err)
- require.Equal(t, 2, registerCalls)
- require.Len(t, upstream.requests, 4)
- // Passthrough uses the same one-shot task recovery contract.
- account.Extra = map[string]any{"openai_passthrough": true}
- account.Credentials["task_id"] = "task-old-passthrough"
- upstream.responses = []*http.Response{
- {StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))},
- {StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader("data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\ndata: [DONE]\n\n"))},
- }
- rec3 := httptest.NewRecorder()
- c3, _ := gin.CreateTestContext(rec3)
- c3.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5.4","instructions":"Reply OK","input":[],"stream":false}`))
- _, err = svc.Forward(context.Background(), c3, account, []byte(`{"model":"gpt-5.4","instructions":"Reply OK","input":[],"stream":false}`))
- require.NoError(t, err)
- require.Equal(t, 3, registerCalls)
- require.Len(t, upstream.requests, 6)
- }
- func TestOpenAIAgentIdentityCompatRoutesRecoverInvalidTaskOnce(t *testing.T) {
- gin.SetMode(gin.TestMode)
- tests := []struct {
- name string
- path string
- body []byte
- call func(*OpenAIGatewayService, context.Context, *gin.Context, *Account, []byte) (*OpenAIForwardResult, error)
- }{
- {
- name: "chat completions",
- path: "/v1/chat/completions",
- body: []byte(`{"model":"gpt-5.4","stream":false,"messages":[{"role":"user","content":"hi"}]}`),
- call: func(s *OpenAIGatewayService, ctx context.Context, c *gin.Context, account *Account, body []byte) (*OpenAIForwardResult, error) {
- return s.ForwardAsChatCompletions(ctx, c, account, body, "", "gpt-5.4")
- },
- },
- {
- name: "anthropic messages",
- path: "/v1/messages",
- body: []byte(`{"model":"gpt-5.4","stream":false,"max_tokens":32,"messages":[{"role":"user","content":"hi"}]}`),
- call: func(s *OpenAIGatewayService, ctx context.Context, c *gin.Context, account *Account, body []byte) (*OpenAIForwardResult, error) {
- return s.ForwardAsAnthropic(ctx, c, account, body, "", "gpt-5.4")
- },
- },
- }
- for index, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- key, privateKey := newTestAgentIdentityKey(t)
- account := &Account{
- ID: int64(40 + index),
- Name: "agent-identity-compat",
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "task_id": "task-compat-old",
- "chatgpt_account_id": "account-compat-recovery",
- },
- }
- repo := &agentIdentityForwardRepo{account: account}
- registerCalls := 0
- registerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- registerCalls++
- _, _ = io.WriteString(w, `{"task_id":"task-compat-new"}`)
- }))
- defer registerServer.Close()
- oldBase := openAIAgentIdentityAuthAPIBaseURL
- openAIAgentIdentityAuthAPIBaseURL = registerServer.URL
- t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = oldBase })
- upstream := &httpUpstreamRecorder{responses: []*http.Response{
- {StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))},
- {StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))},
- }}
- svc := &OpenAIGatewayService{cfg: &config.Config{}, accountRepo: repo, httpUpstream: upstream}
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, tt.path, bytes.NewReader(tt.body))
- _, err := tt.call(svc, context.Background(), c, account, tt.body)
- require.Error(t, err)
- require.Equal(t, 1, registerCalls)
- require.Len(t, upstream.requests, 2)
- require.Equal(t, "task-compat-new", account.GetCredential("task_id"))
- })
- }
- }
- func TestOpenAIAgentIdentityChatRecoveryKeepsAutoDerivedSessionIsolationStable(t *testing.T) {
- gin.SetMode(gin.TestMode)
- key, privateKey := newTestAgentIdentityKey(t)
- account := &Account{
- ID: 52, Name: "agent-identity", Platform: PlatformOpenAI, Type: AccountTypeOAuth,
- Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{
- "auth_mode": OpenAIAuthModeAgentIdentity,
- "agent_runtime_id": key.runtimeID,
- "agent_private_key": privateKey,
- "task_id": "task-cache-old",
- },
- Extra: map[string]any{"openai_responses_supported": true},
- }
- repo := &agentIdentityForwardRepo{account: account}
- registerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- _, _ = io.WriteString(w, `{"task_id":"task-cache-new"}`)
- }))
- defer registerServer.Close()
- oldBase := openAIAgentIdentityAuthAPIBaseURL
- openAIAgentIdentityAuthAPIBaseURL = registerServer.URL
- t.Cleanup(func() { openAIAgentIdentityAuthAPIBaseURL = oldBase })
- invalidTask := func() *http.Response {
- return &http.Response{StatusCode: http.StatusUnauthorized, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_task_id"}}`))}
- }
- upstream := &httpUpstreamRecorder{responses: []*http.Response{invalidTask(), invalidTask()}}
- svc := &OpenAIGatewayService{cfg: &config.Config{}, accountRepo: repo, httpUpstream: upstream}
- body := []byte(`{"model":"gpt-5.4","stream":false,"messages":[{"role":"user","content":"hi"}]}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
- c.Set("api_key", &APIKey{ID: 99})
- _, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "gpt-5.4")
- require.Error(t, err)
- require.Len(t, upstream.requests, 2)
- firstKey := gjson.GetBytes(upstream.bodies[0], "prompt_cache_key").String()
- secondKey := gjson.GetBytes(upstream.bodies[1], "prompt_cache_key").String()
- require.NotEmpty(t, firstKey)
- require.Equal(t, firstKey, secondKey)
- require.Equal(t, generateSessionUUID(isolateOpenAIUpstreamSessionID(99, codexAccountIdentitySource(c, account), firstKey)), upstream.requests[0].Header.Get("session_id"))
- require.Equal(t, upstream.requests[0].Header.Get("session_id"), upstream.requests[1].Header.Get("session_id"))
- }
- func decodeAgentAssertionTask(t *testing.T, header string) string {
- t.Helper()
- encoded := strings.TrimPrefix(header, "AgentAssertion ")
- decoded, err := base64.RawURLEncoding.DecodeString(encoded)
- require.NoError(t, err)
- var envelope struct {
- TaskID string `json:"task_id"`
- }
- require.NoError(t, json.Unmarshal(decoded, &envelope))
- return envelope.TaskID
- }
- type agentIdentityForwardRepo struct {
- AccountRepository
- account *Account
- }
- type agentIdentityWSInvalidationRecorder struct {
- accountIDs []int64
- }
- func (r *agentIdentityWSInvalidationRecorder) InvalidateAgentIdentityWSConnections(accountID int64) {
- r.accountIDs = append(r.accountIDs, accountID)
- }
- type accountTestAgentIdentityRepo struct {
- AccountRepository
- account *Account
- setErrorCalls int
- }
- func (r *accountTestAgentIdentityRepo) GetByID(_ context.Context, _ int64) (*Account, error) {
- return r.account, nil
- }
- func (r *accountTestAgentIdentityRepo) UpdateCredentials(_ context.Context, _ int64, credentials map[string]any) error {
- r.account.Credentials = credentials
- return nil
- }
- func (r *accountTestAgentIdentityRepo) UpdateExtra(_ context.Context, _ int64, _ map[string]any) error {
- return nil
- }
- func (r *accountTestAgentIdentityRepo) SetError(_ context.Context, _ int64, _ string) error {
- r.setErrorCalls++
- return nil
- }
- func (r *agentIdentityForwardRepo) GetByID(_ context.Context, _ int64) (*Account, error) {
- return r.account, nil
- }
- func (r *agentIdentityForwardRepo) UpdateCredentials(_ context.Context, _ int64, credentials map[string]any) error {
- r.account.Credentials = credentials
- return nil
- }
|