Files
sub2api/backend/internal/service/openai_codex_tool_names_test.go
T

251 lines
12 KiB
Go

package service
import (
"encoding/json"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestAliasOpenAIOAuthReservedToolNames_RewritesDeclarationsAndReferences(t *testing.T) {
reqBody := map[string]any{
"tools": []any{
map[string]any{"type": "function", "name": "python"},
map[string]any{"type": "namespace", "name": "code", "tools": []any{
map[string]any{"type": "function", "name": "shell"},
}},
},
"tool_choice": map[string]any{"type": "function", "name": "python"},
"input": []any{
map[string]any{"type": "function_call", "name": "python", "call_id": "fc_1"},
map[string]any{"type": "additional_tools", "tools": []any{
map[string]any{"type": "function", "function": map[string]any{"name": "python"}},
}},
},
}
reverse, changed, err := aliasOpenAIOAuthReservedToolNames(reqBody)
require.NoError(t, err)
require.True(t, changed)
require.Equal(t, "python", reverse[codexPythonToolAlias])
tools, ok := reqBody["tools"].([]any)
require.True(t, ok)
require.NotEmpty(t, tools)
firstTool, ok := tools[0].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, firstTool["name"])
toolChoice, ok := reqBody["tool_choice"].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, toolChoice["name"])
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.NotEmpty(t, input)
firstInput, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, firstInput["name"])
secondInput, ok := input[1].(map[string]any)
require.True(t, ok)
nestedTools, ok := secondInput["tools"].([]any)
require.True(t, ok)
require.NotEmpty(t, nestedTools)
nestedTool, ok := nestedTools[0].(map[string]any)
require.True(t, ok)
nestedFn, ok := nestedTool["function"].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, nestedFn["name"])
}
func TestAliasOpenAIOAuthReservedToolNames_CollisionDoesNotMutate(t *testing.T) {
reqBody := map[string]any{"tools": []any{
map[string]any{"type": "function", "name": "python"},
map[string]any{"type": "function", "name": codexPythonToolAlias},
}}
before, err := json.Marshal(reqBody)
require.NoError(t, err)
reverse, changed, err := aliasOpenAIOAuthReservedToolNames(reqBody)
require.ErrorContains(t, err, `both normalize to "python__sub2api"`)
require.False(t, changed)
require.Nil(t, reverse)
after, marshalErr := json.Marshal(reqBody)
require.NoError(t, marshalErr)
require.JSONEq(t, string(before), string(after))
}
func TestApplyCodexOAuthTransform_ReservedPythonNameIsOAuthOnly(t *testing.T) {
reqBody := map[string]any{
"model": "gpt-5.5",
"tools": []any{map[string]any{"type": "function", "name": "PYTHON"}},
}
result := applyCodexOAuthTransform(reqBody, true, false)
require.NoError(t, result.Error)
require.Equal(t, "PYTHON", result.ToolNameReverse[codexPythonToolAlias])
tools, ok := reqBody["tools"].([]any)
require.True(t, ok)
require.NotEmpty(t, tools)
tool, ok := tools[0].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, tool["name"])
apiKeyBody := []byte(`{"type":"response.create","tools":[{"type":"function","name":"python"}]}`)
normalized, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(apiKeyBody, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey})
require.NoError(t, err)
require.False(t, changed)
require.JSONEq(t, string(apiKeyBody), string(normalized))
}
func TestRestoreCodexToolNamesFromContext_HTTPAndWSPayloadShapes(t *testing.T) {
c, _ := gin.CreateTestContext(nil)
setCodexToolNameReverse(c, map[string]string{codexPythonToolAlias: "python"})
streamEvent := restoreCodexToolNamesFromContext(c, []byte(
`{"type":"response.output_item.done","item":{"type":"function_call","name":"python__sub2api"},"note":"python__sub2api"}`,
))
require.Equal(t, "python", gjson.GetBytes(streamEvent, "item.name").String())
require.Equal(t, "python__sub2api", gjson.GetBytes(streamEvent, "note").String())
nonStreaming := restoreCodexToolNamesFromContext(c, []byte(
`{"id":"resp_1","output":[{"type":"function_call","name":"python__sub2api"}]}`,
))
require.Equal(t, "python", gjson.GetBytes(nonStreaming, "output.0.name").String())
setCodexToolNameReverse(c, nil)
require.JSONEq(t,
`{"type":"response.output_item.added","item":{"name":"python__sub2api"}}`,
string(restoreCodexToolNamesFromContext(c, []byte(`{"type":"response.output_item.added","item":{"name":"python__sub2api"}}`))),
)
}
func TestAliasOpenAIOAuthReservedToolNames_SessionUpdateOnlyTouchesFunctionProtocolNodes(t *testing.T) {
body := []byte(`{"type":"session.update","session":{"tools":[{"type":"function","name":"python"},{"type":"image_generation","name":"python"},{"type":"namespace","name":"python","tools":[{"type":"function","name":"shell"}]}]},"metadata":{"name":"python"},"sequence":900719925474099312345}`)
aliased, reverse, changed, err := aliasOpenAIOAuthReservedToolNamesBody(body)
require.NoError(t, err)
require.True(t, changed)
require.Equal(t, "python", reverse[codexPythonToolAlias])
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(aliased, "session.tools.0.name").String())
require.Equal(t, "python", gjson.GetBytes(aliased, "session.tools.1.name").String())
require.Equal(t, "python", gjson.GetBytes(aliased, "session.tools.2.name").String())
require.Equal(t, "shell", gjson.GetBytes(aliased, "session.tools.2.tools.0.name").String())
require.Equal(t, "python", gjson.GetBytes(aliased, "metadata.name").String())
require.Equal(t, "900719925474099312345", gjson.GetBytes(aliased, "sequence").Raw)
}
func TestRestoreCodexToolNamesInJSON_OnlyTouchesResponseToolCallNodesAndPreservesNumbers(t *testing.T) {
reverse := map[string]string{codexPythonToolAlias: "python"}
body := []byte(`{"type":"response.completed","response":{"output":[{"type":"function_call","name":"python__sub2api"},{"type":"message","name":"python__sub2api","content":[]}]},"metadata":{"name":"python__sub2api"},"sequence":900719925474099312345}`)
restored := restoreCodexToolNamesInJSON(body, reverse)
require.Equal(t, "python", gjson.GetBytes(restored, "response.output.0.name").String())
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(restored, "response.output.1.name").String())
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(restored, "metadata.name").String())
require.Equal(t, "900719925474099312345", gjson.GetBytes(restored, "sequence").Raw)
}
func TestRestoreCodexToolNamesInJSON_ExplicitHTTPAndSSEToolCallProtocols(t *testing.T) {
reverse := map[string]string{codexPythonToolAlias: "python"}
tests := []struct {
name string
body string
path string
}{
{
name: "chat http",
body: `{"choices":[{"message":{"tool_calls":[{"type":"function","function":{"name":"python__sub2api"}}]}}],"metadata":{"name":"python__sub2api"}}`,
path: "choices.0.message.tool_calls.0.function.name",
},
{
name: "chat sse",
body: `{"choices":[{"delta":{"tool_calls":[{"type":"function","function":{"name":"python__sub2api"}}]}}],"metadata":{"name":"python__sub2api"}}`,
path: "choices.0.delta.tool_calls.0.function.name",
},
{
name: "messages tool use",
body: `{"type":"content_block_start","content":[{"type":"tool_use","name":"python__sub2api"}],"metadata":{"name":"python__sub2api"}}`,
path: "content.0.name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
restored := restoreCodexToolNamesInJSON([]byte(tt.body), reverse)
require.Equal(t, "python", gjson.GetBytes(restored, tt.path).String())
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(restored, "metadata.name").String())
})
}
}
func TestAliasOpenAIOAuthReservedToolNames_PromptCompatibilityRunsFirst(t *testing.T) {
body := []byte(`{"model":"gpt-5.5","prompt":[{"type":"function_call","name":"python","call_id":"fc_1"}],"functions":[{"name":"python"}],"function_call":{"name":"python"},"sequence":900719925474099312345}`)
reqBody, err := getOpenAIRequestBodyMap(nil, body)
require.NoError(t, err)
result := applyCodexOAuthTransform(reqBody, true, false)
require.NoError(t, result.Error)
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.NotEmpty(t, input)
firstInput, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, firstInput["name"])
tools, ok := reqBody["tools"].([]any)
require.True(t, ok)
require.NotEmpty(t, tools)
tool, ok := tools[0].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, tool["name"])
toolChoice, ok := reqBody["tool_choice"].(map[string]any)
require.True(t, ok)
require.Equal(t, codexPythonToolAlias, toolChoice["name"])
encoded, err := json.Marshal(reqBody)
require.NoError(t, err)
require.Equal(t, "900719925474099312345", gjson.GetBytes(encoded, "sequence").Raw)
}
func TestCodexToolNameReverse_WSSessionReplacementDoesNotChangeActiveTurn(t *testing.T) {
c, _ := gin.CreateTestContext(nil)
setCodexToolNameReverse(c, nil)
first := []byte(`{"type":"response.create","tools":[{"type":"function","name":"python"}]}`)
updateCodexToolNameReverseForWSFrame(c, first, map[string]string{codexPythonToolAlias: "python"})
update := []byte(`{"type":"session.update","session":{"tools":[{"type":"function","name":"python__sub2api"}]}}`)
updateCodexToolNameReverseForWSFrame(c, update, nil)
currentOutput := restoreCodexToolNamesFromContext(c, []byte(`{"type":"response.output_item.done","item":{"type":"function_call","name":"python__sub2api"}}`))
require.Equal(t, "python", gjson.GetBytes(currentOutput, "item.name").String())
sessionEcho := restoreCodexToolNamesFromContext(c, []byte(`{"type":"session.updated","session":{"tools":[{"type":"function","name":"python__sub2api"}]}}`))
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(sessionEcho, "session.tools.0.name").String())
next := []byte(`{"type":"response.create","input":"next"}`)
updateCodexToolNameReverseForWSFrame(c, next, nil)
nextOutput := restoreCodexToolNamesFromContext(c, []byte(`{"type":"response.output_item.done","item":{"type":"function_call","name":"python__sub2api"}}`))
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(nextOutput, "item.name").String())
sessionPython := []byte(`{"type":"session.update","session":{"tools":[{"type":"function","name":"python"}]}}`)
updateCodexToolNameReverseForWSFrame(c, sessionPython, map[string]string{codexPythonToolAlias: "python"})
explicitLiteral := []byte(`{"type":"response.create","input":[{"type":"additional_tools","tools":[{"type":"function","name":"python__sub2api"}]}]}`)
updateCodexToolNameReverseForWSFrame(c, explicitLiteral, nil)
literalOutput := restoreCodexToolNamesFromContext(c, []byte(`{"type":"response.output_item.done","item":{"type":"function_call","name":"python__sub2api"}}`))
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(literalOutput, "item.name").String())
updateCodexToolNameReverseForWSFrame(c, next, nil)
inheritedOutput := restoreCodexToolNamesFromContext(c, []byte(`{"type":"response.output_item.done","item":{"type":"function_call","name":"python__sub2api"}}`))
require.Equal(t, "python", gjson.GetBytes(inheritedOutput, "item.name").String())
}
func TestDecodeOpenAIJSONUseNumberRejectsTrailingDocument(t *testing.T) {
var decoded map[string]any
require.Error(t, decodeOpenAIJSONUseNumber([]byte(`{"name":"python"}{"extra":true}`), &decoded))
}
func TestRestoreCodexToolNamesFromSSEContextUsesEventLineTypeWithoutAddingType(t *testing.T) {
c, _ := gin.CreateTestContext(nil)
setCodexToolNameReverse(c, map[string]string{codexPythonToolAlias: codexReservedPythonToolName})
payload := []byte(`{"item":{"type":"function_call","name":"python__sub2api"},"metadata":{"name":"python__sub2api"}}`)
restored := restoreCodexToolNamesFromSSEContext(c, payload, "response.output_item.done")
require.Equal(t, codexReservedPythonToolName, gjson.GetBytes(restored, "item.name").String())
require.Equal(t, codexPythonToolAlias, gjson.GetBytes(restored, "metadata.name").String())
require.False(t, gjson.GetBytes(restored, "type").Exists())
}