From 7941315be5af091182e4ec6914671fb9dace0d17 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Thu, 9 Jul 2026 10:37:35 -0400 Subject: [PATCH 1/5] implement in langchain-anthropic, langchain-fireworks, langchain-openai --- .../langchain_anthropic/chat_models.py | 20 ++++++- .../tests/unit_tests/test_chat_models.py | 24 ++++++++ .../langchain_fireworks/chat_models.py | 18 +++++- .../tests/unit_tests/test_chat_models.py | 20 +++++++ .../langchain_openai/chat_models/base.py | 56 ++++++++++++------- .../tests/unit_tests/chat_models/test_base.py | 19 +++++++ 6 files changed, 135 insertions(+), 22 deletions(-) diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index edffea935ad94..1ad3fc5f17f4c 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -6,6 +6,7 @@ import datetime import hashlib import json +import os import re import warnings from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence @@ -76,6 +77,18 @@ _MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES) +_LANGSMITH_GATEWAY_DEFAULT_URL = "https://gateway.smith.langchain.com/anthropic" + + +def _resolve_gateway_base_url() -> str | None: + raw = os.getenv("LANGSMITH_GATEWAY") + if raw is None or raw.lower() in ("false", "0", "no"): + return None + if raw.lower() in ("true", "1", "yes"): + return _LANGSMITH_GATEWAY_DEFAULT_URL + return raw + + _USER_AGENT: Final[str] = f"langchain-anthropic/{__version__}" @@ -945,15 +958,18 @@ class ChatAnthropic(BaseChatModel): anthropic_api_url: str | None = Field( alias="base_url", - default_factory=from_env( + default_factory=lambda: _resolve_gateway_base_url() + or from_env( ["ANTHROPIC_API_URL", "ANTHROPIC_BASE_URL"], default="https://api.anthropic.com", - ), + )(), ) """Base URL for API requests. Only specify if using a proxy or service emulator. If a value isn't passed in, will attempt to read the value first from `ANTHROPIC_API_URL` and if that is not set, `ANTHROPIC_BASE_URL`. + + If `LANGSMITH_GATEWAY` is set, it takes precedence over both env vars. """ anthropic_api_key: SecretStr = Field( diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index f7c4dc669b91d..02cfe40498cbd 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -3574,3 +3574,27 @@ def mock_create(_payload: Any) -> list: message_finish = cast("dict[str, Any]", stream_events[-1]) assert message_finish["event"] == "message-finish" assert message_finish["metadata"]["stop_reason"] == "tool_use" + + +def test_langsmith_gateway_true(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.delenv("ANTHROPIC_API_URL", raising=False) + monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) + llm = ChatAnthropic(model=MODEL_NAME, api_key="test") + assert llm.anthropic_api_url == "https://gateway.smith.langchain.com/anthropic" + + +def test_langsmith_gateway_false(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "false") + monkeypatch.delenv("ANTHROPIC_API_URL", raising=False) + monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) + llm = ChatAnthropic(model=MODEL_NAME, api_key="test") + assert llm.anthropic_api_url == "https://api.anthropic.com" + + +def test_langsmith_gateway_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) + monkeypatch.delenv("ANTHROPIC_API_URL", raising=False) + monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) + llm = ChatAnthropic(model=MODEL_NAME, api_key="test") + assert llm.anthropic_api_url == "https://api.anthropic.com" diff --git a/libs/partners/fireworks/langchain_fireworks/chat_models.py b/libs/partners/fireworks/langchain_fireworks/chat_models.py index ee94e9ed7a827..3139028bffb2b 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -5,6 +5,7 @@ import contextlib import json import logging +import os from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence from operator import itemgetter from typing import ( @@ -108,6 +109,17 @@ _MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES) +_LANGSMITH_GATEWAY_DEFAULT_URL = "https://gateway.smith.langchain.com/fireworks" + + +def _resolve_gateway_base_url() -> str | None: + raw = os.getenv("LANGSMITH_GATEWAY") + if raw is None or raw.lower() in ("false", "0", "no"): + return None + if raw.lower() in ("true", "1", "yes"): + return _LANGSMITH_GATEWAY_DEFAULT_URL + return raw + def _get_default_model_profile(model_name: str) -> ModelProfile: default = _MODEL_PROFILES.get(model_name) or {} @@ -771,10 +783,14 @@ def model(self) -> str: """ fireworks_api_base: str | None = Field( - alias="base_url", default_factory=from_env("FIREWORKS_API_BASE", default=None) + alias="base_url", + default_factory=lambda: _resolve_gateway_base_url() + or from_env("FIREWORKS_API_BASE", default=None)(), ) """Base URL path for API requests, leave blank if not using a proxy or service emulator. + + If `LANGSMITH_GATEWAY` is set, it takes precedence over `FIREWORKS_API_BASE`. """ request_timeout: float | tuple[float, float] | Any | None = Field( diff --git a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py index e586411fa4a32..541badb8273b9 100644 --- a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py +++ b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py @@ -1591,3 +1591,23 @@ def test_request_timeout_tuple_normalized_to_httpx_timeout( assert forwarded.connect == 5.0 assert forwarded.read == 30.0 assert async_mock.call_args.kwargs["timeout"] == forwarded + + +def test_langsmith_gateway_true(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + llm = _make_model() + assert llm.fireworks_api_base == "https://gateway.smith.langchain.com/fireworks" + + +def test_langsmith_gateway_false(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "false") + monkeypatch.delenv("FIREWORKS_API_BASE", raising=False) + llm = _make_model() + assert llm.fireworks_api_base is None + + +def test_langsmith_gateway_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) + monkeypatch.delenv("FIREWORKS_API_BASE", raising=False) + llm = _make_model() + assert llm.fireworks_api_base is None diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 518c471be0dbe..a50dcb9f6f4c5 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -178,6 +178,17 @@ def _get_ssrf_safe_client() -> httpx.Client: _MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES) +_LANGSMITH_GATEWAY_DEFAULT_URL = "https://gateway.smith.langchain.com/openai/v1" + + +def _resolve_gateway_base_url() -> str | None: + raw = os.getenv("LANGSMITH_GATEWAY") + if raw is None or raw.lower() in ("false", "0", "no"): + return None + if raw.lower() in ("true", "1", "yes"): + return _LANGSMITH_GATEWAY_DEFAULT_URL + return raw + def _get_default_model_profile(model_name: str) -> ModelProfile: default = _MODEL_PROFILES.get(model_name) or {} @@ -1167,26 +1178,33 @@ def validate_environment(self) -> Self: or os.getenv("OPENAI_ORG_ID") or os.getenv("OPENAI_ORGANIZATION") ) - self.openai_api_base = self.openai_api_base or os.getenv("OPENAI_API_BASE") - - # Enable stream_usage by default if using default base URL and client - if ( - all( - getattr(self, key, None) is None - for key in ( - "stream_usage", - "openai_proxy", - "openai_api_base", - "base_url", - "client", - "root_client", - "async_client", - "root_async_client", - "http_client", - "http_async_client", - ) + _gateway_base_url = _resolve_gateway_base_url() + _base_url_from_gateway = False + if self.openai_api_base is None: + if _gateway_base_url is not None: + self.openai_api_base = _gateway_base_url + _base_url_from_gateway = True + else: + self.openai_api_base = os.getenv("OPENAI_API_BASE") + + # Enable stream_usage by default if using default base URL and client, + # or when the base URL was set by the LangSmith gateway (which proxies + # to OpenAI and supports streaming token usage). + if all( + getattr(self, key, None) is None + for key in ( + "stream_usage", + "openai_proxy", + "client", + "root_client", + "async_client", + "root_async_client", + "http_client", + "http_async_client", ) - and "OPENAI_BASE_URL" not in os.environ + ) and ( + _base_url_from_gateway + or (self.openai_api_base is None and "OPENAI_BASE_URL" not in os.environ) ): self.stream_usage = True diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index fc690a1e34425..c1a1c7d72dfc8 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -4501,3 +4501,22 @@ def test_defer_loading_in_responses_api_payload() -> None: assert weather_tool["defer_loading"] is True assert weather_tool["type"] == "function" assert {"type": "tool_search"} in result["tools"] + + +def test_langsmith_gateway_true(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key="test") + assert llm.openai_api_base == "https://gateway.smith.langchain.com/openai/v1" + assert llm.stream_usage is True + + +def test_langsmith_gateway_false(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "false") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key="test") + assert llm.openai_api_base is None + + +def test_langsmith_gateway_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key="test") + assert llm.openai_api_base is None From 79168282df599974cf158d1dfa0c2d47a5ddecb5 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Thu, 9 Jul 2026 11:04:50 -0400 Subject: [PATCH 2/5] lint --- .../openai/tests/unit_tests/chat_models/test_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index c1a1c7d72dfc8..3c3dabbcf70c8 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -4505,18 +4505,18 @@ def test_defer_loading_in_responses_api_payload() -> None: def test_langsmith_gateway_true(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("LANGSMITH_GATEWAY", "true") - llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key="test") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key=SecretStr("test")) assert llm.openai_api_base == "https://gateway.smith.langchain.com/openai/v1" assert llm.stream_usage is True def test_langsmith_gateway_false(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("LANGSMITH_GATEWAY", "false") - llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key="test") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key=SecretStr("test")) assert llm.openai_api_base is None def test_langsmith_gateway_unset(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) - llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key="test") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key=SecretStr("test")) assert llm.openai_api_base is None From 0bfc3e6bb84258f1751a65d345c0cf5308295d20 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Thu, 9 Jul 2026 11:48:22 -0400 Subject: [PATCH 3/5] read api key from LANGSMITH_GATEWAY_API_KEY --- .../langchain_anthropic/chat_models.py | 10 ++++++++-- .../tests/unit_tests/test_chat_models.py | 8 ++++++++ .../langchain_fireworks/chat_models.py | 19 ++++++++++++------- .../tests/unit_tests/test_chat_models.py | 8 ++++++++ .../langchain_openai/chat_models/base.py | 5 +++++ .../tests/unit_tests/chat_models/test_base.py | 9 +++++++++ 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index 1ad3fc5f17f4c..640160ded6ef1 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -974,9 +974,15 @@ class ChatAnthropic(BaseChatModel): anthropic_api_key: SecretStr = Field( alias="api_key", - default_factory=secret_from_env("ANTHROPIC_API_KEY", default=""), + default_factory=lambda: SecretStr( + os.getenv("LANGSMITH_GATEWAY_API_KEY") + or secret_from_env("ANTHROPIC_API_KEY", default="")().get_secret_value() + ), ) - """Automatically read from env var `ANTHROPIC_API_KEY` if not provided.""" + """Automatically read from env var `ANTHROPIC_API_KEY` if not provided. + + If `LANGSMITH_GATEWAY_API_KEY` is set, it takes precedence. + """ anthropic_proxy: str | None = Field( default_factory=from_env("ANTHROPIC_PROXY", default=None) diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index 02cfe40498cbd..448a1d5fcea49 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -3598,3 +3598,11 @@ def test_langsmith_gateway_unset(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) llm = ChatAnthropic(model=MODEL_NAME, api_key="test") assert llm.anthropic_api_url == "https://api.anthropic.com" + + +def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + llm = ChatAnthropic(model=MODEL_NAME) + assert llm.anthropic_api_key.get_secret_value() == "gateway-key" diff --git a/libs/partners/fireworks/langchain_fireworks/chat_models.py b/libs/partners/fireworks/langchain_fireworks/chat_models.py index 3139028bffb2b..fefb40203da8a 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -768,18 +768,23 @@ def model(self) -> str: fireworks_api_key: SecretStr = Field( alias="api_key", - default_factory=secret_from_env( - "FIREWORKS_API_KEY", - error_message=( - "You must specify an api key. " - "You can pass it an argument as `api_key=...` or " - "set the environment variable `FIREWORKS_API_KEY`." - ), + default_factory=lambda: SecretStr( + os.getenv("LANGSMITH_GATEWAY_API_KEY") + or secret_from_env( + "FIREWORKS_API_KEY", + error_message=( + "You must specify an api key. " + "You can pass it an argument as `api_key=...` or " + "set the environment variable `FIREWORKS_API_KEY`." + ), + )().get_secret_value() ), ) """Fireworks API key. Automatically read from env variable `FIREWORKS_API_KEY` if not provided. + + If `LANGSMITH_GATEWAY_API_KEY` is set, it takes precedence. """ fireworks_api_base: str | None = Field( diff --git a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py index 541badb8273b9..63ed60a8ddd41 100644 --- a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py +++ b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py @@ -1611,3 +1611,11 @@ def test_langsmith_gateway_unset(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("FIREWORKS_API_BASE", raising=False) llm = _make_model() assert llm.fireworks_api_base is None + + +def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.delenv("FIREWORKS_API_KEY", raising=False) + llm = ChatFireworks(model=MODEL_NAME) # type: ignore[call-arg] + assert llm.fireworks_api_key.get_secret_value() == "gateway-key" diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index a50dcb9f6f4c5..2b15ffb552f30 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -1212,6 +1212,11 @@ def validate_environment(self) -> Self: sync_api_key_value: str | Callable[[], str] | None = None async_api_key_value: str | Callable[[], Awaitable[str]] | None = None + if self.openai_api_key is None: + gateway_api_key = os.getenv("LANGSMITH_GATEWAY_API_KEY") + if gateway_api_key is not None: + self.openai_api_key = SecretStr(gateway_api_key) + if self.openai_api_key is not None: # Because OpenAI and AsyncOpenAI clients support either sync or async # callables for the API key, we need to resolve separate values here. diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index 3c3dabbcf70c8..af122e7f8ff2c 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -4520,3 +4520,12 @@ def test_langsmith_gateway_unset(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key=SecretStr("test")) assert llm.openai_api_base is None + + +def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + llm = ChatOpenAI(model=OPENAI_TEST_MODEL) + assert isinstance(llm.openai_api_key, SecretStr) + assert llm.openai_api_key.get_secret_value() == "gateway-key" From 9e9f6be79d7b84a295fcaf345ba3ea4157a3aa03 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Thu, 9 Jul 2026 14:09:20 -0400 Subject: [PATCH 4/5] cr --- .../anthropic/langchain_anthropic/chat_models.py | 8 ++++++-- .../anthropic/tests/unit_tests/test_chat_models.py | 10 ++++++++++ .../fireworks/langchain_fireworks/chat_models.py | 8 ++++++-- .../fireworks/tests/unit_tests/test_chat_models.py | 10 ++++++++++ .../openai/langchain_openai/chat_models/base.py | 2 +- .../openai/tests/unit_tests/chat_models/test_base.py | 11 +++++++++++ 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index 640160ded6ef1..fe51eb69a3fc5 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -975,13 +975,17 @@ class ChatAnthropic(BaseChatModel): anthropic_api_key: SecretStr = Field( alias="api_key", default_factory=lambda: SecretStr( - os.getenv("LANGSMITH_GATEWAY_API_KEY") + ( + os.getenv("LANGSMITH_GATEWAY_API_KEY") + if _resolve_gateway_base_url() is not None + else None + ) or secret_from_env("ANTHROPIC_API_KEY", default="")().get_secret_value() ), ) """Automatically read from env var `ANTHROPIC_API_KEY` if not provided. - If `LANGSMITH_GATEWAY_API_KEY` is set, it takes precedence. + If `LANGSMITH_GATEWAY` is enabled, `LANGSMITH_GATEWAY_API_KEY` takes precedence. """ anthropic_proxy: str | None = Field( diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index 448a1d5fcea49..420ee57159eb4 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -3606,3 +3606,13 @@ def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) llm = ChatAnthropic(model=MODEL_NAME) assert llm.anthropic_api_key.get_secret_value() == "gateway-key" + + +def test_langsmith_gateway_api_key_not_used_without_gateway( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.setenv("ANTHROPIC_API_KEY", "provider-key") + llm = ChatAnthropic(model=MODEL_NAME) + assert llm.anthropic_api_key.get_secret_value() == "provider-key" diff --git a/libs/partners/fireworks/langchain_fireworks/chat_models.py b/libs/partners/fireworks/langchain_fireworks/chat_models.py index fefb40203da8a..af530d254dab8 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -769,7 +769,11 @@ def model(self) -> str: fireworks_api_key: SecretStr = Field( alias="api_key", default_factory=lambda: SecretStr( - os.getenv("LANGSMITH_GATEWAY_API_KEY") + ( + os.getenv("LANGSMITH_GATEWAY_API_KEY") + if _resolve_gateway_base_url() is not None + else None + ) or secret_from_env( "FIREWORKS_API_KEY", error_message=( @@ -784,7 +788,7 @@ def model(self) -> str: Automatically read from env variable `FIREWORKS_API_KEY` if not provided. - If `LANGSMITH_GATEWAY_API_KEY` is set, it takes precedence. + If `LANGSMITH_GATEWAY` is enabled, `LANGSMITH_GATEWAY_API_KEY` takes precedence. """ fireworks_api_base: str | None = Field( diff --git a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py index 63ed60a8ddd41..a8ef4a6431b11 100644 --- a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py +++ b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py @@ -1619,3 +1619,13 @@ def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("FIREWORKS_API_KEY", raising=False) llm = ChatFireworks(model=MODEL_NAME) # type: ignore[call-arg] assert llm.fireworks_api_key.get_secret_value() == "gateway-key" + + +def test_langsmith_gateway_api_key_not_used_without_gateway( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.setenv("FIREWORKS_API_KEY", "provider-key") + llm = ChatFireworks(model=MODEL_NAME) # type: ignore[call-arg] + assert llm.fireworks_api_key.get_secret_value() == "provider-key" diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 2b15ffb552f30..4588a2527f0c9 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -1212,7 +1212,7 @@ def validate_environment(self) -> Self: sync_api_key_value: str | Callable[[], str] | None = None async_api_key_value: str | Callable[[], Awaitable[str]] | None = None - if self.openai_api_key is None: + if self.openai_api_key is None and _base_url_from_gateway: gateway_api_key = os.getenv("LANGSMITH_GATEWAY_API_KEY") if gateway_api_key is not None: self.openai_api_key = SecretStr(gateway_api_key) diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index af122e7f8ff2c..dc38e603c5898 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -4529,3 +4529,14 @@ def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: llm = ChatOpenAI(model=OPENAI_TEST_MODEL) assert isinstance(llm.openai_api_key, SecretStr) assert llm.openai_api_key.get_secret_value() == "gateway-key" + + +def test_langsmith_gateway_api_key_not_used_without_gateway( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("LANGSMITH_GATEWAY", raising=False) + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.setenv("OPENAI_API_KEY", "provider-key") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL) + assert isinstance(llm.openai_api_key, SecretStr) + assert llm.openai_api_key.get_secret_value() == "provider-key" From dc729dcfa31467a69213378f273663715831706c Mon Sep 17 00:00:00 2001 From: Mukil Loganathan <144731603+langchain-infra@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:12:34 +0000 Subject: [PATCH 5/5] feat(openai): use LangSmith keys for gateway auth When LangSmith Gateway is enabled, provider integrations can use deployment-injected LangSmith keys without requiring duplicate provider-specific API key env vars.\n\nCo-authored-by: open-swe[bot] --- .../langchain_anthropic/chat_models.py | 26 +++++++---- .../tests/unit_tests/test_chat_models.py | 41 +++++++++++++++++ .../langchain_fireworks/chat_models.py | 40 ++++++++++------- .../tests/unit_tests/test_chat_models.py | 41 +++++++++++++++++ .../langchain_openai/chat_models/base.py | 16 +++++-- .../tests/unit_tests/chat_models/test_base.py | 44 +++++++++++++++++++ 6 files changed, 180 insertions(+), 28 deletions(-) diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index fe51eb69a3fc5..3d0452a92dbaf 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -78,6 +78,11 @@ _MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES) _LANGSMITH_GATEWAY_DEFAULT_URL = "https://gateway.smith.langchain.com/anthropic" +_LANGSMITH_GATEWAY_API_KEY_ENV_VARS = ( + "LANGSMITH_GATEWAY_API_KEY", + "LANGSMITH_API_KEY", + "LANGCHAIN_API_KEY", +) def _resolve_gateway_base_url() -> str | None: @@ -89,6 +94,10 @@ def _resolve_gateway_base_url() -> str | None: return raw +def _resolve_gateway_api_key() -> SecretStr | None: + return secret_from_env(_LANGSMITH_GATEWAY_API_KEY_ENV_VARS, default=None)() + + _USER_AGENT: Final[str] = f"langchain-anthropic/{__version__}" @@ -974,18 +983,17 @@ class ChatAnthropic(BaseChatModel): anthropic_api_key: SecretStr = Field( alias="api_key", - default_factory=lambda: SecretStr( - ( - os.getenv("LANGSMITH_GATEWAY_API_KEY") - if _resolve_gateway_base_url() is not None - else None - ) - or secret_from_env("ANTHROPIC_API_KEY", default="")().get_secret_value() - ), + default_factory=lambda: ( + _resolve_gateway_api_key() + if _resolve_gateway_base_url() is not None + else None + ) + or secret_from_env("ANTHROPIC_API_KEY", default="")(), ) """Automatically read from env var `ANTHROPIC_API_KEY` if not provided. - If `LANGSMITH_GATEWAY` is enabled, `LANGSMITH_GATEWAY_API_KEY` takes precedence. + If `LANGSMITH_GATEWAY` is enabled, `LANGSMITH_GATEWAY_API_KEY`, + `LANGSMITH_API_KEY`, and `LANGCHAIN_API_KEY` take precedence. """ anthropic_proxy: str | None = Field( diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index 420ee57159eb4..d6bffbd51759c 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -3608,6 +3608,47 @@ def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: assert llm.anthropic_api_key.get_secret_value() == "gateway-key" +@pytest.mark.parametrize( + ("env_var", "expected"), + [ + ("LANGSMITH_API_KEY", "langsmith-key"), + ("LANGCHAIN_API_KEY", "langchain-key"), + ], +) +def test_langsmith_gateway_api_key_langsmith_fallbacks( + monkeypatch: pytest.MonkeyPatch, env_var: str, expected: str +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.delenv("LANGSMITH_GATEWAY_API_KEY", raising=False) + monkeypatch.delenv("LANGSMITH_API_KEY", raising=False) + monkeypatch.delenv("LANGCHAIN_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.setenv(env_var, expected) + llm = ChatAnthropic(model=MODEL_NAME) + assert llm.anthropic_api_key.get_secret_value() == expected + + +def test_langsmith_gateway_api_key_precedence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.setenv("LANGSMITH_API_KEY", "langsmith-key") + monkeypatch.setenv("LANGCHAIN_API_KEY", "langchain-key") + monkeypatch.setenv("ANTHROPIC_API_KEY", "provider-key") + llm = ChatAnthropic(model=MODEL_NAME) + assert llm.anthropic_api_key.get_secret_value() == "gateway-key" + + +def test_langsmith_gateway_explicit_api_key_takes_precedence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + llm = ChatAnthropic(model=MODEL_NAME, api_key="explicit-key") + assert llm.anthropic_api_key.get_secret_value() == "explicit-key" + + def test_langsmith_gateway_api_key_not_used_without_gateway( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/libs/partners/fireworks/langchain_fireworks/chat_models.py b/libs/partners/fireworks/langchain_fireworks/chat_models.py index af530d254dab8..f8020bd0234ed 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -110,6 +110,11 @@ _MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES) _LANGSMITH_GATEWAY_DEFAULT_URL = "https://gateway.smith.langchain.com/fireworks" +_LANGSMITH_GATEWAY_API_KEY_ENV_VARS = ( + "LANGSMITH_GATEWAY_API_KEY", + "LANGSMITH_API_KEY", + "LANGCHAIN_API_KEY", +) def _resolve_gateway_base_url() -> str | None: @@ -121,6 +126,10 @@ def _resolve_gateway_base_url() -> str | None: return raw +def _resolve_gateway_api_key() -> SecretStr | None: + return secret_from_env(_LANGSMITH_GATEWAY_API_KEY_ENV_VARS, default=None)() + + def _get_default_model_profile(model_name: str) -> ModelProfile: default = _MODEL_PROFILES.get(model_name) or {} return default.copy() @@ -768,27 +777,26 @@ def model(self) -> str: fireworks_api_key: SecretStr = Field( alias="api_key", - default_factory=lambda: SecretStr( - ( - os.getenv("LANGSMITH_GATEWAY_API_KEY") - if _resolve_gateway_base_url() is not None - else None - ) - or secret_from_env( - "FIREWORKS_API_KEY", - error_message=( - "You must specify an api key. " - "You can pass it an argument as `api_key=...` or " - "set the environment variable `FIREWORKS_API_KEY`." - ), - )().get_secret_value() - ), + default_factory=lambda: ( + _resolve_gateway_api_key() + if _resolve_gateway_base_url() is not None + else None + ) + or secret_from_env( + "FIREWORKS_API_KEY", + error_message=( + "You must specify an api key. " + "You can pass it an argument as `api_key=...` or " + "set the environment variable `FIREWORKS_API_KEY`." + ), + )(), ) """Fireworks API key. Automatically read from env variable `FIREWORKS_API_KEY` if not provided. - If `LANGSMITH_GATEWAY` is enabled, `LANGSMITH_GATEWAY_API_KEY` takes precedence. + If `LANGSMITH_GATEWAY` is enabled, `LANGSMITH_GATEWAY_API_KEY`, + `LANGSMITH_API_KEY`, and `LANGCHAIN_API_KEY` take precedence. """ fireworks_api_base: str | None = Field( diff --git a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py index a8ef4a6431b11..18eecfac67891 100644 --- a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py +++ b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py @@ -1621,6 +1621,47 @@ def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: assert llm.fireworks_api_key.get_secret_value() == "gateway-key" +@pytest.mark.parametrize( + ("env_var", "expected"), + [ + ("LANGSMITH_API_KEY", "langsmith-key"), + ("LANGCHAIN_API_KEY", "langchain-key"), + ], +) +def test_langsmith_gateway_api_key_langsmith_fallbacks( + monkeypatch: pytest.MonkeyPatch, env_var: str, expected: str +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.delenv("LANGSMITH_GATEWAY_API_KEY", raising=False) + monkeypatch.delenv("LANGSMITH_API_KEY", raising=False) + monkeypatch.delenv("LANGCHAIN_API_KEY", raising=False) + monkeypatch.delenv("FIREWORKS_API_KEY", raising=False) + monkeypatch.setenv(env_var, expected) + llm = ChatFireworks(model=MODEL_NAME) # type: ignore[call-arg] + assert llm.fireworks_api_key.get_secret_value() == expected + + +def test_langsmith_gateway_api_key_precedence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.setenv("LANGSMITH_API_KEY", "langsmith-key") + monkeypatch.setenv("LANGCHAIN_API_KEY", "langchain-key") + monkeypatch.setenv("FIREWORKS_API_KEY", "provider-key") + llm = ChatFireworks(model=MODEL_NAME) # type: ignore[call-arg] + assert llm.fireworks_api_key.get_secret_value() == "gateway-key" + + +def test_langsmith_gateway_explicit_api_key_takes_precedence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + llm = ChatFireworks(model=MODEL_NAME, api_key="explicit-key") # type: ignore[call-arg] + assert llm.fireworks_api_key.get_secret_value() == "explicit-key" + + def test_langsmith_gateway_api_key_not_used_without_gateway( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 4588a2527f0c9..1dedec3ccd762 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -179,6 +179,11 @@ def _get_ssrf_safe_client() -> httpx.Client: _MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES) _LANGSMITH_GATEWAY_DEFAULT_URL = "https://gateway.smith.langchain.com/openai/v1" +_LANGSMITH_GATEWAY_API_KEY_ENV_VARS = ( + "LANGSMITH_GATEWAY_API_KEY", + "LANGSMITH_API_KEY", + "LANGCHAIN_API_KEY", +) def _resolve_gateway_base_url() -> str | None: @@ -190,6 +195,10 @@ def _resolve_gateway_base_url() -> str | None: return raw +def _resolve_gateway_api_key() -> SecretStr | None: + return secret_from_env(_LANGSMITH_GATEWAY_API_KEY_ENV_VARS, default=None)() + + def _get_default_model_profile(model_name: str) -> ModelProfile: default = _MODEL_PROFILES.get(model_name) or {} return default.copy() @@ -1212,10 +1221,11 @@ def validate_environment(self) -> Self: sync_api_key_value: str | Callable[[], str] | None = None async_api_key_value: str | Callable[[], Awaitable[str]] | None = None - if self.openai_api_key is None and _base_url_from_gateway: - gateway_api_key = os.getenv("LANGSMITH_GATEWAY_API_KEY") + explicit_api_key = bool({"api_key", "openai_api_key"} & self.model_fields_set) + if _base_url_from_gateway and not explicit_api_key: + gateway_api_key = _resolve_gateway_api_key() if gateway_api_key is not None: - self.openai_api_key = SecretStr(gateway_api_key) + self.openai_api_key = gateway_api_key if self.openai_api_key is not None: # Because OpenAI and AsyncOpenAI clients support either sync or async diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index dc38e603c5898..c47eaa8a3704a 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -4531,6 +4531,50 @@ def test_langsmith_gateway_api_key(monkeypatch: pytest.MonkeyPatch) -> None: assert llm.openai_api_key.get_secret_value() == "gateway-key" +@pytest.mark.parametrize( + ("env_var", "expected"), + [ + ("LANGSMITH_API_KEY", "langsmith-key"), + ("LANGCHAIN_API_KEY", "langchain-key"), + ], +) +def test_langsmith_gateway_api_key_langsmith_fallbacks( + monkeypatch: pytest.MonkeyPatch, env_var: str, expected: str +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.delenv("LANGSMITH_GATEWAY_API_KEY", raising=False) + monkeypatch.delenv("LANGSMITH_API_KEY", raising=False) + monkeypatch.delenv("LANGCHAIN_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.setenv(env_var, expected) + llm = ChatOpenAI(model=OPENAI_TEST_MODEL) + assert isinstance(llm.openai_api_key, SecretStr) + assert llm.openai_api_key.get_secret_value() == expected + + +def test_langsmith_gateway_api_key_precedence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + monkeypatch.setenv("LANGSMITH_API_KEY", "langsmith-key") + monkeypatch.setenv("LANGCHAIN_API_KEY", "langchain-key") + monkeypatch.setenv("OPENAI_API_KEY", "provider-key") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL) + assert isinstance(llm.openai_api_key, SecretStr) + assert llm.openai_api_key.get_secret_value() == "gateway-key" + + +def test_langsmith_gateway_explicit_api_key_takes_precedence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LANGSMITH_GATEWAY", "true") + monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "gateway-key") + llm = ChatOpenAI(model=OPENAI_TEST_MODEL, api_key="explicit-key") + assert isinstance(llm.openai_api_key, SecretStr) + assert llm.openai_api_key.get_secret_value() == "explicit-key" + + def test_langsmith_gateway_api_key_not_used_without_gateway( monkeypatch: pytest.MonkeyPatch, ) -> None: