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

1"""Custom Anthropic-compatible (Messages API) endpoint provider. 

2 

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""" 

8 

9from ....security.secure_logging import logger 

10 

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 

16 

17 

18class CustomAnthropicEndpointProvider(AnthropicProvider): 

19 """Custom Anthropic-format endpoint provider. 

20 

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 """ 

26 

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] 

33 

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 

41 

42 @classmethod 

43 def requires_auth_for_models(cls): 

44 """Self-hosted Anthropic-format endpoints may allow keyless listing. 

45 

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 

51 

52 @classmethod 

53 def is_available(cls, settings_snapshot=None): 

54 """Available when either an API key or a custom URL is configured. 

55 

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}") 

74 

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 ) 

90 

91 return False 

92 

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).