@@ -505,10 +505,12 @@ impl StreamState {
505505 }
506506
507507 for choice in chunk. choices {
508+ // Handle reasoning/thinking from various provider fields
508509 if let Some ( reasoning) = choice
509510 . delta
510511 . reasoning_content
511512 . filter ( |value| !value. is_empty ( ) )
513+ . or ( choice. delta . thinking . and_then ( |t| t. content ) . filter ( |value| !value. is_empty ( ) ) )
512514 {
513515 if !self . thinking_started {
514516 self . thinking_started = true ;
@@ -736,6 +738,7 @@ impl ToolCallState {
736738
737739#[ derive( Debug , Deserialize ) ]
738740struct ChatCompletionResponse {
741+ #[ serde( default ) ]
739742 id : String ,
740743 model : String ,
741744 choices : Vec < ChatChoice > ,
@@ -806,6 +809,7 @@ impl OpenAiUsage {
806809
807810#[ derive( Debug , Deserialize ) ]
808811struct ChatCompletionChunk {
812+ #[ serde( default ) ]
809813 id : String ,
810814 #[ serde( default ) ]
811815 model : Option < String > ,
@@ -817,6 +821,7 @@ struct ChatCompletionChunk {
817821
818822#[ derive( Debug , Deserialize ) ]
819823struct ChunkChoice {
824+ #[ serde( default ) ]
820825 delta : ChunkDelta ,
821826 #[ serde( default ) ]
822827 finish_reason : Option < String > ,
@@ -826,12 +831,21 @@ struct ChunkChoice {
826831struct ChunkDelta {
827832 #[ serde( default ) ]
828833 content : Option < String > ,
834+ /// Some providers (GLM, DeepSeek) emit reasoning in `reasoning_content`
829835 #[ serde( default ) ]
830836 reasoning_content : Option < String > ,
837+ #[ serde( default ) ]
838+ thinking : Option < ThinkingDelta > ,
831839 #[ serde( default , deserialize_with = "deserialize_null_as_empty_vec" ) ]
832840 tool_calls : Vec < DeltaToolCall > ,
833841}
834842
843+ #[ derive( Debug , Default , Deserialize ) ]
844+ struct ThinkingDelta {
845+ #[ serde( default ) ]
846+ content : Option < String > ,
847+ }
848+
835849#[ derive( Debug , Deserialize ) ]
836850struct DeltaToolCall {
837851 #[ serde( default ) ]
@@ -1455,7 +1469,50 @@ fn parse_sse_frame(
14551469 data_lines. push ( data. trim_start ( ) ) ;
14561470 }
14571471 }
1472+ // If no SSE data lines found, check if the entire frame is raw JSON (error or otherwise)
14581473 if data_lines. is_empty ( ) {
1474+ // Detect raw JSON error response (not SSE-framed)
1475+ if let Ok ( raw) = serde_json:: from_str :: < serde_json:: Value > ( trimmed) {
1476+ if let Some ( err_obj) = raw. get ( "error" ) {
1477+ let msg = err_obj
1478+ . get ( "message" )
1479+ . and_then ( |m| m. as_str ( ) )
1480+ . unwrap_or ( "provider returned an error" )
1481+ . to_string ( ) ;
1482+ let code = err_obj
1483+ . get ( "code" )
1484+ . and_then ( serde_json:: Value :: as_u64)
1485+ . map ( |c| c as u16 ) ;
1486+ let status = reqwest:: StatusCode :: from_u16 ( code. unwrap_or ( 500 ) )
1487+ . unwrap_or ( reqwest:: StatusCode :: INTERNAL_SERVER_ERROR ) ;
1488+ return Err ( ApiError :: Api {
1489+ status,
1490+ error_type : err_obj
1491+ . get ( "type" )
1492+ . and_then ( |t| t. as_str ( ) )
1493+ . map ( str:: to_owned) ,
1494+ message : Some ( msg) ,
1495+ request_id : None ,
1496+ body : trimmed. chars ( ) . take ( 500 ) . collect ( ) ,
1497+ retryable : false ,
1498+ suggested_action : suggested_action_for_status ( status) ,
1499+ retry_after : None ,
1500+ } ) ;
1501+ }
1502+ }
1503+ // Detect HTML responses
1504+ if trimmed. starts_with ( '<' ) || trimmed. starts_with ( "<!" ) {
1505+ return Err ( ApiError :: Api {
1506+ status : reqwest:: StatusCode :: BAD_REQUEST ,
1507+ error_type : Some ( "invalid_response" . to_string ( ) ) ,
1508+ message : Some ( "provider returned HTML instead of JSON (check endpoint URL)" . to_string ( ) ) ,
1509+ request_id : None ,
1510+ body : trimmed. chars ( ) . take ( 200 ) . collect ( ) ,
1511+ retryable : false ,
1512+ suggested_action : Some ( "verify the API endpoint URL is correct" . to_string ( ) ) ,
1513+ retry_after : None ,
1514+ } ) ;
1515+ }
14591516 return Ok ( None ) ;
14601517 }
14611518 let payload = data_lines. join ( "\n " ) ;
@@ -1492,6 +1549,20 @@ fn parse_sse_frame(
14921549 } ) ;
14931550 }
14941551 }
1552+ // Detect HTML or other non-JSON responses early for better error messages
1553+ let trimmed_payload = payload. trim ( ) ;
1554+ if trimmed_payload. starts_with ( '<' ) || trimmed_payload. starts_with ( "<!" ) {
1555+ return Err ( ApiError :: Api {
1556+ status : reqwest:: StatusCode :: BAD_REQUEST ,
1557+ error_type : Some ( "invalid_response" . to_string ( ) ) ,
1558+ message : Some ( "provider returned HTML instead of JSON (check endpoint URL)" . to_string ( ) ) ,
1559+ request_id : None ,
1560+ body : payload. chars ( ) . take ( 200 ) . collect ( ) ,
1561+ retryable : false ,
1562+ suggested_action : Some ( "verify the API endpoint URL is correct" . to_string ( ) ) ,
1563+ retry_after : None ,
1564+ } ) ;
1565+ }
14951566 serde_json:: from_str :: < ChatCompletionChunk > ( & payload)
14961567 . map ( Some )
14971568 . map_err ( |error| ApiError :: json_deserialize ( provider, model, & payload, error) )
0 commit comments