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
76aae5aa
Unverified
Commit
76aae5aa
authored
Apr 24, 2026
by
Wesley Liddick
Committed by
GitHub
Apr 24, 2026
Browse files
Merge pull request #1911 from gaoren002/fix/codex-responses-payload-normalization-mainbase
fix(openai): normalize codex responses payloads
parents
1ce9dc03
27ee141c
Changes
2
Show whitespace changes
Inline
Side-by-side
backend/internal/service/openai_codex_transform.go
View file @
76aae5aa
package
service
import
(
"encoding/json"
"fmt"
"strings"
)
...
...
@@ -153,6 +154,9 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
if
normalizeCodexTools
(
reqBody
)
{
result
.
Modified
=
true
}
if
normalizeCodexToolChoice
(
reqBody
)
{
result
.
Modified
=
true
}
if
v
,
ok
:=
reqBody
[
"prompt_cache_key"
]
.
(
string
);
ok
{
result
.
PromptCacheKey
=
strings
.
TrimSpace
(
v
)
...
...
@@ -173,6 +177,14 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
// 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。
if
input
,
ok
:=
reqBody
[
"input"
]
.
([]
any
);
ok
{
if
normalizedInput
,
modified
:=
normalizeCodexToolRoleMessages
(
input
);
modified
{
input
=
normalizedInput
result
.
Modified
=
true
}
if
normalizedInput
,
modified
:=
normalizeCodexMessageContentText
(
input
);
modified
{
input
=
normalizedInput
result
.
Modified
=
true
}
input
=
filterCodexInput
(
input
,
needsToolContinuation
)
reqBody
[
"input"
]
=
input
result
.
Modified
=
true
...
...
@@ -197,6 +209,183 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
return
result
}
func
normalizeCodexToolChoice
(
reqBody
map
[
string
]
any
)
bool
{
choice
,
ok
:=
reqBody
[
"tool_choice"
]
if
!
ok
||
choice
==
nil
{
return
false
}
choiceMap
,
ok
:=
choice
.
(
map
[
string
]
any
)
if
!
ok
{
return
false
}
choiceType
:=
strings
.
TrimSpace
(
firstNonEmptyString
(
choiceMap
[
"type"
]))
if
choiceType
==
""
||
codexToolsContainType
(
reqBody
[
"tools"
],
choiceType
)
{
return
false
}
reqBody
[
"tool_choice"
]
=
"auto"
return
true
}
func
codexToolsContainType
(
rawTools
any
,
toolType
string
)
bool
{
tools
,
ok
:=
rawTools
.
([]
any
)
if
!
ok
||
strings
.
TrimSpace
(
toolType
)
==
""
{
return
false
}
for
_
,
rawTool
:=
range
tools
{
tool
,
ok
:=
rawTool
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
if
strings
.
TrimSpace
(
firstNonEmptyString
(
tool
[
"type"
]))
==
toolType
{
return
true
}
}
return
false
}
func
normalizeCodexToolRoleMessages
(
input
[]
any
)
([]
any
,
bool
)
{
if
len
(
input
)
==
0
{
return
input
,
false
}
modified
:=
false
normalized
:=
make
([]
any
,
0
,
len
(
input
))
for
_
,
item
:=
range
input
{
m
,
ok
:=
item
.
(
map
[
string
]
any
)
if
!
ok
{
normalized
=
append
(
normalized
,
item
)
continue
}
role
,
_
:=
m
[
"role"
]
.
(
string
)
if
strings
.
TrimSpace
(
role
)
!=
"tool"
{
normalized
=
append
(
normalized
,
item
)
continue
}
callID
:=
firstNonEmptyString
(
m
[
"call_id"
],
m
[
"tool_call_id"
],
m
[
"id"
])
callID
=
strings
.
TrimSpace
(
callID
)
if
callID
==
""
{
// Responses does not accept role:"tool". If no call id is available,
// preserve the text as a user message instead of sending invalid input.
fallback
:=
make
(
map
[
string
]
any
,
len
(
m
))
for
key
,
value
:=
range
m
{
fallback
[
key
]
=
value
}
fallback
[
"role"
]
=
"user"
delete
(
fallback
,
"tool_call_id"
)
normalized
=
append
(
normalized
,
fallback
)
modified
=
true
continue
}
output
:=
extractTextFromContent
(
m
[
"content"
])
if
output
==
""
{
if
value
,
ok
:=
m
[
"output"
]
.
(
string
);
ok
{
output
=
value
}
}
if
output
==
""
&&
m
[
"content"
]
!=
nil
{
if
b
,
err
:=
json
.
Marshal
(
m
[
"content"
]);
err
==
nil
{
output
=
string
(
b
)
}
}
normalized
=
append
(
normalized
,
map
[
string
]
any
{
"type"
:
"function_call_output"
,
"call_id"
:
callID
,
"output"
:
output
,
})
modified
=
true
}
if
!
modified
{
return
input
,
false
}
return
normalized
,
true
}
func
normalizeCodexMessageContentText
(
input
[]
any
)
([]
any
,
bool
)
{
if
len
(
input
)
==
0
{
return
input
,
false
}
modified
:=
false
normalized
:=
make
([]
any
,
0
,
len
(
input
))
for
_
,
item
:=
range
input
{
m
,
ok
:=
item
.
(
map
[
string
]
any
)
if
!
ok
||
strings
.
TrimSpace
(
firstNonEmptyString
(
m
[
"type"
]))
!=
"message"
{
normalized
=
append
(
normalized
,
item
)
continue
}
parts
,
ok
:=
m
[
"content"
]
.
([]
any
)
if
!
ok
{
normalized
=
append
(
normalized
,
item
)
continue
}
var
newItem
map
[
string
]
any
var
newParts
[]
any
ensureItemCopy
:=
func
()
{
if
newItem
!=
nil
{
return
}
newItem
=
make
(
map
[
string
]
any
,
len
(
m
))
for
key
,
value
:=
range
m
{
newItem
[
key
]
=
value
}
newParts
=
make
([]
any
,
len
(
parts
))
copy
(
newParts
,
parts
)
}
for
i
,
rawPart
:=
range
parts
{
part
,
ok
:=
rawPart
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
text
,
hasText
:=
part
[
"text"
]
if
!
hasText
{
continue
}
if
_
,
ok
:=
text
.
(
string
);
ok
{
continue
}
ensureItemCopy
()
newPart
:=
make
(
map
[
string
]
any
,
len
(
part
))
for
key
,
value
:=
range
part
{
newPart
[
key
]
=
value
}
newPart
[
"text"
]
=
stringifyCodexContentText
(
text
)
newParts
[
i
]
=
newPart
modified
=
true
}
if
newItem
!=
nil
{
newItem
[
"content"
]
=
newParts
normalized
=
append
(
normalized
,
newItem
)
continue
}
normalized
=
append
(
normalized
,
item
)
}
if
!
modified
{
return
input
,
false
}
return
normalized
,
true
}
func
stringifyCodexContentText
(
value
any
)
string
{
switch
v
:=
value
.
(
type
)
{
case
string
:
return
v
case
nil
:
return
""
default
:
if
b
,
err
:=
json
.
Marshal
(
v
);
err
==
nil
{
return
string
(
b
)
}
return
fmt
.
Sprint
(
v
)
}
}
func
normalizeCodexModel
(
model
string
)
string
{
model
=
strings
.
TrimSpace
(
model
)
if
model
==
""
{
...
...
@@ -729,6 +918,22 @@ func filterCodexInput(input []any, preserveReferences bool) []any {
delete
(
newItem
,
"call_id"
)
}
if
codexInputItemRequiresName
(
typ
)
{
if
strings
.
TrimSpace
(
firstNonEmptyString
(
m
[
"name"
]))
==
""
{
name
:=
firstNonEmptyString
(
m
[
"tool_name"
])
if
name
==
""
{
if
function
,
ok
:=
m
[
"function"
]
.
(
map
[
string
]
any
);
ok
{
name
=
firstNonEmptyString
(
function
[
"name"
])
}
}
if
name
==
""
{
name
=
"tool"
}
ensureCopy
()
newItem
[
"name"
]
=
name
}
}
if
!
preserveReferences
{
ensureCopy
()
delete
(
newItem
,
"id"
)
...
...
@@ -746,6 +951,7 @@ func isCodexToolCallItemType(typ string) bool {
"local_shell_call"
,
"tool_search_call"
,
"custom_tool_call"
,
"mcp_tool_call"
,
"function_call_output"
,
"mcp_tool_call_output"
,
"custom_tool_call_output"
,
...
...
@@ -756,6 +962,15 @@ func isCodexToolCallItemType(typ string) bool {
}
}
func
codexInputItemRequiresName
(
typ
string
)
bool
{
switch
strings
.
TrimSpace
(
typ
)
{
case
"function_call"
,
"custom_tool_call"
,
"mcp_tool_call"
:
return
true
default
:
return
false
}
}
func
normalizeCodexTools
(
reqBody
map
[
string
]
any
)
bool
{
rawTools
,
ok
:=
reqBody
[
"tools"
]
if
!
ok
||
rawTools
==
nil
{
...
...
backend/internal/service/openai_codex_transform_test.go
View file @
76aae5aa
...
...
@@ -164,6 +164,163 @@ func TestApplyCodexOAuthTransform_ImageAndWebSearchCallsDoNotGainCallID(t *testi
require
.
False
(
t
,
hasCallID
)
}
func
TestApplyCodexOAuthTransform_ConvertsToolRoleMessageToFunctionCallOutput
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"input"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"message"
,
"role"
:
"tool"
,
"tool_call_id"
:
"call_1"
,
"content"
:
"ok"
,
},
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
input
,
ok
:=
reqBody
[
"input"
]
.
([]
any
)
require
.
True
(
t
,
ok
)
require
.
Len
(
t
,
input
,
1
)
item
,
ok
:=
input
[
0
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"function_call_output"
,
item
[
"type"
])
require
.
Equal
(
t
,
"fc1"
,
item
[
"call_id"
])
require
.
Equal
(
t
,
"ok"
,
item
[
"output"
])
_
,
hasRole
:=
item
[
"role"
]
require
.
False
(
t
,
hasRole
)
}
func
TestApplyCodexOAuthTransform_StringifiesNonStringMessageContentText
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"input"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"message"
,
"role"
:
"user"
,
"content"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"input_text"
,
"text"
:
[]
any
{
"a"
,
"b"
}},
},
},
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
input
,
ok
:=
reqBody
[
"input"
]
.
([]
any
)
require
.
True
(
t
,
ok
)
item
,
ok
:=
input
[
0
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
content
,
ok
:=
item
[
"content"
]
.
([]
any
)
require
.
True
(
t
,
ok
)
part
,
ok
:=
content
[
0
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
`["a","b"]`
,
part
[
"text"
])
}
func
TestApplyCodexOAuthTransform_DowngradesUnknownToolChoice
(
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"
:
"custom"
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
require
.
Equal
(
t
,
"auto"
,
reqBody
[
"tool_choice"
])
}
func
TestApplyCodexOAuthTransform_PreservesKnownToolChoice
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"tools"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"custom"
,
"name"
:
"shell"
},
},
"tool_choice"
:
map
[
string
]
any
{
"type"
:
"custom"
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
choice
,
ok
:=
reqBody
[
"tool_choice"
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"custom"
,
choice
[
"type"
])
}
func
TestApplyCodexOAuthTransform_AddsFallbackNameForFunctionCallInput
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"input"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"message"
,
"role"
:
"user"
,
"content"
:
"run tool"
},
map
[
string
]
any
{
"type"
:
"function_call"
,
"call_id"
:
"call_1"
,
"arguments"
:
"{}"
},
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
input
,
ok
:=
reqBody
[
"input"
]
.
([]
any
)
require
.
True
(
t
,
ok
)
require
.
Len
(
t
,
input
,
2
)
item
,
ok
:=
input
[
1
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"function_call"
,
item
[
"type"
])
require
.
Equal
(
t
,
"tool"
,
item
[
"name"
])
require
.
Equal
(
t
,
"fc1"
,
item
[
"call_id"
])
}
func
TestApplyCodexOAuthTransform_PreservesFunctionCallInputName
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"input"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"custom_tool_call"
,
"call_id"
:
"call_1"
,
"name"
:
"shell"
,
"input"
:
"pwd"
},
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
input
,
ok
:=
reqBody
[
"input"
]
.
([]
any
)
require
.
True
(
t
,
ok
)
require
.
Len
(
t
,
input
,
1
)
item
,
ok
:=
input
[
0
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"shell"
,
item
[
"name"
])
require
.
Equal
(
t
,
"fc1"
,
item
[
"call_id"
])
}
func
TestApplyCodexOAuthTransform_PreservesMCPToolCallIDAndName
(
t
*
testing
.
T
)
{
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.4"
,
"input"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"mcp_tool_call"
,
"call_id"
:
"call_abc"
,
"name"
:
"remote_tool"
,
"arguments"
:
"{}"
,
},
},
}
applyCodexOAuthTransform
(
reqBody
,
true
,
false
)
input
,
ok
:=
reqBody
[
"input"
]
.
([]
any
)
require
.
True
(
t
,
ok
)
require
.
Len
(
t
,
input
,
1
)
item
,
ok
:=
input
[
0
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"mcp_tool_call"
,
item
[
"type"
])
require
.
Equal
(
t
,
"remote_tool"
,
item
[
"name"
])
require
.
Equal
(
t
,
"fcabc"
,
item
[
"call_id"
])
}
func
TestCodexInputItemRequiresNameTypesAllowCallID
(
t
*
testing
.
T
)
{
for
_
,
typ
:=
range
[]
string
{
"function_call"
,
"custom_tool_call"
,
"mcp_tool_call"
}
{
require
.
True
(
t
,
codexInputItemRequiresName
(
typ
),
typ
)
require
.
True
(
t
,
isCodexToolCallItemType
(
typ
),
typ
)
}
}
func
TestApplyCodexOAuthTransform_ExplicitStoreFalsePreserved
(
t
*
testing
.
T
)
{
// 续链场景:显式 store=false 不再强制为 true,保持 false。
...
...
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