Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
ff6fa020
Unverified
Commit
ff6fa020
authored
Apr 29, 2026
by
Wesley Liddick
Committed by
GitHub
Apr 29, 2026
Browse files
Merge pull request #2058 from ivanvolt-labs/fix-responses-function-tool-choice
fix: use Responses-compatible function tool_choice format
parents
4d676ddd
04b2866f
Changes
7
Hide whitespace changes
Inline
Side-by-side
backend/internal/pkg/apicompat/anthropic_responses_test.go
View file @
ff6fa020
...
...
@@ -991,9 +991,40 @@ func TestAnthropicToResponses_ToolChoiceSpecific(t *testing.T) {
var
tc
map
[
string
]
any
require
.
NoError
(
t
,
json
.
Unmarshal
(
resp
.
ToolChoice
,
&
tc
))
assert
.
Equal
(
t
,
"function"
,
tc
[
"type"
])
fn
,
ok
:=
tc
[
"function"
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
assert
.
Equal
(
t
,
"get_weather"
,
fn
[
"name"
])
assert
.
Equal
(
t
,
"get_weather"
,
tc
[
"name"
])
assert
.
NotContains
(
t
,
tc
,
"function"
)
}
func
TestResponsesToAnthropicRequest_ToolChoiceFunctionName
(
t
*
testing
.
T
)
{
req
:=
&
ResponsesRequest
{
Model
:
"gpt-5.2"
,
Input
:
json
.
RawMessage
(
`[{"role":"user","content":"Hello"}]`
),
ToolChoice
:
json
.
RawMessage
(
`{"type":"function","name":"get_weather"}`
),
}
resp
,
err
:=
ResponsesToAnthropicRequest
(
req
)
require
.
NoError
(
t
,
err
)
var
tc
map
[
string
]
string
require
.
NoError
(
t
,
json
.
Unmarshal
(
resp
.
ToolChoice
,
&
tc
))
assert
.
Equal
(
t
,
"tool"
,
tc
[
"type"
])
assert
.
Equal
(
t
,
"get_weather"
,
tc
[
"name"
])
}
func
TestResponsesToAnthropicRequest_ToolChoiceLegacyFunctionName
(
t
*
testing
.
T
)
{
req
:=
&
ResponsesRequest
{
Model
:
"gpt-5.2"
,
Input
:
json
.
RawMessage
(
`[{"role":"user","content":"Hello"}]`
),
ToolChoice
:
json
.
RawMessage
(
`{"type":"function","function":{"name":"get_weather"}}`
),
}
resp
,
err
:=
ResponsesToAnthropicRequest
(
req
)
require
.
NoError
(
t
,
err
)
var
tc
map
[
string
]
string
require
.
NoError
(
t
,
json
.
Unmarshal
(
resp
.
ToolChoice
,
&
tc
))
assert
.
Equal
(
t
,
"tool"
,
tc
[
"type"
])
assert
.
Equal
(
t
,
"get_weather"
,
tc
[
"name"
])
}
// ---------------------------------------------------------------------------
...
...
backend/internal/pkg/apicompat/anthropic_to_responses.go
View file @
ff6fa020
...
...
@@ -75,7 +75,7 @@ func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
// {"type":"auto"} → "auto"
// {"type":"any"} → "required"
// {"type":"none"} → "none"
// {"type":"tool","name":"X"} → {"type":"function","
function":{"
name":"X"}
}
// {"type":"tool","name":"X"} → {"type":"function","name":"X"}
func
convertAnthropicToolChoiceToResponses
(
raw
json
.
RawMessage
)
(
json
.
RawMessage
,
error
)
{
var
tc
struct
{
Type
string
`json:"type"`
...
...
@@ -94,8 +94,8 @@ func convertAnthropicToolChoiceToResponses(raw json.RawMessage) (json.RawMessage
return
json
.
Marshal
(
"none"
)
case
"tool"
:
return
json
.
Marshal
(
map
[
string
]
any
{
"type"
:
"function"
,
"function"
:
map
[
string
]
string
{
"name"
:
tc
.
Name
}
,
"type"
:
"function"
,
"name"
:
tc
.
Name
,
})
default
:
// Pass through unknown types as-is
...
...
backend/internal/pkg/apicompat/chatcompletions_responses_test.go
View file @
ff6fa020
...
...
@@ -281,6 +281,8 @@ func TestChatCompletionsToResponses_LegacyFunctions(t *testing.T) {
var
tc
map
[
string
]
any
require
.
NoError
(
t
,
json
.
Unmarshal
(
resp
.
ToolChoice
,
&
tc
))
assert
.
Equal
(
t
,
"function"
,
tc
[
"type"
])
assert
.
Equal
(
t
,
"get_weather"
,
tc
[
"name"
])
assert
.
NotContains
(
t
,
tc
,
"function"
)
}
func
TestChatCompletionsToResponses_ServiceTier
(
t
*
testing
.
T
)
{
...
...
backend/internal/pkg/apicompat/chatcompletions_to_responses.go
View file @
ff6fa020
...
...
@@ -420,7 +420,7 @@ func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []R
//
// "auto" → "auto"
// "none" → "none"
// {"name":"X"} → {"type":"function","
function":{"
name":"X"}
}
// {"name":"X"} → {"type":"function","name":"X"}
func
convertChatFunctionCallToToolChoice
(
raw
json
.
RawMessage
)
(
json
.
RawMessage
,
error
)
{
// Try string first ("auto", "none", etc.) — pass through as-is.
var
s
string
...
...
@@ -436,7 +436,7 @@ func convertChatFunctionCallToToolChoice(raw json.RawMessage) (json.RawMessage,
return
nil
,
err
}
return
json
.
Marshal
(
map
[
string
]
any
{
"type"
:
"function"
,
"function"
:
map
[
string
]
string
{
"name"
:
obj
.
Name
}
,
"type"
:
"function"
,
"name"
:
obj
.
Name
,
})
}
backend/internal/pkg/apicompat/responses_to_anthropic_request.go
View file @
ff6fa020
...
...
@@ -428,7 +428,8 @@ func normalizeAnthropicInputSchema(schema json.RawMessage) json.RawMessage {
// "auto" → {"type":"auto"}
// "required" → {"type":"any"}
// "none" → {"type":"none"}
// {"type":"function","function":{"name":"X"}} → {"type":"tool","name":"X"}
// {"type":"function","name":"X"} → {"type":"tool","name":"X"}
// {"type":"function","function":{"name":"X"}} → {"type":"tool","name":"X"} // legacy
func
convertResponsesToAnthropicToolChoice
(
raw
json
.
RawMessage
)
(
json
.
RawMessage
,
error
)
{
// Try as string first
var
s
string
...
...
@@ -448,14 +449,22 @@ func convertResponsesToAnthropicToolChoice(raw json.RawMessage) (json.RawMessage
// Try as object with type=function
var
tc
struct
{
Type
string
`json:"type"`
Name
string
`json:"name"`
Function
struct
{
Name
string
`json:"name"`
}
`json:"function"`
}
if
err
:=
json
.
Unmarshal
(
raw
,
&
tc
);
err
==
nil
&&
tc
.
Type
==
"function"
&&
tc
.
Function
.
Name
!=
""
{
if
err
:=
json
.
Unmarshal
(
raw
,
&
tc
);
err
==
nil
&&
tc
.
Type
==
"function"
{
name
:=
strings
.
TrimSpace
(
tc
.
Name
)
if
name
==
""
{
name
=
strings
.
TrimSpace
(
tc
.
Function
.
Name
)
}
if
name
==
""
{
return
raw
,
nil
}
return
json
.
Marshal
(
map
[
string
]
string
{
"type"
:
"tool"
,
"name"
:
tc
.
Function
.
N
ame
,
"name"
:
n
ame
,
})
}
...
...
backend/internal/service/openai_codex_transform.go
View file @
ff6fa020
...
...
@@ -141,9 +141,7 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
if
name
,
ok
:=
fcObj
[
"name"
]
.
(
string
);
ok
&&
strings
.
TrimSpace
(
name
)
!=
""
{
reqBody
[
"tool_choice"
]
=
map
[
string
]
any
{
"type"
:
"function"
,
"function"
:
map
[
string
]
any
{
"name"
:
name
,
},
"name"
:
name
,
}
}
}
...
...
@@ -219,9 +217,38 @@ func normalizeCodexToolChoice(reqBody map[string]any) bool {
return
false
}
choiceType
:=
strings
.
TrimSpace
(
firstNonEmptyString
(
choiceMap
[
"type"
]))
if
choiceType
==
""
||
codexToolsContainType
(
reqBody
[
"tools"
],
choiceType
)
{
if
choiceType
==
""
{
return
false
}
modified
:=
false
if
choiceType
==
"function"
{
name
:=
strings
.
TrimSpace
(
firstNonEmptyString
(
choiceMap
[
"name"
]))
if
name
==
""
{
if
function
,
ok
:=
choiceMap
[
"function"
]
.
(
map
[
string
]
any
);
ok
{
name
=
strings
.
TrimSpace
(
firstNonEmptyString
(
function
[
"name"
]))
}
}
if
name
==
""
{
reqBody
[
"tool_choice"
]
=
"auto"
return
true
}
if
strings
.
TrimSpace
(
firstNonEmptyString
(
choiceMap
[
"name"
]))
!=
name
{
choiceMap
[
"name"
]
=
name
modified
=
true
}
if
_
,
ok
:=
choiceMap
[
"function"
];
ok
{
delete
(
choiceMap
,
"function"
)
modified
=
true
}
if
!
codexToolsContainFunctionName
(
reqBody
[
"tools"
],
name
)
{
reqBody
[
"tool_choice"
]
=
"auto"
return
true
}
return
modified
}
if
codexToolsContainType
(
reqBody
[
"tools"
],
choiceType
)
{
return
modified
}
reqBody
[
"tool_choice"
]
=
"auto"
return
true
}
...
...
@@ -243,6 +270,33 @@ func codexToolsContainType(rawTools any, toolType string) bool {
return
false
}
func
codexToolsContainFunctionName
(
rawTools
any
,
name
string
)
bool
{
tools
,
ok
:=
rawTools
.
([]
any
)
if
!
ok
||
strings
.
TrimSpace
(
name
)
==
""
{
return
false
}
normalizedName
:=
strings
.
TrimSpace
(
name
)
for
_
,
rawTool
:=
range
tools
{
tool
,
ok
:=
rawTool
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
if
strings
.
TrimSpace
(
firstNonEmptyString
(
tool
[
"type"
]))
!=
"function"
{
continue
}
toolName
:=
strings
.
TrimSpace
(
firstNonEmptyString
(
tool
[
"name"
]))
if
toolName
==
""
{
if
function
,
ok
:=
tool
[
"function"
]
.
(
map
[
string
]
any
);
ok
{
toolName
=
strings
.
TrimSpace
(
firstNonEmptyString
(
function
[
"name"
]))
}
}
if
toolName
==
normalizedName
{
return
true
}
}
return
false
}
func
normalizeCodexToolRoleMessages
(
input
[]
any
)
([]
any
,
bool
)
{
if
len
(
input
)
==
0
{
return
input
,
false
...
...
backend/internal/service/openai_codex_transform_test.go
View file @
ff6fa020
...
...
@@ -249,6 +249,44 @@ func TestApplyCodexOAuthTransform_PreservesKnownToolChoice(t *testing.T) {
require
.
Equal
(
t
,
"custom"
,
choice
[
"type"
])
}
func
TestApplyCodexOAuthTransform_NormalizesLegacyFunctionToolChoice
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"tools"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"function"
,
"name"
:
"shell"
},
},
"tool_choice"
:
map
[
string
]
any
{
"type"
:
"function"
,
"function"
:
map
[
string
]
any
{
"name"
:
"shell"
},
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
choice
,
ok
:=
reqBody
[
"tool_choice"
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"function"
,
choice
[
"type"
])
require
.
Equal
(
t
,
"shell"
,
choice
[
"name"
])
require
.
NotContains
(
t
,
choice
,
"function"
)
}
func
TestApplyCodexOAuthTransform_DowngradesMissingFunctionToolChoice
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"tools"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"function"
,
"name"
:
"shell"
},
},
"tool_choice"
:
map
[
string
]
any
{
"type"
:
"function"
,
"function"
:
map
[
string
]
any
{
"name"
:
"missing"
},
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
require
.
Equal
(
t
,
"auto"
,
reqBody
[
"tool_choice"
])
}
func
TestApplyCodexOAuthTransform_AddsFallbackNameForFunctionCallInput
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment