diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 0256ac75e..274faeea7 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -2675,6 +2675,19 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A return nil, fmt.Errorf("parse request: empty request") } + // 防止非 Anthropic/Antigravity 平台的账号被错误地通过 Claude 端点转发。 + // 例如 OpenAI 账号的 token 发到 Anthropic API 会导致 401 并触发 failover 禁用账号。 + if account.Platform != PlatformAnthropic && account.Platform != PlatformAntigravity { + c.JSON(http.StatusBadRequest, gin.H{ + "type": "error", + "error": gin.H{ + "type": "invalid_request_error", + "message": fmt.Sprintf("Account platform %q is not compatible with /v1/messages endpoint", account.Platform), + }, + }) + return nil, fmt.Errorf("platform mismatch: account %d platform=%s not compatible with claude endpoint", account.ID, account.Platform) + } + body := parsed.Body reqModel := parsed.Model reqStream := parsed.Stream diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index d28e13abc..25f75131a 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -135,8 +135,15 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool) codexTran } // 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。 + // 但 store=false 时上游不会持久化 item,引用必然失败, + // 因此即使续链也必须移除 item_reference 并清理 id 字段。 + storeFalse := false + if v, ok := reqBody["store"].(bool); ok && !v { + storeFalse = true + } if input, ok := reqBody["input"].([]any); ok { - input = filterCodexInput(input, needsToolContinuation) + preserveRefs := needsToolContinuation && !storeFalse + input = filterCodexInput(input, preserveRefs) reqBody["input"] = input result.Modified = true } diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 0987c5090..53d0211be 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -11,7 +11,8 @@ import ( ) func TestApplyCodexOAuthTransform_ToolContinuationPreservesInput(t *testing.T) { - // 续链场景:保留 item_reference 与 id,但不再强制 store=true。 + // 续链场景 + store=false:item_reference 被移除(上游不持久化,引用必然 404), + // function_call_output 保留但 id 被清理,call_id 保留用于工具调用关联。 setupCodexCache(t) reqBody := map[string]any{ @@ -25,25 +26,23 @@ func TestApplyCodexOAuthTransform_ToolContinuationPreservesInput(t *testing.T) { applyCodexOAuthTransform(reqBody, false) - // 未显式设置 store=true,默认为 false。 + // store 强制为 false(OAuth 账号)。 store, ok := reqBody["store"].(bool) require.True(t, ok) require.False(t, store) input, ok := reqBody["input"].([]any) require.True(t, ok) - require.Len(t, input, 2) + // item_reference 被移除,只剩 function_call_output。 + require.Len(t, input, 1) - // 校验 input[0] 为 map,避免断言失败导致测试中断。 first, ok := input[0].(map[string]any) require.True(t, ok) - require.Equal(t, "item_reference", first["type"]) - require.Equal(t, "ref1", first["id"]) - - // 校验 input[1] 为 map,确保后续字段断言安全。 - second, ok := input[1].(map[string]any) - require.True(t, ok) - require.Equal(t, "o1", second["id"]) + require.Equal(t, "function_call_output", first["type"]) + require.Equal(t, "call_1", first["call_id"]) + // id 被清理(store=false 下无意义)。 + _, hasID := first["id"] + require.False(t, hasID) } func TestApplyCodexOAuthTransform_ExplicitStoreFalsePreserved(t *testing.T) { diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 52800f077..aa3df23de 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -1163,6 +1163,16 @@ func (s *OpenAIGatewayService) handleErrorResponse(ctx context.Context, resp *ht var statusCode int switch resp.StatusCode { + case 400, 404, 409, 422: + // Client errors: pass through upstream status and message so the caller + // can act on the real problem (e.g. referencing a non-persisted response ID). + statusCode = resp.StatusCode + errType = "invalid_request_error" + if upstreamMsg != "" { + errMsg = upstreamMsg + } else { + errMsg = "Upstream request rejected" + } case 401: statusCode = http.StatusBadGateway errType = "upstream_error"