mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
chore(infra): support langsmith gateway in CI (#39651)
This commit is contained in:
1 parent
f9ee55d94c
commit
0082ce0de9
6 files changed
+63
-7
No files matched your search
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user