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 +8 -0
- lm15-0.1.0/LICENSE +21 -0
- lm15-0.1.0/PKG-INFO +129 -0
- lm15-0.1.0/README.md +106 -0
- lm15-0.1.0/examples/README.md +25 -0
- lm15-0.1.0/lm15/__init__.py +87 -0
- lm15-0.1.0/lm15/auth.py +43 -0
- lm15-0.1.0/lm15/bench_import.py +28 -0
- lm15-0.1.0/lm15/capabilities.py +86 -0
- lm15-0.1.0/lm15/client.py +91 -0
- lm15-0.1.0/lm15/errors.py +59 -0
- lm15-0.1.0/lm15/examples.py +35 -0
- lm15-0.1.0/lm15/factory.py +56 -0
- lm15-0.1.0/lm15/features.py +24 -0
- lm15-0.1.0/lm15/middleware.py +86 -0
- lm15-0.1.0/lm15/model_catalog.py +70 -0
- lm15-0.1.0/lm15/plugins.py +55 -0
- lm15-0.1.0/lm15/protocols.py +62 -0
- lm15-0.1.0/lm15/providers/__init__.py +6 -0
- lm15-0.1.0/lm15/providers/anthropic.py +204 -0
- lm15-0.1.0/lm15/providers/base.py +95 -0
- lm15-0.1.0/lm15/providers/common.py +43 -0
- lm15-0.1.0/lm15/providers/gemini.py +322 -0
- lm15-0.1.0/lm15/providers/openai.py +318 -0
- lm15-0.1.0/lm15/sse.py +36 -0
- lm15-0.1.0/lm15/transports/__init__.py +5 -0
- lm15-0.1.0/lm15/transports/base.py +50 -0
- lm15-0.1.0/lm15/transports/pycurl_transport.py +128 -0
- lm15-0.1.0/lm15/transports/urllib_transport.py +59 -0
- lm15-0.1.0/lm15/types.py +345 -0
- lm15-0.1.0/pyproject.toml +49 -0
lm15-0.1.0/.gitignore
ADDED
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
|
+
]
|
lm15-0.1.0/lm15/auth.py
ADDED
|
@@ -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)
|