From 7aef107dc318a6bdd86ccd001cd1bf35e5e84624 Mon Sep 17 00:00:00 2001 From: "Li, Tianmu" Date: Tue, 14 Jul 2026 11:04:58 -0700 Subject: [PATCH 1/2] Forward reasoning aliases in OpenAI requests --- examples/10_Agentic_Inference/README.md | 3 +- .../agentic_inference_dataset.py | 15 +++++ .../openai/openai_msgspec_adapter.py | 4 +- src/inference_endpoint/openai/types.py | 1 + .../test_agentic_inference_dataset.py | 51 ++++++++++++++++ tests/unit/openai/test_msgspec_adapter.py | 59 +++++++++++++++++++ 6 files changed, 131 insertions(+), 2 deletions(-) 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..d576ff9cd 100644 --- a/tests/unit/dataset_manager/test_agentic_inference_dataset.py +++ b/tests/unit/dataset_manager/test_agentic_inference_dataset.py @@ -973,6 +973,57 @@ def test_build_metadata_pre_built_messages_no_tools(): assert msgs[2] == {"role": "user", "content": "C"} +@pytest.mark.unit +@pytest.mark.parametrize("source_field", ["reasoning_content", "reasoning"]) +def test_pre_built_messages_preserve_reasoning_aliases(source_field: str): + df = pd.DataFrame( + [ + {"conversation_id": "c1", "turn": 1, "role": "user", "content": "A"}, + { + "conversation_id": "c1", + "turn": 2, + "role": "assistant", + "content": "B", + source_field: "thinking", + }, + {"conversation_id": "c1", "turn": 3, "role": "user", "content": "C"}, + ] + ) + ds = AgenticInferenceDataset(df) + ds.load() + + assert ds.conversation_metadata is not None + msgs = ds.conversation_metadata.pre_built_messages_by_key[("c1", 3)] + assert msgs[1][source_field] == "thinking" + + +@pytest.mark.unit +def test_pre_built_messages_reject_both_reasoning_aliases(): + df = pd.DataFrame( + [ + {"conversation_id": "c1", "turn": 1, "role": "user", "content": "A"}, + { + "conversation_id": "c1", + "turn": 2, + "role": "assistant", + "content": "B", + "reasoning_content": "thinking", + "reasoning": "thinking", + }, + {"conversation_id": "c1", "turn": 3, "role": "user", "content": "C"}, + ] + ) + ds = AgenticInferenceDataset(df) + + with pytest.raises( + InputValidationError, + match=( + "conversation 'c1' turn 2 has both " "'reasoning_content' and 'reasoning'" + ), + ): + ds.load() + + @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..51e162479 100644 --- a/tests/unit/openai/test_msgspec_adapter.py +++ b/tests/unit/openai/test_msgspec_adapter.py @@ -165,6 +165,65 @@ 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": "same", "reasoning": "same"}, "same"), + ], + ids=["reasoning_content", "reasoning", "both_equal"], +) +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_to_endpoint_request_uses_reasoning_content_precedence(): + query = Query( + id="q-reasoning-precedence", + data={ + "model": "m", + "messages": [ + { + "role": "assistant", + "content": "answer", + "reasoning_content": "sglang", + "reasoning": "vllm", + } + ], + }, + ) + + payload = json.loads( + msgspec.json.encode(OpenAIMsgspecAdapter.to_endpoint_request(query)) + ) + + msg = payload["messages"][0] + assert msg["reasoning_content"] == "sglang" + assert msg["reasoning"] == "sglang" + + @pytest.mark.unit def test_chat_message_content_optional(): """ChatMessage accepts content=None for tool-dispatching assistant turns.""" From 374caf2ba3d9c36274489f957531d7e908e65374 Mon Sep 17 00:00:00 2001 From: "Li, Tianmu" Date: Tue, 14 Jul 2026 11:17:53 -0700 Subject: [PATCH 2/2] Consolidate reasoning alias tests --- .../test_agentic_inference_dataset.py | 58 +++++++++---------- tests/unit/openai/test_msgspec_adapter.py | 30 +--------- 2 files changed, 28 insertions(+), 60 deletions(-) diff --git a/tests/unit/dataset_manager/test_agentic_inference_dataset.py b/tests/unit/dataset_manager/test_agentic_inference_dataset.py index d576ff9cd..7bcda39ab 100644 --- a/tests/unit/dataset_manager/test_agentic_inference_dataset.py +++ b/tests/unit/dataset_manager/test_agentic_inference_dataset.py @@ -974,8 +974,18 @@ def test_build_metadata_pre_built_messages_no_tools(): @pytest.mark.unit -@pytest.mark.parametrize("source_field", ["reasoning_content", "reasoning"]) -def test_pre_built_messages_preserve_reasoning_aliases(source_field: str): +@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"}, @@ -984,44 +994,28 @@ def test_pre_built_messages_preserve_reasoning_aliases(source_field: str): "turn": 2, "role": "assistant", "content": "B", - source_field: "thinking", + **message_fields, }, {"conversation_id": "c1", "turn": 3, "role": "user", "content": "C"}, ] ) ds = AgenticInferenceDataset(df) - ds.load() + 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][source_field] == "thinking" - - -@pytest.mark.unit -def test_pre_built_messages_reject_both_reasoning_aliases(): - df = pd.DataFrame( - [ - {"conversation_id": "c1", "turn": 1, "role": "user", "content": "A"}, - { - "conversation_id": "c1", - "turn": 2, - "role": "assistant", - "content": "B", - "reasoning_content": "thinking", - "reasoning": "thinking", - }, - {"conversation_id": "c1", "turn": 3, "role": "user", "content": "C"}, - ] - ) - ds = AgenticInferenceDataset(df) - - with pytest.raises( - InputValidationError, - match=( - "conversation 'c1' turn 2 has both " "'reasoning_content' and 'reasoning'" - ), - ): - ds.load() + assert msgs[1][expected_field] == "thinking" @pytest.mark.unit diff --git a/tests/unit/openai/test_msgspec_adapter.py b/tests/unit/openai/test_msgspec_adapter.py index 51e162479..a11c7aa52 100644 --- a/tests/unit/openai/test_msgspec_adapter.py +++ b/tests/unit/openai/test_msgspec_adapter.py @@ -171,9 +171,9 @@ def test_chat_message_from_dict_all_fields(): [ ({"reasoning_content": "thinking via sglang"}, "thinking via sglang"), ({"reasoning": "thinking via vllm"}, "thinking via vllm"), - ({"reasoning_content": "same", "reasoning": "same"}, "same"), + ({"reasoning_content": "sglang", "reasoning": "vllm"}, "sglang"), ], - ids=["reasoning_content", "reasoning", "both_equal"], + ids=["reasoning_content", "reasoning", "reasoning_content_precedence"], ) def test_to_endpoint_request_forwards_both_reasoning_aliases(message_fields, expected): query = Query( @@ -198,32 +198,6 @@ def test_to_endpoint_request_forwards_both_reasoning_aliases(message_fields, exp assert msg["reasoning"] == expected -@pytest.mark.unit -def test_to_endpoint_request_uses_reasoning_content_precedence(): - query = Query( - id="q-reasoning-precedence", - data={ - "model": "m", - "messages": [ - { - "role": "assistant", - "content": "answer", - "reasoning_content": "sglang", - "reasoning": "vllm", - } - ], - }, - ) - - payload = json.loads( - msgspec.json.encode(OpenAIMsgspecAdapter.to_endpoint_request(query)) - ) - - msg = payload["messages"][0] - assert msg["reasoning_content"] == "sglang" - assert msg["reasoning"] == "sglang" - - @pytest.mark.unit def test_chat_message_content_optional(): """ChatMessage accepts content=None for tool-dispatching assistant turns."""