diff --git a/python/packages/mistral/agent_framework_mistral/_chat_client.py b/python/packages/mistral/agent_framework_mistral/_chat_client.py index b97fb8f072..bc95b9dadd 100644 --- a/python/packages/mistral/agent_framework_mistral/_chat_client.py +++ b/python/packages/mistral/agent_framework_mistral/_chat_client.py @@ -154,6 +154,8 @@ class MistralSettings(TypedDict, total=False): api_key: Mistral API key. Resolved from ``MISTRAL_API_KEY``. chat_model: Chat model name. Resolved from ``MISTRAL_CHAT_MODEL``. server_url: Optional server URL override. Resolved from ``MISTRAL_SERVER_URL``. + Ignored when a Mistral SDK client is injected; the injected client's own + endpoint is reported instead. """ api_key: SecretString | None @@ -389,20 +391,23 @@ def __init__( required_fields=[] if client is not None or http_client is not None else ["api_key"], api_key=api_key, chat_model=model, - server_url=server_url, + server_url=None if client is not None else server_url, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) self.model = mistral_settings.get("chat_model") - self.server_url = mistral_settings.get("server_url") self._owns_client = not isinstance(client, Mistral) self._client_closed = False + # Settings are only consulted for a client this wrapper creates; an + # injected SDK client has its own endpoint and requires no key. + self.server_url = mistral_settings.get("server_url") if isinstance(client, Mistral): + # The injected SDK client owns the endpoint: requests go to it, so + # metadata must report its URL, not an ambient MISTRAL_SERVER_URL. + self.server_url = client.sdk_configuration.get_server_details()[0] self.client = client - if self.server_url is None: - self.server_url = client.sdk_configuration.get_server_details()[0] else: client_kwargs: dict[str, Any] = {"timeout_ms": _DEFAULT_TIMEOUT_MS} if resolved_api_key := mistral_settings.get("api_key"): diff --git a/python/packages/mistral/agent_framework_mistral/_embedding_client.py b/python/packages/mistral/agent_framework_mistral/_embedding_client.py index 29b0107c22..ee3990bc40 100644 --- a/python/packages/mistral/agent_framework_mistral/_embedding_client.py +++ b/python/packages/mistral/agent_framework_mistral/_embedding_client.py @@ -76,6 +76,8 @@ class MistralEmbeddingSettings(TypedDict, total=False): api_key: Mistral API key. Resolved from ``MISTRAL_API_KEY``. embedding_model: Embedding model name. Resolved from ``MISTRAL_EMBEDDING_MODEL``. server_url: Optional server URL override. Resolved from ``MISTRAL_SERVER_URL``. + Ignored when a Mistral SDK client is injected; the injected client's own + endpoint is reported instead. """ api_key: SecretString | None @@ -146,19 +148,22 @@ def __init__( required_fields=required_fields, api_key=api_key, embedding_model=model, - server_url=server_url, + server_url=None if client is not None else server_url, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) self.model: str = mistral_settings["embedding_model"] # type: ignore[assignment] - self.server_url = mistral_settings.get("server_url") self._owns_client = not isinstance(client, Mistral) + # Settings are only consulted for a client this wrapper creates; an + # injected SDK client has its own endpoint and requires no key. + self.server_url = mistral_settings.get("server_url") if isinstance(client, Mistral): + # The injected SDK client owns the endpoint: requests go to it, so + # metadata must report its URL, not an ambient MISTRAL_SERVER_URL. + self.server_url = client.sdk_configuration.get_server_details()[0] self.client = client - if self.server_url is None: - self.server_url = client.sdk_configuration.get_server_details()[0] else: client_kwargs: dict[str, Any] = {"timeout_ms": _DEFAULT_TIMEOUT_MS} if resolved_api_key := mistral_settings.get("api_key"): diff --git a/python/packages/mistral/tests/mistral/test_mistral_injected_client_endpoint.py b/python/packages/mistral/tests/mistral/test_mistral_injected_client_endpoint.py new file mode 100644 index 0000000000..14b135f9e1 --- /dev/null +++ b/python/packages/mistral/tests/mistral/test_mistral_injected_client_endpoint.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""An injected Mistral SDK client owns its endpoint (#9965). + +The wrapper used to resolve ``MISTRAL_SERVER_URL`` before inspecting the +injected client, so ``service_url()`` reported the ambient URL (used for +telemetry) while requests went to the injected client's endpoint. +""" + +import os + +from mistralai.client import Mistral + +from agent_framework_mistral import MistralChatClient, MistralEmbeddingClient, RawMistralChatClient + + +def _make_injected_sdk(server_url: str) -> Mistral: + return Mistral(api_key="dummy", server_url=server_url) + + +def test_chat_service_url_uses_injected_client_endpoint(monkeypatch) -> None: + monkeypatch.setenv("MISTRAL_SERVER_URL", "https://env.example") + sdk = _make_injected_sdk("https://client.example") + + client = RawMistralChatClient(client=sdk, model="mistral-test") + + assert client.service_url() == "https://client.example" + assert client.server_url == "https://client.example" + + +def test_embedding_service_url_uses_injected_client_endpoint(monkeypatch) -> None: + monkeypatch.setenv("MISTRAL_SERVER_URL", "https://env.example") + sdk = _make_injected_sdk("https://client.example") + + client = MistralEmbeddingClient(client=sdk, model="mistral-embed") + + assert client.service_url() == "https://client.example" + assert client.server_url == "https://client.example" + + +def test_chat_service_url_uses_env_when_no_client(monkeypatch) -> None: + monkeypatch.setenv("MISTRAL_SERVER_URL", "https://env.example") + monkeypatch.setenv("MISTRAL_API_KEY", "env-key") + + client = MistralChatClient() + + assert client.service_url() == "https://env.example" + + +def test_owned_client_keeps_explicit_server_url(monkeypatch) -> None: + monkeypatch.setenv("MISTRAL_API_KEY", "env-key") + monkeypatch.setenv("MISTRAL_SERVER_URL", "https://env.example") + + raw = RawMistralChatClient(model="mistral-test", server_url="https://explicit.example") + assert raw.server_url == "https://explicit.example" + + wrapped = MistralChatClient(server_url="https://explicit.example") + assert wrapped.service_url() == "https://explicit.example" + + +def test_injected_client_consts_not_loaded(monkeypatch) -> None: + # No MISTRAL_API_KEY in the environment at all; injecting a client must not + # require one. + monkeypatch.delenv("MISTRAL_API_KEY", raising=False) + monkeypatch.delenv("MISTRAL_SERVER_URL", raising=False) + sdk = _make_injected_sdk("https://client.example") + + chat = RawMistralChatClient(client=sdk, model="mistral-test") + embedding = MistralEmbeddingClient(client=sdk, model="mistral-embed") + + assert chat._owns_client is False + assert embedding._owns_client is False + os.environ.pop("MISTRAL_API_KEY", None) + os.environ.pop("MISTRAL_SERVER_URL", None)