ai-provider-tracker 0.7.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.
- ai_provider_tracker-0.7.0/LICENSE +21 -0
- ai_provider_tracker-0.7.0/PKG-INFO +162 -0
- ai_provider_tracker-0.7.0/README.md +134 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/__init__.py +206 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/api/main.py +147 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/__init__.py +0 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/artificial_analysis_fetcher.py +84 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/http_fetcher.py +26 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/openrouter_fetcher.py +36 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/json_exporter.py +55 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/json_repository.py +126 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/models.py +22 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/sqlite_repository.py +200 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/__init__.py +21 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/calculator.py +72 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/models.py +59 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/normalizers.py +174 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/pricing.py +44 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/repository.py +65 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/tracker.py +67 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/utils.py +45 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/data/__init__.py +1 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/data/pricing_catalog.json +32 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/domain/entities.py +101 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/domain/interfaces.py +66 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/domain/services/__init__.py +0 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/domain/services/matching_engine.py +75 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/infrastructure/config.py +37 -0
- ai_provider_tracker-0.7.0/ai_provider_tracker/use_cases/sync_registry.py +153 -0
- ai_provider_tracker-0.7.0/openrouter_insights/__init__.py +3 -0
- ai_provider_tracker-0.7.0/pyproject.toml +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luis Eduardo Farfan Melgar
|
|
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.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-provider-tracker
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Unified usage and cost tracking for AI providers such as FAL.AI and OpenRouter.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: Luis Eduardo Farfan Melgar
|
|
7
|
+
Author-email: lucho.farfan9@gmail.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Provides-Extra: api
|
|
15
|
+
Requires-Dist: aiohttp (>=3.9.3,<4.0.0)
|
|
16
|
+
Requires-Dist: fastapi (>=0.110.0,<0.111.0) ; extra == "api"
|
|
17
|
+
Requires-Dist: pydantic (>=2.6.4,<3.0.0)
|
|
18
|
+
Requires-Dist: pydantic-settings (>=2.2.1,<3.0.0)
|
|
19
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
20
|
+
Requires-Dist: rapidfuzz (>=3.6.2,<4.0.0)
|
|
21
|
+
Requires-Dist: sqlmodel (>=0.0.16,<0.0.17)
|
|
22
|
+
Requires-Dist: uvicorn (>=0.29.0,<0.30.0) ; extra == "api"
|
|
23
|
+
Project-URL: Documentation, https://github.com/luisfarfan/ai-provider-tracker#readme
|
|
24
|
+
Project-URL: Homepage, https://github.com/luisfarfan/ai-provider-tracker
|
|
25
|
+
Project-URL: Repository, https://github.com/luisfarfan/ai-provider-tracker
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# AI Provider Tracker
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/ai-provider-tracker/)
|
|
31
|
+
[](https://pypi.org/project/ai-provider-tracker/)
|
|
32
|
+
[](https://opensource.org/licenses/MIT)
|
|
33
|
+
|
|
34
|
+
**AI Provider Tracker** is a Python library for tracking AI provider usage and estimating per-generation costs across heterogeneous providers.
|
|
35
|
+
|
|
36
|
+
It is designed for apps that already call providers such as **FAL.AI** and **OpenRouter**, but need a centralized way to:
|
|
37
|
+
|
|
38
|
+
- normalize usage metadata
|
|
39
|
+
- calculate request/generation costs
|
|
40
|
+
- store raw request/response payloads for auditability
|
|
41
|
+
- persist optional local analytics in SQLite
|
|
42
|
+
- use bundled pricing snapshots without calling pricing APIs at runtime
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install ai-provider-tracker
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Optional FastAPI dependencies for the legacy model registry API:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "ai-provider-tracker[api]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Cost Tracking
|
|
57
|
+
|
|
58
|
+
Use your provider SDK as usual, then pass the request and response to the tracker.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from ai_provider_tracker import CostTracker
|
|
62
|
+
|
|
63
|
+
tracker = CostTracker()
|
|
64
|
+
|
|
65
|
+
event = tracker.track_generation(
|
|
66
|
+
provider="fal",
|
|
67
|
+
model="fal-ai/flux/dev",
|
|
68
|
+
request={"prompt": "A cyberpunk city", "num_images": 2},
|
|
69
|
+
response={"images": [{"url": "a"}, {"url": "b"}]},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print(event.cost.total) # 0.050
|
|
73
|
+
print(event.cost.source) # public_snapshot
|
|
74
|
+
print(event.cost.confidence) # high
|
|
75
|
+
print(event.cost.breakdown)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Enable local SQLite persistence:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
tracker = CostTracker(sqlite_path="ai_usage.sqlite")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This stores generation events with normalized usage, cost breakdown, raw request, raw response, and metadata.
|
|
85
|
+
|
|
86
|
+
## OpenRouter
|
|
87
|
+
|
|
88
|
+
For OpenRouter, provider-reported usage cost is preferred when present:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
event = tracker.track_generation(
|
|
92
|
+
provider="openrouter",
|
|
93
|
+
model="anthropic/claude-sonnet-4.5",
|
|
94
|
+
request={"messages": [{"role": "user", "content": "Hello"}]},
|
|
95
|
+
response={
|
|
96
|
+
"usage": {
|
|
97
|
+
"prompt_tokens": 1200,
|
|
98
|
+
"completion_tokens": 800,
|
|
99
|
+
"cost": "0.015",
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
print(event.cost.total) # provider-reported cost
|
|
105
|
+
print(event.cost.source) # provider_reported
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If `usage.cost` is not present, the tracker falls back to token-based calculation using the bundled pricing catalog when possible.
|
|
109
|
+
|
|
110
|
+
## Pricing Catalog
|
|
111
|
+
|
|
112
|
+
The package ships with a bundled JSON pricing catalog:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
ai_provider_tracker/data/pricing_catalog.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Runtime requests do not call pricing APIs. Pricing sync is handled by:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python scripts/sync_pricing_catalog.py
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The GitHub Actions workflow `.github/workflows/pricing_sync.yml` runs daily and commits the catalog only when prices actually change.
|
|
125
|
+
|
|
126
|
+
## Legacy Model Registry
|
|
127
|
+
|
|
128
|
+
This package still includes the original OpenRouter model registry APIs:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from ai_provider_tracker import LLMIndexSync
|
|
132
|
+
|
|
133
|
+
client = LLMIndexSync(mode="json")
|
|
134
|
+
models = client.get_cheapest(limit=5)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The old import path remains available temporarily:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from openrouter_insights import CostTracker
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
New code should use:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from ai_provider_tracker import CostTracker
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Accuracy Contract
|
|
150
|
+
|
|
151
|
+
Cost values are intended for product analytics, internal attribution, and budget monitoring.
|
|
152
|
+
|
|
153
|
+
- FAL.AI costs are locally estimated from pricing snapshots and normalized request/response metadata.
|
|
154
|
+
- OpenRouter costs use provider-reported cost when available.
|
|
155
|
+
- Public bundled pricing is a reference snapshot, not a guarantee of account-specific billing.
|
|
156
|
+
|
|
157
|
+
For invoice-grade accounting, reconcile against provider billing/usage APIs.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
|
162
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# AI Provider Tracker
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ai-provider-tracker/)
|
|
4
|
+
[](https://pypi.org/project/ai-provider-tracker/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
**AI Provider Tracker** is a Python library for tracking AI provider usage and estimating per-generation costs across heterogeneous providers.
|
|
8
|
+
|
|
9
|
+
It is designed for apps that already call providers such as **FAL.AI** and **OpenRouter**, but need a centralized way to:
|
|
10
|
+
|
|
11
|
+
- normalize usage metadata
|
|
12
|
+
- calculate request/generation costs
|
|
13
|
+
- store raw request/response payloads for auditability
|
|
14
|
+
- persist optional local analytics in SQLite
|
|
15
|
+
- use bundled pricing snapshots without calling pricing APIs at runtime
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install ai-provider-tracker
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Optional FastAPI dependencies for the legacy model registry API:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install "ai-provider-tracker[api]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Cost Tracking
|
|
30
|
+
|
|
31
|
+
Use your provider SDK as usual, then pass the request and response to the tracker.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from ai_provider_tracker import CostTracker
|
|
35
|
+
|
|
36
|
+
tracker = CostTracker()
|
|
37
|
+
|
|
38
|
+
event = tracker.track_generation(
|
|
39
|
+
provider="fal",
|
|
40
|
+
model="fal-ai/flux/dev",
|
|
41
|
+
request={"prompt": "A cyberpunk city", "num_images": 2},
|
|
42
|
+
response={"images": [{"url": "a"}, {"url": "b"}]},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
print(event.cost.total) # 0.050
|
|
46
|
+
print(event.cost.source) # public_snapshot
|
|
47
|
+
print(event.cost.confidence) # high
|
|
48
|
+
print(event.cost.breakdown)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Enable local SQLite persistence:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
tracker = CostTracker(sqlite_path="ai_usage.sqlite")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This stores generation events with normalized usage, cost breakdown, raw request, raw response, and metadata.
|
|
58
|
+
|
|
59
|
+
## OpenRouter
|
|
60
|
+
|
|
61
|
+
For OpenRouter, provider-reported usage cost is preferred when present:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
event = tracker.track_generation(
|
|
65
|
+
provider="openrouter",
|
|
66
|
+
model="anthropic/claude-sonnet-4.5",
|
|
67
|
+
request={"messages": [{"role": "user", "content": "Hello"}]},
|
|
68
|
+
response={
|
|
69
|
+
"usage": {
|
|
70
|
+
"prompt_tokens": 1200,
|
|
71
|
+
"completion_tokens": 800,
|
|
72
|
+
"cost": "0.015",
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(event.cost.total) # provider-reported cost
|
|
78
|
+
print(event.cost.source) # provider_reported
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If `usage.cost` is not present, the tracker falls back to token-based calculation using the bundled pricing catalog when possible.
|
|
82
|
+
|
|
83
|
+
## Pricing Catalog
|
|
84
|
+
|
|
85
|
+
The package ships with a bundled JSON pricing catalog:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
ai_provider_tracker/data/pricing_catalog.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Runtime requests do not call pricing APIs. Pricing sync is handled by:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python scripts/sync_pricing_catalog.py
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The GitHub Actions workflow `.github/workflows/pricing_sync.yml` runs daily and commits the catalog only when prices actually change.
|
|
98
|
+
|
|
99
|
+
## Legacy Model Registry
|
|
100
|
+
|
|
101
|
+
This package still includes the original OpenRouter model registry APIs:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from ai_provider_tracker import LLMIndexSync
|
|
105
|
+
|
|
106
|
+
client = LLMIndexSync(mode="json")
|
|
107
|
+
models = client.get_cheapest(limit=5)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The old import path remains available temporarily:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from openrouter_insights import CostTracker
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
New code should use:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from ai_provider_tracker import CostTracker
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Accuracy Contract
|
|
123
|
+
|
|
124
|
+
Cost values are intended for product analytics, internal attribution, and budget monitoring.
|
|
125
|
+
|
|
126
|
+
- FAL.AI costs are locally estimated from pricing snapshots and normalized request/response metadata.
|
|
127
|
+
- OpenRouter costs use provider-reported cost when available.
|
|
128
|
+
- Public bundled pricing is a reference snapshot, not a guarantee of account-specific billing.
|
|
129
|
+
|
|
130
|
+
For invoice-grade accounting, reconcile against provider billing/usage APIs.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import List, Optional, Literal
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
from ai_provider_tracker.domain.entities import LLMModel, Pricing, Benchmarks
|
|
6
|
+
from ai_provider_tracker.adapters.persistence.sqlite_repository import SQLiteModelRepository
|
|
7
|
+
from ai_provider_tracker.adapters.persistence.json_repository import JSONModelRepository
|
|
8
|
+
from ai_provider_tracker.use_cases.sync_registry import SyncRegistryUseCase
|
|
9
|
+
from ai_provider_tracker.adapters.gateways.openrouter_fetcher import OpenRouterFetcher
|
|
10
|
+
from ai_provider_tracker.adapters.gateways.artificial_analysis_fetcher import ArtificialAnalysisFetcher
|
|
11
|
+
from ai_provider_tracker.domain.services.matching_engine import MatchingEngine
|
|
12
|
+
from ai_provider_tracker.adapters.persistence.json_exporter import JSONExporter
|
|
13
|
+
from ai_provider_tracker.cost_tracking import (
|
|
14
|
+
CostBreakdownLine,
|
|
15
|
+
CostResult,
|
|
16
|
+
CostTracker,
|
|
17
|
+
GenerationUsageEvent,
|
|
18
|
+
NormalizedUsage,
|
|
19
|
+
PricingCatalog,
|
|
20
|
+
PricingEntry,
|
|
21
|
+
UsageUnit,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
class LLMIndexSync:
|
|
25
|
+
"""
|
|
26
|
+
Synchronous entry point for LLMIndex.
|
|
27
|
+
Perfect for scripts, notebooks, and CLI tools. No 'await' required.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, mode: Literal["sqlite", "json"] = "sqlite", path: Optional[str] = None):
|
|
31
|
+
self.mode = mode
|
|
32
|
+
if mode == "sqlite":
|
|
33
|
+
self.repository = SQLiteModelRepository(database_url=path)
|
|
34
|
+
else:
|
|
35
|
+
self.repository = JSONModelRepository(file_path=path or "ai_provider_tracker.json")
|
|
36
|
+
|
|
37
|
+
def get_models(self, filter_virtual: bool = True, **kwargs) -> List[LLMModel]:
|
|
38
|
+
"""Base query method. Excludes routers like openrouter/auto by default."""
|
|
39
|
+
return self.repository.get_all(filter_virtual=filter_virtual, **kwargs)
|
|
40
|
+
|
|
41
|
+
def get_model(self, model_id: str) -> Optional[LLMModel]:
|
|
42
|
+
return self.repository.get_by_id(model_id)
|
|
43
|
+
|
|
44
|
+
# --- Smart Query Methods ---
|
|
45
|
+
|
|
46
|
+
def get_smartest(self, limit: int = 10) -> List[LLMModel]:
|
|
47
|
+
"""Top models by overall intelligence score."""
|
|
48
|
+
return self.get_models(sort_by="intelligence", page_size=limit)
|
|
49
|
+
|
|
50
|
+
def get_cheapest(self, best_for: Optional[str] = None, filter_virtual: bool = True, limit: int = 10) -> List[LLMModel]:
|
|
51
|
+
"""Models sorted by price (input + output) ascending."""
|
|
52
|
+
return self.get_models(best_for=best_for, filter_virtual=filter_virtual, sort_by="price", sort_order="asc", page_size=limit)
|
|
53
|
+
|
|
54
|
+
def get_fastest(self, limit: int = 10) -> List[LLMModel]:
|
|
55
|
+
"""Top models by Output TPS (Tokens Per Second)."""
|
|
56
|
+
return self.get_models(sort_by="speed", page_size=limit)
|
|
57
|
+
|
|
58
|
+
def get_best_for_coding(self, limit: int = 5) -> List[LLMModel]:
|
|
59
|
+
return self.get_models(best_for="coding", sort_by="intelligence", page_size=limit)
|
|
60
|
+
|
|
61
|
+
def get_best_for_reasoning(self, limit: int = 5) -> List[LLMModel]:
|
|
62
|
+
return self.get_models(best_for="reasoning", sort_by="intelligence", page_size=limit)
|
|
63
|
+
|
|
64
|
+
def get_best_for_rag(self, limit: int = 5) -> List[LLMModel]:
|
|
65
|
+
"""Models with high context and solid intelligence."""
|
|
66
|
+
return self.get_models(best_for="rag", sort_by="intelligence", page_size=limit)
|
|
67
|
+
|
|
68
|
+
def get_best_for_multimodal(self, limit: int = 5) -> List[LLMModel]:
|
|
69
|
+
"""Models with vision/media capabilities and high ELO."""
|
|
70
|
+
return self.get_models(best_for="multimodal", sort_by="elo", page_size=limit)
|
|
71
|
+
|
|
72
|
+
def get_free_models(self) -> List[LLMModel]:
|
|
73
|
+
"""All models with zero input cost."""
|
|
74
|
+
return self.get_models(is_free=True)
|
|
75
|
+
|
|
76
|
+
def get_top_frontier(self, limit: int = 3) -> List[LLMModel]:
|
|
77
|
+
"""The absolute SOTA models (Frontier tier)."""
|
|
78
|
+
# Filter by tier in memory for consistency across repos
|
|
79
|
+
all_models = self.get_models(sort_by="intelligence", page_size=50)
|
|
80
|
+
return [m for m in all_models if m.performance_tier == "frontier"][:limit]
|
|
81
|
+
|
|
82
|
+
def get_by_tier(self, tier: Literal["frontier", "pro", "lite"], filter_virtual: bool = True, limit: int = 10) -> List[LLMModel]:
|
|
83
|
+
"""Get models from a specific performance tier (using native DB filtering)."""
|
|
84
|
+
return self.get_models(best_for=tier, filter_virtual=filter_virtual, sort_by="intelligence", page_size=limit)
|
|
85
|
+
|
|
86
|
+
def get_best_alternative(self, model_id: str, max_price: Optional[float] = None) -> Optional[LLMModel]:
|
|
87
|
+
"""Find the best substitute for a given model (same tier, under budget)."""
|
|
88
|
+
return self.repository.get_best_alternative(model_id, max_price)
|
|
89
|
+
|
|
90
|
+
def get_by_provider(self, provider: str, limit: int = 10) -> List[LLMModel]:
|
|
91
|
+
return self.get_models(provider=provider, page_size=limit)
|
|
92
|
+
|
|
93
|
+
def search(self, query: str, limit: int = 10) -> List[LLMModel]:
|
|
94
|
+
"""Fuzzy search models by name, provider or ID."""
|
|
95
|
+
return self.repository.search(query, limit=limit)
|
|
96
|
+
|
|
97
|
+
def sync(self) -> List[LLMModel]:
|
|
98
|
+
"""Synchronize registry (Sync wrapper for the async use case)."""
|
|
99
|
+
if self.mode == "json":
|
|
100
|
+
raise ValueError("Sync is not supported in JSON mode.")
|
|
101
|
+
return asyncio.run(self._async_sync())
|
|
102
|
+
|
|
103
|
+
async def _async_sync(self):
|
|
104
|
+
fetchers = [OpenRouterFetcher(), ArtificialAnalysisFetcher()]
|
|
105
|
+
use_case = SyncRegistryUseCase(
|
|
106
|
+
repository=self.repository,
|
|
107
|
+
gateways=fetchers,
|
|
108
|
+
matching_engine=MatchingEngine(85.0),
|
|
109
|
+
exporter=JSONExporter("ai_provider_tracker.json")
|
|
110
|
+
)
|
|
111
|
+
return await use_case.execute()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class LLMIndex:
|
|
115
|
+
"""
|
|
116
|
+
Asynchronous entry point for LLMIndex.
|
|
117
|
+
Ideal for FastAPI, Discord bots, and async apps.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, mode: Literal["sqlite", "json"] = "sqlite", path: Optional[str] = None):
|
|
121
|
+
self.mode = mode
|
|
122
|
+
if mode == "sqlite":
|
|
123
|
+
self.repository = SQLiteModelRepository(database_url=path)
|
|
124
|
+
else:
|
|
125
|
+
self.repository = JSONModelRepository(file_path=path or "ai_provider_tracker.json")
|
|
126
|
+
|
|
127
|
+
async def get_models(self, filter_virtual: bool = True, **kwargs) -> List[LLMModel]:
|
|
128
|
+
return self.repository.get_all(filter_virtual=filter_virtual, **kwargs)
|
|
129
|
+
|
|
130
|
+
async def get_model(self, model_id: str) -> Optional[LLMModel]:
|
|
131
|
+
return self.repository.get_by_id(model_id)
|
|
132
|
+
|
|
133
|
+
# --- Smart Query Methods (Async) ---
|
|
134
|
+
|
|
135
|
+
async def get_smartest(self, limit: int = 10) -> List[LLMModel]:
|
|
136
|
+
return await self.get_models(sort_by="intelligence", page_size=limit)
|
|
137
|
+
|
|
138
|
+
async def get_cheapest(self, best_for: Optional[str] = None, filter_virtual: bool = True, limit: int = 10) -> List[LLMModel]:
|
|
139
|
+
return await self.get_models(best_for=best_for, filter_virtual=filter_virtual, sort_by="price", sort_order="asc", page_size=limit)
|
|
140
|
+
|
|
141
|
+
async def get_fastest(self, limit: int = 10) -> List[LLMModel]:
|
|
142
|
+
return await self.get_models(sort_by="speed", page_size=limit)
|
|
143
|
+
|
|
144
|
+
async def get_best_for_coding(self, limit: int = 5) -> List[LLMModel]:
|
|
145
|
+
return await self.get_models(best_for="coding", sort_by="intelligence", page_size=limit)
|
|
146
|
+
|
|
147
|
+
async def get_best_for_reasoning(self, limit: int = 5) -> List[LLMModel]:
|
|
148
|
+
return await self.get_models(best_for="reasoning", sort_by="intelligence", page_size=limit)
|
|
149
|
+
|
|
150
|
+
async def get_best_for_rag(self, limit: int = 5) -> List[LLMModel]:
|
|
151
|
+
return await self.get_models(best_for="rag", sort_by="intelligence", page_size=limit)
|
|
152
|
+
|
|
153
|
+
async def get_best_for_multimodal(self, limit: int = 5) -> List[LLMModel]:
|
|
154
|
+
return await self.get_models(best_for="multimodal", sort_by="elo", page_size=limit)
|
|
155
|
+
|
|
156
|
+
async def get_free_models(self) -> List[LLMModel]:
|
|
157
|
+
return await self.get_models(is_free=True)
|
|
158
|
+
|
|
159
|
+
async def get_top_frontier(self, limit: int = 3) -> List[LLMModel]:
|
|
160
|
+
all_models = await self.get_models(sort_by="intelligence", page_size=50)
|
|
161
|
+
return [m for m in all_models if m.performance_tier == "frontier"][:limit]
|
|
162
|
+
|
|
163
|
+
async def get_by_tier(self, tier: Literal["frontier", "pro", "lite"], filter_virtual: bool = True, limit: int = 10) -> List[LLMModel]:
|
|
164
|
+
return await self.get_models(best_for=tier, filter_virtual=filter_virtual, sort_by="intelligence", page_size=limit)
|
|
165
|
+
|
|
166
|
+
async def get_best_alternative(self, model_id: str, max_price: Optional[float] = None) -> Optional[LLMModel]:
|
|
167
|
+
return self.repository.get_best_alternative(model_id, max_price)
|
|
168
|
+
|
|
169
|
+
async def get_by_provider(self, provider: str, limit: int = 10) -> List[LLMModel]:
|
|
170
|
+
return await self.get_models(provider=provider, page_size=limit)
|
|
171
|
+
|
|
172
|
+
async def search(self, query: str, limit: int = 10) -> List[LLMModel]:
|
|
173
|
+
return self.repository.search(query, limit=limit)
|
|
174
|
+
|
|
175
|
+
async def sync(self) -> List[LLMModel]:
|
|
176
|
+
if self.mode == "json":
|
|
177
|
+
raise ValueError("Sync is not supported in JSON mode.")
|
|
178
|
+
fetchers = [OpenRouterFetcher(), ArtificialAnalysisFetcher()]
|
|
179
|
+
use_case = SyncRegistryUseCase(
|
|
180
|
+
repository=self.repository,
|
|
181
|
+
gateways=fetchers,
|
|
182
|
+
matching_engine=MatchingEngine(85.0),
|
|
183
|
+
exporter=JSONExporter("ai_provider_tracker.json")
|
|
184
|
+
)
|
|
185
|
+
return await use_case.execute()
|
|
186
|
+
|
|
187
|
+
__all__ = [
|
|
188
|
+
"LLMIndex",
|
|
189
|
+
"LLMIndexSync",
|
|
190
|
+
"LLMModel",
|
|
191
|
+
"Pricing",
|
|
192
|
+
"Benchmarks",
|
|
193
|
+
"SQLiteModelRepository",
|
|
194
|
+
"JSONModelRepository",
|
|
195
|
+
"SyncRegistryUseCase",
|
|
196
|
+
"OpenRouterFetcher",
|
|
197
|
+
"ArtificialAnalysisFetcher",
|
|
198
|
+
"CostBreakdownLine",
|
|
199
|
+
"CostResult",
|
|
200
|
+
"CostTracker",
|
|
201
|
+
"GenerationUsageEvent",
|
|
202
|
+
"NormalizedUsage",
|
|
203
|
+
"PricingCatalog",
|
|
204
|
+
"PricingEntry",
|
|
205
|
+
"UsageUnit",
|
|
206
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, Query, HTTPException, Depends
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from ai_provider_tracker import LLMIndex, LLMModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Enum Definitions for better Swagger UI
|
|
11
|
+
class Provider(str, Enum):
|
|
12
|
+
OPENAI = "OpenAI"
|
|
13
|
+
ANTHROPIC = "Anthropic"
|
|
14
|
+
GOOGLE = "Google"
|
|
15
|
+
META = "Meta"
|
|
16
|
+
MISTRAL = "Mistral"
|
|
17
|
+
XAI = "xAI"
|
|
18
|
+
DEEPSEEK = "DeepSeek"
|
|
19
|
+
MICROSOFT = "Microsoft"
|
|
20
|
+
COHERE = "Cohere"
|
|
21
|
+
|
|
22
|
+
class BestFor(str, Enum):
|
|
23
|
+
CODING = "coding"
|
|
24
|
+
REASONING = "reasoning"
|
|
25
|
+
REAL_TIME = "real-time"
|
|
26
|
+
MULTIMODAL_HIGH_FIDELITY = "multimodal-high-fidelity"
|
|
27
|
+
MULTIMODAL = "multimodal"
|
|
28
|
+
RAG = "rag"
|
|
29
|
+
|
|
30
|
+
class SortBy(str, Enum):
|
|
31
|
+
PRICE = "price"
|
|
32
|
+
INTELLIGENCE = "intelligence"
|
|
33
|
+
SPEED = "speed"
|
|
34
|
+
ELO = "elo"
|
|
35
|
+
|
|
36
|
+
class Order(str, Enum):
|
|
37
|
+
ASC = "asc"
|
|
38
|
+
DESC = "desc"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PaginatedModelsResponse(BaseModel):
|
|
42
|
+
total: int
|
|
43
|
+
page: int
|
|
44
|
+
page_size: int
|
|
45
|
+
results: List[LLMModel]
|
|
46
|
+
|
|
47
|
+
app = FastAPI(
|
|
48
|
+
title="LLMIndex API",
|
|
49
|
+
description="The Unified Open-Source LLM Registry API.",
|
|
50
|
+
version="0.1.0"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# CORS Middleware
|
|
54
|
+
app.add_middleware(
|
|
55
|
+
CORSMiddleware,
|
|
56
|
+
allow_origins=["*"],
|
|
57
|
+
allow_credentials=True,
|
|
58
|
+
allow_methods=["*"],
|
|
59
|
+
allow_headers=["*"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Dependency Injection Setup
|
|
63
|
+
def get_index():
|
|
64
|
+
return LLMIndex(mode="sqlite")
|
|
65
|
+
|
|
66
|
+
@app.get("/", tags=["Health"])
|
|
67
|
+
async def root():
|
|
68
|
+
return {"status": "ok", "message": "LLMIndex API is live."}
|
|
69
|
+
|
|
70
|
+
@app.get("/api/v1/models", response_model=PaginatedModelsResponse, tags=["Models"])
|
|
71
|
+
async def get_models(
|
|
72
|
+
provider: Provider = Query(None, description="Filter by provider (e.g., OpenAI, Anthropic)"),
|
|
73
|
+
best_for: BestFor = Query(None, description="Filter by strength (coding, rag, real-time, multimodal)"),
|
|
74
|
+
is_free: bool = Query(False, description="Show only free models"),
|
|
75
|
+
min_intelligence: float = Query(None, description="Minimum benchmark score"),
|
|
76
|
+
sort_by: SortBy = Query(None, description="Sort order (price, intelligence, speed)"),
|
|
77
|
+
order: Order = Query(Order.DESC, description="Sorting direction (asc, desc)"),
|
|
78
|
+
page: int = Query(1, ge=1, description="Page number"),
|
|
79
|
+
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
|
|
80
|
+
index: LLMIndex = Depends(get_index)
|
|
81
|
+
):
|
|
82
|
+
"""
|
|
83
|
+
Query the unified LLM registry with high-fidelity filters.
|
|
84
|
+
"""
|
|
85
|
+
p_val = provider.value if provider else None
|
|
86
|
+
b_val = best_for.value if best_for else None
|
|
87
|
+
s_val = sort_by.value if sort_by else None
|
|
88
|
+
o_val = order.value
|
|
89
|
+
|
|
90
|
+
total = await index.get_count(
|
|
91
|
+
provider=p_val,
|
|
92
|
+
best_for=b_val,
|
|
93
|
+
is_free=is_free,
|
|
94
|
+
min_intelligence=min_intelligence
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
models = await index.get_models(
|
|
98
|
+
provider=p_val,
|
|
99
|
+
best_for=b_val,
|
|
100
|
+
is_free=is_free,
|
|
101
|
+
min_intelligence=min_intelligence,
|
|
102
|
+
sort_by=s_val,
|
|
103
|
+
sort_order=o_val,
|
|
104
|
+
page=page,
|
|
105
|
+
page_size=page_size
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return PaginatedModelsResponse(
|
|
109
|
+
total=total,
|
|
110
|
+
page=page,
|
|
111
|
+
page_size=page_size,
|
|
112
|
+
results=models
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.get("/api/v1/models/{model_id:path}", response_model=LLMModel, tags=["Models"])
|
|
117
|
+
async def get_model_detail(
|
|
118
|
+
model_id: str,
|
|
119
|
+
index: LLMIndex = Depends(get_index)
|
|
120
|
+
):
|
|
121
|
+
"""Get a detailed view of a specific model in the registry."""
|
|
122
|
+
model = await index.get_model(model_id)
|
|
123
|
+
if not model:
|
|
124
|
+
raise HTTPException(status_code=404, detail="Model not found in registry.")
|
|
125
|
+
return model
|
|
126
|
+
|
|
127
|
+
@app.get("/api/v1/search", response_model=List[LLMModel], tags=["Models"])
|
|
128
|
+
async def search_models(
|
|
129
|
+
q: str = Query(..., min_length=2, description="Search term for model name or provider"),
|
|
130
|
+
limit: int = Query(10, ge=1, le=50),
|
|
131
|
+
index: LLMIndex = Depends(get_index)
|
|
132
|
+
):
|
|
133
|
+
"""Fuzzy search models by name or provider."""
|
|
134
|
+
return await index.search(q, limit=limit)
|
|
135
|
+
|
|
136
|
+
@app.post("/api/v1/sync", tags=["Admin"])
|
|
137
|
+
async def trigger_sync(index: LLMIndex = Depends(get_index)):
|
|
138
|
+
"""
|
|
139
|
+
Trigger a manual synchronization of the registry.
|
|
140
|
+
"""
|
|
141
|
+
models = await index.sync()
|
|
142
|
+
return {"status": "success", "synced_models": len(models)}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def run_server():
|
|
146
|
+
import uvicorn
|
|
147
|
+
uvicorn.run("ai_provider_tracker.adapters.api.main:app", host="0.0.0.0", port=8000, reload=True)
|
|
File without changes
|