@@ -523,10 +523,14 @@ impl LlmProvider for NearAiChatProvider {
523523
524524 // Fall back to reasoning_content when content is null (same as
525525 // complete_with_tools — reasoning models may put the answer there).
526- let content = choice
527- . message
528- . content
529- . or ( choice. message . reasoning_content )
526+ let ChatCompletionResponseMessage {
527+ content,
528+ reasoning_content,
529+ reasoning,
530+ ..
531+ } = choice. message ;
532+ let content = content
533+ . or ( reasoning_content. or ( reasoning) )
530534 . unwrap_or_default ( ) ;
531535 let finish_reason = match choice. finish_reason . as_deref ( ) {
532536 Some ( "stop" ) => FinishReason :: Stop ,
@@ -602,9 +606,16 @@ impl LlmProvider for NearAiChatProvider {
602606 provider : "nearai_chat" . to_string ( ) ,
603607 } ) ?;
604608
605- let tool_calls: Vec < ToolCall > = choice
606- . message
607- . tool_calls
609+ let ChatCompletionResponseMessage {
610+ content : message_content,
611+ reasoning_content,
612+ reasoning,
613+ tool_calls : message_tool_calls,
614+ ..
615+ } = choice. message ;
616+ let reasoning_fallback = reasoning_content. or ( reasoning) ;
617+
618+ let tool_calls: Vec < ToolCall > = message_tool_calls
608619 . unwrap_or_default ( )
609620 . into_iter ( )
610621 . map ( |tc| {
@@ -626,9 +637,9 @@ impl LlmProvider for NearAiChatProvider {
626637 // leaking that into conversation history inflates context and
627638 // confuses the model.
628639 let content = if tool_calls. is_empty ( ) {
629- choice . message . content . or ( choice . message . reasoning_content )
640+ message_content . or ( reasoning_fallback )
630641 } else {
631- choice . message . content
642+ message_content
632643 } ;
633644
634645 let finish_reason = match choice. finish_reason . as_deref ( ) {
@@ -1072,8 +1083,10 @@ struct ChatCompletionResponseMessage {
10721083 /// Some models return chain-of-thought reasoning here instead of in
10731084 /// `content`. vLLM/SGLang backends (used by NEAR AI) return the field
10741085 /// as `reasoning`; other APIs (GLM-5, DeepSeek) use `reasoning_content`.
1075- #[ serde( default , alias = "reasoning" ) ]
1086+ #[ serde( default ) ]
10761087 reasoning_content : Option < String > ,
1088+ #[ serde( default ) ]
1089+ reasoning : Option < String > ,
10771090 tool_calls : Option < Vec < ChatCompletionToolCall > > ,
10781091}
10791092
@@ -1454,7 +1467,7 @@ mod tests {
14541467 assert_eq ! ( output, default_out) ;
14551468 }
14561469
1457- /// Regression: reasoning_content must NOT leak into tool-call responses.
1470+ /// Regression: reasoning fallbacks must NOT leak into tool-call responses.
14581471 #[ test]
14591472 fn test_reasoning_content_not_leaked_into_tool_call_response ( ) {
14601473 let response: ChatCompletionResponse = serde_json:: from_value ( serde_json:: json!( {
@@ -1464,6 +1477,7 @@ mod tests {
14641477 "role" : "assistant" ,
14651478 "content" : null,
14661479 "reasoning_content" : "Let me think about which tool to call..." ,
1480+ "reasoning" : "Secondary reasoning fallback text" ,
14671481 "tool_calls" : [ {
14681482 "id" : "call_abc123" ,
14691483 "type" : "function" ,
@@ -1480,9 +1494,15 @@ mod tests {
14801494 . unwrap ( ) ;
14811495
14821496 let choice = response. choices . into_iter ( ) . next ( ) . unwrap ( ) ;
1483- let tool_calls: Vec < ToolCall > = choice
1484- . message
1485- . tool_calls
1497+ let ChatCompletionResponseMessage {
1498+ content : message_content,
1499+ reasoning_content,
1500+ reasoning,
1501+ tool_calls : message_tool_calls,
1502+ ..
1503+ } = choice. message ;
1504+ let reasoning_fallback = reasoning_content. or ( reasoning) ;
1505+ let tool_calls: Vec < ToolCall > = message_tool_calls
14861506 . unwrap_or_default ( )
14871507 . into_iter ( )
14881508 . map ( |tc| {
@@ -1498,14 +1518,14 @@ mod tests {
14981518 . collect ( ) ;
14991519
15001520 let content = if tool_calls. is_empty ( ) {
1501- choice . message . content . or ( choice . message . reasoning_content )
1521+ message_content . or ( reasoning_fallback )
15021522 } else {
1503- choice . message . content
1523+ message_content
15041524 } ;
15051525
15061526 assert ! (
15071527 content. is_none( ) ,
1508- "reasoning_content should NOT leak into tool-call responses, got: {:?}" ,
1528+ "reasoning fallbacks should NOT leak into tool-call responses, got: {:?}" ,
15091529 content
15101530 ) ;
15111531 assert_eq ! ( tool_calls. len( ) , 1 ) ;
@@ -1521,7 +1541,8 @@ mod tests {
15211541 "message" : {
15221542 "role" : "assistant" ,
15231543 "content" : null,
1524- "reasoning_content" : "The answer is 42."
1544+ "reasoning_content" : "The answer is 42." ,
1545+ "reasoning" : "Backup reasoning text"
15251546 } ,
15261547 "finish_reason" : "stop"
15271548 } ] ,
@@ -1530,9 +1551,15 @@ mod tests {
15301551 . unwrap ( ) ;
15311552
15321553 let choice = response. choices . into_iter ( ) . next ( ) . unwrap ( ) ;
1533- let tool_calls: Vec < ToolCall > = choice
1534- . message
1535- . tool_calls
1554+ let ChatCompletionResponseMessage {
1555+ content : message_content,
1556+ reasoning_content,
1557+ reasoning,
1558+ tool_calls : message_tool_calls,
1559+ ..
1560+ } = choice. message ;
1561+ let reasoning_fallback = reasoning_content. or ( reasoning) ;
1562+ let tool_calls: Vec < ToolCall > = message_tool_calls
15361563 . unwrap_or_default ( )
15371564 . into_iter ( )
15381565 . map ( |tc| {
@@ -1548,9 +1575,9 @@ mod tests {
15481575 . collect ( ) ;
15491576
15501577 let content = if tool_calls. is_empty ( ) {
1551- choice . message . content . or ( choice . message . reasoning_content )
1578+ message_content . or ( reasoning_fallback )
15521579 } else {
1553- choice . message . content
1580+ message_content
15541581 } ;
15551582
15561583 assert_eq ! (
@@ -1562,7 +1589,7 @@ mod tests {
15621589 }
15631590
15641591 /// The vLLM/SGLang API returns `reasoning` (not `reasoning_content`).
1565- /// Verify that the serde alias deserializes it correctly .
1592+ /// Verify that this dedicated field is consumed as fallback content .
15661593 #[ test]
15671594 fn test_reasoning_field_alias_accepted ( ) {
15681595 let response: ChatCompletionResponse = serde_json:: from_value ( serde_json:: json!( {
@@ -1581,12 +1608,18 @@ mod tests {
15811608 . unwrap ( ) ;
15821609
15831610 let choice = response. choices . into_iter ( ) . next ( ) . unwrap ( ) ;
1584- let content = choice. message . content . or ( choice. message . reasoning_content ) ;
1611+ let ChatCompletionResponseMessage {
1612+ content,
1613+ reasoning_content,
1614+ reasoning,
1615+ ..
1616+ } = choice. message ;
1617+ let content = content. or ( reasoning_content. or ( reasoning) ) ;
15851618
15861619 assert_eq ! (
15871620 content,
15881621 Some ( "The answer is 42." . to_string( ) ) ,
1589- "reasoning field (vLLM alias) should deserialize into reasoning_content "
1622+ "reasoning should be used as fallback content "
15901623 ) ;
15911624 }
15921625
@@ -1618,9 +1651,15 @@ mod tests {
16181651 . unwrap ( ) ;
16191652
16201653 let choice = response. choices . into_iter ( ) . next ( ) . unwrap ( ) ;
1621- let tool_calls: Vec < ToolCall > = choice
1622- . message
1623- . tool_calls
1654+ let ChatCompletionResponseMessage {
1655+ content : message_content,
1656+ reasoning_content,
1657+ reasoning,
1658+ tool_calls : message_tool_calls,
1659+ ..
1660+ } = choice. message ;
1661+ let reasoning_fallback = reasoning_content. or ( reasoning) ;
1662+ let tool_calls: Vec < ToolCall > = message_tool_calls
16241663 . unwrap_or_default ( )
16251664 . into_iter ( )
16261665 . map ( |tc| {
@@ -1636,9 +1675,9 @@ mod tests {
16361675 . collect ( ) ;
16371676
16381677 let content = if tool_calls. is_empty ( ) {
1639- choice . message . content . or ( choice . message . reasoning_content )
1678+ message_content . or ( reasoning_fallback )
16401679 } else {
1641- choice . message . content
1680+ message_content
16421681 } ;
16431682
16441683 assert ! (
@@ -1648,6 +1687,66 @@ mod tests {
16481687 assert_eq ! ( tool_calls. len( ) , 1 ) ;
16491688 }
16501689
1690+ /// Regression: payloads that include BOTH reasoning fields must parse
1691+ /// successfully and honor fallback precedence:
1692+ /// content -> reasoning_content -> reasoning.
1693+ #[ test]
1694+ fn test_both_reasoning_fields_parse_with_defined_precedence ( ) {
1695+ // Case 1: content is present, so it wins over both reasoning fields.
1696+ let response_with_content: ChatCompletionResponse =
1697+ serde_json:: from_value ( serde_json:: json!( {
1698+ "id" : "chatcmpl-test-content" ,
1699+ "choices" : [ {
1700+ "message" : {
1701+ "role" : "assistant" ,
1702+ "content" : "Final answer in content." ,
1703+ "reasoning_content" : "Reasoning content fallback" ,
1704+ "reasoning" : "Reasoning alias fallback"
1705+ } ,
1706+ "finish_reason" : "stop"
1707+ } ]
1708+ } ) )
1709+ . expect ( "payload with both reasoning fields should deserialize" ) ;
1710+ let choice = response_with_content. choices . into_iter ( ) . next ( ) . unwrap ( ) ;
1711+ let ChatCompletionResponseMessage {
1712+ content,
1713+ reasoning_content,
1714+ reasoning,
1715+ ..
1716+ } = choice. message ;
1717+ let selected = content
1718+ . or ( reasoning_content. or ( reasoning) )
1719+ . expect ( "content should be selected" ) ;
1720+ assert_eq ! ( selected, "Final answer in content." ) ;
1721+
1722+ // Case 2: content is null; reasoning_content should win over reasoning.
1723+ let response_without_content: ChatCompletionResponse =
1724+ serde_json:: from_value ( serde_json:: json!( {
1725+ "id" : "chatcmpl-test-reasoning" ,
1726+ "choices" : [ {
1727+ "message" : {
1728+ "role" : "assistant" ,
1729+ "content" : null,
1730+ "reasoning_content" : "Preferred reasoning_content" ,
1731+ "reasoning" : "Secondary reasoning"
1732+ } ,
1733+ "finish_reason" : "stop"
1734+ } ]
1735+ } ) )
1736+ . expect ( "payload with both reasoning fields should deserialize" ) ;
1737+ let choice = response_without_content. choices . into_iter ( ) . next ( ) . unwrap ( ) ;
1738+ let ChatCompletionResponseMessage {
1739+ content,
1740+ reasoning_content,
1741+ reasoning,
1742+ ..
1743+ } = choice. message ;
1744+ let selected = content
1745+ . or ( reasoning_content. or ( reasoning) )
1746+ . expect ( "reasoning fallback should be selected" ) ;
1747+ assert_eq ! ( selected, "Preferred reasoning_content" ) ;
1748+ }
1749+
16511750 #[ tokio:: test]
16521751 async fn test_resolve_bearer_token_config_api_key ( ) {
16531752 // When config.api_key is set, it takes top priority.
0 commit comments