| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- //go:build unit
- 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 TestNormalizeOpenAIResponsesLiteTools_MovesNamespacesAndKeepsSupportedTools(t *testing.T) {
- reqBody := map[string]any{
- "model": "gpt-5.6-terra",
- "tools": []any{
- map[string]any{"type": "function", "name": "shell"},
- map[string]any{"type": "custom", "name": "exec"},
- map[string]any{"type": "tool_search"},
- map[string]any{"type": "namespace", "name": "collaboration", "tools": []any{
- map[string]any{"type": "function", "name": "spawn_agent"},
- }},
- },
- "input": []any{
- map[string]any{"type": "message", "role": "user", "content": "hello"},
- map[string]any{"type": "additional_tools", "role": "developer", "tools": []any{
- map[string]any{"type": "namespace", "name": "image_gen"},
- map[string]any{"type": "namespace", "name": "collaboration", "tools": []any{
- map[string]any{"type": "function", "name": "spawn_agent"},
- }},
- }},
- },
- "tool_choice": map[string]any{"type": "namespace", "name": "collaboration"},
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.True(t, changed)
- tools := reqBody["tools"].([]any)
- require.Len(t, tools, 3)
- require.Equal(t, "function", tools[0].(map[string]any)["type"])
- require.Equal(t, "custom", tools[1].(map[string]any)["type"])
- require.Equal(t, "tool_search", tools[2].(map[string]any)["type"])
- input := reqBody["input"].([]any)
- require.Len(t, input, 2)
- additional := input[1].(map[string]any)["tools"].([]any)
- require.Len(t, additional, 2)
- require.Equal(t, "image_gen", additional[0].(map[string]any)["name"])
- require.Equal(t, "collaboration", additional[1].(map[string]any)["name"], "existing namespace must not be duplicated")
- require.Equal(t, map[string]any{"type": "namespace", "name": "collaboration"}, reqBody["tool_choice"])
- }
- func TestNormalizeOpenAIResponsesLiteTools_PreservesDeferredFlagsWithToolSearch(t *testing.T) {
- reqBody := map[string]any{
- "tools": []any{
- map[string]any{"type": "tool_search"},
- map[string]any{"type": "function", "name": "shell", "defer_loading": true},
- },
- }
- _, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- tools := reqBody["tools"].([]any)
- require.Equal(t, "tool_search", tools[0].(map[string]any)["type"])
- require.Equal(t, true, tools[1].(map[string]any)["defer_loading"])
- }
- func TestNormalizeOpenAIResponsesLiteTools_RejectsConflictingAdditionalTool(t *testing.T) {
- reqBody := map[string]any{
- "tools": []any{map[string]any{
- "type": "namespace",
- "name": "collaboration",
- "tools": []any{map[string]any{"type": "function", "name": "spawn_agent"}},
- }},
- "input": []any{map[string]any{
- "type": "additional_tools",
- "tools": []any{map[string]any{
- "type": "namespace",
- "name": "collaboration",
- "tools": []any{map[string]any{"type": "function", "name": "send_message"}},
- }},
- }},
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.ErrorContains(t, err, `conflicts with migrated tool type "namespace" name "collaboration"`)
- require.False(t, changed)
- require.Len(t, reqBody["tools"], 1, "conflicts must not partially remove top-level tools")
- }
- func TestNormalizeOpenAIResponsesLiteTools_DeduplicatesAcrossAdditionalToolItems(t *testing.T) {
- namespace := map[string]any{
- "type": "namespace",
- "name": "collaboration",
- "tools": []any{map[string]any{"type": "function", "name": "spawn_agent"}},
- }
- reqBody := map[string]any{
- "tools": []any{namespace},
- "input": []any{
- map[string]any{
- "type": "additional_tools",
- "tools": []any{map[string]any{"type": "custom", "name": "exec"}},
- },
- map[string]any{
- "type": "additional_tools",
- "tools": []any{namespace},
- },
- },
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.True(t, changed)
- require.NotContains(t, reqBody, "tools")
- input := reqBody["input"].([]any)
- require.Len(t, input[0].(map[string]any)["tools"], 1)
- require.Len(t, input[1].(map[string]any)["tools"], 1)
- }
- func TestNormalizeOpenAIResponsesLiteTools_ConvertsStringInput(t *testing.T) {
- reqBody := map[string]any{
- "input": "hello",
- "tools": []any{map[string]any{
- "type": "namespace",
- "name": "collaboration",
- }},
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.True(t, changed)
- require.NotContains(t, reqBody, "tools")
- input := reqBody["input"].([]any)
- require.Len(t, input, 2)
- require.Equal(t, "message", input[0].(map[string]any)["type"])
- require.Equal(t, "hello", input[0].(map[string]any)["content"])
- require.Equal(t, "additional_tools", input[1].(map[string]any)["type"])
- }
- func TestNormalizeOpenAIResponsesLiteTools_KeepsSupportedTopLevelTools(t *testing.T) {
- reqBody := map[string]any{
- "reasoning": map[string]any{"context": "all_turns"},
- "tools": []any{
- map[string]any{"type": "function", "name": "shell"},
- map[string]any{"type": "custom", "name": "exec"},
- map[string]any{"type": "tool_search"},
- "custom shorthand",
- },
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.True(t, changed)
- require.Len(t, reqBody["tools"], 4)
- require.Equal(t, false, reqBody["parallel_tool_calls"])
- }
- func TestNormalizeOpenAIResponsesLiteTools_ForcesParallelToolCallsFalse(t *testing.T) {
- tests := []struct {
- name string
- body map[string]any
- }{
- {
- name: "top-level tools",
- body: map[string]any{
- "tools": []any{map[string]any{"type": "function", "name": "shell"}},
- "parallel_tool_calls": true,
- },
- },
- {
- name: "input additional tools",
- body: map[string]any{
- "input": []any{map[string]any{
- "type": "additional_tools",
- "tools": []any{map[string]any{"type": "namespace", "name": "collaboration"}},
- }},
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- changed, err := normalizeOpenAIResponsesLiteTools(tt.body)
- require.NoError(t, err)
- require.True(t, changed)
- require.Equal(t, false, tt.body["parallel_tool_calls"])
- })
- }
- }
- func TestNormalizeOpenAIResponsesLiteTools_PinsParallelToolCallsWithoutTools(t *testing.T) {
- tests := []struct {
- name string
- parallel any
- include bool
- wantChanged bool
- }{
- {name: "字段缺失", wantChanged: true},
- {name: "值为 true", parallel: true, include: true, wantChanged: true},
- {name: "值为 false", parallel: false, include: true, wantChanged: false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- reqBody := map[string]any{"reasoning": map[string]any{"context": "all_turns"}}
- if tt.include {
- reqBody["parallel_tool_calls"] = tt.parallel
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.Equal(t, tt.wantChanged, changed)
- require.Contains(t, reqBody, "parallel_tool_calls")
- require.Equal(t, false, reqBody["parallel_tool_calls"])
- })
- }
- }
- func TestNormalizeOpenAIResponsesLiteTools_RejectsNonBooleanParallelToolCalls(t *testing.T) {
- for _, value := range []any{"false", float64(0), nil, map[string]any{}} {
- reqBody := map[string]any{
- "tools": []any{map[string]any{"type": "function", "name": "shell"}},
- "parallel_tool_calls": value,
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.ErrorContains(t, err, "parallel_tool_calls to be a boolean")
- require.False(t, changed)
- require.Equal(t, value, reqBody["parallel_tool_calls"])
- }
- reqBody := map[string]any{"parallel_tool_calls": []any{}}
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.ErrorContains(t, err, "parallel_tool_calls to be a boolean")
- require.False(t, changed)
- }
- func TestNormalizeOpenAIResponsesLiteTools_ParallelToolCallsIsIdempotent(t *testing.T) {
- reqBody := map[string]any{
- "reasoning": map[string]any{"context": "all_turns"},
- "tools": []any{map[string]any{"type": "function", "name": "shell"}},
- "parallel_tool_calls": true,
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.True(t, changed)
- require.Equal(t, false, reqBody["parallel_tool_calls"])
- changed, err = normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.False(t, changed)
- require.Equal(t, false, reqBody["parallel_tool_calls"])
- }
- func TestNormalizeOpenAIResponsesLiteTools_EnsuresReasoningContext(t *testing.T) {
- tests := []struct {
- name string
- reasoning any
- }{
- {name: "missing"},
- {name: "missing context", reasoning: map[string]any{"effort": "high"}},
- {name: "wrong context", reasoning: map[string]any{"effort": "medium", "context": "current_turn"}},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- reqBody := map[string]any{"input": "hello"}
- if tt.reasoning != nil {
- reqBody["reasoning"] = tt.reasoning
- }
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.NoError(t, err)
- require.True(t, changed)
- reasoning := reqBody["reasoning"].(map[string]any)
- require.Equal(t, "all_turns", reasoning["context"])
- if tt.name != "missing" {
- require.Equal(t, tt.reasoning.(map[string]any)["effort"], reasoning["effort"])
- }
- })
- }
- }
- func TestNormalizeOpenAIResponsesLiteTools_RejectsNonObjectReasoning(t *testing.T) {
- reqBody := map[string]any{"reasoning": "high"}
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.ErrorContains(t, err, "reasoning to be an object")
- require.False(t, changed)
- require.Equal(t, "high", reqBody["reasoning"])
- }
- func TestNormalizeOpenAIResponsesLiteTools_RejectsUnsupportedTools(t *testing.T) {
- tests := []struct {
- name string
- tool map[string]any
- want string
- }{
- {name: "hosted web search", tool: map[string]any{"type": "web_search"}, want: `top-level tool type "web_search"`},
- {name: "hosted image generation", tool: map[string]any{"type": "image_generation"}, want: `top-level tool type "image_generation"`},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- reqBody := map[string]any{"tools": []any{tt.tool}}
- changed, err := normalizeOpenAIResponsesLiteTools(reqBody)
- require.ErrorContains(t, err, tt.want)
- require.False(t, changed)
- require.Len(t, reqBody["tools"], 1, "validation errors must not partially mutate tools")
- })
- }
- }
- func TestNormalizeOpenAIResponsesLiteToolsPayload_PreservesResponseCreateShape(t *testing.T) {
- body := []byte(`{
- "type":"response.create",
- "model":"gpt-5.6-terra",
- "client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"true"},
- "input":[{"type":"message","role":"user","content":"hello"}],
- "tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent"}]}],
- "tool_choice":{"type":"namespace","name":"collaboration"}
- }`)
- updated, changed, err := normalizeOpenAIResponsesLiteToolsPayload(body)
- require.NoError(t, err)
- require.True(t, changed)
- require.Equal(t, "response.create", gjson.GetBytes(updated, "type").String())
- require.False(t, gjson.GetBytes(updated, "tools").Exists())
- require.Equal(t, "collaboration", gjson.GetBytes(updated, `input.#(type=="additional_tools").tools.0.name`).String())
- require.Equal(t, "namespace", gjson.GetBytes(updated, "tool_choice.type").String())
- require.True(t, gjson.GetBytes(updated, "parallel_tool_calls").Exists())
- require.False(t, gjson.GetBytes(updated, "parallel_tool_calls").Bool())
- }
- func TestNormalizeOpenAIResponsesLitePayloads_PreserveLargeSequence(t *testing.T) {
- body := []byte(`{
- "type":"response.create",
- "sequence":900719925474099312345,
- "tools":[{"type":"function","name":"lookup"}],
- "parallel_tool_calls":true
- }`)
- tests := []struct {
- name string
- normalize func([]byte) ([]byte, bool, error)
- }{
- {name: "OAuth-like tools normalization", normalize: normalizeOpenAIResponsesLiteToolsPayload},
- {name: "API key parallel normalization", normalize: normalizeOpenAIResponsesLiteParallelToolCallsPayload},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- updated, changed, err := tt.normalize(body)
- require.NoError(t, err)
- require.True(t, changed)
- require.Equal(t, "900719925474099312345", gjson.GetBytes(updated, "sequence").Raw)
- require.True(t, gjson.GetBytes(updated, "parallel_tool_calls").Exists())
- require.False(t, gjson.GetBytes(updated, "parallel_tool_calls").Bool())
- })
- }
- }
- func TestApplyCodexOAuthTransform_PreservesLiteNamespaceToolChoice(t *testing.T) {
- reqBody := map[string]any{
- "model": "gpt-5.6-terra",
- "input": []any{map[string]any{
- "type": "additional_tools",
- "tools": []any{map[string]any{
- "type": "namespace",
- "name": "collaboration",
- }},
- }},
- "tool_choice": map[string]any{"type": "namespace", "name": "collaboration"},
- }
- applyCodexOAuthTransform(reqBody, true, false)
- require.Equal(t, map[string]any{"type": "namespace", "name": "collaboration"}, reqBody["tool_choice"])
- }
- func TestOpenAIGatewayServiceForward_NormalizesResponsesLiteToolsForOAuth(t *testing.T) {
- gin.SetMode(gin.TestMode)
- for _, passthrough := range []bool{false, true} {
- name := "managed"
- if passthrough {
- name = "passthrough"
- }
- t.Run(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", "codex_cli_rs/0.144.1")
- c.Request.Header.Set(responsesLiteHeader, "true")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: io.NopCloser(strings.NewReader(
- "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_lite\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}}\n\n" +
- "data: [DONE]\n\n",
- )),
- }}
- svc := &OpenAIGatewayService{cfg: &config.Config{}, httpUpstream: upstream}
- account := &Account{
- ID: 501, Name: "responses-lite", Platform: PlatformOpenAI, Type: AccountTypeOAuth,
- Concurrency: 1, Status: StatusActive, Schedulable: true, RateMultiplier: f64p(1),
- Credentials: map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-account"},
- Extra: map[string]any{"openai_passthrough": passthrough},
- }
- body := []byte(`{
- "model":"gpt-5.6-terra","stream":true,"instructions":"test",
- "reasoning":{"effort":"high","context":"current_turn"},
- "parallel_tool_calls":true,
- "tools":[
- {"type":"function","name":"shell","parameters":{"type":"object"}},
- {"type":"custom","name":"exec"},
- {"type":"tool_search"},
- {"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent","parameters":{"type":"object"}}]}
- ],
- "input":[{"type":"message","role":"user","content":"hello"}],
- "tool_choice":{"type":"namespace","name":"collaboration"}
- }`)
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "true", upstream.lastReq.Header.Get(responsesLiteHeader))
- require.Equal(t, "high", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String())
- require.Equal(t, "all_turns", gjson.GetBytes(upstream.lastBody, "reasoning.context").String())
- require.False(t, gjson.GetBytes(upstream.lastBody, `tools.#(type=="namespace")`).Exists())
- require.Equal(t, "shell", gjson.GetBytes(upstream.lastBody, `tools.#(type=="function").name`).String())
- require.Equal(t, "exec", gjson.GetBytes(upstream.lastBody, `tools.#(type=="custom").name`).String())
- require.True(t, gjson.GetBytes(upstream.lastBody, `tools.#(type=="tool_search")`).Exists())
- require.Equal(t, "collaboration", gjson.GetBytes(upstream.lastBody, `input.#(type=="additional_tools").tools.0.name`).String())
- require.Equal(t, "namespace", gjson.GetBytes(upstream.lastBody, "tool_choice.type").String())
- require.Equal(t, "collaboration", gjson.GetBytes(upstream.lastBody, "tool_choice.name").String())
- require.True(t, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Exists())
- require.False(t, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Bool())
- badRec := httptest.NewRecorder()
- badCtx, _ := gin.CreateTestContext(badRec)
- badCtx.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
- badCtx.Request.Header.Set(responsesLiteHeader, "true")
- badUpstream := &httpUpstreamRecorder{}
- svc.httpUpstream = badUpstream
- result, err = svc.Forward(context.Background(), badCtx, account, []byte(`{"model":"gpt-5.6-terra","tools":[{"type":"function","name":"shell"}],"parallel_tool_calls":"false"}`))
- require.ErrorContains(t, err, "parallel_tool_calls to be a boolean")
- require.Nil(t, result)
- require.Equal(t, http.StatusBadRequest, badRec.Code)
- require.Equal(t, "invalid_request_error", gjson.Get(badRec.Body.String(), "error.type").String())
- require.Equal(t, "parallel_tool_calls", gjson.Get(badRec.Body.String(), "error.param").String())
- require.Contains(t, gjson.Get(badRec.Body.String(), "error.message").String(), "parallel_tool_calls to be a boolean")
- require.Nil(t, badUpstream.lastReq)
- for _, malformed := range []struct {
- body string
- wantParam string
- }{
- {body: `{"model":"gpt-5.6-terra","tools":{}}`, wantParam: "tools"},
- {body: `{"model":"gpt-5.6-terra","reasoning":[]}`, wantParam: "reasoning"},
- } {
- rec := httptest.NewRecorder()
- requestCtx, _ := gin.CreateTestContext(rec)
- requestCtx.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
- requestCtx.Request.Header.Set(responsesLiteHeader, "true")
- result, err = svc.Forward(context.Background(), requestCtx, account, []byte(malformed.body))
- require.Error(t, err)
- require.Nil(t, result)
- require.Equal(t, http.StatusBadRequest, rec.Code)
- require.Equal(t, malformed.wantParam, gjson.Get(rec.Body.String(), "error.param").String())
- }
- })
- }
- }
- func TestOpenAIGatewayServiceForward_PinsParallelToolCallsForToollessResponsesLite(t *testing.T) {
- gin.SetMode(gin.TestMode)
- accountCases := []struct {
- name string
- accountType string
- credentials map[string]any
- }{
- {name: "oauth", accountType: AccountTypeOAuth, credentials: map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-account"}},
- {name: "apikey", accountType: AccountTypeAPIKey, credentials: map[string]any{"api_key": "sk-test"}},
- }
- parallelCases := []struct {
- name string
- field string
- }{
- {name: "字段缺失"},
- {name: "值为 true", field: `,"parallel_tool_calls":true`},
- {name: "值为 false", field: `,"parallel_tool_calls":false`},
- }
- for _, accountCase := range accountCases {
- for _, passthrough := range []bool{false, true} {
- mode := "managed"
- if passthrough {
- mode = "passthrough"
- }
- for _, parallelCase := range parallelCases {
- name := accountCase.name + "/" + mode + "/" + parallelCase.name
- t.Run(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", "codex_cli_rs/0.144.1")
- c.Request.Header.Set(responsesLiteHeader, "true")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: io.NopCloser(strings.NewReader(
- "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_lite\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}}\n\n" +
- "data: [DONE]\n\n",
- )),
- }}
- svc := &OpenAIGatewayService{cfg: &config.Config{}, httpUpstream: upstream}
- account := &Account{
- ID: 502, Name: "responses-lite-no-tools", Platform: PlatformOpenAI, Type: accountCase.accountType,
- Concurrency: 1, Status: StatusActive, Schedulable: true, RateMultiplier: f64p(1),
- Credentials: accountCase.credentials,
- Extra: map[string]any{"openai_passthrough": passthrough},
- }
- body := []byte(`{
- "model":"gpt-5.6-terra","stream":true,"instructions":"test",
- "reasoning":{"effort":"high","context":"current_turn"},
- "input":[{"type":"message","role":"user","content":"hello"}]` + parallelCase.field + `
- }`)
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "true", upstream.lastReq.Header.Get(responsesLiteHeader))
- require.Equal(t, gjson.False, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Type, string(upstream.lastBody))
- })
- }
- }
- }
- }
- func TestOpenAIGatewayServiceForward_DisablesParallelToolCallsForResponsesLiteAPIKey(t *testing.T) {
- gin.SetMode(gin.TestMode)
- for _, passthrough := range []bool{false, true} {
- name := "managed"
- if passthrough {
- name = "passthrough"
- }
- t.Run(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", "codex_cli_rs/0.144.1")
- c.Request.Header.Set(responsesLiteHeader, "true")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: io.NopCloser(strings.NewReader(
- "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_lite\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}}\n\n" +
- "data: [DONE]\n\n",
- )),
- }}
- svc := &OpenAIGatewayService{cfg: &config.Config{}, httpUpstream: upstream}
- account := &Account{
- ID: 503, Name: "responses-lite-api-key", Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
- Concurrency: 1, Status: StatusActive, Schedulable: true, RateMultiplier: f64p(1),
- Credentials: map[string]any{"api_key": "sk-test"},
- Extra: map[string]any{"openai_passthrough": passthrough},
- }
- body := []byte(`{
- "model":"gpt-5.6-terra","stream":true,"instructions":"test",
- "tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}],
- "parallel_tool_calls":true,
- "input":[{"type":"message","role":"user","content":"hello"}]
- }`)
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "true", upstream.lastReq.Header.Get(responsesLiteHeader))
- require.True(t, gjson.GetBytes(upstream.lastBody, "tools").IsArray())
- require.True(t, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Exists())
- require.False(t, gjson.GetBytes(upstream.lastBody, "parallel_tool_calls").Bool())
- })
- }
- }
|