From 5e39801bba119c4e9fe3be08d8bf14821f8d6cae Mon Sep 17 00:00:00 2001 From: zhefox Date: Wed, 3 Jun 2026 10:52:26 +0800 Subject: [PATCH] fix(provider): clamp reasoning effort and filter chat extensions --- .../src/formats/conversion/request.rs | 66 +++++++++++++++++++ .../src/formats/openai/chat/request.rs | 32 ++++++++- .../src/formats/shared/model_directives.rs | 7 +- .../src/formats/shared/standard_normalize.rs | 2 +- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/crates/aether-ai-formats/src/formats/conversion/request.rs b/crates/aether-ai-formats/src/formats/conversion/request.rs index 49cf9132f..c556c963f 100644 --- a/crates/aether-ai-formats/src/formats/conversion/request.rs +++ b/crates/aether-ai-formats/src/formats/conversion/request.rs @@ -165,6 +165,7 @@ mod tests { convert_openai_chat_request_to_claude_request, convert_openai_chat_request_to_openai_responses_request, normalize_claude_request_to_openai_chat_request, + normalize_gemini_request_to_openai_chat_request, normalize_openai_responses_request_to_openai_chat_request, }; @@ -219,6 +220,43 @@ mod tests { assert_eq!(converted["messages"][0]["content"], "hello"); } + #[test] + fn claude_request_to_chat_clamps_max_reasoning_effort_to_high() { + let body = json!({ + "model": "claude-sonnet", + "messages": [{"role": "user", "content": "hello"}], + "thinking": {"type": "enabled", "budget_tokens": 1024}, + "output_config": {"effort": "max"}, + "max_tokens": 128, + }); + + let converted = + normalize_claude_request_to_openai_chat_request(&body).expect("openai chat request"); + + assert_eq!(converted["reasoning_effort"], "high"); + } + + #[test] + fn gemini_request_to_chat_clamps_xhigh_reasoning_effort_to_high() { + let body = json!({ + "contents": [{ + "role": "user", + "parts": [{"text": "hello"}] + }], + "generationConfig": { + "thinkingConfig": {"thinkingBudget": 8192} + } + }); + + let converted = normalize_gemini_request_to_openai_chat_request( + &body, + "/v1beta/models/gemini-2.5-pro:generateContent", + ) + .expect("openai chat request"); + + assert_eq!(converted["reasoning_effort"], "high"); + } + #[test] fn responses_request_normalizer_keeps_tool_history_chat_safe() { let call_id_one = "call_weather_123"; @@ -333,6 +371,34 @@ mod tests { assert_eq!(messages[0]["content"], ""); } + #[test] + fn responses_request_normalizer_clamps_chat_reasoning_effort_and_filters_extensions() { + let body = json!({ + "model": "gpt-5.1", + "input": "hello", + "reasoning": {"effort": "xhigh"}, + "text": {"verbosity": "high"}, + "include": ["reasoning.encrypted_content"], + "store": false, + "service_tier": "priority", + "prompt_cache_key": "cache_123", + "safety_identifier": "user_123" + }); + + let converted = normalize_openai_responses_request_to_openai_chat_request(&body) + .expect("openai chat request"); + + assert_eq!(converted["reasoning_effort"], "high"); + assert_eq!(converted["verbosity"], "high"); + assert_eq!(converted["service_tier"], "priority"); + assert_eq!(converted["prompt_cache_key"], "cache_123"); + assert_eq!(converted["safety_identifier"], "user_123"); + assert!(converted.get("include").is_none()); + assert!(converted.get("store").is_none()); + assert!(converted.get("text").is_none()); + assert!(converted.get("reasoning").is_none()); + } + #[test] fn request_normalizer_preserves_multiple_claude_tool_results() { let body = json!({ diff --git a/crates/aether-ai-formats/src/formats/openai/chat/request.rs b/crates/aether-ai-formats/src/formats/openai/chat/request.rs index dffc7e166..e1369f0fe 100644 --- a/crates/aether-ai-formats/src/formats/openai/chat/request.rs +++ b/crates/aether-ai-formats/src/formats/openai/chat/request.rs @@ -1,4 +1,4 @@ -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; use crate::{ formats::context::FormatContext, @@ -194,6 +194,7 @@ pub fn to_raw(canonical: &CanonicalRequest) -> Value { .and_then(|value| value.get("effort")) .and_then(Value::as_str) }) + .and_then(openai_chat_reasoning_effort) { output.insert( "reasoning_effort".to_string(), @@ -206,12 +207,12 @@ pub fn to_raw(canonical: &CanonicalRequest) -> Value { "openai", &output, )); - output.extend(namespace_extension_object( + output.extend(chat_compatible_openai_responses_extension_object( &canonical.extensions, OPENAI_RESPONSES_EXTENSION_NAMESPACE, &output, )); - output.extend(namespace_extension_object( + output.extend(chat_compatible_openai_responses_extension_object( &canonical.extensions, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE, &output, @@ -219,6 +220,31 @@ pub fn to_raw(canonical: &CanonicalRequest) -> Value { Value::Object(output) } +fn openai_chat_reasoning_effort(value: &str) -> Option<&'static str> { + match value.trim().to_ascii_lowercase().as_str() { + "low" => Some("low"), + "medium" => Some("medium"), + "high" | "xhigh" | "max" => Some("high"), + _ => None, + } +} + +fn chat_compatible_openai_responses_extension_object( + extensions: &std::collections::BTreeMap, + namespace: &str, + existing: &Map, +) -> Map { + namespace_extension_object(extensions, namespace, existing) + .into_iter() + .filter(|(key, _)| { + matches!( + key.as_str(), + "verbosity" | "service_tier" | "prompt_cache_key" | "safety_identifier" | "user" + ) + }) + .collect() +} + fn force_stream_options(body: &mut Value, upstream_is_stream: bool) { if !upstream_is_stream { return; diff --git a/crates/aether-ai-formats/src/formats/shared/model_directives.rs b/crates/aether-ai-formats/src/formats/shared/model_directives.rs index 345cbd354..5eb46d1c5 100644 --- a/crates/aether-ai-formats/src/formats/shared/model_directives.rs +++ b/crates/aether-ai-formats/src/formats/shared/model_directives.rs @@ -38,8 +38,7 @@ impl ReasoningEffort { Self::Low => "low", Self::Medium => "medium", Self::High => "high", - Self::XHigh => "xhigh", - Self::Max => "xhigh", + Self::XHigh | Self::Max => "high", } } @@ -524,7 +523,7 @@ mod tests { "gpt-5.4-xhigh", ) .expect("directive should apply"); - assert_eq!(openai_chat["reasoning_effort"], "xhigh"); + assert_eq!(openai_chat["reasoning_effort"], "high"); let mut responses = json!({ "model": "gpt-5-upstream", @@ -597,7 +596,7 @@ mod tests { "gpt-5.4-fast-xhigh", ) .expect("directive should apply"); - assert_eq!(openai_chat["reasoning_effort"], "xhigh"); + assert_eq!(openai_chat["reasoning_effort"], "high"); assert_eq!(openai_chat["service_tier"], "priority"); let mut reversed = json!({"model": "gpt-5-upstream", "reasoning_effort": "low"}); diff --git a/crates/aether-ai-formats/src/formats/shared/standard_normalize.rs b/crates/aether-ai-formats/src/formats/shared/standard_normalize.rs index 14d8b0cd5..0187685d8 100644 --- a/crates/aether-ai-formats/src/formats/shared/standard_normalize.rs +++ b/crates/aether-ai-formats/src/formats/shared/standard_normalize.rs @@ -738,7 +738,7 @@ mod tests { .expect("openai chat body should build"); assert_eq!(provider_request_body["model"], "gpt-5-upstream"); - assert_eq!(provider_request_body["reasoning_effort"], "xhigh"); + assert_eq!(provider_request_body["reasoning_effort"], "high"); } #[test]