mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
feat(code): report tracing gateway in dcode doctor (#4466)
`dcode doctor` now shows a `Gateway` line under Tracing indicating whether traces route through the LangSmith managed gateway. --- Brian asked to know, at a glance, whether `dcode` is tracing to the LangSmith managed gateway or to a custom dev/staging endpoint. This adds a `Gateway` line under the Tracing section of `dcode doctor` (shown when tracing is enabled): `yes` when traces route through the LangSmith managed/SaaS gateway (`*.smith.langchain.com`, including the default `https://api.smith.langchain.com` when no custom endpoint is set), `no` for a self-hosted/dev/staging endpoint. The existing `Endpoint` line already surfaces the custom URL itself. Assumption worth confirming: "gateway" is defined as any host under `smith.langchain.com` (matches the codebase's existing gateway-host convention). If LangChain's internal dev/staging uses a subdomain of `smith.langchain.com`, it would read as `yes` — happy to narrow it if that's not the intended cut. Made by [Open SWE](https://openswe.vercel.app/agents/61952749-6cc0-a57e-8fe4-c151d099735e) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -3164,8 +3164,8 @@ def _tracing_has_credentials_from(env: dict[str, str]) -> bool:
|
||||
return has_key or _has_langsmith_profile_credentials(env)
|
||||
|
||||
|
||||
def _has_langsmith_runs_endpoints_from(env: dict[str, str]) -> bool:
|
||||
"""Return whether replica trace ingestion targets are configured.
|
||||
def _langsmith_runs_endpoint_urls_from(env: dict[str, str]) -> tuple[str, ...]:
|
||||
"""Return the replica trace ingestion URLs configured via runs-endpoints.
|
||||
|
||||
Mirrors the LangSmith SDK's accepted `LANGSMITH_RUNS_ENDPOINTS` shapes: a
|
||||
JSON list of `{"api_url": "...", "api_key": "..."}` objects, or a JSON
|
||||
@@ -3176,7 +3176,7 @@ def _has_langsmith_runs_endpoints_from(env: dict[str, str]) -> bool:
|
||||
env: Environment mapping to read.
|
||||
|
||||
Returns:
|
||||
`True` when a valid runs-endpoints configuration is present.
|
||||
The configured replica ingestion URLs, in configuration order.
|
||||
"""
|
||||
raw = next(
|
||||
(
|
||||
@@ -3187,23 +3187,40 @@ def _has_langsmith_runs_endpoints_from(env: dict[str, str]) -> bool:
|
||||
None,
|
||||
)
|
||||
if raw is None:
|
||||
return False
|
||||
return ()
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return ()
|
||||
|
||||
if isinstance(parsed, list):
|
||||
return any(
|
||||
isinstance(item, dict)
|
||||
return tuple(
|
||||
item["api_url"]
|
||||
for item in parsed
|
||||
if isinstance(item, dict)
|
||||
and isinstance(item.get("api_url"), str)
|
||||
and isinstance(item.get("api_key"), str)
|
||||
for item in parsed
|
||||
)
|
||||
if isinstance(parsed, dict):
|
||||
return any(isinstance(value, str) for value in parsed.values())
|
||||
return False
|
||||
return tuple(
|
||||
url
|
||||
for url, api_key in parsed.items()
|
||||
if isinstance(url, str) and isinstance(api_key, str)
|
||||
)
|
||||
return ()
|
||||
|
||||
|
||||
def _has_langsmith_runs_endpoints_from(env: dict[str, str]) -> bool:
|
||||
"""Return whether replica trace ingestion targets are configured.
|
||||
|
||||
Args:
|
||||
env: Environment mapping to read.
|
||||
|
||||
Returns:
|
||||
`True` when a valid runs-endpoints configuration is present.
|
||||
"""
|
||||
return bool(_langsmith_runs_endpoint_urls_from(env))
|
||||
|
||||
|
||||
def _tracing_can_upload_from(env: dict[str, str]) -> bool:
|
||||
@@ -3316,6 +3333,9 @@ class TracingStatus:
|
||||
replica_project: str | None
|
||||
"""Extra project agent runs are mirrored to, if configured."""
|
||||
|
||||
runs_endpoints: tuple[str, ...] = ()
|
||||
"""Replica ingestion URLs from `LANGSMITH_RUNS_ENDPOINTS`, if any."""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Reject the contradictory enabled/explicitly-disabled pair.
|
||||
|
||||
@@ -3358,6 +3378,7 @@ def get_tracing_status() -> TracingStatus:
|
||||
replica_project=_get_first_langsmith_replica_project(
|
||||
_get_langsmith_replica_projects_from(env)
|
||||
),
|
||||
runs_endpoints=_langsmith_runs_endpoint_urls_from(env),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -274,6 +274,75 @@ def _sanitize_endpoint(endpoint: str) -> str:
|
||||
return f"{parsed.scheme}://{netloc}"
|
||||
|
||||
|
||||
_LANGSMITH_GATEWAY_HOST = "smith.langchain.com"
|
||||
"""Host identifying LangSmith's managed (SaaS) tracing gateway.
|
||||
|
||||
Traces sent to an endpoint whose host is `smith.langchain.com` (or a subdomain
|
||||
of it) route through the managed gateway; any other host is a self-hosted or
|
||||
dev/staging target. `app.py` keeps the same constant for its model-gateway
|
||||
key-mismatch check, but matches a raw-URL substring; this module matches the
|
||||
parsed hostname exactly or by subdomain suffix so lookalike hosts such as
|
||||
`smith.langchain.com.evil.example` are not treated as the gateway.
|
||||
"""
|
||||
|
||||
|
||||
def _endpoint_gateway_state(endpoint: str) -> str:
|
||||
"""Classify a single tracing endpoint as gateway, non-gateway, or unknown.
|
||||
|
||||
Args:
|
||||
endpoint: A configured tracing endpoint URL.
|
||||
|
||||
Returns:
|
||||
`"yes"` when the endpoint's host is the LangSmith managed gateway (an
|
||||
exact host or a subdomain), `"no"` for any other resolvable host,
|
||||
and `"unknown"` when the endpoint cannot be parsed into a host — so
|
||||
a typo'd or malformed URL is never silently reported as `"no"`.
|
||||
"""
|
||||
try:
|
||||
host = urlsplit(endpoint.strip()).hostname or ""
|
||||
except ValueError:
|
||||
# urlsplit raises on bracket-malformed IPv6 (e.g. `http://[::1`); a
|
||||
# diagnostic must degrade to "unknown" rather than crash `dcode doctor`.
|
||||
return "unknown"
|
||||
if not host:
|
||||
return "unknown"
|
||||
if host == _LANGSMITH_GATEWAY_HOST or host.endswith(f".{_LANGSMITH_GATEWAY_HOST}"):
|
||||
return "yes"
|
||||
return "no"
|
||||
|
||||
|
||||
def _tracing_gateway_state(status: TracingStatus) -> str:
|
||||
"""Report whether all trace ingestion targets are the managed gateway.
|
||||
|
||||
Considers both the primary endpoint and any replica ingestion URLs
|
||||
(`LANGSMITH_RUNS_ENDPOINTS`), since a self-hosted replica means traces leave
|
||||
for a custom target even when the primary endpoint is unset. With no target
|
||||
configured, tracing falls back to the LangSmith SDK default
|
||||
(`https://api.smith.langchain.com`), which is the managed gateway.
|
||||
|
||||
Args:
|
||||
status: The resolved tracing status.
|
||||
|
||||
Returns:
|
||||
`"yes"` when every configured target is the managed gateway (or none is
|
||||
configured, i.e. the SDK default), `"no"` when any target is a
|
||||
self-hosted or dev/staging host, and `"unknown"` when a target
|
||||
cannot be parsed and none is a definite non-gateway host.
|
||||
"""
|
||||
states = [
|
||||
_endpoint_gateway_state(target)
|
||||
for target in (status.endpoint, *status.runs_endpoints)
|
||||
if target
|
||||
]
|
||||
if not states:
|
||||
return "yes"
|
||||
if "no" in states:
|
||||
return "no"
|
||||
if "unknown" in states:
|
||||
return "unknown"
|
||||
return "yes"
|
||||
|
||||
|
||||
def _format_tracing_project(status: TracingStatus) -> str:
|
||||
"""Render the tracing project, marking the unconfigured default.
|
||||
|
||||
@@ -297,7 +366,11 @@ def _collect_tracing() -> DiagnosticSection:
|
||||
value is never read or printed. The `Credentials` item is flagged as a
|
||||
problem only when tracing is enabled without a key and without a custom
|
||||
endpoint, mirroring the runtime's orphaned-tracing guard (a keyless
|
||||
self-hosted endpoint is a valid, healthy setup).
|
||||
self-hosted endpoint is a valid, healthy setup). When tracing is enabled, a
|
||||
`Gateway` item reports whether traces route through LangSmith's managed
|
||||
(SaaS) gateway (`yes`), a custom self-hosted/dev/staging endpoint (`no`), or
|
||||
an endpoint that could not be parsed (`unknown`), accounting for both the
|
||||
primary endpoint and any replica ingestion targets.
|
||||
|
||||
Returns:
|
||||
The `Tracing` section.
|
||||
@@ -323,6 +396,8 @@ def _collect_tracing() -> DiagnosticSection:
|
||||
]
|
||||
if status.endpoint:
|
||||
items.append(DiagnosticItem("Endpoint", _sanitize_endpoint(status.endpoint)))
|
||||
if status.enabled:
|
||||
items.append(DiagnosticItem("Gateway", _tracing_gateway_state(status)))
|
||||
if status.replica_project:
|
||||
items.append(DiagnosticItem("Replica project", status.replica_project))
|
||||
return DiagnosticSection(title="Tracing", items=items)
|
||||
|
||||
@@ -103,7 +103,9 @@ class TestCollectTracing:
|
||||
"replica_project": None,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
status = TracingStatus(**defaults) # type: ignore[arg-type]
|
||||
# `defaults` is intentionally heterogeneous (`dict[str, object]`), so the
|
||||
# unpack can't be statically matched to each field's type.
|
||||
status = TracingStatus(**defaults) # ty: ignore[invalid-argument-type]
|
||||
with patch("deepagents_code.config.get_tracing_status", return_value=status):
|
||||
return _collect_tracing()
|
||||
|
||||
@@ -184,6 +186,72 @@ class TestCollectTracing:
|
||||
assert "secret" not in labels["Endpoint"]
|
||||
assert "api_key" not in labels["Endpoint"]
|
||||
|
||||
def test_gateway_yes_for_default_endpoint(self) -> None:
|
||||
"""No custom endpoint means the SDK default (managed gateway) is used."""
|
||||
section = self._section(enabled=True, has_credentials=True, endpoint=None)
|
||||
labels = {item.label: item.value for item in section.items}
|
||||
assert labels["Gateway"] == "yes"
|
||||
|
||||
def test_gateway_yes_for_langsmith_host(self) -> None:
|
||||
"""A `smith.langchain.com` endpoint routes through the managed gateway."""
|
||||
section = self._section(
|
||||
enabled=True,
|
||||
has_credentials=True,
|
||||
endpoint="https://api.smith.langchain.com",
|
||||
)
|
||||
labels = {item.label: item.value for item in section.items}
|
||||
assert labels["Gateway"] == "yes"
|
||||
# A configured endpoint surfaces both items; they must not interfere.
|
||||
assert "Endpoint" in labels
|
||||
|
||||
def test_gateway_no_for_custom_endpoint(self) -> None:
|
||||
"""A self-hosted/dev endpoint is not the LangSmith managed gateway."""
|
||||
section = self._section(
|
||||
enabled=True,
|
||||
has_credentials=False,
|
||||
endpoint="http://localhost:1984",
|
||||
)
|
||||
labels = {item.label: item.value for item in section.items}
|
||||
assert labels["Gateway"] == "no"
|
||||
|
||||
def test_gateway_unknown_for_unparseable_endpoint(self) -> None:
|
||||
"""An endpoint with no parseable host reports `unknown`, never `no`."""
|
||||
section = self._section(
|
||||
enabled=True,
|
||||
has_credentials=True,
|
||||
endpoint="not a url",
|
||||
)
|
||||
labels = {item.label: item.value for item in section.items}
|
||||
assert labels["Gateway"] == "unknown"
|
||||
|
||||
def test_gateway_no_when_replica_endpoint_is_self_hosted(self) -> None:
|
||||
"""A self-hosted replica target counts even with no primary endpoint."""
|
||||
section = self._section(
|
||||
enabled=True,
|
||||
has_credentials=True,
|
||||
endpoint=None,
|
||||
runs_endpoints=("http://localhost:1984",),
|
||||
)
|
||||
labels = {item.label: item.value for item in section.items}
|
||||
assert labels["Gateway"] == "no"
|
||||
|
||||
def test_gateway_yes_when_replica_endpoint_is_gateway(self) -> None:
|
||||
"""Replica targets on the managed gateway keep the report `yes`."""
|
||||
section = self._section(
|
||||
enabled=True,
|
||||
has_credentials=True,
|
||||
endpoint=None,
|
||||
runs_endpoints=("https://eu.api.smith.langchain.com",),
|
||||
)
|
||||
labels = {item.label: item.value for item in section.items}
|
||||
assert labels["Gateway"] == "yes"
|
||||
|
||||
def test_gateway_absent_when_not_enabled(self) -> None:
|
||||
"""The Gateway line only appears when tracing is enabled."""
|
||||
section = self._section(enabled=False)
|
||||
labels = {item.label: item.value for item in section.items}
|
||||
assert "Gateway" not in labels
|
||||
|
||||
def test_replica_project_listed_when_set(self) -> None:
|
||||
"""A configured replica project is surfaced as its own item."""
|
||||
section = self._section(
|
||||
@@ -195,6 +263,58 @@ class TestCollectTracing:
|
||||
assert labels["Replica project"] == "replica"
|
||||
|
||||
|
||||
class TestEndpointGatewayState:
|
||||
"""Tests for the single-endpoint tracing gateway-host classifier."""
|
||||
|
||||
def test_exact_host_is_gateway(self) -> None:
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert _endpoint_gateway_state("https://smith.langchain.com") == "yes"
|
||||
|
||||
def test_regional_subdomain_is_gateway(self) -> None:
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert _endpoint_gateway_state("https://eu.api.smith.langchain.com") == "yes"
|
||||
|
||||
def test_whitespace_padded_endpoint_is_gateway(self) -> None:
|
||||
"""A padded env value (a classic misconfiguration) still classifies."""
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert _endpoint_gateway_state(" https://smith.langchain.com ") == "yes"
|
||||
|
||||
def test_self_hosted_is_not_gateway(self) -> None:
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert _endpoint_gateway_state("http://localhost:1984") == "no"
|
||||
|
||||
def test_suffix_lookalike_host_is_not_gateway(self) -> None:
|
||||
"""A host containing the domain as a substring is not the gateway."""
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert (
|
||||
_endpoint_gateway_state("https://smith.langchain.com.evil.example") == "no"
|
||||
)
|
||||
|
||||
def test_prefix_lookalike_host_is_not_gateway(self) -> None:
|
||||
"""The leading-dot boundary rejects a host that only shares a suffix."""
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert _endpoint_gateway_state("https://notsmith.langchain.com") == "no"
|
||||
|
||||
def test_unparseable_endpoint_is_unknown(self) -> None:
|
||||
"""A string with no parseable host is `unknown`, not a false `no`."""
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert _endpoint_gateway_state("not a url") == "unknown"
|
||||
assert _endpoint_gateway_state("") == "unknown"
|
||||
|
||||
def test_bracket_malformed_ipv6_is_unknown(self) -> None:
|
||||
"""A `urlsplit` `ValueError` degrades to `unknown` rather than crashing."""
|
||||
from deepagents_code.doctor import _endpoint_gateway_state
|
||||
|
||||
assert _endpoint_gateway_state("http://[::1") == "unknown"
|
||||
|
||||
|
||||
class TestCollectUpdates:
|
||||
"""Tests for the Updates diagnostic section."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user