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
04b2866f
Commit
04b2866f
authored
Apr 28, 2026
by
ivanvolt
Browse files
fix: use Responses-compatible function tool_choice format
parent
b0a2252e
Changes
7
Hide whitespace changes
Inline
Side-by-side
backend/internal/pkg/apicompat/anthropic_responses_test.go
View file @
04b2866f
...
...
@@ -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 @
04b2866f
...
...
@@ -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 @
04b2866f
...
...
@@ -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 @
04b2866f
...
...
@@ -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 @
04b2866f
...
...
@@ -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 @
04b2866f
...
...
@@ -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 @
04b2866f
...
...
@@ -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