diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index 48c72593..6460558e 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -72,7 +72,7 @@ type opencodeCacheMetadata struct { LastChecked int64 `json:"lastChecked"` } -func applyCodexOAuthTransform(reqBody map[string]any) codexTransformResult { +func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool) codexTransformResult { result := codexTransformResult{} // 工具续链需求会影响存储策略与 input 过滤逻辑。 needsToolContinuation := NeedsToolContinuation(reqBody) @@ -118,22 +118,9 @@ func applyCodexOAuthTransform(reqBody map[string]any) codexTransformResult { result.PromptCacheKey = strings.TrimSpace(v) } - instructions := strings.TrimSpace(getOpenCodeCodexHeader()) - existingInstructions, _ := reqBody["instructions"].(string) - existingInstructions = strings.TrimSpace(existingInstructions) - - 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 - } + // instructions 处理逻辑:根据是否是 Codex CLI 分别调用不同方法 + if applyInstructions(reqBody, isCodexCLI) { + result.Modified = true } // 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。 @@ -276,6 +263,72 @@ func GetCodexCLIInstructions() string { 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 指令(必要时)。 func ReplaceWithCodexInstructions(reqBody map[string]any) bool { codexInstructions := strings.TrimSpace(getCodexCLIInstructions()) diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 4cd72ab6..ac384553 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -23,7 +23,7 @@ func TestApplyCodexOAuthTransform_ToolContinuationPreservesInput(t *testing.T) { "tool_choice": "auto", } - applyCodexOAuthTransform(reqBody) + applyCodexOAuthTransform(reqBody, false) // 未显式设置 store=true,默认为 false。 store, ok := reqBody["store"].(bool) @@ -59,7 +59,7 @@ func TestApplyCodexOAuthTransform_ExplicitStoreFalsePreserved(t *testing.T) { "tool_choice": "auto", } - applyCodexOAuthTransform(reqBody) + applyCodexOAuthTransform(reqBody, false) store, ok := reqBody["store"].(bool) require.True(t, ok) @@ -79,7 +79,7 @@ func TestApplyCodexOAuthTransform_ExplicitStoreTrueForcedFalse(t *testing.T) { "tool_choice": "auto", } - applyCodexOAuthTransform(reqBody) + applyCodexOAuthTransform(reqBody, false) store, ok := reqBody["store"].(bool) require.True(t, ok) @@ -97,7 +97,7 @@ func TestApplyCodexOAuthTransform_NonContinuationDefaultsStoreFalseAndStripsIDs( }, } - applyCodexOAuthTransform(reqBody) + applyCodexOAuthTransform(reqBody, false) store, ok := reqBody["store"].(bool) require.True(t, ok) @@ -148,7 +148,7 @@ func TestApplyCodexOAuthTransform_NormalizeCodexTools_PreservesResponsesFunction }, } - applyCodexOAuthTransform(reqBody) + applyCodexOAuthTransform(reqBody, false) tools, ok := reqBody["tools"].([]any) require.True(t, ok) @@ -169,7 +169,7 @@ func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) { "input": []any{}, } - applyCodexOAuthTransform(reqBody) + applyCodexOAuthTransform(reqBody, false) input, ok := reqBody["input"].([]any) require.True(t, ok) @@ -196,3 +196,77 @@ func setupCodexCache(t *testing.T) { require.NoError(t, err) 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) + }) + } +} diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index aa9c00e0..742946d8 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -796,8 +796,8 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco } } - if account.Type == AccountTypeOAuth && !isCodexCLI { - codexResult := applyCodexOAuthTransform(reqBody) + if account.Type == AccountTypeOAuth { + codexResult := applyCodexOAuthTransform(reqBody, isCodexCLI) if codexResult.Modified { bodyModified = true }