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
2add09e6
"...src/components/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "3fa5b8bca5d32c80a28212ad228d2a0d2c6bf667"
Commit
2add09e6
authored
Apr 24, 2026
by
gaoren002
Committed by
陈曦
Apr 27, 2026
Browse files
fix(openai): normalize codex responses payloads
parent
40b643d6
Changes
2
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/openai_codex_transform.go
View file @
2add09e6
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"
)
...
...
@@ -756,6 +961,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 @
2add09e6
...
...
@@ -164,6 +164,131 @@ 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_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