2026-02-11 00:59:39 +08:00
package service
import (
"bytes"
"context"
2026-02-12 19:01:09 +08:00
"fmt"
2026-02-11 00:59:39 +08:00
"io"
"net/http"
"net/http/httptest"
"strings"
2026-02-12 14:16:18 +08:00
"sync"
2026-02-11 00:59:39 +08:00
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
2026-02-12 19:01:09 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
2026-02-11 00:59:39 +08:00
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
2026-02-12 21:02:52 +08:00
"github.com/tidwall/gjson"
2026-02-11 00:59:39 +08:00
)
func f64p ( v float64 ) * float64 { return & v }
type httpUpstreamRecorder struct {
lastReq * http . Request
lastBody [ ] byte
resp * http . Response
err error
}
func ( u * httpUpstreamRecorder ) Do ( req * http . Request , proxyURL string , accountID int64 , accountConcurrency int ) ( * http . Response , error ) {
u . lastReq = req
if req != nil && req . Body != nil {
b , _ := io . ReadAll ( req . Body )
u . lastBody = b
_ = req . Body . Close ( )
req . Body = io . NopCloser ( bytes . NewReader ( b ) )
}
if u . err != nil {
return nil , u . err
}
return u . resp , nil
}
func ( u * httpUpstreamRecorder ) DoWithTLS ( req * http . Request , proxyURL string , accountID int64 , accountConcurrency int , enableTLSFingerprint bool ) ( * http . Response , error ) {
return u . Do ( req , proxyURL , accountID , accountConcurrency )
}
2026-02-12 19:01:09 +08:00
var structuredLogCaptureMu sync . Mutex
2026-02-12 14:16:18 +08:00
2026-02-12 19:01:09 +08:00
type inMemoryLogSink struct {
mu sync . Mutex
events [ ] * logger . LogEvent
}
func ( s * inMemoryLogSink ) WriteLogEvent ( event * logger . LogEvent ) {
if event == nil {
return
}
cloned := * event
if event . Fields != nil {
cloned . Fields = make ( map [ string ] any , len ( event . Fields ) )
for k , v := range event . Fields {
cloned . Fields [ k ] = v
}
}
s . mu . Lock ( )
s . events = append ( s . events , & cloned )
s . mu . Unlock ( )
}
func ( s * inMemoryLogSink ) ContainsMessage ( substr string ) bool {
s . mu . Lock ( )
defer s . mu . Unlock ( )
for _ , ev := range s . events {
if ev != nil && strings . Contains ( ev . Message , substr ) {
return true
}
}
return false
}
2026-02-13 19:27:07 +08:00
func ( s * inMemoryLogSink ) ContainsMessageAtLevel ( substr , level string ) bool {
s . mu . Lock ( )
defer s . mu . Unlock ( )
wantLevel := strings . ToLower ( strings . TrimSpace ( level ) )
for _ , ev := range s . events {
if ev == nil {
continue
}
if strings . Contains ( ev . Message , substr ) && strings . ToLower ( strings . TrimSpace ( ev . Level ) ) == wantLevel {
return true
}
}
return false
}
2026-02-12 19:01:09 +08:00
func ( s * inMemoryLogSink ) ContainsFieldValue ( field , substr string ) bool {
s . mu . Lock ( )
defer s . mu . Unlock ( )
for _ , ev := range s . events {
if ev == nil || ev . Fields == nil {
continue
}
if v , ok := ev . Fields [ field ] ; ok && strings . Contains ( fmt . Sprint ( v ) , substr ) {
return true
2026-02-12 14:16:18 +08:00
}
2026-02-12 19:01:09 +08:00
}
return false
}
2026-02-13 19:27:07 +08:00
func ( s * inMemoryLogSink ) ContainsField ( field string ) bool {
s . mu . Lock ( )
defer s . mu . Unlock ( )
for _ , ev := range s . events {
if ev == nil || ev . Fields == nil {
continue
}
if _ , ok := ev . Fields [ field ] ; ok {
return true
}
}
return false
}
2026-02-12 19:01:09 +08:00
func captureStructuredLog ( t * testing . T ) ( * inMemoryLogSink , func ( ) ) {
t . Helper ( )
structuredLogCaptureMu . Lock ( )
err := logger . Init ( logger . InitOptions {
Level : "debug" ,
Format : "json" ,
ServiceName : "sub2api" ,
Environment : "test" ,
Output : logger . OutputOptions {
ToStdout : true ,
ToFile : false ,
} ,
Sampling : logger . SamplingOptions { Enabled : false } ,
} )
require . NoError ( t , err )
sink := & inMemoryLogSink { }
logger . SetSink ( sink )
return sink , func ( ) {
logger . SetSink ( nil )
structuredLogCaptureMu . Unlock ( )
2026-02-12 14:16:18 +08:00
}
}
2026-02-12 21:02:52 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_StreamKeepsToolNameAndBodyNormalized ( t * testing . T ) {
2026-02-11 00:59:39 +08:00
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
c . Request . Header . Set ( "Authorization" , "Bearer inbound-should-not-forward" )
c . Request . Header . Set ( "Cookie" , "secret=1" )
c . Request . Header . Set ( "X-Api-Key" , "sk-inbound" )
c . Request . Header . Set ( "X-Goog-Api-Key" , "goog-inbound" )
2026-02-11 22:17:38 +08:00
c . Request . Header . Set ( "Accept-Encoding" , "gzip" )
c . Request . Header . Set ( "Proxy-Authorization" , "Basic abc" )
2026-02-11 00:59:39 +08:00
c . Request . Header . Set ( "X-Test" , "keep" )
2026-02-14 10:49:01 +08:00
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"store":true,"instructions":"local-test-instructions","input":[ { "type":"text","text":"hi"}]} ` )
2026-02-11 00:59:39 +08:00
upstreamSSE := strings . Join ( [ ] string {
` data: { "type":"response.output_item.added","item": { "type":"tool_call","tool_calls":[ { "function": { "name":"apply_patch"}}]}} ` ,
"" ,
"data: [DONE]" ,
"" ,
} , "\n" )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( upstreamSSE ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
openAITokenProvider : & OpenAITokenProvider { // minimal: will be bypassed by nil cache/service, but GetAccessToken uses provider only if non-nil
accountRepo : nil ,
} ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
2026-02-12 10:56:07 +08:00
Extra : map [ string ] any { "openai_passthrough" : true } ,
2026-02-11 00:59:39 +08:00
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
// Use the gateway method that reads token from credentials when provider is nil.
svc . openAITokenProvider = nil
result , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
require . NotNil ( t , result )
require . True ( t , result . Stream )
2026-02-12 21:02:52 +08:00
// 1) 透传 OAuth 请求体与旧链路关键行为保持一致: store=false + stream=true。
require . Equal ( t , false , gjson . GetBytes ( upstream . lastBody , "store" ) . Bool ( ) )
require . Equal ( t , true , gjson . GetBytes ( upstream . lastBody , "stream" ) . Bool ( ) )
2026-02-14 10:49:01 +08:00
require . Equal ( t , "local-test-instructions" , strings . TrimSpace ( gjson . GetBytes ( upstream . lastBody , "instructions" ) . String ( ) ) )
2026-02-12 21:02:52 +08:00
// 其余关键字段保持原值。
require . Equal ( t , "gpt-5.2" , gjson . GetBytes ( upstream . lastBody , "model" ) . String ( ) )
require . Equal ( t , "hi" , gjson . GetBytes ( upstream . lastBody , "input.0.text" ) . String ( ) )
2026-02-11 00:59:39 +08:00
// 2) only auth is replaced; inbound auth/cookie are not forwarded
require . Equal ( t , "Bearer oauth-token" , upstream . lastReq . Header . Get ( "Authorization" ) )
2026-02-12 10:56:07 +08:00
require . Equal ( t , "codex_cli_rs/0.1.0" , upstream . lastReq . Header . Get ( "User-Agent" ) )
2026-02-11 00:59:39 +08:00
require . Empty ( t , upstream . lastReq . Header . Get ( "Cookie" ) )
require . Empty ( t , upstream . lastReq . Header . Get ( "X-Api-Key" ) )
require . Empty ( t , upstream . lastReq . Header . Get ( "X-Goog-Api-Key" ) )
2026-02-11 22:17:38 +08:00
require . Empty ( t , upstream . lastReq . Header . Get ( "Accept-Encoding" ) )
require . Empty ( t , upstream . lastReq . Header . Get ( "Proxy-Authorization" ) )
2026-02-12 20:12:15 +08:00
require . Empty ( t , upstream . lastReq . Header . Get ( "X-Test" ) )
2026-02-11 00:59:39 +08:00
// 3) required OAuth headers are present
require . Equal ( t , "chatgpt.com" , upstream . lastReq . Host )
require . Equal ( t , "chatgpt-acc" , upstream . lastReq . Header . Get ( "chatgpt-account-id" ) )
// 4) downstream SSE keeps tool name (no toolCorrector)
body := rec . Body . String ( )
require . Contains ( t , body , "apply_patch" )
require . NotContains ( t , body , "\"name\":\"edit\"" )
}
2026-03-06 18:50:28 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_CompactUsesJSONAndKeepsNonStreaming ( 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.1-codex","stream":true,"store":true,"instructions":"local-test-instructions","input":[ { "type":"text","text":"compact me"}]} ` )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "application/json" } , "x-request-id" : [ ] string { "rid-compact" } } ,
Body : io . NopCloser ( strings . NewReader ( ` { "id":"cmp_123","usage": { "input_tokens":11,"output_tokens":22}} ` ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
result , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
require . NotNil ( t , result )
require . False ( t , result . Stream )
require . False ( t , gjson . GetBytes ( upstream . lastBody , "store" ) . Exists ( ) )
require . False ( t , gjson . GetBytes ( upstream . lastBody , "stream" ) . Exists ( ) )
require . Equal ( t , "gpt-5.1-codex" , gjson . GetBytes ( upstream . lastBody , "model" ) . String ( ) )
require . Equal ( t , "compact me" , gjson . GetBytes ( upstream . lastBody , "input.0.text" ) . String ( ) )
require . Equal ( t , "local-test-instructions" , strings . TrimSpace ( gjson . GetBytes ( upstream . lastBody , "instructions" ) . String ( ) ) )
require . Equal ( t , "application/json" , upstream . lastReq . Header . Get ( "Accept" ) )
require . Equal ( t , codexCLIVersion , upstream . lastReq . Header . Get ( "Version" ) )
require . NotEmpty ( t , upstream . lastReq . Header . Get ( "Session_Id" ) )
require . Equal ( t , "chatgpt.com" , upstream . lastReq . Host )
require . Equal ( t , "chatgpt-acc" , upstream . lastReq . Header . Get ( "chatgpt-account-id" ) )
require . Contains ( t , rec . Body . String ( ) , ` "id":"cmp_123" ` )
}
2026-02-14 10:49:01 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_CodexMissingInstructionsRejectedBeforeUpstream ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
logSink , restore := captureStructuredLog ( t )
defer restore ( )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses?trace=1" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.98.0 (Windows 10.0.19045; x86_64) unknown" )
c . Request . Header . Set ( "Content-Type" , "application/json" )
c . Request . Header . Set ( "OpenAI-Beta" , "responses=experimental" )
// Codex 模型且缺少 instructions, 应在本地直接 403 拒绝,不触达上游。
originalBody := [ ] byte ( ` { "model":"gpt-5.1-codex-max","stream":false,"store":true,"input":[ { "type":"text","text":"hi"}]} ` )
upstream := & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "application/json" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( ` { "output":[],"usage": { "input_tokens":1,"output_tokens":1}} ` ) ) ,
} ,
}
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
result , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . Error ( t , err )
require . Nil ( t , result )
require . Equal ( t , http . StatusForbidden , rec . Code )
require . Contains ( t , rec . Body . String ( ) , "requires a non-empty instructions field" )
require . Nil ( t , upstream . lastReq )
require . True ( t , logSink . ContainsMessage ( "OpenAI passthrough 本地拦截: Codex 请求缺少有效 instructions" ) )
require . True ( t , logSink . ContainsFieldValue ( "request_user_agent" , "codex_cli_rs/0.98.0 (Windows 10.0.19045; x86_64) unknown" ) )
require . True ( t , logSink . ContainsFieldValue ( "reject_reason" , "instructions_missing" ) )
}
2026-02-11 00:59:39 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_DisabledUsesLegacyTransform ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
// store=true + stream=false should be forced to store=false + stream=true by applyCodexOAuthTransform (OAuth legacy path)
inputBody := [ ] byte ( ` { "model":"gpt-5.2","stream":false,"store":true,"input":[ { "type":"text","text":"hi"}]} ` )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( "data: [DONE]\n\n" ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
2026-02-12 10:56:07 +08:00
Extra : map [ string ] any { "openai_passthrough" : false } ,
2026-02-11 00:59:39 +08:00
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , inputBody )
require . NoError ( t , err )
// legacy path rewrites request body (not byte-equal)
require . NotEqual ( t , inputBody , upstream . lastBody )
require . Contains ( t , string ( upstream . lastBody ) , ` "store":false ` )
require . Contains ( t , string ( upstream . lastBody ) , ` "stream":true ` )
}
2026-02-12 21:02:52 +08:00
func TestOpenAIGatewayService_OAuthLegacy_CompositeCodexUAUsesCodexOriginator ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
// 复合 UA( 前缀不是 codex_cli_rs) , 历史实现会误判为非 Codex 并走 opencode。
c . Request . Header . Set ( "User-Agent" , "Mozilla/5.0 codex_cli_rs/0.1.0" )
inputBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"store":false,"input":[ { "type":"text","text":"hi"}]} ` )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( "data: [DONE]\n\n" ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : false } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , inputBody )
require . NoError ( t , err )
require . NotNil ( t , upstream . lastReq )
require . Equal ( t , "codex_cli_rs" , upstream . lastReq . Header . Get ( "originator" ) )
require . NotEqual ( t , "opencode" , upstream . lastReq . Header . Get ( "originator" ) )
}
2026-02-11 00:59:39 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_ResponseHeadersAllowXCodex ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
2026-03-12 17:22:01 +08:00
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"input":[ { "type":"text","text":"hi"}]} ` )
2026-02-11 00:59:39 +08:00
headers := make ( http . Header )
headers . Set ( "Content-Type" , "application/json" )
headers . Set ( "x-request-id" , "rid" )
headers . Set ( "x-codex-primary-used-percent" , "12" )
headers . Set ( "x-codex-secondary-used-percent" , "34" )
headers . Set ( "x-codex-primary-window-minutes" , "300" )
headers . Set ( "x-codex-secondary-window-minutes" , "10080" )
headers . Set ( "x-codex-primary-reset-after-seconds" , "1" )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : headers ,
2026-03-12 17:22:01 +08:00
Body : io . NopCloser ( strings . NewReader ( strings . Join ( [ ] string {
` data: { "type":"response.output_text.delta","delta":"h"} ` ,
"" ,
` data: { "type":"response.completed","response": { "usage": { "input_tokens":1,"output_tokens":1,"input_tokens_details": { "cached_tokens":0}}}} ` ,
"" ,
"data: [DONE]" ,
"" ,
} , "\n" ) ) ) ,
2026-02-11 00:59:39 +08:00
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
2026-02-12 10:56:07 +08:00
Extra : map [ string ] any { "openai_passthrough" : true } ,
2026-02-11 00:59:39 +08:00
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
require . Equal ( t , "12" , rec . Header ( ) . Get ( "x-codex-primary-used-percent" ) )
require . Equal ( t , "34" , rec . Header ( ) . Get ( "x-codex-secondary-used-percent" ) )
}
func TestOpenAIGatewayService_OAuthPassthrough_UpstreamErrorIncludesPassthroughFlag ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":false,"input":[ { "type":"text","text":"hi"}]} ` )
resp := & http . Response {
StatusCode : http . StatusBadRequest ,
Header : http . Header { "Content-Type" : [ ] string { "application/json" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( ` { "error": { "message":"bad"}} ` ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
2026-02-12 10:56:07 +08:00
Extra : map [ string ] any { "openai_passthrough" : true } ,
2026-02-11 00:59:39 +08:00
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . Error ( t , err )
// should append an upstream error event with passthrough=true
v , ok := c . Get ( OpsUpstreamErrorsKey )
require . True ( t , ok )
arr , ok := v . ( [ ] * OpsUpstreamErrorEvent )
require . True ( t , ok )
require . NotEmpty ( t , arr )
require . True ( t , arr [ len ( arr ) - 1 ] . Passthrough )
}
2026-03-24 01:24:30 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_429PersistsRateLimit ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":false,"instructions":"local-test-instructions","input":[ { "type":"text","text":"hi"}]} ` )
resetAt := time . Now ( ) . Add ( 7 * 24 * time . Hour ) . Unix ( )
resp := & http . Response {
StatusCode : http . StatusTooManyRequests ,
Header : http . Header {
"Content-Type" : [ ] string { "application/json" } ,
"x-request-id" : [ ] string { "rid-rate-limit" } ,
} ,
Body : io . NopCloser ( strings . NewReader ( fmt . Sprintf ( ` { "error": { "message":"The usage limit has been reached","type":"usage_limit_reached","resets_at":%d}} ` , resetAt ) ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
repo := & openAIWSRateLimitSignalRepo { }
rateSvc := & RateLimitService { accountRepo : repo }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
rateLimitService : rateSvc ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . Error ( t , err )
require . Equal ( t , http . StatusTooManyRequests , rec . Code )
require . Contains ( t , rec . Body . String ( ) , "usage_limit_reached" )
require . Len ( t , repo . rateLimitCalls , 1 )
require . WithinDuration ( t , time . Unix ( resetAt , 0 ) , repo . rateLimitCalls [ 0 ] , 2 * time . Second )
}
2026-02-12 20:12:15 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_NonCodexUAFallbackToCodexUA ( t * testing . T ) {
2026-02-11 00:59:39 +08:00
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
// Non-Codex UA
c . Request . Header . Set ( "User-Agent" , "curl/8.0" )
inputBody := [ ] byte ( ` { "model":"gpt-5.2","stream":false,"store":true,"input":[ { "type":"text","text":"hi"}]} ` )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( "data: [DONE]\n\n" ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
2026-02-12 10:56:07 +08:00
Extra : map [ string ] any { "openai_passthrough" : true } ,
2026-02-11 00:59:39 +08:00
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , inputBody )
require . NoError ( t , err )
2026-02-12 21:02:52 +08:00
require . Equal ( t , false , gjson . GetBytes ( upstream . lastBody , "store" ) . Bool ( ) )
require . Equal ( t , true , gjson . GetBytes ( upstream . lastBody , "stream" ) . Bool ( ) )
2026-02-28 15:01:20 +08:00
require . Equal ( t , "codex_cli_rs/0.104.0" , upstream . lastReq . Header . Get ( "User-Agent" ) )
2026-02-11 00:59:39 +08:00
}
2026-02-12 22:32:59 +08:00
func TestOpenAIGatewayService_CodexCLIOnly_RejectsNonCodexClient ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "curl/8.0" )
inputBody := [ ] byte ( ` { "model":"gpt-5.2","stream":false,"store":true,"input":[ { "type":"text","text":"hi"}]} ` )
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true , "codex_cli_only" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , inputBody )
require . Error ( t , err )
require . Equal ( t , http . StatusForbidden , rec . Code )
require . Contains ( t , rec . Body . String ( ) , "Codex official clients" )
}
func TestOpenAIGatewayService_CodexCLIOnly_AllowOfficialClientFamilies ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
tests := [ ] struct {
2026-02-21 12:06:24 +08:00
name string
ua string
originator string
2026-02-12 22:32:59 +08:00
} {
2026-02-21 12:06:24 +08:00
{ name : "codex_cli_rs" , ua : "codex_cli_rs/0.99.0" , originator : "" } ,
{ name : "codex_vscode" , ua : "codex_vscode/1.0.0" , originator : "" } ,
{ name : "codex_app" , ua : "codex_app/2.1.0" , originator : "" } ,
{ name : "originator_codex_chatgpt_desktop" , ua : "curl/8.0" , originator : "codex_chatgpt_desktop" } ,
2026-02-12 22:32:59 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , tt . ua )
2026-02-21 12:06:24 +08:00
if tt . originator != "" {
c . Request . Header . Set ( "originator" , tt . originator )
}
2026-02-12 22:32:59 +08:00
inputBody := [ ] byte ( ` { "model":"gpt-5.2","stream":false,"store":true,"input":[ { "type":"text","text":"hi"}]} ` )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( "data: [DONE]\n\n" ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true , "codex_cli_only" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , inputBody )
require . NoError ( t , err )
require . NotNil ( t , upstream . lastReq )
} )
}
}
2026-02-11 00:59:39 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_StreamingSetsFirstTokenMs ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
2026-03-08 23:22:28 +08:00
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"service_tier":"fast","input":[ { "type":"text","text":"hi"}]} ` )
2026-02-11 00:59:39 +08:00
upstreamSSE := strings . Join ( [ ] string {
` data: { "type":"response.output_text.delta","delta":"h"} ` ,
"" ,
"data: [DONE]" ,
"" ,
} , "\n" )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( upstreamSSE ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
2026-02-12 10:56:07 +08:00
Extra : map [ string ] any { "openai_passthrough" : true } ,
2026-02-11 00:59:39 +08:00
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
start := time . Now ( )
result , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
// sanity: duration after start
require . GreaterOrEqual ( t , time . Since ( start ) , time . Duration ( 0 ) )
require . NotNil ( t , result . FirstTokenMs )
require . GreaterOrEqual ( t , * result . FirstTokenMs , 0 )
2026-03-08 23:22:28 +08:00
require . NotNil ( t , result . ServiceTier )
require . Equal ( t , "priority" , * result . ServiceTier )
2026-02-11 00:59:39 +08:00
}
2026-02-11 22:17:38 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_StreamClientDisconnectStillCollectsUsage ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
// 首次写入成功,后续写入失败,模拟客户端中途断开。
c . Writer = & failingGinWriter { ResponseWriter : c . Writer , failAfter : 1 }
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"input":[ { "type":"text","text":"hi"}]} ` )
upstreamSSE := strings . Join ( [ ] string {
` data: { "type":"response.output_text.delta","delta":"h"} ` ,
"" ,
` data: { "type":"response.completed","response": { "usage": { "input_tokens":11,"output_tokens":7,"input_tokens_details": { "cached_tokens":3}}}} ` ,
"" ,
"data: [DONE]" ,
"" ,
} , "\n" )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( upstreamSSE ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 123 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
2026-02-12 10:56:07 +08:00
Extra : map [ string ] any { "openai_passthrough" : true } ,
2026-02-11 22:17:38 +08:00
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
result , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
require . NotNil ( t , result )
require . True ( t , result . Stream )
require . NotNil ( t , result . FirstTokenMs )
require . Equal ( t , 11 , result . Usage . InputTokens )
require . Equal ( t , 7 , result . Usage . OutputTokens )
require . Equal ( t , 3 , result . Usage . CacheReadInputTokens )
}
2026-02-12 10:56:07 +08:00
func TestOpenAIGatewayService_APIKeyPassthrough_PreservesBodyAndUsesResponsesEndpoint ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "curl/8.0" )
c . Request . Header . Set ( "X-Test" , "keep" )
2026-03-08 23:22:28 +08:00
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":false,"service_tier":"flex","max_output_tokens":128,"input":[ { "type":"text","text":"hi"}]} ` )
2026-02-12 10:56:07 +08:00
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "application/json" } , "x-request-id" : [ ] string { "rid" } } ,
Body : io . NopCloser ( strings . NewReader ( ` { "output":[],"usage": { "input_tokens":1,"output_tokens":1,"input_tokens_details": { "cached_tokens":0}}} ` ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 456 ,
Name : "apikey-acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeAPIKey ,
Concurrency : 1 ,
Credentials : map [ string ] any { "api_key" : "sk-api-key" , "base_url" : "https://api.openai.com" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
2026-03-08 23:22:28 +08:00
result , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
2026-02-12 10:56:07 +08:00
require . NoError ( t , err )
2026-03-08 23:22:28 +08:00
require . NotNil ( t , result )
require . NotNil ( t , result . ServiceTier )
require . Equal ( t , "flex" , * result . ServiceTier )
2026-02-12 10:56:07 +08:00
require . NotNil ( t , upstream . lastReq )
require . Equal ( t , originalBody , upstream . lastBody )
require . Equal ( t , "https://api.openai.com/v1/responses" , upstream . lastReq . URL . String ( ) )
require . Equal ( t , "Bearer sk-api-key" , upstream . lastReq . Header . Get ( "Authorization" ) )
require . Equal ( t , "curl/8.0" , upstream . lastReq . Header . Get ( "User-Agent" ) )
2026-02-12 20:12:15 +08:00
require . Empty ( t , upstream . lastReq . Header . Get ( "X-Test" ) )
2026-02-12 10:56:07 +08:00
}
2026-02-12 14:16:18 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_WarnOnTimeoutHeadersForStream ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
2026-02-12 19:01:09 +08:00
logSink , restore := captureStructuredLog ( t )
2026-02-12 14:16:18 +08:00
defer restore ( )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
c . Request . Header . Set ( "x-stainless-timeout" , "10000" )
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"input":[ { "type":"text","text":"hi"}]} ` )
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "X-Request-Id" : [ ] string { "rid-timeout" } } ,
Body : io . NopCloser ( strings . NewReader ( "data: [DONE]\n\n" ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 321 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
2026-02-12 19:01:09 +08:00
require . True ( t , logSink . ContainsMessage ( "检测到超时相关请求头,将按配置过滤以降低断流风险" ) )
require . True ( t , logSink . ContainsFieldValue ( "timeout_headers" , "x-stainless-timeout=10000" ) )
2026-02-12 14:16:18 +08:00
}
2026-02-13 19:27:07 +08:00
func TestOpenAIGatewayService_OAuthPassthrough_InfoWhenStreamEndsWithoutDone ( t * testing . T ) {
2026-02-12 14:16:18 +08:00
gin . SetMode ( gin . TestMode )
2026-02-12 19:01:09 +08:00
logSink , restore := captureStructuredLog ( t )
2026-02-12 14:16:18 +08:00
defer restore ( )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"input":[ { "type":"text","text":"hi"}]} ` )
// 注意:刻意不发送 [DONE],模拟上游中途断流。
resp := & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "X-Request-Id" : [ ] string { "rid-truncate" } } ,
Body : io . NopCloser ( strings . NewReader ( "data: {\"type\":\"response.output_text.delta\",\"delta\":\"h\"}\n\n" ) ) ,
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 654 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
2026-03-12 17:22:01 +08:00
require . EqualError ( t , err , "stream usage incomplete: missing terminal event" )
2026-02-12 19:01:09 +08:00
require . True ( t , logSink . ContainsMessage ( "上游流在未收到 [DONE] 时结束,疑似断流" ) )
2026-02-13 19:27:07 +08:00
require . True ( t , logSink . ContainsMessageAtLevel ( "上游流在未收到 [DONE] 时结束,疑似断流" , "info" ) )
2026-02-12 19:01:09 +08:00
require . True ( t , logSink . ContainsFieldValue ( "upstream_request_id" , "rid-truncate" ) )
2026-02-12 14:16:18 +08:00
}
func TestOpenAIGatewayService_OAuthPassthrough_DefaultFiltersTimeoutHeaders ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
c . Request . Header . Set ( "x-stainless-timeout" , "120000" )
c . Request . Header . Set ( "X-Test" , "keep" )
2026-03-12 17:22:01 +08:00
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"input":[ { "type":"text","text":"hi"}]} ` )
2026-02-12 14:16:18 +08:00
resp := & http . Response {
StatusCode : http . StatusOK ,
2026-03-12 17:22:01 +08:00
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "X-Request-Id" : [ ] string { "rid-filter-default" } } ,
Body : io . NopCloser ( strings . NewReader ( strings . Join ( [ ] string {
` data: { "type":"response.completed","response": { "usage": { "input_tokens":1,"output_tokens":1,"input_tokens_details": { "cached_tokens":0}}}} ` ,
"" ,
"data: [DONE]" ,
"" ,
} , "\n" ) ) ) ,
2026-02-12 14:16:18 +08:00
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig { ForceCodexCLI : false } } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 111 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
require . NotNil ( t , upstream . lastReq )
require . Empty ( t , upstream . lastReq . Header . Get ( "x-stainless-timeout" ) )
2026-02-12 20:12:15 +08:00
require . Empty ( t , upstream . lastReq . Header . Get ( "X-Test" ) )
2026-02-12 14:16:18 +08:00
}
func TestOpenAIGatewayService_OAuthPassthrough_AllowTimeoutHeadersWhenConfigured ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ( )
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/responses" , bytes . NewReader ( nil ) )
c . Request . Header . Set ( "User-Agent" , "codex_cli_rs/0.1.0" )
c . Request . Header . Set ( "x-stainless-timeout" , "120000" )
c . Request . Header . Set ( "X-Test" , "keep" )
2026-03-12 17:22:01 +08:00
originalBody := [ ] byte ( ` { "model":"gpt-5.2","stream":true,"input":[ { "type":"text","text":"hi"}]} ` )
2026-02-12 14:16:18 +08:00
resp := & http . Response {
StatusCode : http . StatusOK ,
2026-03-12 17:22:01 +08:00
Header : http . Header { "Content-Type" : [ ] string { "text/event-stream" } , "X-Request-Id" : [ ] string { "rid-filter-allow" } } ,
Body : io . NopCloser ( strings . NewReader ( strings . Join ( [ ] string {
` data: { "type":"response.completed","response": { "usage": { "input_tokens":1,"output_tokens":1,"input_tokens_details": { "cached_tokens":0}}}} ` ,
"" ,
"data: [DONE]" ,
"" ,
} , "\n" ) ) ) ,
2026-02-12 14:16:18 +08:00
}
upstream := & httpUpstreamRecorder { resp : resp }
svc := & OpenAIGatewayService {
cfg : & config . Config { Gateway : config . GatewayConfig {
ForceCodexCLI : false ,
OpenAIPassthroughAllowTimeoutHeaders : true ,
} } ,
httpUpstream : upstream ,
}
account := & Account {
ID : 222 ,
Name : "acc" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Concurrency : 1 ,
Credentials : map [ string ] any { "access_token" : "oauth-token" , "chatgpt_account_id" : "chatgpt-acc" } ,
Extra : map [ string ] any { "openai_passthrough" : true } ,
Status : StatusActive ,
Schedulable : true ,
RateMultiplier : f64p ( 1 ) ,
}
_ , err := svc . Forward ( context . Background ( ) , c , account , originalBody )
require . NoError ( t , err )
require . NotNil ( t , upstream . lastReq )
require . Equal ( t , "120000" , upstream . lastReq . Header . Get ( "x-stainless-timeout" ) )
2026-02-12 20:12:15 +08:00
require . Empty ( t , upstream . lastReq . Header . Get ( "X-Test" ) )
2026-02-12 14:16:18 +08:00
}