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
a82029b0
Unverified
Commit
a82029b0
authored
Jan 18, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 18, 2026
Browse files
Merge pull request #318 from IanShaw027/main
fix(openai): OpenCode 兼容性增强 - 工具过滤和粘性会话修复
parents
0c2a901a
a61cc2cb
Changes
7
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/openai_gateway_handler.go
View file @
a82029b0
...
@@ -186,8 +186,8 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
...
@@ -186,8 +186,8 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
return
return
}
}
// Generate session hash (
from
header f
or OpenAI
)
// Generate session hash (header f
irst; fallback to prompt_cache_key
)
sessionHash
:=
h
.
gatewayService
.
GenerateSessionHash
(
c
)
sessionHash
:=
h
.
gatewayService
.
GenerateSessionHash
(
c
,
reqBody
)
const
maxAccountSwitches
=
3
const
maxAccountSwitches
=
3
switchCount
:=
0
switchCount
:=
0
...
...
backend/internal/service/openai_codex_transform.go
View file @
a82029b0
...
@@ -394,19 +394,35 @@ func normalizeCodexTools(reqBody map[string]any) bool {
...
@@ -394,19 +394,35 @@ func normalizeCodexTools(reqBody map[string]any) bool {
}
}
modified
:=
false
modified
:=
false
for
idx
,
tool
:=
range
tools
{
validTools
:=
make
([]
any
,
0
,
len
(
tools
))
for
_
,
tool
:=
range
tools
{
toolMap
,
ok
:=
tool
.
(
map
[
string
]
any
)
toolMap
,
ok
:=
tool
.
(
map
[
string
]
any
)
if
!
ok
{
if
!
ok
{
// Keep unknown structure as-is to avoid breaking upstream behavior.
validTools
=
append
(
validTools
,
tool
)
continue
continue
}
}
toolType
,
_
:=
toolMap
[
"type"
]
.
(
string
)
toolType
,
_
:=
toolMap
[
"type"
]
.
(
string
)
if
strings
.
TrimSpace
(
toolType
)
!=
"function"
{
toolType
=
strings
.
TrimSpace
(
toolType
)
if
toolType
!=
"function"
{
validTools
=
append
(
validTools
,
toolMap
)
continue
continue
}
}
function
,
ok
:=
toolMap
[
"function"
]
.
(
map
[
string
]
any
)
// OpenAI Responses-style tools use top-level name/parameters.
if
!
ok
{
if
name
,
ok
:=
toolMap
[
"name"
]
.
(
string
);
ok
&&
strings
.
TrimSpace
(
name
)
!=
""
{
validTools
=
append
(
validTools
,
toolMap
)
continue
}
// ChatCompletions-style tools use {type:"function", function:{...}}.
functionValue
,
hasFunction
:=
toolMap
[
"function"
]
function
,
ok
:=
functionValue
.
(
map
[
string
]
any
)
if
!
hasFunction
||
functionValue
==
nil
||
!
ok
||
function
==
nil
{
// Drop invalid function tools.
modified
=
true
continue
continue
}
}
...
@@ -435,11 +451,11 @@ func normalizeCodexTools(reqBody map[string]any) bool {
...
@@ -435,11 +451,11 @@ func normalizeCodexTools(reqBody map[string]any) bool {
}
}
}
}
tools
[
idx
]
=
toolMap
validTools
=
append
(
validTools
,
toolMap
)
}
}
if
modified
{
if
modified
{
reqBody
[
"tools"
]
=
t
ools
reqBody
[
"tools"
]
=
validT
ools
}
}
return
modified
return
modified
...
...
backend/internal/service/openai_codex_transform_test.go
View file @
a82029b0
...
@@ -129,6 +129,37 @@ func TestFilterCodexInput_RemovesItemReferenceWhenNotPreserved(t *testing.T) {
...
@@ -129,6 +129,37 @@ func TestFilterCodexInput_RemovesItemReferenceWhenNotPreserved(t *testing.T) {
require
.
False
(
t
,
hasID
)
require
.
False
(
t
,
hasID
)
}
}
func
TestApplyCodexOAuthTransform_NormalizeCodexTools_PreservesResponsesFunctionTools
(
t
*
testing
.
T
)
{
setupCodexCache
(
t
)
reqBody
:=
map
[
string
]
any
{
"model"
:
"gpt-5.1"
,
"tools"
:
[]
any
{
map
[
string
]
any
{
"type"
:
"function"
,
"name"
:
"bash"
,
"description"
:
"desc"
,
"parameters"
:
map
[
string
]
any
{
"type"
:
"object"
},
},
map
[
string
]
any
{
"type"
:
"function"
,
"function"
:
nil
,
},
},
}
applyCodexOAuthTransform
(
reqBody
)
tools
,
ok
:=
reqBody
[
"tools"
]
.
([]
any
)
require
.
True
(
t
,
ok
)
require
.
Len
(
t
,
tools
,
1
)
first
,
ok
:=
tools
[
0
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"function"
,
first
[
"type"
])
require
.
Equal
(
t
,
"bash"
,
first
[
"name"
])
}
func
TestApplyCodexOAuthTransform_EmptyInput
(
t
*
testing
.
T
)
{
func
TestApplyCodexOAuthTransform_EmptyInput
(
t
*
testing
.
T
)
{
// 空 input 应保持为空且不触发异常。
// 空 input 应保持为空且不触发异常。
setupCodexCache
(
t
)
setupCodexCache
(
t
)
...
...
backend/internal/service/openai_gateway_service.go
View file @
a82029b0
...
@@ -133,12 +133,30 @@ func NewOpenAIGatewayService(
...
@@ -133,12 +133,30 @@ func NewOpenAIGatewayService(
}
}
}
}
// GenerateSessionHash generates session hash from header (OpenAI uses session_id header)
// GenerateSessionHash generates a sticky-session hash for OpenAI requests.
func
(
s
*
OpenAIGatewayService
)
GenerateSessionHash
(
c
*
gin
.
Context
)
string
{
//
sessionID
:=
c
.
GetHeader
(
"session_id"
)
// Priority:
// 1. Header: session_id
// 2. Header: conversation_id
// 3. Body: prompt_cache_key (opencode)
func
(
s
*
OpenAIGatewayService
)
GenerateSessionHash
(
c
*
gin
.
Context
,
reqBody
map
[
string
]
any
)
string
{
if
c
==
nil
{
return
""
}
sessionID
:=
strings
.
TrimSpace
(
c
.
GetHeader
(
"session_id"
))
if
sessionID
==
""
{
sessionID
=
strings
.
TrimSpace
(
c
.
GetHeader
(
"conversation_id"
))
}
if
sessionID
==
""
&&
reqBody
!=
nil
{
if
v
,
ok
:=
reqBody
[
"prompt_cache_key"
]
.
(
string
);
ok
{
sessionID
=
strings
.
TrimSpace
(
v
)
}
}
if
sessionID
==
""
{
if
sessionID
==
""
{
return
""
return
""
}
}
hash
:=
sha256
.
Sum256
([]
byte
(
sessionID
))
hash
:=
sha256
.
Sum256
([]
byte
(
sessionID
))
return
hex
.
EncodeToString
(
hash
[
:
])
return
hex
.
EncodeToString
(
hash
[
:
])
}
}
...
...
backend/internal/service/openai_gateway_service_test.go
View file @
a82029b0
...
@@ -49,6 +49,49 @@ func (c stubConcurrencyCache) GetAccountsLoadBatch(ctx context.Context, accounts
...
@@ -49,6 +49,49 @@ func (c stubConcurrencyCache) GetAccountsLoadBatch(ctx context.Context, accounts
return
out
,
nil
return
out
,
nil
}
}
func
TestOpenAIGatewayService_GenerateSessionHash_Priority
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
rec
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
rec
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/openai/v1/responses"
,
nil
)
svc
:=
&
OpenAIGatewayService
{}
// 1) session_id header wins
c
.
Request
.
Header
.
Set
(
"session_id"
,
"sess-123"
)
c
.
Request
.
Header
.
Set
(
"conversation_id"
,
"conv-456"
)
h1
:=
svc
.
GenerateSessionHash
(
c
,
map
[
string
]
any
{
"prompt_cache_key"
:
"ses_aaa"
})
if
h1
==
""
{
t
.
Fatalf
(
"expected non-empty hash"
)
}
// 2) conversation_id used when session_id absent
c
.
Request
.
Header
.
Del
(
"session_id"
)
h2
:=
svc
.
GenerateSessionHash
(
c
,
map
[
string
]
any
{
"prompt_cache_key"
:
"ses_aaa"
})
if
h2
==
""
{
t
.
Fatalf
(
"expected non-empty hash"
)
}
if
h1
==
h2
{
t
.
Fatalf
(
"expected different hashes for different keys"
)
}
// 3) prompt_cache_key used when both headers absent
c
.
Request
.
Header
.
Del
(
"conversation_id"
)
h3
:=
svc
.
GenerateSessionHash
(
c
,
map
[
string
]
any
{
"prompt_cache_key"
:
"ses_aaa"
})
if
h3
==
""
{
t
.
Fatalf
(
"expected non-empty hash"
)
}
if
h2
==
h3
{
t
.
Fatalf
(
"expected different hashes for different keys"
)
}
// 4) empty when no signals
h4
:=
svc
.
GenerateSessionHash
(
c
,
map
[
string
]
any
{})
if
h4
!=
""
{
t
.
Fatalf
(
"expected empty hash when no signals"
)
}
}
func
TestOpenAISelectAccountWithLoadAwareness_FiltersUnschedulable
(
t
*
testing
.
T
)
{
func
TestOpenAISelectAccountWithLoadAwareness_FiltersUnschedulable
(
t
*
testing
.
T
)
{
now
:=
time
.
Now
()
now
:=
time
.
Now
()
resetAt
:=
now
.
Add
(
10
*
time
.
Minute
)
resetAt
:=
now
.
Add
(
10
*
time
.
Minute
)
...
...
backend/internal/service/openai_tool_corrector.go
View file @
a82029b0
...
@@ -27,6 +27,11 @@ var codexToolNameMapping = map[string]string{
...
@@ -27,6 +27,11 @@ var codexToolNameMapping = map[string]string{
"executeBash"
:
"bash"
,
"executeBash"
:
"bash"
,
"exec_bash"
:
"bash"
,
"exec_bash"
:
"bash"
,
"execBash"
:
"bash"
,
"execBash"
:
"bash"
,
// Some clients output generic fetch names.
"fetch"
:
"webfetch"
,
"web_fetch"
:
"webfetch"
,
"webFetch"
:
"webfetch"
,
}
}
// ToolCorrectionStats 记录工具修正的统计信息(导出用于 JSON 序列化)
// ToolCorrectionStats 记录工具修正的统计信息(导出用于 JSON 序列化)
...
@@ -208,27 +213,67 @@ func (c *CodexToolCorrector) correctToolParameters(toolName string, functionCall
...
@@ -208,27 +213,67 @@ func (c *CodexToolCorrector) correctToolParameters(toolName string, functionCall
// 根据工具名称应用特定的参数修正规则
// 根据工具名称应用特定的参数修正规则
switch
toolName
{
switch
toolName
{
case
"bash"
:
case
"bash"
:
// 移除 workdir 参数(OpenCode 不支持)
// OpenCode bash 支持 workdir;有些来源会输出 work_dir。
if
_
,
exists
:=
argsMap
[
"workdir"
];
exists
{
if
_
,
hasWorkdir
:=
argsMap
[
"workdir"
];
!
hasWorkdir
{
delete
(
argsMap
,
"workdir"
)
if
workDir
,
exists
:=
argsMap
[
"work_dir"
];
exists
{
argsMap
[
"workdir"
]
=
workDir
delete
(
argsMap
,
"work_dir"
)
corrected
=
true
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Re
mov
ed 'workdir'
parameter from
bash tool"
)
log
.
Printf
(
"[CodexToolCorrector] Re
nam
ed 'work
_
dir'
to 'workdir' in
bash tool"
)
}
}
}
else
{
if
_
,
exists
:=
argsMap
[
"work_dir"
];
exists
{
if
_
,
exists
:=
argsMap
[
"work_dir"
];
exists
{
delete
(
argsMap
,
"work_dir"
)
delete
(
argsMap
,
"work_dir"
)
corrected
=
true
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Removed 'work_dir' parameter from bash tool"
)
log
.
Printf
(
"[CodexToolCorrector] Removed duplicate 'work_dir' parameter from bash tool"
)
}
}
}
case
"edit"
:
case
"edit"
:
// OpenCode edit 使用 old_string/new_string,Codex 可能使用其他名称
// OpenCode edit 参数为 filePath/oldString/newString(camelCase)。
// 这里可以添加参数名称的映射逻辑
if
_
,
exists
:=
argsMap
[
"filePath"
];
!
exists
{
if
_
,
exists
:=
argsMap
[
"file_path"
];
!
exists
{
if
filePath
,
exists
:=
argsMap
[
"file_path"
];
exists
{
if
path
,
exists
:=
argsMap
[
"path"
];
exists
{
argsMap
[
"filePath"
]
=
filePath
argsMap
[
"file_path"
]
=
path
delete
(
argsMap
,
"file_path"
)
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Renamed 'file_path' to 'filePath' in edit tool"
)
}
else
if
filePath
,
exists
:=
argsMap
[
"path"
];
exists
{
argsMap
[
"filePath"
]
=
filePath
delete
(
argsMap
,
"path"
)
delete
(
argsMap
,
"path"
)
corrected
=
true
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Renamed 'path' to 'file_path' in edit tool"
)
log
.
Printf
(
"[CodexToolCorrector] Renamed 'path' to 'filePath' in edit tool"
)
}
else
if
filePath
,
exists
:=
argsMap
[
"file"
];
exists
{
argsMap
[
"filePath"
]
=
filePath
delete
(
argsMap
,
"file"
)
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Renamed 'file' to 'filePath' in edit tool"
)
}
}
if
_
,
exists
:=
argsMap
[
"oldString"
];
!
exists
{
if
oldString
,
exists
:=
argsMap
[
"old_string"
];
exists
{
argsMap
[
"oldString"
]
=
oldString
delete
(
argsMap
,
"old_string"
)
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Renamed 'old_string' to 'oldString' in edit tool"
)
}
}
if
_
,
exists
:=
argsMap
[
"newString"
];
!
exists
{
if
newString
,
exists
:=
argsMap
[
"new_string"
];
exists
{
argsMap
[
"newString"
]
=
newString
delete
(
argsMap
,
"new_string"
)
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Renamed 'new_string' to 'newString' in edit tool"
)
}
}
if
_
,
exists
:=
argsMap
[
"replaceAll"
];
!
exists
{
if
replaceAll
,
exists
:=
argsMap
[
"replace_all"
];
exists
{
argsMap
[
"replaceAll"
]
=
replaceAll
delete
(
argsMap
,
"replace_all"
)
corrected
=
true
log
.
Printf
(
"[CodexToolCorrector] Renamed 'replace_all' to 'replaceAll' in edit tool"
)
}
}
}
}
}
}
...
...
backend/internal/service/openai_tool_corrector_test.go
View file @
a82029b0
...
@@ -416,22 +416,23 @@ func TestCorrectToolParameters(t *testing.T) {
...
@@ -416,22 +416,23 @@ func TestCorrectToolParameters(t *testing.T) {
expected
map
[
string
]
bool
// key: 期待存在的参数, value: true表示应该存在
expected
map
[
string
]
bool
// key: 期待存在的参数, value: true表示应该存在
}{
}{
{
{
name
:
"re
mov
e workdir
from
bash tool"
,
name
:
"re
nam
e work
_
dir
to workdir in
bash tool"
,
input
:
`{
input
:
`{
"tool_calls": [{
"tool_calls": [{
"function": {
"function": {
"name": "bash",
"name": "bash",
"arguments": "{\"command\":\"ls\",\"workdir\":\"/tmp\"}"
"arguments": "{\"command\":\"ls\",\"work
_
dir\":\"/tmp\"}"
}
}
}]
}]
}`
,
}`
,
expected
:
map
[
string
]
bool
{
expected
:
map
[
string
]
bool
{
"command"
:
true
,
"command"
:
true
,
"workdir"
:
false
,
"workdir"
:
true
,
"work_dir"
:
false
,
},
},
},
},
{
{
name
:
"rename
path to file_path in edit tool
"
,
name
:
"rename
snake_case edit params to camelCase
"
,
input
:
`{
input
:
`{
"tool_calls": [{
"tool_calls": [{
"function": {
"function": {
...
@@ -441,10 +442,12 @@ func TestCorrectToolParameters(t *testing.T) {
...
@@ -441,10 +442,12 @@ func TestCorrectToolParameters(t *testing.T) {
}]
}]
}`
,
}`
,
expected
:
map
[
string
]
bool
{
expected
:
map
[
string
]
bool
{
"file
_p
ath"
:
true
,
"file
P
ath"
:
true
,
"path"
:
false
,
"path"
:
false
,
"old_string"
:
true
,
"oldString"
:
true
,
"new_string"
:
true
,
"old_string"
:
false
,
"newString"
:
true
,
"new_string"
:
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