fix(proxy): preserve scoped reasoning_content for tool calls (#2367)

- Preserve `reasoning_content` for Kimi/Moonshot OpenAI Chat compatibility paths.
- Keep generic OpenAI-compatible requests free of non-standard `reasoning_content` fields.
- Continue skipping thinking-only assistant messages.
- Add regressions for generic skip and Kimi/Moonshot preservation behavior.
This commit is contained in:
codeasier
2026-04-28 17:08:59 +08:00
committed by GitHub
parent 6441bc5c01
commit 21e2d68d76
2 changed files with 224 additions and 3 deletions
+110 -1
View File
@@ -82,6 +82,40 @@ pub fn claude_api_format_needs_transform(api_format: &str) -> bool {
)
}
fn is_moonshot_or_kimi_identifier(value: &str) -> bool {
let value = value.to_ascii_lowercase();
value.contains("moonshot") || value.contains("kimi")
}
fn should_preserve_reasoning_content_for_openai_chat(
provider: &Provider,
body: &serde_json::Value,
) -> bool {
if body
.get("model")
.and_then(|m| m.as_str())
.is_some_and(is_moonshot_or_kimi_identifier)
{
return true;
}
let settings = &provider.settings_config;
let base_urls = [
settings
.get("env")
.and_then(|env| env.get("ANTHROPIC_BASE_URL"))
.and_then(|v| v.as_str()),
settings.get("base_url").and_then(|v| v.as_str()),
settings.get("baseURL").and_then(|v| v.as_str()),
settings.get("apiEndpoint").and_then(|v| v.as_str()),
];
base_urls
.into_iter()
.flatten()
.any(is_moonshot_or_kimi_identifier)
}
pub fn transform_claude_request_for_api_format(
body: serde_json::Value,
provider: &Provider,
@@ -156,7 +190,12 @@ pub fn transform_claude_request_for_api_format(
)
}
"openai_chat" => {
let mut result = super::transform::anthropic_to_openai(body)?;
let preserve_reasoning_content =
should_preserve_reasoning_content_for_openai_chat(provider, &body);
let mut result = super::transform::anthropic_to_openai_with_reasoning_content(
body,
preserve_reasoning_content,
)?;
// Inject prompt_cache_key only if explicitly configured in meta
if let Some(key) = provider
.meta
@@ -1453,4 +1492,74 @@ mod tests {
assert_eq!(transformed["prompt_cache_key"], "claude-cache-route");
}
#[test]
fn test_transform_openai_chat_skips_reasoning_content_for_generic_provider() {
let provider = create_provider_with_meta(
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com",
"ANTHROPIC_API_KEY": "test-key"
}
}),
ProviderMeta {
api_format: Some("openai_chat".to_string()),
..Default::default()
},
);
let body = json!({
"model": "gpt-5.4",
"max_tokens": 64,
"messages": [{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "I should call the tool."},
{"type": "tool_use", "id": "call_123", "name": "get_weather", "input": {"location": "Tokyo"}}
]
}]
});
let transformed =
transform_claude_request_for_api_format(body, &provider, "openai_chat", None, None)
.unwrap();
let msg = &transformed["messages"][0];
assert!(msg.get("tool_calls").is_some());
assert!(msg.get("reasoning_content").is_none());
}
#[test]
fn test_transform_openai_chat_preserves_reasoning_content_for_kimi_provider() {
let provider = create_provider_with_meta(
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.moonshot.cn/v1",
"ANTHROPIC_API_KEY": "test-key"
}
}),
ProviderMeta {
api_format: Some("openai_chat".to_string()),
..Default::default()
},
);
let body = json!({
"model": "kimi-k2.6",
"max_tokens": 64,
"messages": [{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "I should call the tool."},
{"type": "tool_use", "id": "call_123", "name": "get_weather", "input": {"location": "Tokyo"}}
]
}]
});
let transformed =
transform_claude_request_for_api_format(body, &provider, "openai_chat", None, None)
.unwrap();
let msg = &transformed["messages"][0];
assert_eq!(msg["reasoning_content"], "I should call the tool.");
assert!(msg.get("tool_calls").is_some());
}
}