diff --git a/libs/core/langchain_core/utils/gateway.py b/libs/core/langchain_core/utils/gateway.py index d6f833e0b54..3b3644a0ff6 100644 --- a/libs/core/langchain_core/utils/gateway.py +++ b/libs/core/langchain_core/utils/gateway.py @@ -1,15 +1,11 @@ """Shared configuration for LangSmith Gateway provider endpoints.""" -from __future__ import annotations - import os -from typing import TYPE_CHECKING, Final, Literal, TypedDict - -if TYPE_CHECKING: - from collections.abc import Mapping - - from typing_extensions import NotRequired +from collections.abc import Mapping +from typing import Final, Literal, TypedDict +from urllib.parse import urlsplit, urlunsplit +from typing_extensions import NotRequired GatewayProvider = Literal["anthropic", "fireworks", "google_genai", "openai"] @@ -50,6 +46,16 @@ _DISABLED_GATEWAY_VALUES: Final = frozenset({"false", "0", "no"}) _ENABLED_GATEWAY_VALUES: Final = frozenset({"true", "1", "yes"}) +def _append_gateway_provider_path(url: str, provider_path: str) -> str: + """Append a provider path without changing a URL's query or fragment.""" + parsed_url = urlsplit(url) + if parsed_url.path.rstrip("/").endswith(provider_path): + return url + return urlunsplit( + parsed_url._replace(path=f"{parsed_url.path.rstrip('/')}{provider_path}") + ) + + def resolve_langsmith_gateway_url(provider: GatewayProvider) -> str | None: """Resolve the configured LangSmith Gateway URL for a provider. @@ -78,11 +84,6 @@ def resolve_langsmith_gateway_url(provider: GatewayProvider) -> str | None: provider_path = provider_config["path"] if raw_url.lower() not in _ENABLED_GATEWAY_VALUES: - if raw_url.rstrip("/").endswith(provider_path): - return raw_url - return f"{raw_url.rstrip('/')}{provider_path}" + return _append_gateway_provider_path(raw_url, provider_path) - gateway_url = LANGSMITH_GATEWAY_URL - if gateway_url.endswith(provider_path): - return gateway_url - return f"{gateway_url}{provider_path}" + return _append_gateway_provider_path(LANGSMITH_GATEWAY_URL, provider_path) diff --git a/libs/core/tests/unit_tests/utils/test_gateway.py b/libs/core/tests/unit_tests/utils/test_gateway.py index f4d9b3daba0..35127d0ba75 100644 --- a/libs/core/tests/unit_tests/utils/test_gateway.py +++ b/libs/core/tests/unit_tests/utils/test_gateway.py @@ -1,10 +1,13 @@ """Tests for LangSmith Gateway URL configuration.""" +from typing import get_type_hints + import pytest from langchain_core.utils.gateway import ( LANGSMITH_GATEWAY_PROVIDERS, LANGSMITH_GATEWAY_URL, + GatewayProviderConfig, resolve_langsmith_gateway_url, ) @@ -93,3 +96,36 @@ def test_langsmith_gateway_provider_matrix() -> None: }, "openai": {"path": "/openai/v1", "oauth": True, "static_key": True}, } + + +@pytest.mark.parametrize( + ("provider", "url", "expected_url"), + [ + ( + "openai", + "https://gateway.example.com?api-version=1#gateway", + "https://gateway.example.com/openai/v1?api-version=1#gateway", + ), + ( + "openai", + "https://gateway.example.com/openai/v1?api-version=1#gateway", + "https://gateway.example.com/openai/v1?api-version=1#gateway", + ), + ], +) +def test_resolve_langsmith_gateway_url_preserves_query_and_fragment( + monkeypatch: pytest.MonkeyPatch, + provider: str, + url: str, + expected_url: str, +) -> None: + """Provider paths are added before URL query and fragment components.""" + monkeypatch.setenv("LANGSMITH_GATEWAY", url) + + assert resolve_langsmith_gateway_url(provider) == expected_url # type: ignore[arg-type] + + +def test_gateway_provider_config_runtime_type_metadata() -> None: + """The public provider configuration retains optional-key metadata at runtime.""" + assert GatewayProviderConfig.__optional_keys__ == {"notes"} + assert get_type_hints(GatewayProviderConfig)["notes"] is str diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml index b4f170452b6..e00353d4b99 100644 --- a/libs/partners/anthropic/pyproject.toml +++ b/libs/partners/anthropic/pyproject.toml @@ -24,7 +24,7 @@ version = "1.4.8" requires-python = ">=3.10.0,<4.0.0" dependencies = [ "anthropic>=0.96.0,<1.0.0", - "langchain-core>=1.4.7,<2.0.0", + "langchain-core>=1.4.10,<2.0.0", "pydantic>=2.7.4,<3.0.0", ] diff --git a/libs/partners/anthropic/uv.lock b/libs/partners/anthropic/uv.lock index 1e2d357d411..19268f6b51e 100644 --- a/libs/partners/anthropic/uv.lock +++ b/libs/partners/anthropic/uv.lock @@ -552,7 +552,7 @@ wheels = [ [[package]] name = "langchain" -version = "1.3.11" +version = "1.3.12" source = { editable = "../../langchain_v1" } dependencies = [ { name = "langchain-core" }, @@ -662,7 +662,7 @@ requires-dist = [ ] [package.metadata.requires-dev] -lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }] +lint = [{ name = "ruff", specifier = ">=0.13.1,<0.16.0" }] test = [ { name = "blockbuster", specifier = ">=1.5.5,<1.6" }, { name = "defusedxml", specifier = ">=0.7.1,<1.0.0" }, @@ -689,7 +689,7 @@ typing = [ [[package]] name = "langchain-core" -version = "1.4.8" +version = "1.4.9" source = { editable = "../../core" } dependencies = [ { name = "jsonpatch" }, diff --git a/libs/partners/fireworks/pyproject.toml b/libs/partners/fireworks/pyproject.toml index 7fffa830d5d..309fe5c5d13 100644 --- a/libs/partners/fireworks/pyproject.toml +++ b/libs/partners/fireworks/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ version = "1.4.3" requires-python = ">=3.10.0,<4.0.0" dependencies = [ - "langchain-core>=1.4.7,<2.0.0", + "langchain-core>=1.4.10,<2.0.0", "fireworks-ai>=1.2.0a71,<2.0.0", "openai>=2.0.0,<3.0.0", "requests>=2.0.0,<3.0.0", diff --git a/libs/partners/fireworks/uv.lock b/libs/partners/fireworks/uv.lock index b8dc8ad85da..0f6c2a8d1c4 100644 --- a/libs/partners/fireworks/uv.lock +++ b/libs/partners/fireworks/uv.lock @@ -737,7 +737,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.4.8" +version = "1.4.9" source = { editable = "../../core" } dependencies = [ { name = "jsonpatch" }, diff --git a/libs/partners/openai/pyproject.toml b/libs/partners/openai/pyproject.toml index 0b85ed6f3ce..aa205681fea 100644 --- a/libs/partners/openai/pyproject.toml +++ b/libs/partners/openai/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ version = "1.3.4" requires-python = ">=3.10.0,<4.0.0" dependencies = [ - "langchain-core>=1.4.9,<2.0.0", + "langchain-core>=1.4.10,<2.0.0", "openai>=2.26.0,<3.0.0", "tiktoken>=0.7.0,<1.0.0", ]