Coverage for src/local_deep_research/llm/providers/implementations/custom_anthropic_endpoint.py: 100%
34 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
1"""Custom Anthropic-compatible (Messages API) endpoint provider.
3Connects to a self-hosted LLM service that speaks the Anthropic Messages API
4(``/v1/messages``) rather than the OpenAI chat-completions format. Mirrors
5``CustomOpenAIEndpointProvider`` but builds a ``ChatAnthropic`` client (via the
6parent ``AnthropicProvider``) pointed at an operator-configured ``base_url``.
7"""
9from ....security.secure_logging import logger
11from ....config.thread_settings import get_setting_from_snapshot
12from ....security.log_sanitizer import redact_secrets
13from ..base import Exposure
14from .anthropic import AnthropicProvider
17class CustomAnthropicEndpointProvider(AnthropicProvider):
18 """Custom Anthropic-format endpoint provider.
20 Lets users connect to any service implementing the Anthropic Messages API
21 by specifying a base URL in settings. The URL is SSRF-validated and passed
22 to ``ChatAnthropic`` by the inherited ``create_llm`` (which activates its
23 URL/keyless branch because ``url_setting`` is set here).
24 """
26 provider_name = "Anthropic-Compatible Endpoint"
27 api_key_setting = "llm.anthropic_endpoint.api_key"
28 # Many self-hosted Anthropic-format gateways don't require auth; fall back
29 # to the shared placeholder instead of raising at construction time.
30 api_key_optional = True
31 url_setting = "llm.anthropic_endpoint.url" # type: ignore[assignment]
33 # Metadata for auto-discovery (registered as the "anthropic_endpoint"
34 # provider string). The egress policy classifies it generically via
35 # ``llm.anthropic_endpoint.url`` — keep the setting name in sync.
36 provider_key = "ANTHROPIC_ENDPOINT"
37 company_name = "Anthropic-Compatible"
38 is_cloud = None # Unknown — could be local or cloud
39 egress_exposure = Exposure.EXPOSING # URL-configurable; fail closed to exposing until classifier refines by URL
41 @classmethod
42 def requires_auth_for_models(cls):
43 """Self-hosted Anthropic-format endpoints may allow keyless listing.
45 Return False so model listing is attempted without an API key. If the
46 endpoint requires auth, the Anthropic client raises and we degrade to
47 an empty list.
48 """
49 return False
51 @classmethod
52 def is_available(cls, settings_snapshot=None):
53 """Available when either an API key or a custom URL is configured.
55 Unlike the cloud Anthropic provider, this custom endpoint supports
56 keyless local servers — so a configured URL alone makes it usable.
57 """
58 api_key = None
59 try:
60 api_key = get_setting_from_snapshot(
61 cls.api_key_setting,
62 default=None,
63 settings_snapshot=settings_snapshot,
64 )
65 if api_key and str(api_key).strip():
66 return True
67 except Exception as e:
68 # Redact in case a settings-layer error message embeds the key.
69 safe_msg = redact_secrets(str(e), api_key)
70 logger.debug(f"Error checking provider availability: {safe_msg}")
72 try:
73 custom_url = get_setting_from_snapshot(
74 cls.url_setting,
75 default=None,
76 settings_snapshot=settings_snapshot,
77 )
78 if custom_url and str(custom_url).strip():
79 return True
80 except Exception:
81 logger.debug(
82 f"Error reading URL setting '{cls.url_setting}'",
83 exc_info=True,
84 )
86 return False
88 # list_models_for_api is inherited from AnthropicProvider, which lists
89 # models via the anthropic SDK and honors cls.url_setting (SSRF-guarding
90 # the configured base_url; returning [] when no URL is set).