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.
Files changed (31) hide show
  1. ai_provider_tracker-0.7.0/LICENSE +21 -0
  2. ai_provider_tracker-0.7.0/PKG-INFO +162 -0
  3. ai_provider_tracker-0.7.0/README.md +134 -0
  4. ai_provider_tracker-0.7.0/ai_provider_tracker/__init__.py +206 -0
  5. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/api/main.py +147 -0
  6. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/__init__.py +0 -0
  7. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/artificial_analysis_fetcher.py +84 -0
  8. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/http_fetcher.py +26 -0
  9. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/gateways/openrouter_fetcher.py +36 -0
  10. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/json_exporter.py +55 -0
  11. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/json_repository.py +126 -0
  12. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/models.py +22 -0
  13. ai_provider_tracker-0.7.0/ai_provider_tracker/adapters/persistence/sqlite_repository.py +200 -0
  14. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/__init__.py +21 -0
  15. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/calculator.py +72 -0
  16. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/models.py +59 -0
  17. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/normalizers.py +174 -0
  18. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/pricing.py +44 -0
  19. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/repository.py +65 -0
  20. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/tracker.py +67 -0
  21. ai_provider_tracker-0.7.0/ai_provider_tracker/cost_tracking/utils.py +45 -0
  22. ai_provider_tracker-0.7.0/ai_provider_tracker/data/__init__.py +1 -0
  23. ai_provider_tracker-0.7.0/ai_provider_tracker/data/pricing_catalog.json +32 -0
  24. ai_provider_tracker-0.7.0/ai_provider_tracker/domain/entities.py +101 -0
  25. ai_provider_tracker-0.7.0/ai_provider_tracker/domain/interfaces.py +66 -0
  26. ai_provider_tracker-0.7.0/ai_provider_tracker/domain/services/__init__.py +0 -0
  27. ai_provider_tracker-0.7.0/ai_provider_tracker/domain/services/matching_engine.py +75 -0
  28. ai_provider_tracker-0.7.0/ai_provider_tracker/infrastructure/config.py +37 -0
  29. ai_provider_tracker-0.7.0/ai_provider_tracker/use_cases/sync_registry.py +153 -0
  30. ai_provider_tracker-0.7.0/openrouter_insights/__init__.py +3 -0
  31. 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
+ [![PyPI version](https://img.shields.io/pypi/v/ai-provider-tracker.svg)](https://pypi.org/project/ai-provider-tracker/)
31
+ [![Python versions](https://img.shields.io/pypi/pyversions/ai-provider-tracker.svg)](https://pypi.org/project/ai-provider-tracker/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/ai-provider-tracker.svg)](https://pypi.org/project/ai-provider-tracker/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/ai-provider-tracker.svg)](https://pypi.org/project/ai-provider-tracker/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)