| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- 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": "<codex_delegation>\n <source_thread_id>source-task</source_thread_id>\n <input>Reply with DELEGATION_OK.</input>\n</codex_delegation>"},
- )
- 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())
- }
|