fix(openai): 统一 OAuth instructions 处理逻辑,修复 Codex CLI 400 错误
- 修改 applyCodexOAuthTransform 函数签名,增加 isCodexCLI 参数 - 移除 && !isCodexCLI 条件,对所有 OAuth 请求统一处理 - 新增 applyInstructions/applyCodexCLIInstructions/applyOpenCodeInstructions 辅助函数 - 新增 isInstructionsEmpty 函数检查 instructions 字段是否为空 - 添加 Codex CLI 和非 Codex CLI 场景的测试用例 逻辑说明: - Codex CLI + 有 instructions: 保持不变 - Codex CLI + 无 instructions: 补充 opencode 指令 - 非 Codex CLI: 使用 opencode 指令覆盖
This commit is contained in:
@@ -72,7 +72,7 @@ type opencodeCacheMetadata struct {
|
|||||||
LastChecked int64 `json:"lastChecked"`
|
LastChecked int64 `json:"lastChecked"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCodexOAuthTransform(reqBody map[string]any) codexTransformResult {
|
func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool) codexTransformResult {
|
||||||
result := codexTransformResult{}
|
result := codexTransformResult{}
|
||||||
// 工具续链需求会影响存储策略与 input 过滤逻辑。
|
// 工具续链需求会影响存储策略与 input 过滤逻辑。
|
||||||
needsToolContinuation := NeedsToolContinuation(reqBody)
|
needsToolContinuation := NeedsToolContinuation(reqBody)
|
||||||
@@ -118,22 +118,9 @@ func applyCodexOAuthTransform(reqBody map[string]any) codexTransformResult {
|
|||||||
result.PromptCacheKey = strings.TrimSpace(v)
|
result.PromptCacheKey = strings.TrimSpace(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
// instructions 处理逻辑:根据是否是 Codex CLI 分别调用不同方法
|
||||||
existingInstructions, _ := reqBody["instructions"].(string)
|
if applyInstructions(reqBody, isCodexCLI) {
|
||||||
existingInstructions = strings.TrimSpace(existingInstructions)
|
result.Modified = true
|
||||||
|
|
||||||
if instructions != "" {
|
|
||||||
if existingInstructions != instructions {
|
|
||||||
reqBody["instructions"] = instructions
|
|
||||||
result.Modified = true
|
|
||||||
}
|
|
||||||
} else if existingInstructions == "" {
|
|
||||||
// 未获取到 opencode 指令时,回退使用 Codex CLI 指令。
|
|
||||||
codexInstructions := strings.TrimSpace(getCodexCLIInstructions())
|
|
||||||
if codexInstructions != "" {
|
|
||||||
reqBody["instructions"] = codexInstructions
|
|
||||||
result.Modified = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。
|
// 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。
|
||||||
@@ -276,6 +263,72 @@ func GetCodexCLIInstructions() string {
|
|||||||
return getCodexCLIInstructions()
|
return getCodexCLIInstructions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyInstructions 处理 instructions 字段
|
||||||
|
// isCodexCLI=true: 仅补充缺失的 instructions(使用 opencode 指令)
|
||||||
|
// isCodexCLI=false: 优先使用 opencode 指令覆盖
|
||||||
|
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
||||||
|
if isCodexCLI {
|
||||||
|
return applyCodexCLIInstructions(reqBody)
|
||||||
|
}
|
||||||
|
return applyOpenCodeInstructions(reqBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyCodexCLIInstructions 为 Codex CLI 请求补充缺失的 instructions
|
||||||
|
// 仅在 instructions 为空时添加 opencode 指令
|
||||||
|
func applyCodexCLIInstructions(reqBody map[string]any) bool {
|
||||||
|
if !isInstructionsEmpty(reqBody) {
|
||||||
|
return false // 已有有效 instructions,不修改
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
||||||
|
if instructions != "" {
|
||||||
|
reqBody["instructions"] = instructions
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyOpenCodeInstructions 为非 Codex CLI 请求应用 opencode 指令
|
||||||
|
// 优先使用 opencode 指令覆盖
|
||||||
|
func applyOpenCodeInstructions(reqBody map[string]any) bool {
|
||||||
|
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
||||||
|
existingInstructions, _ := reqBody["instructions"].(string)
|
||||||
|
existingInstructions = strings.TrimSpace(existingInstructions)
|
||||||
|
|
||||||
|
if instructions != "" {
|
||||||
|
if existingInstructions != instructions {
|
||||||
|
reqBody["instructions"] = instructions
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if existingInstructions == "" {
|
||||||
|
codexInstructions := strings.TrimSpace(getCodexCLIInstructions())
|
||||||
|
if codexInstructions != "" {
|
||||||
|
reqBody["instructions"] = codexInstructions
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInstructionsEmpty 检查 instructions 字段是否为空
|
||||||
|
// 处理以下情况:字段不存在、nil、空字符串、纯空白字符串
|
||||||
|
func isInstructionsEmpty(reqBody map[string]any) bool {
|
||||||
|
val, exists := reqBody["instructions"]
|
||||||
|
if !exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if val == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
str, ok := val.(string)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(str) == ""
|
||||||
|
}
|
||||||
|
|
||||||
// ReplaceWithCodexInstructions 将请求 instructions 替换为内置 Codex 指令(必要时)。
|
// ReplaceWithCodexInstructions 将请求 instructions 替换为内置 Codex 指令(必要时)。
|
||||||
func ReplaceWithCodexInstructions(reqBody map[string]any) bool {
|
func ReplaceWithCodexInstructions(reqBody map[string]any) bool {
|
||||||
codexInstructions := strings.TrimSpace(getCodexCLIInstructions())
|
codexInstructions := strings.TrimSpace(getCodexCLIInstructions())
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestApplyCodexOAuthTransform_ToolContinuationPreservesInput(t *testing.T) {
|
|||||||
"tool_choice": "auto",
|
"tool_choice": "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCodexOAuthTransform(reqBody)
|
applyCodexOAuthTransform(reqBody, false)
|
||||||
|
|
||||||
// 未显式设置 store=true,默认为 false。
|
// 未显式设置 store=true,默认为 false。
|
||||||
store, ok := reqBody["store"].(bool)
|
store, ok := reqBody["store"].(bool)
|
||||||
@@ -59,7 +59,7 @@ func TestApplyCodexOAuthTransform_ExplicitStoreFalsePreserved(t *testing.T) {
|
|||||||
"tool_choice": "auto",
|
"tool_choice": "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCodexOAuthTransform(reqBody)
|
applyCodexOAuthTransform(reqBody, false)
|
||||||
|
|
||||||
store, ok := reqBody["store"].(bool)
|
store, ok := reqBody["store"].(bool)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
@@ -79,7 +79,7 @@ func TestApplyCodexOAuthTransform_ExplicitStoreTrueForcedFalse(t *testing.T) {
|
|||||||
"tool_choice": "auto",
|
"tool_choice": "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCodexOAuthTransform(reqBody)
|
applyCodexOAuthTransform(reqBody, false)
|
||||||
|
|
||||||
store, ok := reqBody["store"].(bool)
|
store, ok := reqBody["store"].(bool)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
@@ -97,7 +97,7 @@ func TestApplyCodexOAuthTransform_NonContinuationDefaultsStoreFalseAndStripsIDs(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCodexOAuthTransform(reqBody)
|
applyCodexOAuthTransform(reqBody, false)
|
||||||
|
|
||||||
store, ok := reqBody["store"].(bool)
|
store, ok := reqBody["store"].(bool)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
@@ -148,7 +148,7 @@ func TestApplyCodexOAuthTransform_NormalizeCodexTools_PreservesResponsesFunction
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCodexOAuthTransform(reqBody)
|
applyCodexOAuthTransform(reqBody, false)
|
||||||
|
|
||||||
tools, ok := reqBody["tools"].([]any)
|
tools, ok := reqBody["tools"].([]any)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
@@ -169,7 +169,7 @@ func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) {
|
|||||||
"input": []any{},
|
"input": []any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCodexOAuthTransform(reqBody)
|
applyCodexOAuthTransform(reqBody, false)
|
||||||
|
|
||||||
input, ok := reqBody["input"].([]any)
|
input, ok := reqBody["input"].([]any)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
@@ -196,3 +196,77 @@ func setupCodexCache(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(cacheDir, "opencode-codex-header-meta.json"), data, 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(cacheDir, "opencode-codex-header-meta.json"), data, 0o644))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexOAuthTransform_CodexCLI_PreservesExistingInstructions(t *testing.T) {
|
||||||
|
// Codex CLI 场景:已有 instructions 时不修改
|
||||||
|
setupCodexCache(t)
|
||||||
|
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"model": "gpt-5.1",
|
||||||
|
"instructions": "existing instructions",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := applyCodexOAuthTransform(reqBody, true) // isCodexCLI=true
|
||||||
|
|
||||||
|
instructions, ok := reqBody["instructions"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "existing instructions", instructions)
|
||||||
|
// Modified 仍可能为 true(因为其他字段被修改),但 instructions 应保持不变
|
||||||
|
_ = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexOAuthTransform_CodexCLI_SuppliesDefaultWhenEmpty(t *testing.T) {
|
||||||
|
// Codex CLI 场景:无 instructions 时补充默认值
|
||||||
|
setupCodexCache(t)
|
||||||
|
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"model": "gpt-5.1",
|
||||||
|
// 没有 instructions 字段
|
||||||
|
}
|
||||||
|
|
||||||
|
result := applyCodexOAuthTransform(reqBody, true) // isCodexCLI=true
|
||||||
|
|
||||||
|
instructions, ok := reqBody["instructions"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotEmpty(t, instructions)
|
||||||
|
require.True(t, result.Modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexOAuthTransform_NonCodexCLI_OverridesInstructions(t *testing.T) {
|
||||||
|
// 非 Codex CLI 场景:使用 opencode 指令覆盖
|
||||||
|
setupCodexCache(t)
|
||||||
|
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"model": "gpt-5.1",
|
||||||
|
"instructions": "old instructions",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := applyCodexOAuthTransform(reqBody, false) // isCodexCLI=false
|
||||||
|
|
||||||
|
instructions, ok := reqBody["instructions"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotEqual(t, "old instructions", instructions)
|
||||||
|
require.True(t, result.Modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsInstructionsEmpty(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
reqBody map[string]any
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"missing field", map[string]any{}, true},
|
||||||
|
{"nil value", map[string]any{"instructions": nil}, true},
|
||||||
|
{"empty string", map[string]any{"instructions": ""}, true},
|
||||||
|
{"whitespace only", map[string]any{"instructions": " "}, true},
|
||||||
|
{"non-string", map[string]any{"instructions": 123}, true},
|
||||||
|
{"valid string", map[string]any{"instructions": "hello"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := isInstructionsEmpty(tt.reqBody)
|
||||||
|
require.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -796,8 +796,8 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if account.Type == AccountTypeOAuth && !isCodexCLI {
|
if account.Type == AccountTypeOAuth {
|
||||||
codexResult := applyCodexOAuthTransform(reqBody)
|
codexResult := applyCodexOAuthTransform(reqBody, isCodexCLI)
|
||||||
if codexResult.Modified {
|
if codexResult.Modified {
|
||||||
bodyModified = true
|
bodyModified = true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user