aistatus 0.0.1__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.
- aistatus-0.0.1/LICENSE +21 -0
- aistatus-0.0.1/PKG-INFO +181 -0
- aistatus-0.0.1/README.md +144 -0
- aistatus-0.0.1/aistatus/__init__.py +139 -0
- aistatus-0.0.1/aistatus/_defaults.py +39 -0
- aistatus-0.0.1/aistatus/api.py +114 -0
- aistatus-0.0.1/aistatus/exceptions.py +58 -0
- aistatus-0.0.1/aistatus/gateway/__init__.py +44 -0
- aistatus-0.0.1/aistatus/gateway/__main__.py +71 -0
- aistatus-0.0.1/aistatus/gateway/config.py +250 -0
- aistatus-0.0.1/aistatus/gateway/health.py +92 -0
- aistatus-0.0.1/aistatus/gateway/server.py +440 -0
- aistatus-0.0.1/aistatus/gateway/translate.py +201 -0
- aistatus-0.0.1/aistatus/models.py +102 -0
- aistatus-0.0.1/aistatus/providers/__init__.py +16 -0
- aistatus-0.0.1/aistatus/providers/anthropic_.py +108 -0
- aistatus-0.0.1/aistatus/providers/base.py +62 -0
- aistatus-0.0.1/aistatus/providers/compatible_.py +94 -0
- aistatus-0.0.1/aistatus/providers/google_.py +92 -0
- aistatus-0.0.1/aistatus/providers/openai_.py +81 -0
- aistatus-0.0.1/aistatus/providers/openrouter_.py +67 -0
- aistatus-0.0.1/aistatus/router.py +411 -0
- aistatus-0.0.1/aistatus.egg-info/PKG-INFO +181 -0
- aistatus-0.0.1/aistatus.egg-info/SOURCES.txt +27 -0
- aistatus-0.0.1/aistatus.egg-info/dependency_links.txt +1 -0
- aistatus-0.0.1/aistatus.egg-info/requires.txt +24 -0
- aistatus-0.0.1/aistatus.egg-info/top_level.txt +1 -0
- aistatus-0.0.1/pyproject.toml +43 -0
- aistatus-0.0.1/setup.cfg +4 -0
aistatus-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LeTau Robotics
|
|
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.
|
aistatus-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aistatus
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Smart AI model routing with real-time status awareness. Pre-flight checks, auto-fallback, budget constraints.
|
|
5
|
+
Author-email: LeTau Robotics <hello@aistatus.cc>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://aistatus.cc
|
|
8
|
+
Project-URL: Documentation, https://aistatus.cc/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/fangxm233/aistatus-python
|
|
10
|
+
Keywords: ai,llm,routing,fallback,agent,openai,anthropic,gemini
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
21
|
+
Provides-Extra: openai
|
|
22
|
+
Requires-Dist: openai>=1.50; extra == "openai"
|
|
23
|
+
Provides-Extra: google
|
|
24
|
+
Requires-Dist: google-genai>=1.0; extra == "google"
|
|
25
|
+
Provides-Extra: gateway
|
|
26
|
+
Requires-Dist: aiohttp>=3.9; extra == "gateway"
|
|
27
|
+
Requires-Dist: pyyaml>=6.0; extra == "gateway"
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: anthropic>=0.40; extra == "all"
|
|
30
|
+
Requires-Dist: openai>=1.50; extra == "all"
|
|
31
|
+
Requires-Dist: google-genai>=1.0; extra == "all"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# aistatus
|
|
39
|
+
|
|
40
|
+
Smart AI model routing with real-time status awareness.
|
|
41
|
+
|
|
42
|
+
`aistatus` checks provider/model availability through `aistatus.cc`, then calls
|
|
43
|
+
your installed provider SDK directly. Your prompts and API keys stay on your side.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install aistatus
|
|
49
|
+
pip install aistatus[anthropic]
|
|
50
|
+
pip install aistatus[openai]
|
|
51
|
+
pip install aistatus[google]
|
|
52
|
+
pip install aistatus[all]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
Set at least one provider API key, then route by model name:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from aistatus import route
|
|
61
|
+
|
|
62
|
+
resp = route(
|
|
63
|
+
"Summarize the latest deployment status.",
|
|
64
|
+
model="claude-sonnet-4-6",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
print(resp.content)
|
|
68
|
+
print(resp.model_used)
|
|
69
|
+
print(resp.provider_used)
|
|
70
|
+
print(resp.was_fallback)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If the primary provider is unavailable, `aistatus` tries other compatible providers
|
|
74
|
+
that are available in your environment.
|
|
75
|
+
|
|
76
|
+
## Tier Routing
|
|
77
|
+
|
|
78
|
+
Tier routing is supported, but tiers must be configured explicitly:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from aistatus import Router
|
|
82
|
+
|
|
83
|
+
router = Router(check_timeout=2.0)
|
|
84
|
+
router.add_tier("fast", [
|
|
85
|
+
"claude-haiku-4-5",
|
|
86
|
+
"gpt-4o-mini",
|
|
87
|
+
"gemini-2.0-flash",
|
|
88
|
+
])
|
|
89
|
+
router.add_tier("standard", [
|
|
90
|
+
"claude-sonnet-4-6",
|
|
91
|
+
"gpt-4o",
|
|
92
|
+
"gemini-2.5-pro",
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
resp = router.route(
|
|
96
|
+
"Explain quantum computing in one sentence.",
|
|
97
|
+
tier="fast",
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Async
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from aistatus import aroute
|
|
105
|
+
|
|
106
|
+
resp = await aroute(
|
|
107
|
+
[{"role": "user", "content": "Hello"}],
|
|
108
|
+
model="gpt-4o-mini",
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Status API
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from aistatus import StatusAPI
|
|
116
|
+
|
|
117
|
+
api = StatusAPI()
|
|
118
|
+
|
|
119
|
+
check = api.check_provider("anthropic")
|
|
120
|
+
print(check.status)
|
|
121
|
+
print(check.is_available)
|
|
122
|
+
|
|
123
|
+
for provider in api.providers():
|
|
124
|
+
print(provider.name, provider.status.value)
|
|
125
|
+
|
|
126
|
+
for model in api.search_models("sonnet"):
|
|
127
|
+
print(model.id, model.prompt_price, model.completion_price)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Response Object
|
|
131
|
+
|
|
132
|
+
Every `route()` call returns a `RouteResponse`:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
@dataclass
|
|
136
|
+
class RouteResponse:
|
|
137
|
+
content: str
|
|
138
|
+
model_used: str
|
|
139
|
+
provider_used: str
|
|
140
|
+
was_fallback: bool
|
|
141
|
+
fallback_reason: str | None = None
|
|
142
|
+
input_tokens: int = 0
|
|
143
|
+
output_tokens: int = 0
|
|
144
|
+
cost_usd: float = 0.0
|
|
145
|
+
raw: Any = None
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Errors
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from aistatus import AllProvidersDown, ProviderNotInstalled, route
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
resp = route("Hello", model="claude-sonnet-4-6")
|
|
155
|
+
except AllProvidersDown as e:
|
|
156
|
+
print(e.tried)
|
|
157
|
+
except ProviderNotInstalled as e:
|
|
158
|
+
print(f"Install support for: {e.provider}")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Environment Variables
|
|
162
|
+
|
|
163
|
+
The SDK auto-discovers providers from standard environment variables:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
ANTHROPIC_API_KEY=...
|
|
167
|
+
OPENAI_API_KEY=...
|
|
168
|
+
GEMINI_API_KEY=...
|
|
169
|
+
OPENROUTER_API_KEY=...
|
|
170
|
+
DEEPSEEK_API_KEY=...
|
|
171
|
+
MISTRAL_API_KEY=...
|
|
172
|
+
XAI_API_KEY=...
|
|
173
|
+
GROQ_API_KEY=...
|
|
174
|
+
TOGETHER_API_KEY=...
|
|
175
|
+
MOONSHOT_API_KEY=...
|
|
176
|
+
DASHSCOPE_API_KEY=...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT. See [LICENSE](LICENSE).
|
aistatus-0.0.1/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# aistatus
|
|
2
|
+
|
|
3
|
+
Smart AI model routing with real-time status awareness.
|
|
4
|
+
|
|
5
|
+
`aistatus` checks provider/model availability through `aistatus.cc`, then calls
|
|
6
|
+
your installed provider SDK directly. Your prompts and API keys stay on your side.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install aistatus
|
|
12
|
+
pip install aistatus[anthropic]
|
|
13
|
+
pip install aistatus[openai]
|
|
14
|
+
pip install aistatus[google]
|
|
15
|
+
pip install aistatus[all]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
Set at least one provider API key, then route by model name:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from aistatus import route
|
|
24
|
+
|
|
25
|
+
resp = route(
|
|
26
|
+
"Summarize the latest deployment status.",
|
|
27
|
+
model="claude-sonnet-4-6",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
print(resp.content)
|
|
31
|
+
print(resp.model_used)
|
|
32
|
+
print(resp.provider_used)
|
|
33
|
+
print(resp.was_fallback)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If the primary provider is unavailable, `aistatus` tries other compatible providers
|
|
37
|
+
that are available in your environment.
|
|
38
|
+
|
|
39
|
+
## Tier Routing
|
|
40
|
+
|
|
41
|
+
Tier routing is supported, but tiers must be configured explicitly:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from aistatus import Router
|
|
45
|
+
|
|
46
|
+
router = Router(check_timeout=2.0)
|
|
47
|
+
router.add_tier("fast", [
|
|
48
|
+
"claude-haiku-4-5",
|
|
49
|
+
"gpt-4o-mini",
|
|
50
|
+
"gemini-2.0-flash",
|
|
51
|
+
])
|
|
52
|
+
router.add_tier("standard", [
|
|
53
|
+
"claude-sonnet-4-6",
|
|
54
|
+
"gpt-4o",
|
|
55
|
+
"gemini-2.5-pro",
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
resp = router.route(
|
|
59
|
+
"Explain quantum computing in one sentence.",
|
|
60
|
+
tier="fast",
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Async
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from aistatus import aroute
|
|
68
|
+
|
|
69
|
+
resp = await aroute(
|
|
70
|
+
[{"role": "user", "content": "Hello"}],
|
|
71
|
+
model="gpt-4o-mini",
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Status API
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from aistatus import StatusAPI
|
|
79
|
+
|
|
80
|
+
api = StatusAPI()
|
|
81
|
+
|
|
82
|
+
check = api.check_provider("anthropic")
|
|
83
|
+
print(check.status)
|
|
84
|
+
print(check.is_available)
|
|
85
|
+
|
|
86
|
+
for provider in api.providers():
|
|
87
|
+
print(provider.name, provider.status.value)
|
|
88
|
+
|
|
89
|
+
for model in api.search_models("sonnet"):
|
|
90
|
+
print(model.id, model.prompt_price, model.completion_price)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Response Object
|
|
94
|
+
|
|
95
|
+
Every `route()` call returns a `RouteResponse`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
@dataclass
|
|
99
|
+
class RouteResponse:
|
|
100
|
+
content: str
|
|
101
|
+
model_used: str
|
|
102
|
+
provider_used: str
|
|
103
|
+
was_fallback: bool
|
|
104
|
+
fallback_reason: str | None = None
|
|
105
|
+
input_tokens: int = 0
|
|
106
|
+
output_tokens: int = 0
|
|
107
|
+
cost_usd: float = 0.0
|
|
108
|
+
raw: Any = None
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Errors
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from aistatus import AllProvidersDown, ProviderNotInstalled, route
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
resp = route("Hello", model="claude-sonnet-4-6")
|
|
118
|
+
except AllProvidersDown as e:
|
|
119
|
+
print(e.tried)
|
|
120
|
+
except ProviderNotInstalled as e:
|
|
121
|
+
print(f"Install support for: {e.provider}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Environment Variables
|
|
125
|
+
|
|
126
|
+
The SDK auto-discovers providers from standard environment variables:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
ANTHROPIC_API_KEY=...
|
|
130
|
+
OPENAI_API_KEY=...
|
|
131
|
+
GEMINI_API_KEY=...
|
|
132
|
+
OPENROUTER_API_KEY=...
|
|
133
|
+
DEEPSEEK_API_KEY=...
|
|
134
|
+
MISTRAL_API_KEY=...
|
|
135
|
+
XAI_API_KEY=...
|
|
136
|
+
GROQ_API_KEY=...
|
|
137
|
+
TOGETHER_API_KEY=...
|
|
138
|
+
MOONSHOT_API_KEY=...
|
|
139
|
+
DASHSCOPE_API_KEY=...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""aistatus — Smart AI model routing with real-time status awareness.
|
|
2
|
+
|
|
3
|
+
Quickstart::
|
|
4
|
+
|
|
5
|
+
from aistatus import route
|
|
6
|
+
|
|
7
|
+
resp = route("Hello!", model="claude-sonnet-4-6")
|
|
8
|
+
print(resp.content) # "Hello! How can I help?"
|
|
9
|
+
print(resp.model_used) # "anthropic/claude-sonnet-4-6"
|
|
10
|
+
print(resp.was_fallback) # False
|
|
11
|
+
|
|
12
|
+
Tier-based routing (requires configuration)::
|
|
13
|
+
|
|
14
|
+
from aistatus import Router
|
|
15
|
+
|
|
16
|
+
router = Router()
|
|
17
|
+
router.add_tier("fast", ["claude-haiku-4-5", "gpt-4o-mini"])
|
|
18
|
+
resp = router.route("Hello!", tier="fast")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "0.0.1"
|
|
22
|
+
|
|
23
|
+
# Core routing
|
|
24
|
+
from .router import Router # noqa: F401
|
|
25
|
+
|
|
26
|
+
# Data models (for type hints and advanced usage)
|
|
27
|
+
from .models import ( # noqa: F401
|
|
28
|
+
RouteResponse,
|
|
29
|
+
RouteConfig,
|
|
30
|
+
CheckResult,
|
|
31
|
+
Status,
|
|
32
|
+
ProviderStatus,
|
|
33
|
+
ProviderConfig,
|
|
34
|
+
ModelInfo,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Status API (for direct queries without routing)
|
|
38
|
+
from .api import StatusAPI # noqa: F401
|
|
39
|
+
|
|
40
|
+
# Exceptions
|
|
41
|
+
from .exceptions import ( # noqa: F401
|
|
42
|
+
AIStatusError,
|
|
43
|
+
AllProvidersDown,
|
|
44
|
+
ProviderCallFailed,
|
|
45
|
+
NoBudgetMatch,
|
|
46
|
+
ProviderNotInstalled,
|
|
47
|
+
CheckAPIUnreachable,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Trigger adapter registration on import
|
|
51
|
+
from . import providers as _providers # noqa: F401
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Module-level convenience functions (lazy-initialized default Router)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
_default_router: Router | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_default_router() -> Router:
|
|
61
|
+
global _default_router
|
|
62
|
+
if _default_router is None:
|
|
63
|
+
_default_router = Router()
|
|
64
|
+
return _default_router
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def route(
|
|
68
|
+
messages: str | list[dict],
|
|
69
|
+
*,
|
|
70
|
+
model: str | None = None,
|
|
71
|
+
tier: str | None = None,
|
|
72
|
+
system: str | None = None,
|
|
73
|
+
allow_fallback: bool = True,
|
|
74
|
+
timeout: float = 30.0,
|
|
75
|
+
prefer: list[str] | None = None,
|
|
76
|
+
**kwargs,
|
|
77
|
+
) -> RouteResponse:
|
|
78
|
+
"""One-liner routing with auto-discovered providers.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
messages: User message string or OpenAI-style message list.
|
|
82
|
+
model: Model name (e.g. "claude-sonnet-4-6"). Provider resolved via API.
|
|
83
|
+
tier: Tier name. Requires prior configuration on the default router.
|
|
84
|
+
system: Optional system prompt (convenience for string messages).
|
|
85
|
+
allow_fallback: Try alternatives if primary provider is down.
|
|
86
|
+
timeout: Provider call timeout in seconds.
|
|
87
|
+
prefer: Provider preference order for fallback.
|
|
88
|
+
**kwargs: Passed to provider SDK (max_tokens, temperature, etc.).
|
|
89
|
+
"""
|
|
90
|
+
return _get_default_router().route(
|
|
91
|
+
messages, model=model, tier=tier, system=system,
|
|
92
|
+
allow_fallback=allow_fallback, timeout=timeout, prefer=prefer,
|
|
93
|
+
**kwargs,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def aroute(
|
|
98
|
+
messages: str | list[dict],
|
|
99
|
+
*,
|
|
100
|
+
model: str | None = None,
|
|
101
|
+
tier: str | None = None,
|
|
102
|
+
system: str | None = None,
|
|
103
|
+
allow_fallback: bool = True,
|
|
104
|
+
timeout: float = 30.0,
|
|
105
|
+
prefer: list[str] | None = None,
|
|
106
|
+
**kwargs,
|
|
107
|
+
) -> RouteResponse:
|
|
108
|
+
"""Async version of route()."""
|
|
109
|
+
return await _get_default_router().aroute(
|
|
110
|
+
messages, model=model, tier=tier, system=system,
|
|
111
|
+
allow_fallback=allow_fallback, timeout=timeout, prefer=prefer,
|
|
112
|
+
**kwargs,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
__all__ = [
|
|
117
|
+
# One-liners
|
|
118
|
+
"route",
|
|
119
|
+
"aroute",
|
|
120
|
+
# Router class
|
|
121
|
+
"Router",
|
|
122
|
+
# API client
|
|
123
|
+
"StatusAPI",
|
|
124
|
+
# Models
|
|
125
|
+
"RouteResponse",
|
|
126
|
+
"RouteConfig",
|
|
127
|
+
"CheckResult",
|
|
128
|
+
"Status",
|
|
129
|
+
"ProviderStatus",
|
|
130
|
+
"ProviderConfig",
|
|
131
|
+
"ModelInfo",
|
|
132
|
+
# Exceptions
|
|
133
|
+
"AIStatusError",
|
|
134
|
+
"AllProvidersDown",
|
|
135
|
+
"ProviderCallFailed",
|
|
136
|
+
"NoBudgetMatch",
|
|
137
|
+
"ProviderNotInstalled",
|
|
138
|
+
"CheckAPIUnreachable",
|
|
139
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Default configuration constants for auto-discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Provider slug → (default_env_var, adapter_type_name)
|
|
6
|
+
# Used by Router to auto-discover available providers from environment variables.
|
|
7
|
+
AUTO_PROVIDERS: dict[str, tuple[str, str]] = {
|
|
8
|
+
"anthropic": ("ANTHROPIC_API_KEY", "anthropic"),
|
|
9
|
+
"openai": ("OPENAI_API_KEY", "openai"),
|
|
10
|
+
"google": ("GEMINI_API_KEY", "google"),
|
|
11
|
+
"openrouter": ("OPENROUTER_API_KEY", "openrouter"),
|
|
12
|
+
"deepseek": ("DEEPSEEK_API_KEY", "deepseek"),
|
|
13
|
+
"mistral": ("MISTRAL_API_KEY", "mistralai"),
|
|
14
|
+
"xai": ("XAI_API_KEY", "xai"),
|
|
15
|
+
"groq": ("GROQ_API_KEY", "groq"),
|
|
16
|
+
"together": ("TOGETHER_API_KEY", "together"),
|
|
17
|
+
"moonshot": ("MOONSHOT_API_KEY", "moonshotai"),
|
|
18
|
+
"qwen": ("DASHSCOPE_API_KEY", "qwen"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Model name prefix → provider slug.
|
|
22
|
+
# Used as fallback when aistatus.cc API is unreachable.
|
|
23
|
+
MODEL_PREFIX_MAP: dict[str, str] = {
|
|
24
|
+
"claude": "anthropic",
|
|
25
|
+
"gpt": "openai",
|
|
26
|
+
"o1": "openai",
|
|
27
|
+
"o3": "openai",
|
|
28
|
+
"o4": "openai",
|
|
29
|
+
"chatgpt": "openai",
|
|
30
|
+
"gemini": "google",
|
|
31
|
+
"deepseek": "deepseek",
|
|
32
|
+
"mistral": "mistral",
|
|
33
|
+
"codestral": "mistral",
|
|
34
|
+
"pixtral": "mistral",
|
|
35
|
+
"grok": "xai",
|
|
36
|
+
"llama": "groq",
|
|
37
|
+
"qwen": "qwen",
|
|
38
|
+
"moonshot": "moonshot",
|
|
39
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Thin client for the aistatus.cc public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .models import Alternative, CheckResult, ModelInfo, ProviderStatus, Status
|
|
8
|
+
|
|
9
|
+
BASE_URL = "https://aistatus.cc"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StatusAPI:
|
|
13
|
+
"""Stateless HTTP client for aistatus.cc. All methods are class-level."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, base_url: str = BASE_URL, timeout: float = 3.0):
|
|
16
|
+
self._base = base_url.rstrip("/")
|
|
17
|
+
self._timeout = timeout
|
|
18
|
+
|
|
19
|
+
# ---- sync helpers ------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def _get(self, path: str, params: dict | None = None) -> dict:
|
|
22
|
+
with httpx.Client(timeout=self._timeout) as c:
|
|
23
|
+
r = c.get(f"{self._base}{path}", params=params)
|
|
24
|
+
r.raise_for_status()
|
|
25
|
+
return r.json()
|
|
26
|
+
|
|
27
|
+
async def _aget(self, path: str, params: dict | None = None) -> dict:
|
|
28
|
+
async with httpx.AsyncClient(timeout=self._timeout) as c:
|
|
29
|
+
r = await c.get(f"{self._base}{path}", params=params)
|
|
30
|
+
r.raise_for_status()
|
|
31
|
+
return r.json()
|
|
32
|
+
|
|
33
|
+
# ---- public methods (sync) ----------------------------------------
|
|
34
|
+
|
|
35
|
+
def check_provider(self, slug: str) -> CheckResult:
|
|
36
|
+
"""Pre-flight check for a provider. GET /api/check?provider=..."""
|
|
37
|
+
data = self._get("/api/check", {"provider": slug})
|
|
38
|
+
return self._parse_check(data)
|
|
39
|
+
|
|
40
|
+
def check_model(self, model_id: str) -> CheckResult:
|
|
41
|
+
"""Pre-flight check for a specific model. GET /api/check?model=..."""
|
|
42
|
+
data = self._get("/api/check", {"model": model_id})
|
|
43
|
+
return self._parse_check(data)
|
|
44
|
+
|
|
45
|
+
def providers(self) -> list[ProviderStatus]:
|
|
46
|
+
"""All provider statuses. GET /api/providers"""
|
|
47
|
+
data = self._get("/api/providers")
|
|
48
|
+
return [
|
|
49
|
+
ProviderStatus(
|
|
50
|
+
slug=p["slug"],
|
|
51
|
+
name=p["name"],
|
|
52
|
+
status=Status(p["status"]),
|
|
53
|
+
status_detail=p.get("statusDetail"),
|
|
54
|
+
model_count=p.get("modelCount", 0),
|
|
55
|
+
)
|
|
56
|
+
for p in data.get("providers", [])
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
def model(self, model_id: str) -> ModelInfo | None:
|
|
60
|
+
"""Get single model info. GET /api/models/:provider/:model"""
|
|
61
|
+
try:
|
|
62
|
+
data = self._get(f"/api/models/{model_id}")
|
|
63
|
+
except httpx.HTTPStatusError:
|
|
64
|
+
return None
|
|
65
|
+
return self._parse_model(data)
|
|
66
|
+
|
|
67
|
+
def search_models(self, query: str) -> list[ModelInfo]:
|
|
68
|
+
"""Search models. GET /api/models?q=..."""
|
|
69
|
+
data = self._get("/api/models", {"q": query})
|
|
70
|
+
return [self._parse_model(m) for m in data.get("models", [])]
|
|
71
|
+
|
|
72
|
+
# ---- public methods (async) ----------------------------------------
|
|
73
|
+
|
|
74
|
+
async def acheck_provider(self, slug: str) -> CheckResult:
|
|
75
|
+
data = await self._aget("/api/check", {"provider": slug})
|
|
76
|
+
return self._parse_check(data)
|
|
77
|
+
|
|
78
|
+
async def acheck_model(self, model_id: str) -> CheckResult:
|
|
79
|
+
data = await self._aget("/api/check", {"model": model_id})
|
|
80
|
+
return self._parse_check(data)
|
|
81
|
+
|
|
82
|
+
# ---- parsers -------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _parse_check(data: dict) -> CheckResult:
|
|
86
|
+
return CheckResult(
|
|
87
|
+
provider=data.get("provider", data.get("slug", "")),
|
|
88
|
+
status=Status(data.get("status", "unknown")),
|
|
89
|
+
status_detail=data.get("statusDetail"),
|
|
90
|
+
model=data.get("model"),
|
|
91
|
+
alternatives=[
|
|
92
|
+
Alternative(
|
|
93
|
+
slug=a["slug"],
|
|
94
|
+
name=a["name"],
|
|
95
|
+
status=Status(a["status"]),
|
|
96
|
+
suggested_model=a.get("suggestedModel", ""),
|
|
97
|
+
)
|
|
98
|
+
for a in data.get("alternatives", [])
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _parse_model(data: dict) -> ModelInfo:
|
|
104
|
+
pricing = data.get("pricing", {})
|
|
105
|
+
prov = data.get("provider", {})
|
|
106
|
+
return ModelInfo(
|
|
107
|
+
id=data.get("id", ""),
|
|
108
|
+
name=data.get("name", ""),
|
|
109
|
+
provider_slug=prov.get("slug", "") if isinstance(prov, dict) else "",
|
|
110
|
+
context_length=data.get("context_length", 0),
|
|
111
|
+
modality=data.get("modality", "text->text"),
|
|
112
|
+
prompt_price=float(pricing.get("prompt", 0)),
|
|
113
|
+
completion_price=float(pricing.get("completion", 0)),
|
|
114
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Exceptions for aistatus SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AIStatusError(Exception):
|
|
5
|
+
"""Base exception for all aistatus errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AllProvidersDown(AIStatusError):
|
|
9
|
+
"""Raised when no provider in the routing chain is available."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, tried: list[str]):
|
|
12
|
+
self.tried = tried
|
|
13
|
+
super().__init__(
|
|
14
|
+
f"All providers unavailable. Tried: {', '.join(tried)}. "
|
|
15
|
+
f"Check https://aistatus.cc for current status."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProviderCallFailed(AIStatusError):
|
|
20
|
+
"""Raised when a provider API call fails (after status was operational)."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, provider: str, model: str, cause: Exception):
|
|
23
|
+
self.provider = provider
|
|
24
|
+
self.model = model
|
|
25
|
+
self.cause = cause
|
|
26
|
+
super().__init__(f"{provider} ({model}) call failed: {cause}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoBudgetMatch(AIStatusError):
|
|
30
|
+
"""Raised when no available model fits the budget constraint."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, max_cost: float, tier: str):
|
|
33
|
+
self.max_cost = max_cost
|
|
34
|
+
super().__init__(
|
|
35
|
+
f"No operational model in tier '{tier}' under ${max_cost}/M tokens."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProviderNotInstalled(AIStatusError):
|
|
40
|
+
"""Raised when the required provider SDK is not installed."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, provider: str, package: str):
|
|
43
|
+
self.provider = provider
|
|
44
|
+
self.package = package
|
|
45
|
+
super().__init__(
|
|
46
|
+
f"Provider '{provider}' requires package '{package}'. "
|
|
47
|
+
f"Install with: pip install aistatus[{provider}]"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CheckAPIUnreachable(AIStatusError):
|
|
52
|
+
"""Raised when aistatus.cc API itself is unreachable."""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
super().__init__(
|
|
56
|
+
"Could not reach aistatus.cc API. "
|
|
57
|
+
"Proceeding with primary provider without status check."
|
|
58
|
+
)
|