diff --git a/examples/10_Agentic_Inference/README.md b/examples/10_Agentic_Inference/README.md index ab3673b51..e8eb565d9 100644 --- a/examples/10_Agentic_Inference/README.md +++ b/examples/10_Agentic_Inference/README.md @@ -19,7 +19,8 @@ contiguous and ordered by increasing `turn`. Required fields are `conversation_id`, `turn`, and `role`. User rows normally include `content`; agentic rows can also include `system`, `tools`, -`tool_calls`, `tool_results`, `reasoning_content`, and `delay_seconds`. +`tool_calls`, `tool_results`, `reasoning_content`, `reasoning`, and +`delay_seconds`. Place the dataset under `examples/10_Agentic_Inference/datasets/` or point the YAML at another accessible JSONL path. diff --git a/src/inference_endpoint/dataset_manager/agentic_inference_dataset.py b/src/inference_endpoint/dataset_manager/agentic_inference_dataset.py index aa054d15d..dbae868f8 100644 --- a/src/inference_endpoint/dataset_manager/agentic_inference_dataset.py +++ b/src/inference_endpoint/dataset_manager/agentic_inference_dataset.py @@ -147,6 +147,20 @@ def _build_conversation_metadata( for _, row in sorted_group.iterrows(): role = row.get("role") + reasoning_content = row.get("reasoning_content") + reasoning = row.get("reasoning") + if ( + reasoning_content is not None + and not ( + isinstance(reasoning_content, float) and pd.isna(reasoning_content) + ) + and reasoning is not None + and not (isinstance(reasoning, float) and pd.isna(reasoning)) + ): + raise InputValidationError( + f"conversation {row.get('conversation_id')!r} turn {row.get('turn')} " + "has both 'reasoning_content' and 'reasoning' values" + ) # Format this row into message(s) using the same field extraction as before. expanded = _expand_tool_results(row) @@ -161,6 +175,7 @@ def _build_conversation_metadata( "tool_calls", "tool_results", "reasoning_content", + "reasoning", ): val = row.get(key) if val is not None and not (isinstance(val, float) and pd.isna(val)): diff --git a/src/inference_endpoint/openai/openai_msgspec_adapter.py b/src/inference_endpoint/openai/openai_msgspec_adapter.py index 63318c88e..d639cd949 100644 --- a/src/inference_endpoint/openai/openai_msgspec_adapter.py +++ b/src/inference_endpoint/openai/openai_msgspec_adapter.py @@ -49,13 +49,15 @@ def _chat_message_from_dict(msg: dict) -> "ChatMessage": """Build a ChatMessage from a dict, forwarding all supported fields.""" + reasoning_value = msg.get("reasoning_content") or msg.get("reasoning") return ChatMessage( role=msg["role"], content=msg.get("content"), name=msg.get("name"), tool_calls=msg.get("tool_calls"), tool_call_id=msg.get("tool_call_id"), - reasoning_content=msg.get("reasoning_content"), + reasoning_content=reasoning_value, + reasoning=reasoning_value, ) diff --git a/src/inference_endpoint/openai/types.py b/src/inference_endpoint/openai/types.py index 400baf704..77fa16f2f 100644 --- a/src/inference_endpoint/openai/types.py +++ b/src/inference_endpoint/openai/types.py @@ -101,6 +101,7 @@ class ChatMessage( tool_calls: list[dict[str, Any]] | None = None tool_call_id: str | None = None reasoning_content: str | None = None + reasoning: str | None = None # gc=False: audit 2026-05: request containers set at construction; frozen=True blocks field reassignment. diff --git a/tests/unit/dataset_manager/test_agentic_inference_dataset.py b/tests/unit/dataset_manager/test_agentic_inference_dataset.py index 9d0df6a01..7bcda39ab 100644 --- a/tests/unit/dataset_manager/test_agentic_inference_dataset.py +++ b/tests/unit/dataset_manager/test_agentic_inference_dataset.py @@ -973,6 +973,51 @@ def test_build_metadata_pre_built_messages_no_tools(): assert msgs[2] == {"role": "user", "content": "C"} +@pytest.mark.unit +@pytest.mark.parametrize( + ("message_fields", "expected_field"), + [ + ({"reasoning_content": "thinking"}, "reasoning_content"), + ({"reasoning": "thinking"}, "reasoning"), + ({"reasoning_content": "thinking", "reasoning": "thinking"}, None), + ], + ids=["reasoning_content", "reasoning", "both_rejected"], +) +def test_pre_built_messages_reasoning_alias_policy( + message_fields: dict[str, str], expected_field: str | None +): + df = pd.DataFrame( + [ + {"conversation_id": "c1", "turn": 1, "role": "user", "content": "A"}, + { + "conversation_id": "c1", + "turn": 2, + "role": "assistant", + "content": "B", + **message_fields, + }, + {"conversation_id": "c1", "turn": 3, "role": "user", "content": "C"}, + ] + ) + ds = AgenticInferenceDataset(df) + + if expected_field is None: + with pytest.raises( + InputValidationError, + match=( + "conversation 'c1' turn 2 has both " + "'reasoning_content' and 'reasoning'" + ), + ): + ds.load() + return + + ds.load() + assert ds.conversation_metadata is not None + msgs = ds.conversation_metadata.pre_built_messages_by_key[("c1", 3)] + assert msgs[1][expected_field] == "thinking" + + @pytest.mark.unit def test_load_sample_includes_messages(): """load_sample returns messages with the complete message list.""" diff --git a/tests/unit/openai/test_msgspec_adapter.py b/tests/unit/openai/test_msgspec_adapter.py index 1fac94d26..a11c7aa52 100644 --- a/tests/unit/openai/test_msgspec_adapter.py +++ b/tests/unit/openai/test_msgspec_adapter.py @@ -165,6 +165,39 @@ def test_chat_message_from_dict_all_fields(): assert msg.tool_call_id is None +@pytest.mark.unit +@pytest.mark.parametrize( + ("message_fields", "expected"), + [ + ({"reasoning_content": "thinking via sglang"}, "thinking via sglang"), + ({"reasoning": "thinking via vllm"}, "thinking via vllm"), + ({"reasoning_content": "sglang", "reasoning": "vllm"}, "sglang"), + ], + ids=["reasoning_content", "reasoning", "reasoning_content_precedence"], +) +def test_to_endpoint_request_forwards_both_reasoning_aliases(message_fields, expected): + query = Query( + id="q-reasoning", + data={ + "model": "m", + "messages": [ + { + "role": "assistant", + "content": "answer", + **message_fields, + } + ], + }, + ) + + request = OpenAIMsgspecAdapter.to_endpoint_request(query) + payload = json.loads(msgspec.json.encode(request)) + + msg = payload["messages"][0] + assert msg["reasoning_content"] == expected + assert msg["reasoning"] == expected + + @pytest.mark.unit def test_chat_message_content_optional(): """ChatMessage accepts content=None for tool-dispatching assistant turns."""