chore(infra): support langsmith gateway in CI (#39651)

This commit is contained in:
ccurme authored and GitHub committed 2026-08-14 09:28:46 -04:00
1 parent f9ee55d94c
commit 0082ce0de9
6 files changed
+63 -7

No files matched your search

+4 -2
View File
@@ -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 }}
+2 -1
View File
@@ -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 }}
@@ -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"
@@ -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",
@@ -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()
@@ -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