@@ -31,12 +31,22 @@ pub struct OpenAiCompatConfig {
3131 pub api_key_env : & ' static str ,
3232 pub base_url_env : & ' static str ,
3333 pub default_base_url : & ' static str ,
34+ /// Maximum request body size in bytes. Provider-specific limits:
35+ /// - DashScope: 6MB (6_291_456 bytes) - observed in dogfood testing
36+ /// - OpenAI: 100MB (104_857_600 bytes)
37+ /// - xAI: 50MB (52_428_800 bytes)
38+ pub max_request_body_bytes : usize ,
3439}
3540
3641const XAI_ENV_VARS : & [ & str ] = & [ "XAI_API_KEY" ] ;
3742const OPENAI_ENV_VARS : & [ & str ] = & [ "OPENAI_API_KEY" ] ;
3843const DASHSCOPE_ENV_VARS : & [ & str ] = & [ "DASHSCOPE_API_KEY" ] ;
3944
45+ // Provider-specific request body size limits in bytes
46+ const XAI_MAX_REQUEST_BODY_BYTES : usize = 52_428_800 ; // 50MB
47+ const OPENAI_MAX_REQUEST_BODY_BYTES : usize = 104_857_600 ; // 100MB
48+ const DASHSCOPE_MAX_REQUEST_BODY_BYTES : usize = 6_291_456 ; // 6MB (observed limit in dogfood)
49+
4050impl OpenAiCompatConfig {
4151 #[ must_use]
4252 pub const fn xai ( ) -> Self {
@@ -45,6 +55,7 @@ impl OpenAiCompatConfig {
4555 api_key_env : "XAI_API_KEY" ,
4656 base_url_env : "XAI_BASE_URL" ,
4757 default_base_url : DEFAULT_XAI_BASE_URL ,
58+ max_request_body_bytes : XAI_MAX_REQUEST_BODY_BYTES ,
4859 }
4960 }
5061
@@ -55,6 +66,7 @@ impl OpenAiCompatConfig {
5566 api_key_env : "OPENAI_API_KEY" ,
5667 base_url_env : "OPENAI_BASE_URL" ,
5768 default_base_url : DEFAULT_OPENAI_BASE_URL ,
69+ max_request_body_bytes : OPENAI_MAX_REQUEST_BODY_BYTES ,
5870 }
5971 }
6072
@@ -69,6 +81,7 @@ impl OpenAiCompatConfig {
6981 api_key_env : "DASHSCOPE_API_KEY" ,
7082 base_url_env : "DASHSCOPE_BASE_URL" ,
7183 default_base_url : DEFAULT_DASHSCOPE_BASE_URL ,
84+ max_request_body_bytes : DASHSCOPE_MAX_REQUEST_BODY_BYTES ,
7285 }
7386 }
7487
@@ -249,6 +262,9 @@ impl OpenAiCompatClient {
249262 & self ,
250263 request : & MessageRequest ,
251264 ) -> Result < reqwest:: Response , ApiError > {
265+ // Pre-flight check: verify request body size against provider limits
266+ check_request_body_size ( request, self . config ( ) ) ?;
267+
252268 let request_url = chat_completions_endpoint ( & self . base_url ) ;
253269 self . http
254270 . post ( & request_url)
@@ -791,9 +807,41 @@ fn strip_routing_prefix(model: &str) -> &str {
791807 }
792808}
793809
810+ /// Estimate the serialized JSON size of a request payload in bytes.
811+ /// This is a pre-flight check to avoid hitting provider-specific size limits.
812+ pub fn estimate_request_body_size ( request : & MessageRequest , config : OpenAiCompatConfig ) -> usize {
813+ let payload = build_chat_completion_request ( request, config) ;
814+ // serde_json::to_vec gives us the exact byte size of the serialized JSON
815+ serde_json:: to_vec ( & payload) . map_or ( 0 , |v| v. len ( ) )
816+ }
817+
818+ /// Pre-flight check for request body size against provider limits.
819+ /// Returns Ok(()) if the request is within limits, or an error with
820+ /// a clear message about the size limit being exceeded.
821+ pub fn check_request_body_size (
822+ request : & MessageRequest ,
823+ config : OpenAiCompatConfig ,
824+ ) -> Result < ( ) , ApiError > {
825+ let estimated_bytes = estimate_request_body_size ( request, config) ;
826+ let max_bytes = config. max_request_body_bytes ;
827+
828+ if estimated_bytes > max_bytes {
829+ Err ( ApiError :: RequestBodySizeExceeded {
830+ estimated_bytes,
831+ max_bytes,
832+ provider : config. provider_name ,
833+ } )
834+ } else {
835+ Ok ( ( ) )
836+ }
837+ }
838+
794839/// Builds a chat completion request payload from a `MessageRequest`.
795840/// Public for benchmarking purposes.
796- pub fn build_chat_completion_request ( request : & MessageRequest , config : OpenAiCompatConfig ) -> Value {
841+ pub fn build_chat_completion_request (
842+ request : & MessageRequest ,
843+ config : OpenAiCompatConfig ,
844+ ) -> Value {
797845 let mut messages = Vec :: new ( ) ;
798846 if let Some ( system) = request. system . as_ref ( ) . filter ( |value| !value. is_empty ( ) ) {
799847 messages. push ( json ! ( {
@@ -2031,4 +2079,102 @@ mod tests {
20312079 assert_eq ! ( tool_msg_gpt[ "content" ] , json!( "file contents" ) ) ;
20322080 assert_eq ! ( tool_msg_kimi[ "content" ] , json!( "file contents" ) ) ;
20332081 }
2082+
2083+ // ============================================================================
2084+ // US-021: Request body size pre-flight check tests
2085+ // ============================================================================
2086+
2087+ #[ test]
2088+ fn estimate_request_body_size_returns_reasonable_estimate ( ) {
2089+ let request = MessageRequest {
2090+ model : "gpt-4o" . to_string ( ) ,
2091+ max_tokens : 100 ,
2092+ messages : vec ! [ InputMessage :: user_text( "Hello world" . to_string( ) ) ] ,
2093+ stream : false ,
2094+ ..Default :: default ( )
2095+ } ;
2096+
2097+ let size = super :: estimate_request_body_size ( & request, OpenAiCompatConfig :: openai ( ) ) ;
2098+ // Should be non-zero and reasonable for a small request
2099+ assert ! ( size > 0 , "estimated size should be positive" ) ;
2100+ assert ! ( size < 10_000 , "small request should be under 10KB" ) ;
2101+ }
2102+
2103+ #[ test]
2104+ fn check_request_body_size_passes_for_small_requests ( ) {
2105+ let request = MessageRequest {
2106+ model : "gpt-4o" . to_string ( ) ,
2107+ max_tokens : 100 ,
2108+ messages : vec ! [ InputMessage :: user_text( "Hello" . to_string( ) ) ] ,
2109+ stream : false ,
2110+ ..Default :: default ( )
2111+ } ;
2112+
2113+ // Should pass for all providers with a small request
2114+ assert ! ( super :: check_request_body_size( & request, OpenAiCompatConfig :: openai( ) ) . is_ok( ) ) ;
2115+ assert ! ( super :: check_request_body_size( & request, OpenAiCompatConfig :: xai( ) ) . is_ok( ) ) ;
2116+ assert ! ( super :: check_request_body_size( & request, OpenAiCompatConfig :: dashscope( ) ) . is_ok( ) ) ;
2117+ }
2118+
2119+ #[ test]
2120+ fn check_request_body_size_fails_for_dashscope_when_exceeds_6mb ( ) {
2121+ // Create a request that exceeds DashScope's 6MB limit
2122+ let large_content = "x" . repeat ( 7_000_000 ) ; // 7MB of content
2123+ let request = MessageRequest {
2124+ model : "qwen-plus" . to_string ( ) ,
2125+ max_tokens : 100 ,
2126+ messages : vec ! [ InputMessage :: user_text( large_content) ] ,
2127+ stream : false ,
2128+ ..Default :: default ( )
2129+ } ;
2130+
2131+ let result = super :: check_request_body_size ( & request, OpenAiCompatConfig :: dashscope ( ) ) ;
2132+ assert ! ( result. is_err( ) , "should fail for 7MB request to DashScope" ) ;
2133+
2134+ let err = result. unwrap_err ( ) ;
2135+ match err {
2136+ crate :: error:: ApiError :: RequestBodySizeExceeded {
2137+ estimated_bytes,
2138+ max_bytes,
2139+ provider,
2140+ } => {
2141+ assert_eq ! ( provider, "DashScope" ) ;
2142+ assert_eq ! ( max_bytes, 6_291_456 ) ; // 6MB limit
2143+ assert ! ( estimated_bytes > max_bytes) ;
2144+ }
2145+ _ => panic ! ( "expected RequestBodySizeExceeded error, got {:?}" , err) ,
2146+ }
2147+ }
2148+
2149+ #[ test]
2150+ fn check_request_body_size_allows_large_requests_for_openai ( ) {
2151+ // Create a request that exceeds DashScope's limit but is under OpenAI's 100MB limit
2152+ let large_content = "x" . repeat ( 10_000_000 ) ; // 10MB of content
2153+ let request = MessageRequest {
2154+ model : "gpt-4o" . to_string ( ) ,
2155+ max_tokens : 100 ,
2156+ messages : vec ! [ InputMessage :: user_text( large_content) ] ,
2157+ stream : false ,
2158+ ..Default :: default ( )
2159+ } ;
2160+
2161+ // Should pass for OpenAI (100MB limit)
2162+ assert ! (
2163+ super :: check_request_body_size( & request, OpenAiCompatConfig :: openai( ) ) . is_ok( ) ,
2164+ "10MB request should pass for OpenAI's 100MB limit"
2165+ ) ;
2166+
2167+ // Should fail for DashScope (6MB limit)
2168+ assert ! (
2169+ super :: check_request_body_size( & request, OpenAiCompatConfig :: dashscope( ) ) . is_err( ) ,
2170+ "10MB request should fail for DashScope's 6MB limit"
2171+ ) ;
2172+ }
2173+
2174+ #[ test]
2175+ fn provider_specific_size_limits_are_correct ( ) {
2176+ assert_eq ! ( OpenAiCompatConfig :: dashscope( ) . max_request_body_bytes, 6_291_456 ) ; // 6MB
2177+ assert_eq ! ( OpenAiCompatConfig :: openai( ) . max_request_body_bytes, 104_857_600 ) ; // 100MB
2178+ assert_eq ! ( OpenAiCompatConfig :: xai( ) . max_request_body_bytes, 52_428_800 ) ; // 50MB
2179+ }
20342180}
0 commit comments