diff --git a/CHANGELOG.md b/CHANGELOG.md index 040d34c8..253dad10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `ChatOllama()` can now be created without a `model`, so `ChatOllama().list_models()` works. Sending a message without a model still raises an error that lists the available models. (#166) * `ChatGoogle()` now reports `finish_reason == "content_filter"` for Gemini's `IMAGE_SAFETY`, `IMAGE_PROHIBITED_CONTENT`, `IMAGE_RECITATION` and `LANGUAGE` finish reasons, instead of passing the raw Gemini string through, and reports `"tool_use"` (rather than `"success"`) when a turn ends with a tool request, as `ChatOpenAI()` does. (#473) * The error raised for a tool request with invalid JSON arguments from `ChatOpenAI()` and `ChatOpenAICompletions()` (and the providers built on them) now shows the offending arguments, instead of the literal text `{args}`. +* `ChatMistral(seed=0)` now sends `random_seed: 0` instead of silently dropping the seed, and a seed no longer replaces an `extra_body` passed through `kwargs`: Mistral's `random_seed` is merged into it. (#475) * `content_image_url()` with a `data:` URL no longer silently drops the `detail` argument: the setting is now carried by inline image content and forwarded to OpenAI's Responses and Chat Completions APIs (regular image URLs already forwarded it). Other providers continue to ignore `detail`. (#467) * `ChatBedrock()` on a Converse model no longer drops redacted reasoning blocks when streaming. They were discarded as they arrived, so a streamed turn lost them, while a non-streamed one kept them. (#468) * `ChatBedrockAnthropic()` now uses native structured outputs for `data_model` on Claude 4.5 and later, as `ChatAnthropic()` does. Its Bedrock model IDs (`anthropic.claude-...`, `us.anthropic.claude-...`) weren't recognized, so it fell back to forcing a tool call, which Claude Opus 5.5 and Sonnet 5.5 reject with a 400. diff --git a/chatlas/_provider_mistral.py b/chatlas/_provider_mistral.py index f7826f17..826bca63 100644 --- a/chatlas/_provider_mistral.py +++ b/chatlas/_provider_mistral.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Optional, cast from ._chat import Chat from ._logging import log_model_default @@ -170,12 +170,16 @@ def _chat_perform_args( del kwargs2["stream_options"] # Mistral wants random_seed, not seed - if seed := kwargs2.pop("seed", None): - if isinstance(seed, int): - kwargs2["extra_body"] = {"random_seed": seed} - elif seed is not None: + seed = kwargs2.pop("seed", None) + if seed is not None: + if not isinstance(seed, int): raise ValueError( "MistralProvider only accepts an integer seed, or None." ) + extra_body: dict[str, Any] = dict( + cast("dict[str, Any]", kwargs2.get("extra_body") or {}) + ) + extra_body["random_seed"] = seed + kwargs2["extra_body"] = extra_body return kwargs2 diff --git a/tests/test_provider_mistral.py b/tests/test_provider_mistral.py index bb3c4a41..f5455209 100644 --- a/tests/test_provider_mistral.py +++ b/tests/test_provider_mistral.py @@ -78,3 +78,18 @@ def test_data_extraction(): @pytest.mark.vcr def test_mistral_model_list(): assert_list_models(ChatMistral) + + +def test_mistral_seed_zero_is_sent_as_random_seed(): + chat = ChatMistral(api_key="key", seed=0) + args = chat.provider._chat_perform_args(False, [], {}) + assert "seed" not in args + assert args["extra_body"] == {"random_seed": 0} + + +def test_mistral_seed_keeps_user_extra_body(): + chat = ChatMistral(api_key="key", seed=7) + args = chat.provider._chat_perform_args( + False, [], {}, kwargs={"extra_body": {"safe_prompt": True}} + ) + assert args["extra_body"] == {"safe_prompt": True, "random_seed": 7}