diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index 4d0f46575b..8a2e89553b 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -497,9 +497,10 @@ jobs: # Uses the Makefile's `integration_tests` target for the specified package if: ${{ startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/partners/') }} env: + LANGSMITH_GATEWAY: ${{ secrets.LANGSMITH_GATEWAY }} + LANGSMITH_GATEWAY_API_KEY: ${{ secrets.LANGSMITH_GATEWAY_API_KEY }} AI21_API_KEY: ${{ secrets.AI21_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -600,7 +601,8 @@ jobs: partner: [ anthropic, openai ] fail-fast: false # Continue testing other partners if one fails env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LANGSMITH_GATEWAY: ${{ secrets.LANGSMITH_GATEWAY }} + LANGSMITH_GATEWAY_API_KEY: ${{ secrets.LANGSMITH_GATEWAY_API_KEY }} ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }} ANTHROPIC_FILES_API_PDF_ID: ${{ secrets.ANTHROPIC_FILES_API_PDF_ID }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index a6d234226f..c9f858e1dd 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -288,8 +288,9 @@ jobs: LANGSMITH_PROJECT: ${{ vars.LANGSMITH_PROJECT || 'scheduled-testing-py' }} LANGSMITH_TAGS: "github-actions,${{ matrix.working-directory }},python-${{ matrix.python-version }},sha-${{ github.sha }}" + LANGSMITH_GATEWAY: ${{ secrets.LANGSMITH_GATEWAY }} + LANGSMITH_GATEWAY_API_KEY: ${{ secrets.LANGSMITH_GATEWAY_API_KEY }} AI21_API_KEY: ${{ secrets.AI21_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }} ANTHROPIC_FILES_API_PDF_ID: ${{ secrets.ANTHROPIC_FILES_API_PDF_ID }} ASTRA_DB_API_ENDPOINT: ${{ secrets.ASTRA_DB_API_ENDPOINT }} diff --git a/libs/partners/anthropic/tests/conftest.py b/libs/partners/anthropic/tests/conftest.py index 26d854f671..6e14b77642 100644 --- a/libs/partners/anthropic/tests/conftest.py +++ b/libs/partners/anthropic/tests/conftest.py @@ -8,6 +8,10 @@ from vcr import VCR # type: ignore[import-untyped] def remove_request_headers(request: Any) -> Any: for k in request.headers: request.headers[k] = "**REDACTED**" + # Redact the URI so cassettes stay host-agnostic (e.g. direct API vs. the + # LangSmith gateway). Matching drops the `uri` matcher below, so this only + # affects what newly recorded cassettes persist. + request.uri = "**REDACTED**" return request @@ -21,6 +25,9 @@ def remove_response_headers(response: dict) -> dict: def vcr_config() -> dict: """Extend the default configuration coming from langchain_tests.""" config = base_vcr_config() + # Match on method + body only. Dropping `uri` keeps cassettes host-agnostic + # so they replay whether requests go to the API directly or via the gateway. + config["match_on"] = [m for m in config.get("match_on", []) if m != "uri"] config["before_record_request"] = remove_request_headers config["before_record_response"] = remove_response_headers config["serializer"] = "yaml.gz" diff --git a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py index ffac6bfbeb..9c166e5eed 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -1268,7 +1268,9 @@ def test_structured_output_thinking_force_tool_use() -> None: # Structured output currently relies on forced tool use, which is not supported # when `thinking` is enabled. When this test fails, it means that the feature # is supported and the workarounds in `with_structured_output` should be removed. - client = anthropic.Anthropic() + # Use the client resolved off `ChatAnthropic` so requests honor any + # configured base URL / credentials (e.g. the LangSmith gateway). + client = ChatAnthropic(model="claude-sonnet-4-5-20250929")._client # type: ignore[call-arg] with pytest.raises(anthropic.BadRequestError): _ = client.messages.create( model="claude-sonnet-4-5-20250929", diff --git a/libs/partners/anthropic/tests/integration_tests/test_llms.py b/libs/partners/anthropic/tests/integration_tests/test_llms.py index 2bb8279744..3fdca58385 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_llms.py +++ b/libs/partners/anthropic/tests/integration_tests/test_llms.py @@ -1,5 +1,6 @@ """Test Anthropic API wrapper.""" +import os from collections.abc import Generator import pytest @@ -11,6 +12,21 @@ from tests.unit_tests._utils import FakeCallbackHandler MODEL = "claude-sonnet-4-5-20250929" +# The deprecated `AnthropicLLM` class has no LangSmith gateway support, so it +# cannot authenticate when requests are routed through the gateway. Skip the +# network-calling tests in that case; they still run against a direct +# `ANTHROPIC_API_KEY`. Mirrors the gateway truthiness in `langchain_core`. +_GATEWAY_ENABLED = (os.getenv("LANGSMITH_GATEWAY") or "").lower() not in ( + "", + "false", + "0", + "no", +) +_skip_under_gateway = pytest.mark.skipif( + _GATEWAY_ENABLED, + reason="AnthropicLLM is deprecated and not compatible with the LangSmith gateway", +) + @pytest.mark.requires("anthropic") def test_anthropic_model_name_param() -> None: @@ -24,6 +40,7 @@ def test_anthropic_model_param() -> None: assert llm.model == "foo" +@_skip_under_gateway def test_anthropic_call() -> None: """Test valid call to anthropic.""" llm = AnthropicLLM(model=MODEL) # type: ignore[call-arg] @@ -31,6 +48,7 @@ def test_anthropic_call() -> None: assert isinstance(output, str) +@_skip_under_gateway def test_anthropic_streaming() -> None: """Test streaming tokens from anthropic.""" llm = AnthropicLLM(model=MODEL) # type: ignore[call-arg] @@ -42,6 +60,7 @@ def test_anthropic_streaming() -> None: assert isinstance(token, str) +@_skip_under_gateway def test_anthropic_streaming_callback() -> None: """Test that streaming correctly invokes on_llm_new_token callback.""" callback_handler = FakeCallbackHandler() @@ -56,6 +75,7 @@ def test_anthropic_streaming_callback() -> None: assert callback_handler.llm_streams > 1 +@_skip_under_gateway async def test_anthropic_async_generate() -> None: """Test async generate.""" llm = AnthropicLLM(model=MODEL) # type: ignore[call-arg] @@ -63,6 +83,7 @@ async def test_anthropic_async_generate() -> None: assert isinstance(output, LLMResult) +@_skip_under_gateway async def test_anthropic_async_streaming_callback() -> None: """Test that streaming correctly invokes on_llm_new_token callback.""" callback_handler = FakeCallbackHandler() diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py index 5ca28f3384..f1b0f962f4 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py @@ -34,6 +34,26 @@ if TYPE_CHECKING: MAX_TOKEN_COUNT = 100 +# Whether requests route through the LangSmith gateway. Mirrors the truthiness +# used by `langchain_core`'s gateway resolution. +_GATEWAY_ENABLED = (os.environ.get("LANGSMITH_GATEWAY") or "").lower() not in ( + "", + "false", + "0", + "no", +) + + +def _gateway_or_provider_key() -> str: + """Return an API key valid for the endpoint the base URL resolves to. + + When the LangSmith gateway is enabled, requests route through it and must + authenticate with the gateway key rather than the provider key. + """ + if _GATEWAY_ENABLED: + return os.environ["LANGSMITH_GATEWAY_API_KEY"] + return os.environ["OPENAI_API_KEY"] + @pytest.mark.scheduled def test_chat_openai() -> None: @@ -66,7 +86,7 @@ def test_chat_openai_model() -> None: def test_callable_api_key(monkeypatch: pytest.MonkeyPatch) -> None: - original_key = os.environ["OPENAI_API_KEY"] + original_key = _gateway_or_provider_key() calls = {"sync": 0} @@ -83,7 +103,7 @@ def test_callable_api_key(monkeypatch: pytest.MonkeyPatch) -> None: async def test_callable_api_key_async(monkeypatch: pytest.MonkeyPatch) -> None: - original_key = os.environ["OPENAI_API_KEY"] + original_key = _gateway_or_provider_key() calls = {"sync": 0, "async": 0} @@ -366,7 +386,10 @@ async def test_astream() -> None: assert full.usage_metadata["input_tokens"] > 0 assert full.usage_metadata["output_tokens"] > 0 assert full.usage_metadata["total_tokens"] > 0 - else: + # The LangSmith gateway always emits a usage chunk regardless of + # `stream_options.include_usage`, so the opt-out assertions below only + # hold when not routing through it. + elif not _GATEWAY_ENABLED: assert chunks_with_token_counts == 0 assert full.usage_metadata is None