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