fix(codex): preserve allowed_tools selection constraints

Keep allowed_tools policies intact for upstream validation and rewrite their
function references alongside reserved tool-name aliases.
This commit is contained in:
zhuanzhi
2026-09-07 10:47:51 +08:00
parent ab99d56e96
commit 2e31d8b702
3 changed files with 78 additions and 0 deletions
@@ -115,6 +115,9 @@ func collectOpenAIResponsesToolNameFields(reqBody map[string]any) []codexToolNam
}
}
if choice, ok := reqBody["tool_choice"].(map[string]any); ok {
if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(choice["type"])), "allowed_tools") {
collectTools(choice["tools"])
}
if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(choice["type"])), "function") {
appendName(choice, "name")
if function, ok := choice["function"].(map[string]any); ok {
@@ -357,6 +357,12 @@ func normalizeCodexToolChoice(reqBody map[string]any) bool {
if choiceType == "" {
return false
}
if choiceType == "allowed_tools" {
// This is a selection policy, not a declared tool type. Preserve it for
// upstream validation (including references to input.additional_tools);
// falling back to auto would silently discard the caller's restriction.
return false
}
modified := false
if choiceType == "function" {
name := strings.TrimSpace(firstNonEmptyString(choiceMap["name"]))
@@ -1,6 +1,7 @@
package service
import (
"encoding/json"
"fmt"
"strings"
"testing"
@@ -477,6 +478,74 @@ func TestApplyCodexOAuthTransform_StringifiesNonStringMessageContentText(t *test
require.Equal(t, `["a","b"]`, part["text"])
}
func TestApplyCodexOAuthTransform_PreservesAllowedTools(t *testing.T) {
for _, placement := range []string{"top_level", "additional_tools"} {
for _, mode := range []string{"auto", "required"} {
t.Run(placement+"/"+mode, func(t *testing.T) {
decision := map[string]any{"type": "function", "name": "ProbeAccept"}
choice := map[string]any{
"type": "allowed_tools", "mode": mode,
"tools": []any{map[string]any{"type": "function", "name": "ProbeAccept"}},
}
tools := []any{
map[string]any{"type": "function", "name": "ProbeBase"},
map[string]any{"type": "web_search"},
map[string]any{"type": "image_generation"},
}
input := []any{map[string]any{"type": "message", "role": "user", "content": "probe"}}
if placement == "top_level" {
tools = append(tools, decision)
} else {
input = append(input, map[string]any{
"type": "additional_tools", "role": "developer", "tools": []any{decision},
})
}
reqBody := map[string]any{"tools": tools, "input": input, "tool_choice": choice}
before, err := json.Marshal(reqBody)
require.NoError(t, err)
reqBody["model"] = "gpt-6-astra"
result := applyCodexOAuthTransform(reqBody, true, false)
require.NoError(t, result.Error)
after, err := json.Marshal(map[string]any{
"tools": reqBody["tools"], "input": reqBody["input"], "tool_choice": reqBody["tool_choice"],
})
require.NoError(t, err)
require.JSONEq(t, string(before), string(after))
})
}
}
}
func TestNormalizeCodexToolChoice_InvalidAllowedToolsNeverBecomesAuto(t *testing.T) {
for _, choice := range []map[string]any{
{"type": "allowed_tools"},
{"type": "allowed_tools", "mode": "invalid", "tools": []any{}},
{"type": "allowed_tools", "mode": "required", "tools": "invalid"},
{"type": "allowed_tools", "mode": "required", "tools": []any{map[string]any{"type": "function", "name": "missing"}}},
} {
reqBody := map[string]any{"tool_choice": choice}
require.False(t, normalizeCodexToolChoice(reqBody))
// The upstream owns schema validation. A malformed restriction must never
// silently become permission to call every supplied tool.
require.Equal(t, choice, reqBody["tool_choice"])
}
}
func TestApplyCodexOAuthTransform_AllowedToolsKeepsReservedNameReferences(t *testing.T) {
declaration := map[string]any{"type": "function", "name": "python"}
reference := map[string]any{"type": "function", "name": "python"}
choice := map[string]any{"type": "allowed_tools", "mode": "required", "tools": []any{reference}}
reqBody := map[string]any{
"model": "gpt-6-astra", "tools": []any{declaration}, "tool_choice": choice,
}
result := applyCodexOAuthTransform(reqBody, true, false)
require.NoError(t, result.Error)
require.Equal(t, choice, reqBody["tool_choice"])
require.Equal(t, codexPythonToolAlias, declaration["name"])
require.Equal(t, codexPythonToolAlias, reference["name"])
require.Equal(t, "python", result.ToolNameReverse[codexPythonToolAlias])
}
func TestApplyCodexOAuthTransform_DowngradesUnknownToolChoice(t *testing.T) {
reqBody := map[string]any{
"model": "gpt-5.4",