mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-03 08:40:36 +00:00
对齐 Grok ModelInput 对 function_call.status 的剥离,恢复畸形 tools 不改写 tool_choice 的行为,并检查测试中的类型断言。
164 lines
6.8 KiB
Go
164 lines
6.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestSanitizeOpenAIResponsesOrphanToolOutputs(t *testing.T) {
|
|
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 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())
|
|
}
|