Coverage for packages/client/src/langgate/client/http.py: 46%
83 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-08-29 16:00 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-08-29 16:00 +0000
1"""HTTP client for LangGate API."""
3from contextlib import asynccontextmanager
4from datetime import timedelta
5from typing import Generic, get_args
7import httpx
8from pydantic import SecretStr
10from langgate.client.protocol import BaseRegistryClient, ImageInfoT, LLMInfoT
11from langgate.core.logging import get_logger
12from langgate.core.models import ImageModelInfo, LLMInfo
14logger = get_logger(__name__)
17def create_registry_http_client(
18 base_url: str,
19 api_key: SecretStr | None = None,
20 timeout: float | httpx.Timeout | None = 10.0,
21 **kwargs,
22) -> httpx.AsyncClient:
23 """
24 Creates and configures an httpx.AsyncClient for the Registry API.
25 """
26 headers = kwargs.pop("headers", {})
27 if api_key:
28 headers["X-API-Key"] = api_key.get_secret_value()
30 processed_base_url = base_url.rstrip("/")
32 return httpx.AsyncClient(
33 base_url=processed_base_url,
34 headers=headers,
35 timeout=timeout,
36 **kwargs,
37 )
40class BaseHTTPRegistryClient(
41 BaseRegistryClient[LLMInfoT, ImageInfoT], Generic[LLMInfoT, ImageInfoT]
42):
43 """
44 Base HTTP client for the Model Registry API.
45 Supports LLMInfo-derived and ImageModelInfo-derived schemas for response parsing and httpx client injection.
47 Handles infrequent HTTP requests via temporary clients by default or uses an
48 injected client as the request engine. Configuration (base_url, api_key)
49 stored in this instance is always used for requests.
51 Type Parameters:
52 LLMInfoT: The LLMInfo-derived model class for response parsing
53 ImageInfoT: The ImageModelInfo-derived model class for response parsing
54 """
56 __orig_bases__: tuple
57 llm_info_cls: type[LLMInfoT]
58 image_info_cls: type[ImageInfoT]
60 def __init__(
61 self,
62 base_url: str,
63 api_key: SecretStr | None = None,
64 cache_ttl: timedelta | None = None,
65 llm_info_cls: type[LLMInfoT] | None = None,
66 image_info_cls: type[ImageInfoT] | None = None,
67 http_client: httpx.AsyncClient | None = None,
68 ):
69 """Initialize the client.
70 Args:
71 base_url: The base URL of the registry service
72 api_key: Registry server API key for authentication
73 cache_ttl: Cache time-to-live
74 llm_info_cls: Override for LLM info class
75 image_info_cls: Override for image model info class
76 http_client: Optional HTTP client for making requests
77 """
78 super().__init__(cache_ttl=cache_ttl)
79 self.base_url = base_url.rstrip("/")
80 self.api_key = api_key
81 self._http_client = http_client
83 # Set model info classes if provided, otherwise they are inferred from the class
84 if llm_info_cls is not None:
85 self.llm_info_cls = llm_info_cls
86 if image_info_cls is not None:
87 self.image_info_cls = image_info_cls
89 logger.debug(
90 "initialized_base_http_registry_client",
91 base_url=self.base_url,
92 api_key=self.api_key,
93 llm_info_cls=getattr(self, "llm_info_cls", None),
94 image_info_cls=getattr(self, "image_info_cls", None),
95 )
97 def __init_subclass__(cls, **kwargs):
98 """Set up model classes when this class is subclassed."""
99 super().__init_subclass__(**kwargs)
101 # Extract the model classes from generic parameters
102 if not hasattr(cls, "llm_info_cls") or not hasattr(cls, "image_info_cls"):
103 llm_cls, image_cls = cls._get_model_info_classes()
104 if not hasattr(cls, "llm_info_cls"):
105 cls.llm_info_cls = llm_cls
106 if not hasattr(cls, "image_info_cls"):
107 cls.image_info_cls = image_cls
109 @classmethod
110 def _get_model_info_classes(cls) -> tuple[type[LLMInfoT], type[ImageInfoT]]:
111 """Extract the model classes from generic type parameters."""
112 args = get_args(cls.__orig_bases__[0])
113 return args[0], args[1]
115 @asynccontextmanager
116 async def _get_client_for_request(self):
117 """Provides the httpx client to use (injected or temporary)."""
118 if self._http_client:
119 yield self._http_client
120 else:
121 async with httpx.AsyncClient() as temp_client:
122 yield temp_client
124 async def _request(self, method: str, url_path: str, **kwargs) -> httpx.Response:
125 """Makes an HTTP request using the appropriate client engine."""
126 url = f"{self.base_url}{url_path}"
127 headers = kwargs.pop("headers", {})
128 if self.api_key:
129 headers["X-API-Key"] = self.api_key.get_secret_value()
131 async with self._get_client_for_request() as client:
132 response = await client.request(method, url, headers=headers, **kwargs)
133 return response
135 async def _fetch_llm_info(self, model_id: str) -> LLMInfoT:
136 """Fetch LLM info from the source via HTTP."""
137 response = await self._request("GET", f"/models/llms/{model_id}")
138 response.raise_for_status()
139 return self.llm_info_cls.model_validate(response.json())
141 async def _fetch_image_model_info(self, model_id: str) -> ImageInfoT:
142 """Fetch image model info from the source via HTTP."""
143 response = await self._request("GET", f"/models/images/{model_id}")
144 response.raise_for_status()
145 return self.image_info_cls.model_validate(response.json())
147 async def _fetch_all_llms(self) -> list[LLMInfoT]:
148 """Fetch all LLMs from the source via HTTP."""
149 response = await self._request("GET", "/models/llms")
150 response.raise_for_status()
151 return [self.llm_info_cls.model_validate(model) for model in response.json()]
153 async def _fetch_all_image_models(self) -> list[ImageInfoT]:
154 """Fetch all image models from the source via HTTP."""
155 response = await self._request("GET", "/models/images")
156 response.raise_for_status()
157 return [self.image_info_cls.model_validate(model) for model in response.json()]
160class HTTPRegistryClient(BaseHTTPRegistryClient[LLMInfo, ImageModelInfo]):
161 """HTTP client singleton for the Model Registry API using the default schemas."""
163 _instance = None
165 def __new__(cls, *args, **kwargs):
166 if cls._instance is None:
167 cls._instance = super().__new__(cls)
168 logger.debug("creating_http_registry_client_singleton")
169 return cls._instance
171 def __init__(
172 self,
173 base_url: str,
174 api_key: SecretStr | None = None,
175 cache_ttl: timedelta | None = None,
176 http_client: httpx.AsyncClient | None = None,
177 ):
178 if not hasattr(self, "_initialized"):
179 super().__init__(base_url, api_key, cache_ttl, http_client=http_client)
180 self._initialized = True
181 logger.debug("initialized_http_registry_client_singleton")