package service import ( "context" "encoding/json" "fmt" "net/http" "strings" "testing" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) func TestSanitizeOpenAIResponsesOrphanToolOutputs(t *testing.T) { t.Run("named standalone inputs do not legitimize orphan results", func(t *testing.T) { named := map[string]any{"type": "function_call_output", "name": "send_message_to_thread", "namespace": "codex_app", "output": "delegation"} input := []any{ named, map[string]any{"type": "function_call_output", "output": "missing name and call id"}, map[string]any{"type": "function_call_output", "name": " ", "output": "blank name"}, map[string]any{"type": "function_call_output", "name": "send_message_to_thread", "call_id": "missing", "output": "orphan result"}, map[string]any{"type": "custom_tool_call_output", "name": "apply_patch", "output": "missing call id"}, } reqBody := map[string]any{"input": input} require.True(t, sanitizeOpenAIResponsesOrphanToolOutputs(reqBody, input, false)) require.Equal(t, []any{named}, reqBody["input"]) }) t.Run("preserves matches regardless of item order", func(t *testing.T) { input := []any{ map[string]any{"type": "tool_search_output", "call_id": "search_1", "output": "first"}, map[string]any{"type": "tool_search_call", "id": "search_1", "query": "docs"}, map[string]any{"type": "custom_tool_call_output", "call_id": "custom_1", "output": "second"}, map[string]any{"type": "item_reference", "id": "custom_1"}, } reqBody := map[string]any{"input": input} require.False(t, sanitizeOpenAIResponsesOrphanToolOutputs(reqBody, input, false)) require.Equal(t, input, reqBody["input"]) }) t.Run("outputs do not legitimize each other", func(t *testing.T) { input := []any{ map[string]any{"type": "function_call_output", "call_id": "missing", "output": "one"}, map[string]any{"type": "tool_search_output", "call_id": "missing", "output": "two"}, map[string]any{"type": "custom_tool_call_output", "call_id": "missing", "output": "three"}, map[string]any{"type": "mcp_tool_call_output", "call_id": "missing", "output": "four"}, } reqBody := map[string]any{"input": input} require.True(t, sanitizeOpenAIResponsesOrphanToolOutputs(reqBody, input, false)) got, ok := reqBody["input"].([]any) require.True(t, ok) require.Empty(t, got) }) t.Run("preserves all output variants with matching calls", func(t *testing.T) { pairs := []struct { callType string outputType string }{ {callType: "function_call", outputType: "function_call_output"}, {callType: "tool_search_call", outputType: "tool_search_output"}, {callType: "custom_tool_call", outputType: "custom_tool_call_output"}, {callType: "mcp_tool_call", outputType: "mcp_tool_call_output"}, } input := make([]any, 0, len(pairs)*2) for index, pair := range pairs { callID := string(rune('a' + index)) input = append(input, map[string]any{"type": pair.callType, "call_id": callID}, map[string]any{"type": pair.outputType, "call_id": callID, "output": "ok"}, ) } reqBody := map[string]any{"input": input} require.False(t, sanitizeOpenAIResponsesOrphanToolOutputs(reqBody, input, false)) }) t.Run("previous response may contain the missing call", func(t *testing.T) { input := []any{map[string]any{"type": "function_call_output", "call_id": "remote", "output": "ok"}} reqBody := map[string]any{"input": input, "previous_response_id": "resp_1"} require.False(t, sanitizeOpenAIResponsesOrphanToolOutputs(reqBody, input, true)) require.Equal(t, input, reqBody["input"]) }) } func TestWebSocketCompatibilityPreservesNamedDelegation(t *testing.T) { for _, toolName := range []string{"create_thread", "send_message_to_thread"} { for _, withHistory := range []bool{false, true} { for _, previousResponseID := range []string{"", "resp_previous"} { name := fmt.Sprintf("%s/history=%t/previous=%t", toolName, withHistory, previousResponseID != "") t.Run(name, func(t *testing.T) { input := []any{} if withHistory { input = append(input, map[string]any{"type": "function_call", "call_id": "fc_history", "name": "lookup", "arguments": "{}"}, map[string]any{"type": "function_call_output", "call_id": "fc_history", "output": "historical result"}, ) } input = append(input, map[string]any{"type": "message", "role": "user", "content": []any{map[string]any{"type": "input_text", "text": "Updated environment context."}}}, map[string]any{"type": "function_call_output", "name": toolName, "namespace": "codex_app", "output": "\n source-task\n Reply with DELEGATION_OK.\n"}, ) reqBody := map[string]any{"type": "response.create", "model": "gpt-5.5", "input": input} if previousResponseID != "" { reqBody["previous_response_id"] = previousResponseID } body, err := json.Marshal(reqBody) require.NoError(t, err) account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth} normalized, _, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, account, false) require.NoError(t, err) var got map[string]any require.NoError(t, json.Unmarshal(normalized, &got)) require.Equal(t, input, got["input"], "preserve the delegation envelope, native type and history in order") require.Equal(t, reqBody["previous_response_id"], got["previous_response_id"]) again, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(normalized, account, false) require.NoError(t, err) require.False(t, changed, "normalization must be idempotent") require.JSONEq(t, string(normalized), string(again)) }) } } } } func TestOpenAIResponsesInputTextIsNeverSilentlyTruncated(t *testing.T) { atLimit := strings.Repeat("z", openAIResponsesInputTextMaxChars) oversized := strings.Repeat("a", openAIResponsesInputTextMaxChars) + "中" input := []any{ map[string]any{"type": "function_call_output", "call_id": "limit", "output": atLimit}, map[string]any{"type": "function_call_output", "call_id": "a", "output": oversized}, map[string]any{"type": "tool_search_output", "call_id": "b", "output": oversized}, map[string]any{"type": "custom_tool_call_output", "call_id": "c", "output": oversized}, map[string]any{"type": "mcp_tool_call_output", "call_id": "d", "output": oversized}, map[string]any{ "role": "user", "content": []any{ map[string]any{"type": "input_text", "text": "short"}, map[string]any{"type": "input_text", "text": oversized}, }, }, } reqBody := map[string]any{"input": input} require.False(t, truncateOpenAIResponsesInputText(reqBody)) first, ok := input[0].(map[string]any) require.True(t, ok) require.Equal(t, atLimit, first["output"]) for _, rawItem := range input[1:5] { item, ok := rawItem.(map[string]any) require.True(t, ok) require.Equal(t, oversized, item["output"]) } last, ok := input[5].(map[string]any) require.True(t, ok) content, ok := last["content"].([]any) require.True(t, ok) require.Len(t, content, 2) shortPart, ok := content[0].(map[string]any) require.True(t, ok) oversizedPart, ok := content[1].(map[string]any) require.True(t, ok) require.Equal(t, "short", shortPart["text"]) require.Equal(t, oversized, oversizedPart["text"]) } func TestOpenAIResponsesInputNeverRequestsPreemptiveTruncation(t *testing.T) { short := []byte(`{"input":[{"type":"function_call_output","output":"ok"}]}`) largeUnrelated := []byte(`{"input":"` + strings.Repeat("x", openAIResponsesInputTextMaxChars+1) + `"}`) largeOutput := []byte(`{"input":[{"type":"function_call_output","output":"` + strings.Repeat("x", openAIResponsesInputTextMaxChars+1) + `"}]}`) require.False(t, openAIResponsesInputMayNeedTruncation(short)) require.False(t, openAIResponsesInputMayNeedTruncation(largeUnrelated)) require.False(t, openAIResponsesInputMayNeedTruncation(largeOutput)) } func TestOpenAIGatewayService_OAuthDropsOrphanAfterDroppingPreviousResponse(t *testing.T) { body := []byte(`{"model":"gpt-5.5","stream":false,"previous_response_id":"resp_missing","input":[{"type":"function_call_output","call_id":"call_missing","output":"keep this result"}]}`) upstream := &httpUpstreamRecorder{responses: []*http.Response{ newOpenAIRejectedFieldTestResponse(http.StatusOK, `{"id":"resp_ok","output":[],"usage":{"input_tokens":1,"output_tokens":1,"input_tokens_details":{"cached_tokens":0}}}`), }} result, err := newOpenAIRejectedFieldTestService(upstream).Forward( context.Background(), newOpenAIRejectedFieldTestContext(body), newOpenAIOAuthNamespaceTestAccount(), body, ) require.NoError(t, err) require.NotNil(t, result) require.Len(t, upstream.bodies, 1) require.False(t, gjson.GetBytes(upstream.bodies[0], "previous_response_id").Exists()) require.Empty(t, gjson.GetBytes(upstream.bodies[0], "input").Array()) } func TestOpenAIGatewayService_PreservesOversizedToolOutputForUpstream(t *testing.T) { oversized := strings.Repeat("x", openAIResponsesInputTextMaxChars) + "中" body := []byte(`{"model":"gpt-5.5","stream":false,"input":[{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{}"},{"type":"function_call_output","call_id":"call_1","output":"` + oversized + `"}]}`) upstream := &httpUpstreamRecorder{responses: []*http.Response{ newOpenAIRejectedFieldTestResponse(http.StatusOK, `{"id":"resp_ok","output":[],"usage":{"input_tokens":1,"output_tokens":1,"input_tokens_details":{"cached_tokens":0}}}`), }} result, err := newOpenAIRejectedFieldTestService(upstream).Forward( context.Background(), newOpenAIRejectedFieldTestContext(body), newOpenAIRejectedFieldTestAccount(), body, ) require.NoError(t, err) require.NotNil(t, result) require.Len(t, upstream.bodies, 1) require.Equal(t, oversized, gjson.GetBytes(upstream.bodies[0], "input.1.output").String()) }