ainative-ai-sdk-provider 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.
- ainative_ai_sdk_provider-0.1.0/LICENSE +21 -0
- ainative_ai_sdk_provider-0.1.0/PKG-INFO +304 -0
- ainative_ai_sdk_provider-0.1.0/README.md +270 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider/__init__.py +66 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider/client.py +565 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider/errors.py +267 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider/models.py +150 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider.egg-info/PKG-INFO +304 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider.egg-info/SOURCES.txt +15 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider.egg-info/dependency_links.txt +1 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider.egg-info/requires.txt +7 -0
- ainative_ai_sdk_provider-0.1.0/ainative_ai_sdk_provider.egg-info/top_level.txt +1 -0
- ainative_ai_sdk_provider-0.1.0/pyproject.toml +88 -0
- ainative_ai_sdk_provider-0.1.0/setup.cfg +4 -0
- ainative_ai_sdk_provider-0.1.0/tests/test_client.py +625 -0
- ainative_ai_sdk_provider-0.1.0/tests/test_errors.py +263 -0
- ainative_ai_sdk_provider-0.1.0/tests/test_transport.py +179 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AINative Studio
|
|
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,304 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ainative-ai-sdk-provider
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for AINative's multi-provider AI inference API: chat completions, streaming, and agentic tool-calling over one interface, with server-side routing and automatic fallback across Cerebras, Meta Llama, NVIDIA NIM & DigitalOcean.
|
|
5
|
+
Author-email: AINative Studio <dev@ainative.studio>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AINative-Studio/core
|
|
8
|
+
Project-URL: Documentation, https://docs.ainative.studio
|
|
9
|
+
Project-URL: Repository, https://github.com/AINative-Studio/core
|
|
10
|
+
Project-URL: Issues, https://github.com/AINative-Studio/core/issues
|
|
11
|
+
Keywords: ainative,ai-sdk,llm,llm-client,llm-sdk,python-sdk,chat-completion,chat-completions,inference,inference-api,ai-gateway,model-router,llm-router,multi-provider,openai-compatible,openai-alternative,httpx,async,streaming,sse,server-sent-events,tool-calling,function-calling,agentic,ai-agent,llama,meta-llama,qwen,deepseek,kimi,cerebras,nvidia-nim,digitalocean,zerodb
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: httpx>=0.24
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
32
|
+
Requires-Dist: respx>=0.20; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# ainative-ai-sdk-provider
|
|
36
|
+
|
|
37
|
+
**Python SDK for multi-provider AI inference** — chat completions, streaming, and agentic tool-calling over one interface, with server-side routing and automatic fallback across Cerebras, Meta Llama, NVIDIA NIM & DigitalOcean.
|
|
38
|
+
|
|
39
|
+
AINative's backend routes every chat completion across Cerebras, Meta Llama,
|
|
40
|
+
NVIDIA NIM, DigitalOcean and BYOK based on your plan tier, live per-provider
|
|
41
|
+
rate-limit state, daily token and cost caps, and model availability — then
|
|
42
|
+
transparently falls back through its chain when a provider is exhausted or
|
|
43
|
+
geo-blocked. This SDK is the client for that. You ask for a model; the platform
|
|
44
|
+
picks the upstream.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install ainative-ai-sdk-provider
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ainative_ai_sdk_provider import AINativeProvider
|
|
54
|
+
|
|
55
|
+
provider = AINativeProvider() # reads AINATIVE_API_KEY
|
|
56
|
+
|
|
57
|
+
result = provider.complete(
|
|
58
|
+
messages=[{"role": "user", "content": "Explain SSE in one sentence."}],
|
|
59
|
+
model="deepseek-4-flash",
|
|
60
|
+
)
|
|
61
|
+
print(result.text)
|
|
62
|
+
print(result.total_tokens, "tokens")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Why there is no `provider=` parameter
|
|
66
|
+
|
|
67
|
+
This is the deliberate design of the SDK, not a missing feature.
|
|
68
|
+
|
|
69
|
+
The server-side router picks an upstream per request. Pinning a provider from
|
|
70
|
+
the client would switch that off — no tier-aware selection, no rate-limit
|
|
71
|
+
awareness, no cost-aware ordering, no automatic fallback when an upstream 429s
|
|
72
|
+
or geo-blocks you. So the SDK never sends a `provider` field, which is exactly
|
|
73
|
+
the signal the endpoint needs to do its job.
|
|
74
|
+
|
|
75
|
+
Two consequences worth knowing:
|
|
76
|
+
|
|
77
|
+
- **`result.provider` is informational.** The backend intentionally reports
|
|
78
|
+
several internal upstreams as `"ainative"`. Don't branch on it.
|
|
79
|
+
- **The SDK does not retry rate limits for you.** The backend already walked
|
|
80
|
+
its own fallback chain before returning a 429 or 503, and tells you how long
|
|
81
|
+
to wait. Retrying immediately would fight its circuit breaker — honour
|
|
82
|
+
`err.retry_after` instead.
|
|
83
|
+
|
|
84
|
+
## Authentication
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# 1. Environment variable (recommended)
|
|
88
|
+
# export AINATIVE_API_KEY=your_key
|
|
89
|
+
provider = AINativeProvider()
|
|
90
|
+
|
|
91
|
+
# 2. Explicit
|
|
92
|
+
provider = AINativeProvider(api_key="your_key")
|
|
93
|
+
|
|
94
|
+
# 3. Point at another environment
|
|
95
|
+
provider = AINativeProvider(api_key="k", base_url="http://localhost:8000/api/v1")
|
|
96
|
+
# or: export AINATIVE_BASE_URL=...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The client sends your key as both `X-API-Key` and `Authorization: Bearer`,
|
|
100
|
+
since the platform's unified auth accepts either — so the same client works
|
|
101
|
+
against the public gateway and a directly-addressed backend without
|
|
102
|
+
reconfiguration.
|
|
103
|
+
|
|
104
|
+
## Streaming
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
for chunk in provider.stream(
|
|
108
|
+
messages=[{"role": "user", "content": "Count to five."}],
|
|
109
|
+
model="deepseek-4-flash",
|
|
110
|
+
):
|
|
111
|
+
print(chunk.content, end="", flush=True)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Streaming is served by upstreams that speak OpenAI SSE natively. If the router
|
|
115
|
+
lands your request on one that doesn't, the API returns a normal JSON body and
|
|
116
|
+
this method yields a single chunk with the full completion — so your loop keeps
|
|
117
|
+
working either way.
|
|
118
|
+
|
|
119
|
+
If an upstream fails mid-stream, the API injects an error frame; the SDK raises
|
|
120
|
+
`UpstreamUnavailableError` rather than silently truncating your output.
|
|
121
|
+
|
|
122
|
+
## Async
|
|
123
|
+
|
|
124
|
+
Every method has an async counterpart. `acomplete` mirrors `complete`,
|
|
125
|
+
`astream` mirrors `stream`.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import asyncio
|
|
129
|
+
from ainative_ai_sdk_provider import AINativeProvider
|
|
130
|
+
|
|
131
|
+
async def main():
|
|
132
|
+
async with AINativeProvider() as provider:
|
|
133
|
+
result = await provider.acomplete(
|
|
134
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
135
|
+
)
|
|
136
|
+
print(result.text)
|
|
137
|
+
|
|
138
|
+
async for chunk in provider.astream(
|
|
139
|
+
messages=[{"role": "user", "content": "Count to three."}],
|
|
140
|
+
):
|
|
141
|
+
print(chunk.content, end="")
|
|
142
|
+
|
|
143
|
+
asyncio.run(main())
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Tool calling
|
|
147
|
+
|
|
148
|
+
The API runs an agentic tool-calling loop server-side, up to `max_iterations`.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
tools = [{
|
|
152
|
+
"type": "function",
|
|
153
|
+
"function": {
|
|
154
|
+
"name": "get_weather",
|
|
155
|
+
"description": "Get current weather for a city",
|
|
156
|
+
"parameters": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"properties": {"city": {"type": "string"}},
|
|
159
|
+
"required": ["city"],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}]
|
|
163
|
+
|
|
164
|
+
result = provider.complete(
|
|
165
|
+
messages=[{"role": "user", "content": "Weather in Oakland?"}],
|
|
166
|
+
model="deepseek-4-flash",
|
|
167
|
+
tools=tools,
|
|
168
|
+
tool_choice="auto",
|
|
169
|
+
max_iterations=3,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if result.tool_calls:
|
|
173
|
+
for call in result.tool_calls:
|
|
174
|
+
print(call["function"]["name"], call["function"]["arguments"])
|
|
175
|
+
else:
|
|
176
|
+
print(result.text)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`tool_subset=["get_weather"]` restricts which registered server-side tools get
|
|
180
|
+
loaded for the request.
|
|
181
|
+
|
|
182
|
+
## Error handling
|
|
183
|
+
|
|
184
|
+
Errors map onto the API's real failure modes, each carrying the payload the API
|
|
185
|
+
supplies with it.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from ainative_ai_sdk_provider import (
|
|
189
|
+
AINativeAPIError,
|
|
190
|
+
AuthenticationError,
|
|
191
|
+
InsufficientCreditsError,
|
|
192
|
+
ModelDeprecatedError,
|
|
193
|
+
RateLimitError,
|
|
194
|
+
RegionUnavailableError,
|
|
195
|
+
UpstreamUnavailableError,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
result = provider.complete(messages=[{"role": "user", "content": "hi"}])
|
|
200
|
+
except InsufficientCreditsError as err:
|
|
201
|
+
# 402 — chat completions need a paid plan
|
|
202
|
+
print(err.details["upgrade"]["action"])
|
|
203
|
+
except RateLimitError as err:
|
|
204
|
+
# 429 — the backend already tried its fallback chain
|
|
205
|
+
print(f"Retry in {err.retry_after}s")
|
|
206
|
+
except RegionUnavailableError as err:
|
|
207
|
+
# 403 — model geo-blocked; the API names alternatives
|
|
208
|
+
print("Try instead:", err.details["suggested_models"])
|
|
209
|
+
except ModelDeprecatedError as err:
|
|
210
|
+
# 410 — model decommissioned, migrate
|
|
211
|
+
print(err.message)
|
|
212
|
+
except UpstreamUnavailableError as err:
|
|
213
|
+
# 502/503/504 — upstream overloaded, erroring, or timed out
|
|
214
|
+
print(f"Retry in {err.retry_after}s")
|
|
215
|
+
except AINativeAPIError as err:
|
|
216
|
+
print(err.status_code, err.code, err.message)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Exception | Status | Meaning |
|
|
220
|
+
|-----------|--------|---------|
|
|
221
|
+
| `AuthenticationError` | 401, 403 | Missing, invalid, or unauthorized API key |
|
|
222
|
+
| `InvalidRequestError` | 400, 404, 422 | Malformed or rejected request |
|
|
223
|
+
| `InsufficientCreditsError` | 402 | Chat completions require a paid plan |
|
|
224
|
+
| `RegionUnavailableError` | 403 | Model geo-blocked; backend fallback also failed |
|
|
225
|
+
| `ModelDeprecatedError` | 410 | Model decommissioned — migrate |
|
|
226
|
+
| `RateLimitError` | 429 | Rate limited; honour `retry_after` |
|
|
227
|
+
| `UpstreamUnavailableError` | 502, 503, 504 | Upstream overloaded/erroring/timed out |
|
|
228
|
+
| `ServerError` | 500 | Unexpected server-side failure |
|
|
229
|
+
| `AINativeConnectionError` | — | API unreachable (DNS/TCP/TLS) |
|
|
230
|
+
| `AINativeTimeoutError` | — | Client-side timeout |
|
|
231
|
+
|
|
232
|
+
All of them subclass `AINativeError`; all HTTP ones subclass
|
|
233
|
+
`AINativeAPIError`, which exposes `.status_code`, `.code`, `.message`,
|
|
234
|
+
`.details`, `.retry_after` and `.response`.
|
|
235
|
+
|
|
236
|
+
## API reference
|
|
237
|
+
|
|
238
|
+
### `AINativeProvider(api_key=None, *, base_url=None, timeout=180.0, client=None, async_client=None)`
|
|
239
|
+
|
|
240
|
+
Pass `client` / `async_client` to reuse your own `httpx` client (connection
|
|
241
|
+
pooling, proxies, custom transports). A client you supply is yours to close —
|
|
242
|
+
the SDK won't close it for you.
|
|
243
|
+
|
|
244
|
+
The 180s default outlasts the endpoint's own 150s budget across provider
|
|
245
|
+
fallbacks, so you receive the API's structured timeout error instead of the
|
|
246
|
+
client aborting first.
|
|
247
|
+
|
|
248
|
+
### `complete(messages, *, model=None, temperature=None, max_tokens=None, system=None, tools=None, tool_choice=None, tool_subset=None, max_iterations=None, user_id=None, extra_body=None) -> ChatCompletion`
|
|
249
|
+
|
|
250
|
+
`messages` is required and non-empty. `temperature` is bounded 0.0-2.0 and
|
|
251
|
+
`max_iterations` 1-10, both validated locally so mistakes fail before a round
|
|
252
|
+
trip. Unset parameters are omitted entirely, letting the API's own defaults
|
|
253
|
+
apply. `extra_body` merges extra fields for endpoint parameters newer than this
|
|
254
|
+
SDK version.
|
|
255
|
+
|
|
256
|
+
### `stream(...) -> Iterator[StreamChunk]`
|
|
257
|
+
|
|
258
|
+
Same parameters minus the tool-calling ones — use `complete` for agentic loops.
|
|
259
|
+
|
|
260
|
+
### `acomplete(...)` / `astream(...)`
|
|
261
|
+
|
|
262
|
+
Async equivalents.
|
|
263
|
+
|
|
264
|
+
### `ChatCompletion`
|
|
265
|
+
|
|
266
|
+
| Field | Description |
|
|
267
|
+
|-------|-------------|
|
|
268
|
+
| `text` | Assistant text from the first choice (`""` on a tool-call-only reply) |
|
|
269
|
+
| `tool_calls` | Tool calls the model requested, or `[]` |
|
|
270
|
+
| `id`, `model`, `created` | Completion identity |
|
|
271
|
+
| `provider` | Provider label — informational only, see above |
|
|
272
|
+
| `finish_reason` | `stop`, `tool_calls`, … |
|
|
273
|
+
| `prompt_tokens`, `completion_tokens`, `total_tokens` | Usage |
|
|
274
|
+
| `iterations`, `total_tokens_used` | Tool-loop metadata, when reported |
|
|
275
|
+
| `model_substituted` | Set when the backend swapped your model for an equivalent |
|
|
276
|
+
| `choices`, `usage`, `raw` | Unprocessed response data |
|
|
277
|
+
|
|
278
|
+
### `StreamChunk`
|
|
279
|
+
|
|
280
|
+
`content` (text fragment), `finish_reason` (final frame only), `raw`.
|
|
281
|
+
|
|
282
|
+
## Scope (v0.1)
|
|
283
|
+
|
|
284
|
+
**Implemented:** chat completions — non-streaming and streaming, sync and
|
|
285
|
+
async, server-side tool calling, the full error surface of the completions
|
|
286
|
+
endpoint, and typed results.
|
|
287
|
+
|
|
288
|
+
**Not yet:** embeddings, the chat *session* endpoints (`/chat/sessions/...`
|
|
289
|
+
persistence and history), client-side BYOK key passthrough, and a published
|
|
290
|
+
model catalog. Those are separate endpoints and a separate surface — see
|
|
291
|
+
`core#8066` for what a v2 would cover.
|
|
292
|
+
|
|
293
|
+
## Development
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
pip install -e ".[dev]"
|
|
297
|
+
pytest tests/ -v --cov=ainative_ai_sdk_provider --cov-report=term-missing
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Tests are fully mocked with `respx` — no live API calls and no API key needed.
|
|
301
|
+
|
|
302
|
+
## License
|
|
303
|
+
|
|
304
|
+
MIT
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# ainative-ai-sdk-provider
|
|
2
|
+
|
|
3
|
+
**Python SDK for multi-provider AI inference** — chat completions, streaming, and agentic tool-calling over one interface, with server-side routing and automatic fallback across Cerebras, Meta Llama, NVIDIA NIM & DigitalOcean.
|
|
4
|
+
|
|
5
|
+
AINative's backend routes every chat completion across Cerebras, Meta Llama,
|
|
6
|
+
NVIDIA NIM, DigitalOcean and BYOK based on your plan tier, live per-provider
|
|
7
|
+
rate-limit state, daily token and cost caps, and model availability — then
|
|
8
|
+
transparently falls back through its chain when a provider is exhausted or
|
|
9
|
+
geo-blocked. This SDK is the client for that. You ask for a model; the platform
|
|
10
|
+
picks the upstream.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install ainative-ai-sdk-provider
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from ainative_ai_sdk_provider import AINativeProvider
|
|
20
|
+
|
|
21
|
+
provider = AINativeProvider() # reads AINATIVE_API_KEY
|
|
22
|
+
|
|
23
|
+
result = provider.complete(
|
|
24
|
+
messages=[{"role": "user", "content": "Explain SSE in one sentence."}],
|
|
25
|
+
model="deepseek-4-flash",
|
|
26
|
+
)
|
|
27
|
+
print(result.text)
|
|
28
|
+
print(result.total_tokens, "tokens")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why there is no `provider=` parameter
|
|
32
|
+
|
|
33
|
+
This is the deliberate design of the SDK, not a missing feature.
|
|
34
|
+
|
|
35
|
+
The server-side router picks an upstream per request. Pinning a provider from
|
|
36
|
+
the client would switch that off — no tier-aware selection, no rate-limit
|
|
37
|
+
awareness, no cost-aware ordering, no automatic fallback when an upstream 429s
|
|
38
|
+
or geo-blocks you. So the SDK never sends a `provider` field, which is exactly
|
|
39
|
+
the signal the endpoint needs to do its job.
|
|
40
|
+
|
|
41
|
+
Two consequences worth knowing:
|
|
42
|
+
|
|
43
|
+
- **`result.provider` is informational.** The backend intentionally reports
|
|
44
|
+
several internal upstreams as `"ainative"`. Don't branch on it.
|
|
45
|
+
- **The SDK does not retry rate limits for you.** The backend already walked
|
|
46
|
+
its own fallback chain before returning a 429 or 503, and tells you how long
|
|
47
|
+
to wait. Retrying immediately would fight its circuit breaker — honour
|
|
48
|
+
`err.retry_after` instead.
|
|
49
|
+
|
|
50
|
+
## Authentication
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# 1. Environment variable (recommended)
|
|
54
|
+
# export AINATIVE_API_KEY=your_key
|
|
55
|
+
provider = AINativeProvider()
|
|
56
|
+
|
|
57
|
+
# 2. Explicit
|
|
58
|
+
provider = AINativeProvider(api_key="your_key")
|
|
59
|
+
|
|
60
|
+
# 3. Point at another environment
|
|
61
|
+
provider = AINativeProvider(api_key="k", base_url="http://localhost:8000/api/v1")
|
|
62
|
+
# or: export AINATIVE_BASE_URL=...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The client sends your key as both `X-API-Key` and `Authorization: Bearer`,
|
|
66
|
+
since the platform's unified auth accepts either — so the same client works
|
|
67
|
+
against the public gateway and a directly-addressed backend without
|
|
68
|
+
reconfiguration.
|
|
69
|
+
|
|
70
|
+
## Streaming
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
for chunk in provider.stream(
|
|
74
|
+
messages=[{"role": "user", "content": "Count to five."}],
|
|
75
|
+
model="deepseek-4-flash",
|
|
76
|
+
):
|
|
77
|
+
print(chunk.content, end="", flush=True)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Streaming is served by upstreams that speak OpenAI SSE natively. If the router
|
|
81
|
+
lands your request on one that doesn't, the API returns a normal JSON body and
|
|
82
|
+
this method yields a single chunk with the full completion — so your loop keeps
|
|
83
|
+
working either way.
|
|
84
|
+
|
|
85
|
+
If an upstream fails mid-stream, the API injects an error frame; the SDK raises
|
|
86
|
+
`UpstreamUnavailableError` rather than silently truncating your output.
|
|
87
|
+
|
|
88
|
+
## Async
|
|
89
|
+
|
|
90
|
+
Every method has an async counterpart. `acomplete` mirrors `complete`,
|
|
91
|
+
`astream` mirrors `stream`.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
from ainative_ai_sdk_provider import AINativeProvider
|
|
96
|
+
|
|
97
|
+
async def main():
|
|
98
|
+
async with AINativeProvider() as provider:
|
|
99
|
+
result = await provider.acomplete(
|
|
100
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
101
|
+
)
|
|
102
|
+
print(result.text)
|
|
103
|
+
|
|
104
|
+
async for chunk in provider.astream(
|
|
105
|
+
messages=[{"role": "user", "content": "Count to three."}],
|
|
106
|
+
):
|
|
107
|
+
print(chunk.content, end="")
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Tool calling
|
|
113
|
+
|
|
114
|
+
The API runs an agentic tool-calling loop server-side, up to `max_iterations`.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
tools = [{
|
|
118
|
+
"type": "function",
|
|
119
|
+
"function": {
|
|
120
|
+
"name": "get_weather",
|
|
121
|
+
"description": "Get current weather for a city",
|
|
122
|
+
"parameters": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {"city": {"type": "string"}},
|
|
125
|
+
"required": ["city"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}]
|
|
129
|
+
|
|
130
|
+
result = provider.complete(
|
|
131
|
+
messages=[{"role": "user", "content": "Weather in Oakland?"}],
|
|
132
|
+
model="deepseek-4-flash",
|
|
133
|
+
tools=tools,
|
|
134
|
+
tool_choice="auto",
|
|
135
|
+
max_iterations=3,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if result.tool_calls:
|
|
139
|
+
for call in result.tool_calls:
|
|
140
|
+
print(call["function"]["name"], call["function"]["arguments"])
|
|
141
|
+
else:
|
|
142
|
+
print(result.text)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`tool_subset=["get_weather"]` restricts which registered server-side tools get
|
|
146
|
+
loaded for the request.
|
|
147
|
+
|
|
148
|
+
## Error handling
|
|
149
|
+
|
|
150
|
+
Errors map onto the API's real failure modes, each carrying the payload the API
|
|
151
|
+
supplies with it.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from ainative_ai_sdk_provider import (
|
|
155
|
+
AINativeAPIError,
|
|
156
|
+
AuthenticationError,
|
|
157
|
+
InsufficientCreditsError,
|
|
158
|
+
ModelDeprecatedError,
|
|
159
|
+
RateLimitError,
|
|
160
|
+
RegionUnavailableError,
|
|
161
|
+
UpstreamUnavailableError,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
result = provider.complete(messages=[{"role": "user", "content": "hi"}])
|
|
166
|
+
except InsufficientCreditsError as err:
|
|
167
|
+
# 402 — chat completions need a paid plan
|
|
168
|
+
print(err.details["upgrade"]["action"])
|
|
169
|
+
except RateLimitError as err:
|
|
170
|
+
# 429 — the backend already tried its fallback chain
|
|
171
|
+
print(f"Retry in {err.retry_after}s")
|
|
172
|
+
except RegionUnavailableError as err:
|
|
173
|
+
# 403 — model geo-blocked; the API names alternatives
|
|
174
|
+
print("Try instead:", err.details["suggested_models"])
|
|
175
|
+
except ModelDeprecatedError as err:
|
|
176
|
+
# 410 — model decommissioned, migrate
|
|
177
|
+
print(err.message)
|
|
178
|
+
except UpstreamUnavailableError as err:
|
|
179
|
+
# 502/503/504 — upstream overloaded, erroring, or timed out
|
|
180
|
+
print(f"Retry in {err.retry_after}s")
|
|
181
|
+
except AINativeAPIError as err:
|
|
182
|
+
print(err.status_code, err.code, err.message)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Exception | Status | Meaning |
|
|
186
|
+
|-----------|--------|---------|
|
|
187
|
+
| `AuthenticationError` | 401, 403 | Missing, invalid, or unauthorized API key |
|
|
188
|
+
| `InvalidRequestError` | 400, 404, 422 | Malformed or rejected request |
|
|
189
|
+
| `InsufficientCreditsError` | 402 | Chat completions require a paid plan |
|
|
190
|
+
| `RegionUnavailableError` | 403 | Model geo-blocked; backend fallback also failed |
|
|
191
|
+
| `ModelDeprecatedError` | 410 | Model decommissioned — migrate |
|
|
192
|
+
| `RateLimitError` | 429 | Rate limited; honour `retry_after` |
|
|
193
|
+
| `UpstreamUnavailableError` | 502, 503, 504 | Upstream overloaded/erroring/timed out |
|
|
194
|
+
| `ServerError` | 500 | Unexpected server-side failure |
|
|
195
|
+
| `AINativeConnectionError` | — | API unreachable (DNS/TCP/TLS) |
|
|
196
|
+
| `AINativeTimeoutError` | — | Client-side timeout |
|
|
197
|
+
|
|
198
|
+
All of them subclass `AINativeError`; all HTTP ones subclass
|
|
199
|
+
`AINativeAPIError`, which exposes `.status_code`, `.code`, `.message`,
|
|
200
|
+
`.details`, `.retry_after` and `.response`.
|
|
201
|
+
|
|
202
|
+
## API reference
|
|
203
|
+
|
|
204
|
+
### `AINativeProvider(api_key=None, *, base_url=None, timeout=180.0, client=None, async_client=None)`
|
|
205
|
+
|
|
206
|
+
Pass `client` / `async_client` to reuse your own `httpx` client (connection
|
|
207
|
+
pooling, proxies, custom transports). A client you supply is yours to close —
|
|
208
|
+
the SDK won't close it for you.
|
|
209
|
+
|
|
210
|
+
The 180s default outlasts the endpoint's own 150s budget across provider
|
|
211
|
+
fallbacks, so you receive the API's structured timeout error instead of the
|
|
212
|
+
client aborting first.
|
|
213
|
+
|
|
214
|
+
### `complete(messages, *, model=None, temperature=None, max_tokens=None, system=None, tools=None, tool_choice=None, tool_subset=None, max_iterations=None, user_id=None, extra_body=None) -> ChatCompletion`
|
|
215
|
+
|
|
216
|
+
`messages` is required and non-empty. `temperature` is bounded 0.0-2.0 and
|
|
217
|
+
`max_iterations` 1-10, both validated locally so mistakes fail before a round
|
|
218
|
+
trip. Unset parameters are omitted entirely, letting the API's own defaults
|
|
219
|
+
apply. `extra_body` merges extra fields for endpoint parameters newer than this
|
|
220
|
+
SDK version.
|
|
221
|
+
|
|
222
|
+
### `stream(...) -> Iterator[StreamChunk]`
|
|
223
|
+
|
|
224
|
+
Same parameters minus the tool-calling ones — use `complete` for agentic loops.
|
|
225
|
+
|
|
226
|
+
### `acomplete(...)` / `astream(...)`
|
|
227
|
+
|
|
228
|
+
Async equivalents.
|
|
229
|
+
|
|
230
|
+
### `ChatCompletion`
|
|
231
|
+
|
|
232
|
+
| Field | Description |
|
|
233
|
+
|-------|-------------|
|
|
234
|
+
| `text` | Assistant text from the first choice (`""` on a tool-call-only reply) |
|
|
235
|
+
| `tool_calls` | Tool calls the model requested, or `[]` |
|
|
236
|
+
| `id`, `model`, `created` | Completion identity |
|
|
237
|
+
| `provider` | Provider label — informational only, see above |
|
|
238
|
+
| `finish_reason` | `stop`, `tool_calls`, … |
|
|
239
|
+
| `prompt_tokens`, `completion_tokens`, `total_tokens` | Usage |
|
|
240
|
+
| `iterations`, `total_tokens_used` | Tool-loop metadata, when reported |
|
|
241
|
+
| `model_substituted` | Set when the backend swapped your model for an equivalent |
|
|
242
|
+
| `choices`, `usage`, `raw` | Unprocessed response data |
|
|
243
|
+
|
|
244
|
+
### `StreamChunk`
|
|
245
|
+
|
|
246
|
+
`content` (text fragment), `finish_reason` (final frame only), `raw`.
|
|
247
|
+
|
|
248
|
+
## Scope (v0.1)
|
|
249
|
+
|
|
250
|
+
**Implemented:** chat completions — non-streaming and streaming, sync and
|
|
251
|
+
async, server-side tool calling, the full error surface of the completions
|
|
252
|
+
endpoint, and typed results.
|
|
253
|
+
|
|
254
|
+
**Not yet:** embeddings, the chat *session* endpoints (`/chat/sessions/...`
|
|
255
|
+
persistence and history), client-side BYOK key passthrough, and a published
|
|
256
|
+
model catalog. Those are separate endpoints and a separate surface — see
|
|
257
|
+
`core#8066` for what a v2 would cover.
|
|
258
|
+
|
|
259
|
+
## Development
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
pip install -e ".[dev]"
|
|
263
|
+
pytest tests/ -v --cov=ainative_ai_sdk_provider --cov-report=term-missing
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Tests are fully mocked with `respx` — no live API calls and no API key needed.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ainative-ai-sdk-provider — Python client for AINative's inference API.
|
|
3
|
+
|
|
4
|
+
One interface, many providers — chosen server-side. AINative's backend routes
|
|
5
|
+
each request across Cerebras, Meta Llama, NVIDIA NIM, DigitalOcean and BYOK
|
|
6
|
+
based on your plan tier, live rate-limit state and cost caps. This SDK is the
|
|
7
|
+
client for that: you ask for a model, the platform picks the upstream.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from ainative_ai_sdk_provider import AINativeProvider
|
|
11
|
+
|
|
12
|
+
provider = AINativeProvider() # reads AINATIVE_API_KEY
|
|
13
|
+
|
|
14
|
+
result = provider.complete(
|
|
15
|
+
messages=[{"role": "user", "content": "Explain SSE in one sentence."}],
|
|
16
|
+
model="deepseek-4-flash",
|
|
17
|
+
)
|
|
18
|
+
print(result.text)
|
|
19
|
+
|
|
20
|
+
# Streaming
|
|
21
|
+
for chunk in provider.stream(messages=[{"role": "user", "content": "Count to 5."}]):
|
|
22
|
+
print(chunk.content, end="")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
from .client import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, AINativeProvider
|
|
28
|
+
from .errors import (
|
|
29
|
+
AINativeAPIError,
|
|
30
|
+
AINativeConnectionError,
|
|
31
|
+
AINativeError,
|
|
32
|
+
AINativeTimeoutError,
|
|
33
|
+
AuthenticationError,
|
|
34
|
+
InsufficientCreditsError,
|
|
35
|
+
InvalidRequestError,
|
|
36
|
+
ModelDeprecatedError,
|
|
37
|
+
RateLimitError,
|
|
38
|
+
RegionUnavailableError,
|
|
39
|
+
ServerError,
|
|
40
|
+
UpstreamUnavailableError,
|
|
41
|
+
)
|
|
42
|
+
from .models import ChatCompletion, StreamChunk
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"__version__",
|
|
46
|
+
# Client
|
|
47
|
+
"AINativeProvider",
|
|
48
|
+
"DEFAULT_BASE_URL",
|
|
49
|
+
"DEFAULT_TIMEOUT",
|
|
50
|
+
# Models
|
|
51
|
+
"ChatCompletion",
|
|
52
|
+
"StreamChunk",
|
|
53
|
+
# Errors
|
|
54
|
+
"AINativeError",
|
|
55
|
+
"AINativeAPIError",
|
|
56
|
+
"AINativeConnectionError",
|
|
57
|
+
"AINativeTimeoutError",
|
|
58
|
+
"AuthenticationError",
|
|
59
|
+
"InsufficientCreditsError",
|
|
60
|
+
"InvalidRequestError",
|
|
61
|
+
"ModelDeprecatedError",
|
|
62
|
+
"RateLimitError",
|
|
63
|
+
"RegionUnavailableError",
|
|
64
|
+
"ServerError",
|
|
65
|
+
"UpstreamUnavailableError",
|
|
66
|
+
]
|