diff --git a/CHANGELOG.md b/CHANGELOG.md index 040d34c8..673eadbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `Chat.export()` and `interpolate_file()` now always read and write files as UTF-8. Previously they relied on the platform's default locale encoding, so on a non-UTF-8 locale `export()` could raise `UnicodeEncodeError` and `interpolate_file()` could silently return a corrupted (mojibake) prompt instead of the file's contents. (#437) * `ChatAnthropic()` no longer drops assistant turns that have no content; doing so could produce two consecutive user messages, violating the API's user/assistant alternation requirement. A `"[empty string]"` placeholder is sent instead, matching how empty text content is already normalized. (#416) * Providers built on the Chat Completions API (`ChatOpenAICompletions()`, `ChatGroq()`, `ChatOpenRouter()`, `ChatDeepSeek()`, etc.) now put the tool results of a turn ahead of the user's text in the same turn, as the API requires. Resuming a chat that ended with an unanswered tool request (or any turn that mixes tool results and new text) sent the user message first, which these APIs reject because a tool message must directly follow the assistant message that requested it. (#476) +* `ChatMistral()` now reports `"context_window"` as the finish reason when a response stops with Mistral's `model_length` (the request reached the model's context length), instead of passing the raw string through. As for other providers, this makes chatlas warn about the truncated response (or raise an error when a data model was requested). (#477) ## [0.23.0] - 2026-09-04 diff --git a/chatlas/_provider_openai_completions.py b/chatlas/_provider_openai_completions.py index 5051bcfc..1d9f6106 100644 --- a/chatlas/_provider_openai_completions.py +++ b/chatlas/_provider_openai_completions.py @@ -150,6 +150,9 @@ def ChatOpenAICompletions( "tool_calls": "tool_use", "length": "max_tokens", "content_filter": "content_filter", + # Mistral: the request ran into the model's context length + # https://docs.mistral.ai/api/endpoint/chat + "model_length": "context_window", } diff --git a/tests/test_provider_openai_completions.py b/tests/test_provider_openai_completions.py index eb5178ee..053ab6d1 100644 --- a/tests/test_provider_openai_completions.py +++ b/tests/test_provider_openai_completions.py @@ -45,6 +45,7 @@ def test_normalize_finish_reason_maps_known_reasons(): assert completions_normalize_finish_reason("length") == "max_tokens" assert completions_normalize_finish_reason("content_filter") == "content_filter" assert completions_normalize_finish_reason("tool_calls") == "tool_use" + assert completions_normalize_finish_reason("model_length") == "context_window" def test_normalize_finish_reason_passes_through_unknown(): @@ -597,4 +598,24 @@ def test_load_tool_request_args_error_names_the_arguments(): assert "(get_weather)" in message assert '\'{"city": "Paris"\'' in message assert "{args}" not in message - assert 'input arguments: \'{"city": "Paris"\'. This can happen' in message \ No newline at end of file + assert 'input arguments: \'{"city": "Paris"\'. This can happen' in message + + +def test_model_length_finish_reason_warns_about_the_context_window(): + completion = ChatCompletion.construct( + id="x", + choices=[ + { + "index": 0, + "finish_reason": "model_length", + "message": {"role": "assistant", "content": "partial"}, + } + ], + ) + provider = OpenAICompletionsProvider(api_key="key", model="m") + + turn = provider.value_turn(completion, has_data_model=False) + assert turn.finish_reason == "context_window" + + with pytest.raises(ValueError, match="context window"): + provider.value_turn(completion, has_data_model=True)