fix(ops): keep the path of self-hosted LangSmith and Weave endpoints (#40584)

This commit is contained in:
Xiyuan Chen
2026-08-13 03:18:22 +00:00
committed by GitHub
parent af2fac75db
commit 5bacb68115
9 changed files with 98 additions and 13 deletions
+14 -4
View File
@@ -105,7 +105,13 @@ def validate_url(url: str, default_url: str, allowed_schemes: tuple = ("https",
return normalized_url
def validate_url_with_path(url: str, default_url: str, required_suffix: str | None = None) -> str:
def validate_url_with_path(
url: str,
default_url: str,
required_suffix: str | None = None,
*,
allowed_schemes: tuple[str, ...] = ("https", "http"),
) -> str:
"""
Validate URL that may include path components
@@ -113,22 +119,26 @@ def validate_url_with_path(url: str, default_url: str, required_suffix: str | No
url: The URL to validate
default_url: Default URL to use if input is None or empty
required_suffix: Optional suffix that URL must end with
allowed_schemes: Tuple of allowed URL schemes (default: https, http)
Returns:
Validated URL string
Validated URL string, returned verbatim so path, query and trailing
separators survive — `required_suffix` consumers depend on that
Raises:
ValueError: If URL format is invalid or doesn't match required suffix
"""
if not url or url.strip() == "":
return default_url
url = url.strip()
# Parse URL to validate format
parsed = urlparse(url)
# Check if scheme is allowed
if parsed.scheme not in ("https", "http"):
raise ValueError("URL must start with https:// or http://")
if parsed.scheme not in allowed_schemes:
expected = " or ".join(f"{scheme}://" for scheme in allowed_schemes)
raise ValueError(f"URL must start with {expected}")
# Check required suffix if specified
if required_suffix and not url.endswith(required_suffix):
@@ -1,7 +1,7 @@
from pydantic import ValidationInfo, field_validator
from core.ops.entities.config_entity import BaseTracingConfig
from core.ops.utils import validate_url
from core.ops.utils import validate_url_with_path
class LangSmithConfig(BaseTracingConfig):
@@ -16,5 +16,5 @@ class LangSmithConfig(BaseTracingConfig):
@field_validator("endpoint")
@classmethod
def endpoint_validator(cls, v, info: ValidationInfo):
# LangSmith only allows HTTPS
return validate_url(v, "https://api.smith.langchain.com", allowed_schemes=("https",))
# LangSmith only allows HTTPS; self-hosted deployments may sit under a path prefix
return validate_url_with_path(v, "https://api.smith.langchain.com", allowed_schemes=("https",))
@@ -63,6 +63,16 @@ def test_init(langsmith_config, monkeypatch: pytest.MonkeyPatch):
assert instance.file_base_url == "http://test.url"
def test_init_passes_self_hosted_path_to_client(monkeypatch: pytest.MonkeyPatch):
config = LangSmithConfig(api_key="ls-123", project="default", endpoint="https://langsmith.internal/api")
mock_client_class = MagicMock()
monkeypatch.setattr("dify_trace_langsmith.langsmith_trace.Client", mock_client_class)
LangSmithDataTrace(config)
mock_client_class.assert_called_once_with(api_key="ls-123", api_url="https://langsmith.internal/api")
def test_trace_dispatch(trace_instance, monkeypatch: pytest.MonkeyPatch):
methods = [
"workflow_trace",
@@ -31,5 +31,15 @@ class TestLangSmithConfig:
def test_endpoint_validation_https_only(self):
"""Test endpoint validation only allows HTTPS"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
with pytest.raises(ValidationError, match="URL must start with https://"):
LangSmithConfig(api_key="key", project="project", endpoint="http://insecure.com")
def test_endpoint_preserves_path(self):
"""Self-hosted LangSmith endpoints keep their API path prefix"""
config = LangSmithConfig(api_key="key", project="project", endpoint="https://langsmith.internal/api")
assert config.endpoint == "https://langsmith.internal/api"
def test_endpoint_preserves_versioned_path(self):
"""Self-hosted LangSmith endpoints keep multi-segment API paths"""
config = LangSmithConfig(api_key="key", project="project", endpoint="https://langsmith.internal/api/v1")
assert config.endpoint == "https://langsmith.internal/api/v1"
@@ -1,7 +1,7 @@
from pydantic import ValidationInfo, field_validator
from core.ops.entities.config_entity import BaseTracingConfig
from core.ops.utils import validate_url
from core.ops.utils import validate_url_with_path
class WeaveConfig(BaseTracingConfig):
@@ -19,11 +19,11 @@ class WeaveConfig(BaseTracingConfig):
@classmethod
def endpoint_validator(cls, v, info: ValidationInfo):
# Weave only allows HTTPS for endpoint
return validate_url(v, "https://trace.wandb.ai", allowed_schemes=("https",))
return validate_url_with_path(v, "https://trace.wandb.ai", allowed_schemes=("https",))
@field_validator("host")
@classmethod
def host_validator(cls, v, info: ValidationInfo):
if v is not None and v.strip() != "":
return validate_url(v, v, allowed_schemes=("https", "http"))
return validate_url_with_path(v, v, allowed_schemes=("https", "http"))
return v
@@ -41,7 +41,7 @@ class TestWeaveConfig:
def test_endpoint_validation_https_only(self):
"""Test endpoint validation only allows HTTPS"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
with pytest.raises(ValidationError, match="URL must start with https://"):
WeaveConfig(api_key="key", project="project", endpoint="http://insecure.wandb.ai")
def test_host_validation_optional(self):
@@ -57,5 +57,20 @@ class TestWeaveConfig:
def test_host_validation_invalid_scheme(self):
"""Test host validation rejects invalid schemes when provided"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
with pytest.raises(ValidationError, match="URL must start with https:// or http://"):
WeaveConfig(api_key="key", project="project", host="ftp://invalid.host.com")
def test_endpoint_preserves_path(self):
"""Self-hosted Weave endpoints keep their path prefix"""
config = WeaveConfig(api_key="key", project="project", endpoint="https://wandb.internal/api")
assert config.endpoint == "https://wandb.internal/api"
def test_host_preserves_path(self):
"""Self-hosted W&B hosts keep their path prefix"""
config = WeaveConfig(api_key="key", project="project", host="https://wandb.internal/wandb")
assert config.host == "https://wandb.internal/wandb"
def test_host_preserves_http_path(self):
"""Self-hosted W&B hosts may be plain http and keep their path"""
config = WeaveConfig(api_key="key", project="project", host="http://wandb.internal/wandb")
assert config.host == "http://wandb.internal/wandb"
@@ -251,6 +251,16 @@ class TestInit:
)
assert instance.host == "https://my.wandb.host"
def test_init_with_host_path(self, mock_wandb, mock_weave):
"""A self-hosted host keeps its path prefix all the way to wandb.login."""
config = _make_weave_config(host="https://wandb.internal/api")
instance = WeaveDataTrace(config)
mock_wandb.login.assert_called_once_with(
key="wv-api-key", verify=True, relogin=True, host="https://wandb.internal/api"
)
assert instance.host == "https://wandb.internal/api"
def test_init_without_entity(self, mock_wandb, mock_weave):
"""Test __init__ initializes weave without entity prefix when entity is None."""
mock_w, weave_client = mock_weave
@@ -48,11 +48,17 @@ class TestConfigIntegration:
aliyun_config = AliyunConfig(
license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com/api/v1/traces"
)
langsmith_config = LangSmithConfig(
api_key="key", project="project", endpoint="https://langsmith.internal/api/v1"
)
weave_config = WeaveConfig(api_key="key", project="project", endpoint="https://weave.internal/wandb")
assert arize_config.endpoint == "https://arize.com"
assert phoenix_with_path_config.endpoint == "https://app.phoenix.arize.com/s/dify-integration"
assert phoenix_without_path_config.endpoint == "https://app.phoenix.arize.com"
assert aliyun_config.endpoint == "https://tracing-analysis-dc-hz.aliyuncs.com/api/v1/traces"
assert langsmith_config.endpoint == "https://langsmith.internal/api/v1"
assert weave_config.endpoint == "https://weave.internal/wandb"
def test_project_default_values(self):
"""Test that project default values are set correctly"""
@@ -155,6 +155,30 @@ class TestValidateUrlWithPath:
with pytest.raises(ValueError, match="URL must start with https:// or http://"):
validate_url_with_path("example.com", "https://default.com")
def test_restricted_scheme_accepts_allowed_scheme(self):
"""Test https-only validation keeps the path of an https URL"""
result = validate_url_with_path(
"https://langsmith.internal/api", "https://default.com", allowed_schemes=("https",)
)
assert result == "https://langsmith.internal/api"
def test_restricted_scheme_rejects_http(self):
"""Test https-only validation rejects http and names only https in the error"""
with pytest.raises(ValueError) as excinfo:
validate_url_with_path("http://langsmith.internal/api", "https://default.com", allowed_schemes=("https",))
assert str(excinfo.value) == "URL must start with https://"
def test_default_schemes_keep_original_error_message(self):
"""Test the two-scheme default keeps the exact message existing providers assert on"""
with pytest.raises(ValueError) as excinfo:
validate_url_with_path("ftp://example.com", "https://default.com")
assert str(excinfo.value) == "URL must start with https:// or http://"
def test_surrounding_whitespace_is_stripped(self):
"""Test surrounding whitespace is removed while the path is preserved"""
result = validate_url_with_path(" https://example.com/api/v1 ", "https://default.com")
assert result == "https://example.com/api/v1"
class TestValidateProjectName:
"""Test cases for validate_project_name function"""