From 5d5880d75e2648a35a4c42b70066b9d82f6b1994 Mon Sep 17 00:00:00 2001 From: dalamudx Date: Tue, 29 Sep 2026 20:18:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(claude-code):=20=E8=AE=A9=20openai:response?= =?UTF-8?q?s/chat=20=E8=BD=AC=20claude=20=E7=9A=84=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B9=9F=E5=BA=94=E7=94=A8=E8=AF=B7=E6=B1=82=E4=BD=93=E4=BC=AA?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openai:responses 与 openai:chat 的决策构建器各自构建上游 body,不经过 apply_transport_request_body_semantics,导致跨格式请求转发到 claude_code provider 时请求体伪装没有生效,上游仍返回 429。 - responses/decision/request.rs:finalize 之后应用伪装 - chat/decision/request.rs:finalize 成功后应用伪装 - 同格式路径改为经 ai_serving::transport 门面引用,满足架构守卫 (ai_serving 不得直接依赖 crate::provider_transport) - 更新 chat -> claude_code 用例断言为新的请求体形态 --- .../passthrough/provider/family/request.rs | 2 +- .../standard/openai/chat/decision/request.rs | 14 ++++++++++-- .../openai/responses/decision/request.rs | 7 ++++++ .../ai_execute/sync/chat/local_decision.rs | 22 ++++++++++++++++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs index 28f701b58..4bd50da98 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs @@ -316,7 +316,7 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts( // Same-format requests skip `apply_transport_request_body_semantics`, so the opt-in // Claude Code body mimicry has to be applied here as well. - if crate::provider_transport::claude_code::apply_claude_code_body_mimicry_for_transport( + if crate::ai_serving::transport::claude_code::apply_claude_code_body_mimicry_for_transport( &mut base_provider_request_body, &transport, prepared.provider_api_format.as_str(), diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs index ce719f126..87d17fd6d 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs @@ -140,7 +140,7 @@ fn finalize_openai_chat_provider_request_body( mapped_model, source_model, ); - crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy( + let finalization_failure = crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy( provider_request_body, crate::ai_serving::OpenAiProviderRequestFinalization { source_api_format: "openai:chat", @@ -170,7 +170,17 @@ fn finalize_openai_chat_provider_request_body( provider_api_format, "openai_chat_request_finalization", ) - }) + }); + if finalization_failure.is_none() { + // This builder does not go through `apply_transport_request_body_semantics`, so the + // Claude Code body mimicry must be applied here for Chat -> claude_code requests. + crate::ai_serving::transport::claude_code::apply_claude_code_body_mimicry_for_transport( + provider_request_body, + transport, + provider_api_format, + ); + } + finalization_failure } #[allow(clippy::too_many_arguments)] diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs index a9170ce59..0a52493bc 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs @@ -635,6 +635,13 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts_with_ { log_responses_to_chat_tool_conversion(trace_id, body_json, &base_provider_request_body); } + // This builder does not go through `apply_transport_request_body_semantics`, so the + // Claude Code body mimicry must be applied here for Responses -> claude_code requests. + crate::ai_serving::transport::claude_code::apply_claude_code_body_mimicry_for_transport( + &mut base_provider_request_body, + &transport, + provider_api_format, + ); let provider_request_body = base_provider_request_body; if let Some(kiro_auth) = kiro_auth.as_ref() { diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs b/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs index 9d97ebd83..71465d9f3 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs @@ -2028,14 +2028,30 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_claude_cli_syn "claude-code-upstream" ); assert_eq!(seen_execution_runtime_request.body["max_tokens"], 64); + // claude_code providers get the Claude Code body shape: billing header + identity + + // generic prompt, with the client's own system moved into the message history. + let system = seen_execution_runtime_request.body["system"] + .as_array() + .expect("claude_code system should be rewritten into blocks"); + assert_eq!(system.len(), 3); + assert!(system[0]["text"] + .as_str() + .is_some_and(|text| text.starts_with("x-anthropic-billing-header: cc_version="))); assert_eq!( - seen_execution_runtime_request.body["system"], - "You are terse." + system[1]["text"], + "You are Claude Code, Anthropic's official CLI for Claude." ); assert_eq!( seen_execution_runtime_request.body["messages"], - json!([{"role":"user","content":"Say hello"}]) + json!([ + {"role":"user","content":[{"type":"text","text":"[System Instructions]\nYou are terse."}]}, + {"role":"assistant","content":[{"type":"text","text":"Understood. I will follow these instructions."}]}, + {"role":"user","content":"Say hello"} + ]) ); + assert!(seen_execution_runtime_request.body["metadata"]["user_id"] + .as_str() + .is_some_and(|user_id| user_id.contains("\"session_id\""))); assert_eq!( seen_execution_runtime_request.client_api_format, "openai:chat"