lm15 0.1.0__tar.gz

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.
lm15-0.1.0/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
lm15-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lm15 contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
lm15-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: lm15
3
+ Version: 0.1.0
4
+ Summary: Universal LM core with pluggable provider adapters
5
+ Project-URL: Homepage, https://github.com/maxime/lm15
6
+ Project-URL: Documentation, https://github.com/maxime/lm15
7
+ Project-URL: Issues, https://github.com/maxime/lm15/issues
8
+ Author: lm15 contributors
9
+ License-File: LICENSE
10
+ Keywords: adapters,ai,anthropic,gemini,llm,openai,streaming
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Requires-Python: >=3.10
16
+ Provides-Extra: dev
17
+ Requires-Dist: build>=1.2.2; extra == 'dev'
18
+ Requires-Dist: pycurl>=7.45.0; extra == 'dev'
19
+ Requires-Dist: twine>=5.1.1; extra == 'dev'
20
+ Provides-Extra: speed
21
+ Requires-Dist: pycurl>=7.45.0; extra == 'speed'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # lm15 (Universal LM) — thin core, full plugin contract
25
+
26
+ A universal LM core optimized for low import/runtime overhead with provider plugins.
27
+
28
+ ## Core architecture
29
+
30
+ - **Universal contract** (`types.py`): normalized request/response/stream/live types.
31
+ - **Provider plugin contract** (`providers/base.py`): complete/stream/live/embeddings/files/batch/images/audio methods.
32
+ - **Transport boundary** (`transports/*`): urllib and pycurl implementations.
33
+ - **Capability resolver** (`capabilities.py`): static + optional hydration from models.dev.
34
+ - **Model catalog bridge** (`model_catalog.py`): loads `https://models.dev/api.json`.
35
+ - **Middleware pipeline** (`middleware.py`): retries/history/cache wrappers.
36
+ - **Completeness harness** (`completeness/*`): fixture + live probes, score output.
37
+
38
+ ## Quick start
39
+
40
+ ```python
41
+ from lm15 import Message, LMRequest, Part, build_default
42
+
43
+ lm = build_default(use_pycurl=True)
44
+ req = LMRequest(model="claude-sonnet-4-5", messages=(Message(role="user", parts=(Part.text_part("Say hi."),)),))
45
+ resp = lm.complete(req)
46
+ print(resp.message.parts[0].text)
47
+ ```
48
+
49
+ ## External plugin adapters (no core PR)
50
+
51
+ LM15 auto-discovers installed entry-points in group `lm15.providers`.
52
+
53
+ ```toml
54
+ # in external package pyproject.toml
55
+ [project.entry-points."lm15.providers"]
56
+ myprovider = "ulm_x_myprovider:build_adapter"
57
+ ```
58
+
59
+ ```python
60
+ from lm15 import build_default
61
+
62
+ lm = build_default(discover_plugins=True)
63
+ ```
64
+
65
+ ## Optional models.dev hydration
66
+
67
+ ```python
68
+ lm = build_default(hydrate_models_dev_catalog=True)
69
+ ```
70
+
71
+ ## Completeness
72
+
73
+ ```bash
74
+ python3 completeness/runner.py --mode fixture --fail-under 1.0
75
+ python3 completeness/runner.py --mode live --fail-under 0.0
76
+ ```
77
+
78
+ Outputs:
79
+ - `completeness/report.json`
80
+ - `completeness/report.md`
81
+
82
+ ## Packaging and publishing (uv + twine)
83
+
84
+ ```bash
85
+ # build sdist + wheel
86
+ uv run python -m build
87
+
88
+ # upload to TestPyPI
89
+ # twine upload --repository testpypi dist/*
90
+
91
+ # upload to PyPI
92
+ # twine upload dist/*
93
+ ```
94
+
95
+ ## Environment variables
96
+
97
+ - `OPENAI_API_KEY`
98
+ - `ANTHROPIC_API_KEY`
99
+ - `GEMINI_API_KEY` or `GOOGLE_API_KEY`
100
+
101
+ ## Docs
102
+
103
+ ### Core
104
+
105
+ - `docs/GETTING_STARTED.md`
106
+ - `docs/CONCEPTS.md`
107
+ - `docs/ARCHITECTURE.md`
108
+ - `docs/CONTRACT.md`
109
+ - `docs/ERRORS.md`
110
+ - `docs/STREAMING.md`
111
+ - `docs/COMPLETENESS.md`
112
+ - `docs/PRODUCTION_CHECKLIST.md`
113
+
114
+ ### Provider development
115
+
116
+ - `docs/ADAPTER_GUIDE.md`
117
+ - `docs/ADD_PROVIDER_GUIDE.md`
118
+ - `docs/COOKBOOK_TEMPLATE.md`
119
+
120
+ ### Cookbooks (learning order)
121
+
122
+ - `docs/COOKBOOKS/01-basic-text.md`
123
+ - `docs/COOKBOOKS/02-streaming.md`
124
+ - `docs/COOKBOOKS/03-tools.md`
125
+ - `docs/COOKBOOKS/04-multimodal.md`
126
+ - `docs/COOKBOOKS/05-files-batches.md`
127
+ - `docs/COOKBOOKS/06-reliability.md`
128
+ - `docs/COOKBOOKS/07-external-plugins.md`
129
+ - `docs/COOKBOOKS/08-models-dev-hydration.md`
lm15-0.1.0/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # lm15 (Universal LM) — thin core, full plugin contract
2
+
3
+ A universal LM core optimized for low import/runtime overhead with provider plugins.
4
+
5
+ ## Core architecture
6
+
7
+ - **Universal contract** (`types.py`): normalized request/response/stream/live types.
8
+ - **Provider plugin contract** (`providers/base.py`): complete/stream/live/embeddings/files/batch/images/audio methods.
9
+ - **Transport boundary** (`transports/*`): urllib and pycurl implementations.
10
+ - **Capability resolver** (`capabilities.py`): static + optional hydration from models.dev.
11
+ - **Model catalog bridge** (`model_catalog.py`): loads `https://models.dev/api.json`.
12
+ - **Middleware pipeline** (`middleware.py`): retries/history/cache wrappers.
13
+ - **Completeness harness** (`completeness/*`): fixture + live probes, score output.
14
+
15
+ ## Quick start
16
+
17
+ ```python
18
+ from lm15 import Message, LMRequest, Part, build_default
19
+
20
+ lm = build_default(use_pycurl=True)
21
+ req = LMRequest(model="claude-sonnet-4-5", messages=(Message(role="user", parts=(Part.text_part("Say hi."),)),))
22
+ resp = lm.complete(req)
23
+ print(resp.message.parts[0].text)
24
+ ```
25
+
26
+ ## External plugin adapters (no core PR)
27
+
28
+ LM15 auto-discovers installed entry-points in group `lm15.providers`.
29
+
30
+ ```toml
31
+ # in external package pyproject.toml
32
+ [project.entry-points."lm15.providers"]
33
+ myprovider = "ulm_x_myprovider:build_adapter"
34
+ ```
35
+
36
+ ```python
37
+ from lm15 import build_default
38
+
39
+ lm = build_default(discover_plugins=True)
40
+ ```
41
+
42
+ ## Optional models.dev hydration
43
+
44
+ ```python
45
+ lm = build_default(hydrate_models_dev_catalog=True)
46
+ ```
47
+
48
+ ## Completeness
49
+
50
+ ```bash
51
+ python3 completeness/runner.py --mode fixture --fail-under 1.0
52
+ python3 completeness/runner.py --mode live --fail-under 0.0
53
+ ```
54
+
55
+ Outputs:
56
+ - `completeness/report.json`
57
+ - `completeness/report.md`
58
+
59
+ ## Packaging and publishing (uv + twine)
60
+
61
+ ```bash
62
+ # build sdist + wheel
63
+ uv run python -m build
64
+
65
+ # upload to TestPyPI
66
+ # twine upload --repository testpypi dist/*
67
+
68
+ # upload to PyPI
69
+ # twine upload dist/*
70
+ ```
71
+
72
+ ## Environment variables
73
+
74
+ - `OPENAI_API_KEY`
75
+ - `ANTHROPIC_API_KEY`
76
+ - `GEMINI_API_KEY` or `GOOGLE_API_KEY`
77
+
78
+ ## Docs
79
+
80
+ ### Core
81
+
82
+ - `docs/GETTING_STARTED.md`
83
+ - `docs/CONCEPTS.md`
84
+ - `docs/ARCHITECTURE.md`
85
+ - `docs/CONTRACT.md`
86
+ - `docs/ERRORS.md`
87
+ - `docs/STREAMING.md`
88
+ - `docs/COMPLETENESS.md`
89
+ - `docs/PRODUCTION_CHECKLIST.md`
90
+
91
+ ### Provider development
92
+
93
+ - `docs/ADAPTER_GUIDE.md`
94
+ - `docs/ADD_PROVIDER_GUIDE.md`
95
+ - `docs/COOKBOOK_TEMPLATE.md`
96
+
97
+ ### Cookbooks (learning order)
98
+
99
+ - `docs/COOKBOOKS/01-basic-text.md`
100
+ - `docs/COOKBOOKS/02-streaming.md`
101
+ - `docs/COOKBOOKS/03-tools.md`
102
+ - `docs/COOKBOOKS/04-multimodal.md`
103
+ - `docs/COOKBOOKS/05-files-batches.md`
104
+ - `docs/COOKBOOKS/06-reliability.md`
105
+ - `docs/COOKBOOKS/07-external-plugins.md`
106
+ - `docs/COOKBOOKS/08-models-dev-hydration.md`
@@ -0,0 +1,25 @@
1
+ # Examples
2
+
3
+ Run all examples in offline-safe mode:
4
+
5
+ ```bash
6
+ python examples/validate_examples.py
7
+ ```
8
+
9
+ Run one example with live API calls:
10
+
11
+ ```bash
12
+ # ensure provider keys are set and do not set LM15_EXAMPLES_SKIP_LIVE
13
+ python examples/01_basic_text.py
14
+ ```
15
+
16
+ Files:
17
+
18
+ - `01_basic_text.py`
19
+ - `02_streaming.py`
20
+ - `03_tools.py`
21
+ - `04_multimodal.py`
22
+ - `05_files_batches.py`
23
+ - `06_reliability.py`
24
+ - `07_external_plugins.py`
25
+ - `08_models_dev_hydration.py`
@@ -0,0 +1,87 @@
1
+ from .capabilities import hydrate_with_specs
2
+ from .client import UniversalLM
3
+ from .middleware import with_cache, with_history, with_retries
4
+ from .model_catalog import build_provider_model_index, fetch_models_dev
5
+ from .plugins import discover_provider_entry_points, load_plugins
6
+ from .transports.base import TransportPolicy
7
+ from .types import (
8
+ AudioFormat,
9
+ AudioGenerationRequest,
10
+ AudioGenerationResponse,
11
+ BatchRequest,
12
+ BatchResponse,
13
+ Config,
14
+ DataSource,
15
+ EmbeddingRequest,
16
+ EmbeddingResponse,
17
+ FileUploadRequest,
18
+ FileUploadResponse,
19
+ ImageGenerationRequest,
20
+ ImageGenerationResponse,
21
+ LMRequest,
22
+ LMResponse,
23
+ LiveClientEvent,
24
+ LiveConfig,
25
+ LiveServerEvent,
26
+ Message,
27
+ Part,
28
+ StreamEvent,
29
+ Tool,
30
+ ToolConfig,
31
+ Usage,
32
+ )
33
+
34
+
35
+ def build_default(
36
+ use_pycurl: bool = True,
37
+ policy: TransportPolicy | None = None,
38
+ hydrate_models_dev_catalog: bool = False,
39
+ discover_plugins: bool = True,
40
+ ):
41
+ from .factory import build_default as _build_default
42
+
43
+ return _build_default(
44
+ use_pycurl=use_pycurl,
45
+ policy=policy,
46
+ hydrate_models_dev=hydrate_models_dev_catalog,
47
+ discover_plugins=discover_plugins,
48
+ )
49
+
50
+
51
+ __all__ = [
52
+ "UniversalLM",
53
+ "build_default",
54
+ "TransportPolicy",
55
+ "with_cache",
56
+ "with_history",
57
+ "with_retries",
58
+ "hydrate_with_specs",
59
+ "load_plugins",
60
+ "discover_provider_entry_points",
61
+ "fetch_models_dev",
62
+ "build_provider_model_index",
63
+ "Config",
64
+ "DataSource",
65
+ "LMRequest",
66
+ "LMResponse",
67
+ "Message",
68
+ "Part",
69
+ "StreamEvent",
70
+ "EmbeddingRequest",
71
+ "EmbeddingResponse",
72
+ "FileUploadRequest",
73
+ "FileUploadResponse",
74
+ "BatchRequest",
75
+ "BatchResponse",
76
+ "ImageGenerationRequest",
77
+ "ImageGenerationResponse",
78
+ "AudioGenerationRequest",
79
+ "AudioGenerationResponse",
80
+ "LiveConfig",
81
+ "LiveClientEvent",
82
+ "LiveServerEvent",
83
+ "AudioFormat",
84
+ "Tool",
85
+ "ToolConfig",
86
+ "Usage",
87
+ ]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ class AuthStrategy:
7
+ def apply_headers(self, headers: dict[str, str]) -> dict[str, str]:
8
+ return headers
9
+
10
+ def apply_params(self, params: dict[str, str]) -> dict[str, str]:
11
+ return params
12
+
13
+
14
+ @dataclass(slots=True, frozen=True)
15
+ class BearerAuth(AuthStrategy):
16
+ token: str
17
+
18
+ def apply_headers(self, headers: dict[str, str]) -> dict[str, str]:
19
+ out = dict(headers)
20
+ out["Authorization"] = f"Bearer {self.token}"
21
+ return out
22
+
23
+
24
+ @dataclass(slots=True, frozen=True)
25
+ class HeaderKeyAuth(AuthStrategy):
26
+ header: str
27
+ key: str
28
+
29
+ def apply_headers(self, headers: dict[str, str]) -> dict[str, str]:
30
+ out = dict(headers)
31
+ out[self.header] = self.key
32
+ return out
33
+
34
+
35
+ @dataclass(slots=True, frozen=True)
36
+ class QueryKeyAuth(AuthStrategy):
37
+ param: str
38
+ key: str
39
+
40
+ def apply_params(self, params: dict[str, str]) -> dict[str, str]:
41
+ out = dict(params)
42
+ out[self.param] = self.key
43
+ return out
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import statistics
4
+ import subprocess
5
+ import sys
6
+
7
+
8
+ def bench(module: str, runs: int = 30):
9
+ vals = []
10
+ for _ in range(runs):
11
+ p = subprocess.run(
12
+ [sys.executable, "-c", f"import time; t=time.perf_counter(); import {module}; print((time.perf_counter()-t)*1000)"],
13
+ capture_output=True,
14
+ text=True,
15
+ check=True,
16
+ )
17
+ vals.append(float(p.stdout.strip()))
18
+ vals.sort()
19
+ return {
20
+ "median_ms": statistics.median(vals),
21
+ "p95_ms": vals[int(0.95 * (len(vals) - 1))],
22
+ "min_ms": vals[0],
23
+ "max_ms": vals[-1],
24
+ }
25
+
26
+
27
+ if __name__ == "__main__":
28
+ print({"lm15": bench("lm15")})
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .model_catalog import ModelSpec
6
+ from .protocols import Capabilities
7
+
8
+
9
+ @dataclass(slots=True, frozen=True)
10
+ class ModelCapabilities:
11
+ provider: str
12
+ pattern: str
13
+ caps: Capabilities
14
+
15
+
16
+ REGISTRY: tuple[ModelCapabilities, ...] = (
17
+ ModelCapabilities(
18
+ provider="anthropic",
19
+ pattern="claude",
20
+ caps=Capabilities(
21
+ input_modalities=frozenset({"text", "image", "document"}),
22
+ output_modalities=frozenset({"text"}),
23
+ features=frozenset({"streaming", "tools", "reasoning"}),
24
+ ),
25
+ ),
26
+ ModelCapabilities(
27
+ provider="gemini",
28
+ pattern="gemini",
29
+ caps=Capabilities(
30
+ input_modalities=frozenset({"text", "image", "audio", "video", "document"}),
31
+ output_modalities=frozenset({"text"}),
32
+ features=frozenset({"streaming", "tools", "json_output", "live"}),
33
+ ),
34
+ ),
35
+ ModelCapabilities(
36
+ provider="openai",
37
+ pattern="gpt",
38
+ caps=Capabilities(
39
+ input_modalities=frozenset({"text", "image", "audio", "video", "document"}),
40
+ output_modalities=frozenset({"text", "audio"}),
41
+ features=frozenset({"streaming", "tools", "json_output", "reasoning", "live", "embeddings"}),
42
+ ),
43
+ ),
44
+ )
45
+
46
+
47
+ class CapabilityResolver:
48
+ def __init__(self):
49
+ self._model_index: dict[str, ModelSpec] = {}
50
+
51
+ def hydrate(self, specs: list[ModelSpec]) -> None:
52
+ self._model_index = {s.id: s for s in specs}
53
+
54
+ def resolve_provider(self, model: str) -> str:
55
+ if model in self._model_index:
56
+ return self._model_index[model].provider
57
+ lower = model.lower()
58
+ for item in REGISTRY:
59
+ if lower.startswith(item.pattern):
60
+ return item.provider
61
+ return "openai"
62
+
63
+ def resolve_capabilities(self, model: str) -> Capabilities:
64
+ spec = self._model_index.get(model)
65
+ if spec:
66
+ return spec.to_capabilities()
67
+ lower = model.lower()
68
+ for item in REGISTRY:
69
+ if lower.startswith(item.pattern):
70
+ return item.caps
71
+ return REGISTRY[-1].caps
72
+
73
+
74
+ _DEFAULT_RESOLVER = CapabilityResolver()
75
+
76
+
77
+ def hydrate_with_specs(specs: list[ModelSpec]) -> None:
78
+ _DEFAULT_RESOLVER.hydrate(specs)
79
+
80
+
81
+ def resolve_provider(model: str) -> str:
82
+ return _DEFAULT_RESOLVER.resolve_provider(model)
83
+
84
+
85
+ def resolve_capabilities(model: str) -> Capabilities:
86
+ return _DEFAULT_RESOLVER.resolve_capabilities(model)
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Iterator
5
+
6
+ from .capabilities import resolve_provider
7
+ from .errors import ProviderError, UnsupportedFeatureError
8
+ from .middleware import MiddlewarePipeline
9
+ from .protocols import LMAdapter, LiveSession
10
+ from .types import (
11
+ AudioGenerationRequest,
12
+ AudioGenerationResponse,
13
+ BatchRequest,
14
+ BatchResponse,
15
+ EmbeddingRequest,
16
+ EmbeddingResponse,
17
+ FileUploadRequest,
18
+ FileUploadResponse,
19
+ ImageGenerationRequest,
20
+ ImageGenerationResponse,
21
+ LMRequest,
22
+ LMResponse,
23
+ LiveConfig,
24
+ StreamEvent,
25
+ )
26
+
27
+
28
+ @dataclass(slots=True)
29
+ class UniversalLM:
30
+ adapters: dict[str, LMAdapter] = field(default_factory=dict)
31
+ middleware: MiddlewarePipeline = field(default_factory=MiddlewarePipeline)
32
+
33
+ def register(self, adapter: LMAdapter) -> None:
34
+ self.adapters[adapter.provider] = adapter
35
+
36
+ def _adapter(self, model: str, provider: str | None = None) -> LMAdapter:
37
+ p = provider or resolve_provider(model)
38
+ adapter = self.adapters.get(p)
39
+ if not adapter:
40
+ raise ProviderError(f"no adapter registered for provider '{p}'")
41
+ return adapter
42
+
43
+ def complete(self, request: LMRequest, provider: str | None = None) -> LMResponse:
44
+ adapter = self._adapter(request.model, provider)
45
+ if not adapter.supports.complete:
46
+ raise UnsupportedFeatureError(f"{adapter.provider}: complete not supported")
47
+ run = self.middleware.wrap_complete(adapter.complete)
48
+ return run(request)
49
+
50
+ def stream(self, request: LMRequest, provider: str | None = None) -> Iterator[StreamEvent]:
51
+ adapter = self._adapter(request.model, provider)
52
+ if not adapter.supports.stream:
53
+ raise UnsupportedFeatureError(f"{adapter.provider}: stream not supported")
54
+ run = self.middleware.wrap_stream(adapter.stream)
55
+ yield from run(request)
56
+
57
+ def live(self, config: LiveConfig, provider: str | None = None) -> LiveSession:
58
+ adapter = self._adapter(config.model, provider)
59
+ if not adapter.supports.live:
60
+ raise UnsupportedFeatureError(f"{adapter.provider}: live not supported")
61
+ return adapter.live(config)
62
+
63
+ def embeddings(self, request: EmbeddingRequest, provider: str | None = None) -> EmbeddingResponse:
64
+ adapter = self._adapter(request.model, provider)
65
+ if not adapter.supports.embeddings:
66
+ raise UnsupportedFeatureError(f"{adapter.provider}: embeddings not supported")
67
+ return adapter.embeddings(request)
68
+
69
+ def file_upload(self, request: FileUploadRequest, provider: str) -> FileUploadResponse:
70
+ adapter = self._adapter(request.model or "", provider)
71
+ if not adapter.supports.files:
72
+ raise UnsupportedFeatureError(f"{adapter.provider}: files not supported")
73
+ return adapter.file_upload(request)
74
+
75
+ def batch_submit(self, request: BatchRequest, provider: str | None = None) -> BatchResponse:
76
+ adapter = self._adapter(request.model, provider)
77
+ if not adapter.supports.batches:
78
+ raise UnsupportedFeatureError(f"{adapter.provider}: batches not supported")
79
+ return adapter.batch_submit(request)
80
+
81
+ def image_generate(self, request: ImageGenerationRequest, provider: str | None = None) -> ImageGenerationResponse:
82
+ adapter = self._adapter(request.model, provider)
83
+ if not adapter.supports.images:
84
+ raise UnsupportedFeatureError(f"{adapter.provider}: images not supported")
85
+ return adapter.image_generate(request)
86
+
87
+ def audio_generate(self, request: AudioGenerationRequest, provider: str | None = None) -> AudioGenerationResponse:
88
+ adapter = self._adapter(request.model, provider)
89
+ if not adapter.supports.audio:
90
+ raise UnsupportedFeatureError(f"{adapter.provider}: audio not supported")
91
+ return adapter.audio_generate(request)
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ULMError(Exception):
5
+ pass
6
+
7
+
8
+ class TransportError(ULMError):
9
+ pass
10
+
11
+
12
+ class ProviderError(ULMError):
13
+ pass
14
+
15
+
16
+ class AuthError(ProviderError):
17
+ pass
18
+
19
+
20
+ class RateLimitError(ProviderError):
21
+ pass
22
+
23
+
24
+ class TimeoutError(ProviderError):
25
+ pass
26
+
27
+
28
+ class InvalidRequestError(ProviderError):
29
+ pass
30
+
31
+
32
+ class ServerError(ProviderError):
33
+ pass
34
+
35
+
36
+ class UnsupportedModelError(ProviderError):
37
+ pass
38
+
39
+
40
+ class UnsupportedFeatureError(ProviderError):
41
+ pass
42
+
43
+
44
+ class NotConfiguredError(ProviderError):
45
+ pass
46
+
47
+
48
+ def map_http_error(status: int, message: str) -> ProviderError:
49
+ if status in (401, 403):
50
+ return AuthError(message)
51
+ if status == 408:
52
+ return TimeoutError(message)
53
+ if status == 429:
54
+ return RateLimitError(message)
55
+ if status in (400, 404, 409, 422):
56
+ return InvalidRequestError(message)
57
+ if 500 <= status <= 599:
58
+ return ServerError(message)
59
+ return ProviderError(message)