diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index fe51eb69a3..3d0452a92d 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -78,6 +78,11 @@ _message_type_lookups = { _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 420ee57159..d6bffbd517 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 af530d254d..f8020bd023 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -110,6 +110,11 @@ logger = logging.getLogger(__name__) _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 @@ class ChatFireworks(BaseChatModel): 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 a8ef4a6431..18eecfac67 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 4588a2527f..1dedec3ccd 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 @@ class BaseChatOpenAI(BaseChatModel): 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 dc38e603c5..c47eaa8a37 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: