diff --git a/README.md b/README.md index 0e08a2a..0726b15 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ The response contains generated text in `choices`. For other clients, use base URL `http://127.0.0.1:11500/v1`, model `flint`, and the API key from `rec auth token`. +When replaying assistant messages, either `reasoning` or `reasoning_content` is +accepted. If both are present, `reasoning` takes precedence, including when its +value is empty or null. + The Anthropic-compatible API is available at `http://127.0.0.1:11500/v1/messages`. Point `ANTHROPIC_BASE_URL` at `http://127.0.0.1:11500` and send the token from `rec auth token` as a bearer diff --git a/engine/src/rec_mlx/server.py b/engine/src/rec_mlx/server.py index 4b81ab0..8a38c92 100644 --- a/engine/src/rec_mlx/server.py +++ b/engine/src/rec_mlx/server.py @@ -286,8 +286,7 @@ def handle_completion(self, request, stop_words): try: for message in request.messages: if "reasoning" in message: - if "reasoning_content" in message: - raise ValueError("Use one reasoning field per message") + # Prefer the public response carrier when clients replay both aliases. message["reasoning_content"] = message.pop("reasoning") process_message_content(request.messages) mode = self.body["grammar"] if "grammar" in self.body else grammar_mode diff --git a/engine/tests/test_serving.py b/engine/tests/test_serving.py index f3d9cbf..4f57fa2 100644 --- a/engine/tests/test_serving.py +++ b/engine/tests/test_serving.py @@ -92,6 +92,29 @@ def generate_tokens(self, ids, sampling, *, should_stop, return_logprobs=False, class ServingTest(unittest.TestCase): + def test_reasoning_alias_precedence(self): + for stream in (False, True): + for fields, expected in ( + ({"reasoning": "a"}, "a"), + ({"reasoning_content": "b"}, "b"), + ({"reasoning": "a", "reasoning_content": "a"}, "a"), + ({"reasoning": "a", "reasoning_content": "b"}, "a"), + ({"reasoning": "", "reasoning_content": "b"}, ""), + ({"reasoning": None, "reasoning_content": "b"}, None), + ): + with self.subTest(stream=stream, fields=fields): + with patch.object(self.engine, "prompt", wraps=self.engine.prompt) as prompt: + status, body = self.request( + "B", + max_tokens=1, + stream=stream, + messages=[{"role": "assistant", "content": "B", **fields}], + ) + self.assertEqual(status, 200, body) + seen = prompt.call_args.args[0][0] + self.assertEqual(seen["reasoning_content"], expected) + self.assertNotIn("reasoning", seen) + def test_idle_preparation_follows_response_and_yields_to_queued_request(self): preparing = threading.Event() proceed = threading.Event()