fix(openai): normalize Responses Lite tool declarations

This commit is contained in:
superman2003
2026-07-15 11:37:38 +08:00
parent 3f605c3543
commit bc6d9c47d2
7 changed files with 539 additions and 4 deletions
@@ -312,13 +312,30 @@ func normalizeCodexToolChoice(reqBody map[string]any) bool {
}
return modified
}
if codexToolsContainType(reqBody["tools"], choiceType) {
if codexToolsContainType(reqBody["tools"], choiceType) || codexInputAdditionalToolsContainType(reqBody["input"], choiceType) {
return modified
}
reqBody["tool_choice"] = "auto"
return true
}
func codexInputAdditionalToolsContainType(rawInput any, toolType string) bool {
input, ok := rawInput.([]any)
if !ok || strings.TrimSpace(toolType) == "" {
return false
}
for _, rawItem := range input {
item, ok := rawItem.(map[string]any)
if !ok || strings.TrimSpace(firstNonEmptyString(item["type"])) != "additional_tools" {
continue
}
if codexToolsContainType(item["tools"], toolType) {
return true
}
}
return false
}
func codexToolsContainType(rawTools any, toolType string) bool {
tools, ok := rawTools.([]any)
if !ok || strings.TrimSpace(toolType) == "" {
@@ -44,6 +44,19 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
if normalized {
body = normalizedBody
}
if account.IsOpenAIOAuth() && isOpenAIResponsesLiteHeader(c.GetHeader(responsesLiteHeader)) {
liteBody, changed, liteErr := normalizeOpenAIResponsesLiteToolsPayload(body)
if liteErr != nil {
setOpsUpstreamError(c, http.StatusBadRequest, liteErr.Error(), "")
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
"type": "invalid_request_error", "message": liteErr.Error(), "param": "tools",
}})
return nil, liteErr
}
if changed {
body = liteBody
}
}
wsDecision := s.getOpenAIWSProtocolResolver().Resolve(account)
// 仅允许 WS 入站请求走 WS 上游,避免出现 HTTP -> WS 协议混用。
wsDecision = resolveOpenAIWSDecisionByClientTransport(wsDecision, GetOpenAIClientTransport(c))
@@ -0,0 +1,191 @@
package service
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// normalizeOpenAIResponsesLiteTools converts private namespace declarations
// into the input.additional_tools carrier required by Responses Lite. Other
// top-level tools must belong to the small set accepted by the Lite endpoint;
// rejecting unsupported hosted tools is intentional because silently dropping
// them would change the client's requested behavior.
func normalizeOpenAIResponsesLiteTools(reqBody map[string]any) (bool, error) {
if reqBody == nil {
return false, nil
}
rawTools, exists := reqBody["tools"]
if !exists || rawTools == nil {
return false, nil
}
tools, ok := rawTools.([]any)
if !ok {
return false, fmt.Errorf("responses Lite requires tools to be an array")
}
topLevelTools := make([]any, 0, len(tools))
namespaceTools := make([]any, 0, len(tools))
for index, rawTool := range tools {
if customTool, ok := rawTool.(string); ok {
if strings.TrimSpace(customTool) == "" {
return false, fmt.Errorf("responses Lite custom tool at index %d must not be empty", index)
}
topLevelTools = append(topLevelTools, rawTool)
continue
}
tool, ok := rawTool.(map[string]any)
if !ok {
return false, fmt.Errorf("responses Lite tool at index %d must be an object", index)
}
toolType := strings.TrimSpace(firstNonEmptyString(tool["type"]))
switch toolType {
case "function", "custom", "tool_search":
topLevelTools = append(topLevelTools, rawTool)
case "namespace":
namespaceTools = append(namespaceTools, rawTool)
case "":
return false, fmt.Errorf("responses Lite tool at index %d is missing type", index)
default:
return false, fmt.Errorf("responses Lite does not support top-level tool type %q at index %d", toolType, index)
}
}
if len(namespaceTools) == 0 {
return false, nil
}
input, err := appendOpenAIResponsesLiteAdditionalTools(reqBody["input"], namespaceTools)
if err != nil {
return false, err
}
reqBody["input"] = input
if len(topLevelTools) == 0 {
delete(reqBody, "tools")
} else {
reqBody["tools"] = topLevelTools
}
return true, nil
}
func appendOpenAIResponsesLiteAdditionalTools(input any, namespaceTools []any) ([]any, error) {
var items []any
switch typed := input.(type) {
case nil:
items = make([]any, 0, 1)
case string:
items = []any{map[string]any{
"type": "message",
"role": "user",
"content": typed,
}}
case []any:
items = typed
default:
return nil, fmt.Errorf("responses Lite namespace tools require input to be a string or array")
}
var target map[string]any
var targetTools []any
var allAdditionalTools []any
for _, rawItem := range items {
item, ok := rawItem.(map[string]any)
if !ok || strings.TrimSpace(firstNonEmptyString(item["type"])) != "additional_tools" {
continue
}
rawAdditionalTools, exists := item["tools"]
additionalTools := []any(nil)
toolsOK := true
if exists && rawAdditionalTools != nil {
additionalTools, toolsOK = rawAdditionalTools.([]any)
}
if !toolsOK {
return nil, fmt.Errorf("responses Lite input.additional_tools tools must be an array")
}
if target == nil {
target = item
targetTools = additionalTools
}
allAdditionalTools = append(allAdditionalTools, additionalTools...)
}
merged, err := mergeOpenAIResponsesLiteAdditionalTools(allAdditionalTools, namespaceTools)
if err != nil {
return nil, err
}
newTools := merged[len(allAdditionalTools):]
if target != nil {
if len(newTools) > 0 {
target["tools"] = append(append([]any(nil), targetTools...), newTools...)
}
return items, nil
}
items = append(items, map[string]any{
"type": "additional_tools",
"role": "developer",
"tools": newTools,
})
return items, nil
}
func mergeOpenAIResponsesLiteAdditionalTools(existing []any, moved []any) ([]any, error) {
merged := append([]any(nil), existing...)
seen := make(map[string]any, len(existing)+len(moved))
for _, rawTool := range existing {
if identity := openAIResponsesLiteToolIdentity(rawTool); identity != "" {
if previous, exists := seen[identity]; exists && !reflect.DeepEqual(previous, rawTool) {
return nil, fmt.Errorf("responses Lite additional_tools contains conflicting definitions for %s", openAIResponsesLiteToolIdentityForError(rawTool))
}
seen[identity] = rawTool
}
}
for _, rawTool := range moved {
identity := openAIResponsesLiteToolIdentity(rawTool)
if identity != "" {
if previous, exists := seen[identity]; exists {
if reflect.DeepEqual(previous, rawTool) {
continue
}
return nil, fmt.Errorf("responses Lite additional_tools conflicts with migrated %s", openAIResponsesLiteToolIdentityForError(rawTool))
}
seen[identity] = rawTool
}
merged = append(merged, rawTool)
}
return merged, nil
}
func openAIResponsesLiteToolIdentity(rawTool any) string {
tool, ok := rawTool.(map[string]any)
if !ok {
return ""
}
toolType := strings.TrimSpace(firstNonEmptyString(tool["type"]))
name := strings.TrimSpace(firstNonEmptyString(tool["name"]))
if toolType == "" || name == "" {
return ""
}
return toolType + "\x00" + name
}
func openAIResponsesLiteToolIdentityForError(rawTool any) string {
tool, _ := rawTool.(map[string]any)
return fmt.Sprintf("tool type %q name %q", strings.TrimSpace(firstNonEmptyString(tool["type"])), strings.TrimSpace(firstNonEmptyString(tool["name"])))
}
func normalizeOpenAIResponsesLiteToolsPayload(body []byte) ([]byte, bool, error) {
var requestBody map[string]any
if err := json.Unmarshal(body, &requestBody); err != nil {
return body, false, fmt.Errorf("decode responses Lite request body: %w", err)
}
changed, err := normalizeOpenAIResponsesLiteTools(requestBody)
if err != nil || !changed {
return body, false, err
}
rebuilt, err := marshalOpenAIUpstreamJSON(requestBody)
if err != nil {
return body, false, fmt.Errorf("encode responses Lite request body: %w", err)
}
return rebuilt, true, nil
}
@@ -0,0 +1,267 @@
//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_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{
"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.False(t, changed)
require.Len(t, reqBody["tools"], 4)
}
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())
}
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",
"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.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())
})
}
}
@@ -232,6 +232,17 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
}
normalized = next
}
if account.IsOpenAIOAuth() && isOpenAIResponsesLiteWebSocketPayload(normalized) {
litePayload, _, liteErr := normalizeOpenAIResponsesLiteToolsPayload(normalized)
if liteErr != nil {
return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
coderws.StatusPolicyViolation,
liteErr.Error(),
liteErr,
)
}
normalized = litePayload
}
apiKey := getAPIKeyFromContext(c)
imageGenerationAllowed := GroupAllowsImageGeneration(apiKeyGroup(apiKey))
codexImageGenerationExplicitToolPolicy := codexImageGenerationExplicitToolPolicyAllow
@@ -531,10 +531,12 @@ func TestOpenAIGatewayService_ProxyResponsesWebSocketFromClient_CodexImageBridge
"stream":false,
"previous_response_id":"resp_codex_image_bridge",
"client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"true"},
"tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent"}]}],
"input":[
{"type":"additional_tools","role":"developer","tools":[{"type":"custom","name":"exec","description":"Execute code-mode tools, including image_gen.imagegen."}]},
{"type":"message","role":"user","content":[{"type":"input_text","text":"draw a cat"}]}
]
],
"tool_choice":{"type":"namespace","name":"collaboration"}
}`))
cancelWrite()
require.NoError(t, err)
@@ -564,10 +566,13 @@ func TestOpenAIGatewayService_ProxyResponsesWebSocketFromClient_CodexImageBridge
litePayload := requestToJSONString(captureConn.writes[1])
require.False(t, gjson.Get(litePayload, `tools.#(type=="image_generation")`).Exists())
require.False(t, gjson.Get(litePayload, "tool_choice").Exists())
require.NotContains(t, gjson.Get(litePayload, "instructions").String(), "image_generation")
require.Equal(t, "exec", gjson.Get(litePayload, `input.#(type=="additional_tools").tools.0.name`).String())
require.Contains(t, gjson.Get(litePayload, `input.#(type=="additional_tools").tools.0.description`).String(), "image_gen.imagegen")
require.False(t, gjson.Get(litePayload, `tools.#(type=="namespace")`).Exists())
require.Equal(t, "collaboration", gjson.Get(litePayload, `input.#(type=="additional_tools").tools.1.name`).String())
require.Equal(t, "namespace", gjson.Get(litePayload, "tool_choice.type").String())
require.Equal(t, "collaboration", gjson.Get(litePayload, "tool_choice.name").String())
}
func TestOpenAIGatewayService_ProxyResponsesWebSocketFromClient_DedicatedModeDoesNotReuseConnAcrossSessions(t *testing.T) {
@@ -940,7 +945,16 @@ func TestOpenAIGatewayService_ProxyResponsesWebSocketFromClient_PassthroughHeade
}()
writeCtx, cancelWrite := context.WithTimeout(context.Background(), 3*time.Second)
err = clientConn.Write(writeCtx, coderws.MessageText, []byte(`{"type":"response.create","model":"gpt-5.1","stream":false,"prompt_cache_key":"pcache_passthrough"}`))
err = clientConn.Write(writeCtx, coderws.MessageText, []byte(`{
"type":"response.create",
"model":"gpt-5.1",
"stream":false,
"prompt_cache_key":"pcache_passthrough",
"client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"true"},
"tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent"}]}],
"input":[{"type":"message","role":"user","content":"hello"}],
"tool_choice":{"type":"namespace","name":"collaboration"}
}`))
cancelWrite()
require.NoError(t, err)
@@ -963,6 +977,12 @@ func TestOpenAIGatewayService_ProxyResponsesWebSocketFromClient_PassthroughHeade
require.Equal(t, isolateOpenAISessionID(0, "pcache_passthrough"), captureDialer.lastHeaders.Get("session_id"))
require.Equal(t, "turn-state-1", captureDialer.lastHeaders.Get(openAIWSTurnStateHeader))
require.Equal(t, "turn-meta-1", captureDialer.lastHeaders.Get(openAIWSTurnMetadataHeader))
require.Len(t, upstreamConn.writes, 1)
forwarded := requestToJSONString(upstreamConn.writes[0])
require.False(t, gjson.Get(forwarded, `tools.#(type=="namespace")`).Exists())
require.Equal(t, "collaboration", gjson.Get(forwarded, `input.#(type=="additional_tools").tools.0.name`).String())
require.Equal(t, "namespace", gjson.Get(forwarded, "tool_choice.type").String())
require.Equal(t, "collaboration", gjson.Get(forwarded, "tool_choice.name").String())
}
func TestOpenAIGatewayService_ProxyResponsesWebSocketFromClient_HTTPBridgeModeRelaysHTTPStream(t *testing.T) {
@@ -246,6 +246,13 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
if err := validateOpenAIWSBearerToken(account, token); err != nil {
return err
}
if account.IsOpenAIOAuth() && isOpenAIResponsesLiteWebSocketPayload(firstClientMessage) {
liteFirstMessage, _, liteErr := normalizeOpenAIResponsesLiteToolsPayload(firstClientMessage)
if liteErr != nil {
return NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, liteErr.Error(), liteErr)
}
firstClientMessage = liteFirstMessage
}
requestModel := strings.TrimSpace(gjson.GetBytes(firstClientMessage, "model").String())
requestPreviousResponseID := strings.TrimSpace(gjson.GetBytes(firstClientMessage, "previous_response_id").String())
logOpenAIWSV2Passthrough(
@@ -428,6 +435,15 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
if msgType != coderws.MessageText {
return payload, nil, nil
}
if strings.TrimSpace(gjson.GetBytes(payload, "type").String()) == "response.create" {
if account.IsOpenAIOAuth() && isOpenAIResponsesLiteWebSocketPayload(payload) {
litePayload, _, liteErr := normalizeOpenAIResponsesLiteToolsPayload(payload)
if liteErr != nil {
return payload, nil, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, liteErr.Error(), liteErr)
}
payload = litePayload
}
}
if strings.TrimSpace(gjson.GetBytes(payload, "type").String()) == "response.create" && hooks != nil && hooks.BeforeRequest != nil {
turnNo := int(completedTurns.Load()) + 1
if turnNo < 2 {