idside 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ # Patents: https://sdk.idside.eu/patents
2
+ # This software may be covered by the patents and applications listed there.
3
+
4
+ """Provider adapters: the descriptor-driven engine + auto-detection registry.
5
+
6
+ Standard providers are described entirely by a JSON descriptor (see
7
+ :mod:`idside.providers`). Exotic providers can register a ``ProviderAdapter``
8
+ subclass in ``_OVERRIDES`` to customize the mechanics.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+ from urllib.parse import urlsplit
15
+
16
+ from idside.adapters.base import ProviderAdapter
17
+ from idside.adapters.gemini import GeminiAdapter
18
+ from idside.exceptions import UnsupportedProviderError
19
+ from idside.providers import ProviderDescriptor, load_descriptors
20
+
21
+ __all__ = ["GeminiAdapter", "ProviderAdapter", "get_adapter_for", "register_adapter"]
22
+
23
+ #: Per-provider engine overrides for exotic APIs (default: the generic engine).
24
+ #: google-genai takes no free-form kwargs, so Gemini ships a built-in override.
25
+ _OVERRIDES: dict[str, type[ProviderAdapter]] = {"gemini": GeminiAdapter}
26
+
27
+ #: Maps a client's module (or module prefix, e.g. "google.genai") to the
28
+ #: descriptors that use it. A LIST, not one descriptor: OpenAI-compatible
29
+ #: providers (DeepSeek, and Groq/xAI/Together when they get descriptors) are all
30
+ #: reached through the `openai` client, so a module can legitimately name
31
+ #: several. Prefixes let namespace packages match without capturing the whole
32
+ #: namespace ("google.genai" matches google.genai.client, not google.*).
33
+ _BY_MODULE: dict[str, list[ProviderDescriptor]] = {}
34
+ for _descriptor in load_descriptors().values():
35
+ _BY_MODULE.setdefault(_descriptor.client_module, []).append(_descriptor)
36
+
37
+
38
+ def _base_url_host(client: Any) -> str | None:
39
+ """The host a client is pointed at, or None if it does not say.
40
+
41
+ ``openai.OpenAI`` exposes ``base_url`` as an ``httpx.URL``; other clients
42
+ expose a string, or nothing at all. Everything here is best-effort and
43
+ failure-tolerant, because this only ever *narrows* a choice.
44
+ """
45
+ raw = getattr(client, "base_url", None)
46
+ if raw is None:
47
+ return None
48
+ try:
49
+ host = urlsplit(str(raw)).hostname
50
+ except ValueError: # a malformed URL is simply not a match
51
+ return None
52
+ return host.lower() if host else None
53
+
54
+
55
+ def _resolve(client: Any) -> ProviderDescriptor | None:
56
+ """Pick the descriptor for ``client``: by module, then by base URL.
57
+
58
+ The second step is not a nicety. An `OpenAI` instance pointed at
59
+ api.deepseek.com is an `OpenAI` instance, so the class name says nothing
60
+ about who receives the request — and the provider name is what tells the
61
+ cloud which registry rules to apply. Answering "openai" for a DeepSeek call
62
+ would apply OpenAI's parameters to a server that never agreed to them.
63
+ """
64
+ parts = type(client).__module__.split(".")
65
+ candidates = next(
66
+ (
67
+ found
68
+ for end in range(len(parts), 0, -1)
69
+ if (found := _BY_MODULE.get(".".join(parts[:end]))) is not None
70
+ ),
71
+ None,
72
+ )
73
+ if not candidates:
74
+ return None
75
+ if len(candidates) == 1:
76
+ return candidates[0]
77
+
78
+ host = _base_url_host(client)
79
+ if host is not None:
80
+ for descriptor in candidates:
81
+ if host in descriptor.client_base_url_hosts:
82
+ return descriptor
83
+
84
+ # No host matched: fall back to the descriptor that claims no host in
85
+ # particular — the module's default (openai itself). If several do, the
86
+ # descriptors are ambiguous and guessing would be worse than refusing.
87
+ defaults = [d for d in candidates if not d.client_base_url_hosts]
88
+ return defaults[0] if len(defaults) == 1 else None
89
+
90
+
91
+ def get_adapter_for(client: Any) -> ProviderAdapter:
92
+ """Return the adapter matching ``client``'s provider, or raise."""
93
+ descriptor = _resolve(client)
94
+ if descriptor is None:
95
+ raise UnsupportedProviderError(
96
+ f"No ID Side adapter for client of type "
97
+ f"{type(client).__module__}.{type(client).__qualname__}."
98
+ )
99
+ adapter_cls = _OVERRIDES.get(descriptor.name, ProviderAdapter)
100
+ return adapter_cls(descriptor)
101
+
102
+
103
+ def register_adapter(name: str, adapter_cls: type[ProviderAdapter]) -> None:
104
+ """Register a ``ProviderAdapter`` subclass to override the generic engine.
105
+
106
+ Use this for exotic providers whose API does not fit the declarative model
107
+ (e.g. SDKs without per-request header support). ``name`` must match a
108
+ descriptor's ``name``.
109
+ """
110
+ _OVERRIDES[name] = adapter_cls