mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 19:43:15 +00:00
Some checks are pending
CI / Lint (push) Waiting to run
CI / Format check (push) Waiting to run
CI / Unit tests (push) Waiting to run
CI / Playwright E2E (push) Waiting to run
CI / Docker build smoke (push) Waiting to run
CI / Triage ledger up to date (push) Waiting to run
CI / ui bun.lock in sync (push) Waiting to run
* feat: port LangSmith LLM Gateway routing from upstream (#1671, #1673, #1674, #1678) Ports four upstream commits that add opt-in LLM call routing through the LangSmith Gateway, preserving fork conventions (Bedrock/Fireworks model IDs, no-agent-attribution, bun toolchain). - #1671 (e9dc6e01): opt-in gateway routing — new gateway.py, team-settings toggle, admin UI section, wired into make_model for all graph entrypoints - #1673 (702ef908): dedicated LANGSMITH_GATEWAY_API_KEY precedence over platform LANGSMITH_API_KEY - #1674 (5f7c2f46): fix Fireworks gateway base URL to /fireworks (bare host, SDK appends /v1/chat/completions) + SanitizeFireworksMessagesMiddleware - #1678 (73b7d1c0): fix OpenAI Responses reasoning replay — SanitizeOpenAIResponsesMiddleware, store/include config for encrypted reasoning content, reasoning_effort coercion for Chat Completions fallback Refs #134 * fix: downgrade gateway not-routed log to debug, add Bedrock UI note, add sanitizer parity - Downgrade logger.warning to logger.debug in gateway_overrides for not-routed providers and missing API key (Bedrock is the default provider in this fork, so these are expected steady states) - Add Bedrock to the LLMGatewaySection route-toggle description so admins know it is not routed through the gateway - Add SanitizeOpenAIResponsesMiddleware to chat.py for parity with server.py and reviewer.py - Restore the Bedrock region comment in model.py that explains the AWS_REGION / AWS_DEFAULT_REGION precedence Refs #138 --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
126 lines
4.7 KiB
Python
126 lines
4.7 KiB
Python
"""LangSmith LLM Gateway routing for model construction.
|
|
|
|
The LLM Gateway (https://docs.langchain.com/langsmith/llm-gateway) proxies
|
|
provider calls through LangSmith: the client authenticates with a LangSmith API
|
|
key and the gateway resolves the real provider key from workspace Provider
|
|
Secrets, enforcing spend/PII/secrets policies and tracing every call. Routing is
|
|
opt-in via ``LANGSMITH_GATEWAY_ENABLED`` (deployment default) or the
|
|
``gateway_enabled`` team setting, and is applied centrally in
|
|
:func:`agent.utils.model.make_model`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_GATEWAY_BASE_URL = "https://gateway.smith.langchain.com"
|
|
|
|
# Provider prefix -> base-URL suffix appended to the gateway host. Each suffix
|
|
# matches the SDK's own path handling: the OpenAI SDK appends
|
|
# ``/chat/completions`` to a ``/v1`` base, Fireworks appends
|
|
# ``/v1/chat/completions`` to a bare provider host, Anthropic appends
|
|
# ``/v1/messages`` to a bare host, and google-genai appends
|
|
# ``/<api_version>/models/...`` to a bare host. Vertex (``google_vertexai``, which
|
|
# uses service-account auth rather than a bearer key) and any other provider are
|
|
# not routed and call the provider directly.
|
|
_GATEWAY_PROVIDER_PATHS: dict[str, str] = {
|
|
"openai": "/openai/v1",
|
|
"anthropic": "/anthropic",
|
|
"fireworks": "/fireworks",
|
|
"google_genai": "/gemini",
|
|
}
|
|
|
|
|
|
def _env_bool(value: str | None) -> bool:
|
|
return (value or "").strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _langsmith_api_key() -> str | None:
|
|
"""LangSmith API key used to authenticate gateway calls.
|
|
|
|
Prefer a gateway-specific key, then the prod LangSmith key. LangGraph Cloud
|
|
may inject ``LANGSMITH_API_KEY`` for tracing/platform APIs, and that key can
|
|
lack the ``gateway:invoke`` permission required by the LLM Gateway.
|
|
"""
|
|
return (
|
|
os.environ.get("LANGSMITH_GATEWAY_API_KEY")
|
|
or os.environ.get("LANGSMITH_API_KEY_PROD")
|
|
or os.environ.get("LANGSMITH_API_KEY")
|
|
)
|
|
|
|
|
|
def gateway_base_url() -> str:
|
|
"""Gateway host, overridable via ``LANGSMITH_GATEWAY_BASE_URL`` (regional/self-hosted)."""
|
|
return (os.environ.get("LANGSMITH_GATEWAY_BASE_URL") or DEFAULT_GATEWAY_BASE_URL).rstrip("/")
|
|
|
|
|
|
def gateway_env_default() -> bool:
|
|
"""Deployment-level default for gateway routing (``LANGSMITH_GATEWAY_ENABLED``)."""
|
|
return _env_bool(os.environ.get("LANGSMITH_GATEWAY_ENABLED"))
|
|
|
|
|
|
def gateway_openai_use_responses() -> bool:
|
|
"""Whether gateway-routed OpenAI keeps the Responses API.
|
|
|
|
Defaults to ``True`` because OpenAI reasoning models with tool calls reject
|
|
``reasoning_effort`` on Chat Completions. Set
|
|
``LANGSMITH_GATEWAY_OPENAI_USE_RESPONSES=false`` only for deployments that
|
|
need to force Chat Completions through the gateway.
|
|
"""
|
|
raw = os.environ.get("LANGSMITH_GATEWAY_OPENAI_USE_RESPONSES")
|
|
if raw is None:
|
|
return True
|
|
return _env_bool(raw)
|
|
|
|
|
|
def resolve_gateway_enabled(team_value: bool | None) -> bool:
|
|
"""Combine the team-settings toggle with the env default.
|
|
|
|
A team value of ``True``/``False`` is authoritative; ``None`` inherits the
|
|
``LANGSMITH_GATEWAY_ENABLED`` deployment default.
|
|
"""
|
|
if team_value is None:
|
|
return gateway_env_default()
|
|
return team_value
|
|
|
|
|
|
def _provider_of(model_id: str) -> str:
|
|
return model_id.split(":", 1)[0]
|
|
|
|
|
|
def gateway_overrides(model_id: str) -> dict[str, object] | None:
|
|
"""``init_chat_model`` kwargs that route ``model_id`` through the gateway.
|
|
|
|
Returns ``None`` (so the caller keeps talking to the provider directly) when
|
|
the provider isn't routable through the gateway or no LangSmith API key is
|
|
available — both cases are logged rather than raised, so a run never fails
|
|
just because gateway routing couldn't be applied.
|
|
"""
|
|
provider = _provider_of(model_id)
|
|
path = _GATEWAY_PROVIDER_PATHS.get(provider)
|
|
if path is None:
|
|
logger.debug(
|
|
"LangSmith gateway enabled but provider %r is not routed; calling it directly",
|
|
provider,
|
|
)
|
|
return None
|
|
api_key = _langsmith_api_key()
|
|
if not api_key:
|
|
logger.debug(
|
|
"LangSmith gateway enabled but no LANGSMITH_GATEWAY_API_KEY or "
|
|
"LANGSMITH_API_KEY(_PROD) is set; "
|
|
"calling the provider directly"
|
|
)
|
|
return None
|
|
overrides: dict[str, object] = {
|
|
"base_url": f"{gateway_base_url()}{path}",
|
|
"api_key": api_key,
|
|
}
|
|
if provider == "openai":
|
|
# Use HTTPS Responses through the gateway by default; tool-calling OpenAI
|
|
# reasoning models reject reasoning_effort on Chat Completions.
|
|
overrides["use_responses_api"] = gateway_openai_use_responses()
|
|
return overrides
|