aiyallm 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.
- aiyallm-0.1.0/PKG-INFO +300 -0
- aiyallm-0.1.0/README.md +287 -0
- aiyallm-0.1.0/pyproject.toml +28 -0
- aiyallm-0.1.0/setup.cfg +4 -0
- aiyallm-0.1.0/src/aiya/__init__.py +49 -0
- aiyallm-0.1.0/src/aiya/client.py +449 -0
- aiyallm-0.1.0/src/aiya/exceptions.py +40 -0
- aiyallm-0.1.0/src/aiya/messages.py +218 -0
- aiyallm-0.1.0/src/aiya/models.py +147 -0
- aiyallm-0.1.0/src/aiya/providers/__init__.py +13 -0
- aiyallm-0.1.0/src/aiya/providers/anthropic_compatible.py +383 -0
- aiyallm-0.1.0/src/aiya/providers/base.py +94 -0
- aiyallm-0.1.0/src/aiya/providers/openai_compatible.py +346 -0
- aiyallm-0.1.0/src/aiya/registry.py +176 -0
- aiyallm-0.1.0/src/aiya/router.py +66 -0
- aiyallm-0.1.0/src/aiya/usage.py +361 -0
- aiyallm-0.1.0/src/aiyallm.egg-info/PKG-INFO +300 -0
- aiyallm-0.1.0/src/aiyallm.egg-info/SOURCES.txt +23 -0
- aiyallm-0.1.0/src/aiyallm.egg-info/dependency_links.txt +1 -0
- aiyallm-0.1.0/src/aiyallm.egg-info/requires.txt +7 -0
- aiyallm-0.1.0/src/aiyallm.egg-info/top_level.txt +1 -0
- aiyallm-0.1.0/tests/test_client.py +77 -0
- aiyallm-0.1.0/tests/test_messages.py +41 -0
- aiyallm-0.1.0/tests/test_response_parsing.py +47 -0
- aiyallm-0.1.0/tests/test_usage.py +54 -0
aiyallm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiyallm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small, provider-agnostic gateway for OpenAI/Anthropic-style language and vision models.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: openai>=1.66
|
|
8
|
+
Requires-Dist: anthropic>=0.49
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8.3; extra == "test"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
13
|
+
|
|
14
|
+
# Aiya
|
|
15
|
+
|
|
16
|
+
Aiya is a lightweight Python gateway for OpenAI/Anthropic-style language and vision models.
|
|
17
|
+
|
|
18
|
+
> Aiya does not ship a built-in model catalog and does not read API keys implicitly. Every provider, `base_url`, API key, and model is declared by the application.
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Features](#features)
|
|
23
|
+
- [Install](#install)
|
|
24
|
+
- [Quick Start](#quick-start)
|
|
25
|
+
- [Provider Configuration](#provider-configuration)
|
|
26
|
+
- [Routing](#routing)
|
|
27
|
+
- [Vision and Capability Filtering](#vision-and-capability-filtering)
|
|
28
|
+
- [Usage Ledger](#usage-ledger)
|
|
29
|
+
- [Cost Calculation](#cost-calculation)
|
|
30
|
+
- [Streaming and Async](#streaming-and-async)
|
|
31
|
+
- [Extending Providers](#extending-providers)
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- OpenAI and OpenAI-compatible providers
|
|
36
|
+
- Anthropic and Anthropic-compatible providers
|
|
37
|
+
- Chinese OpenAI-compatible providers such as DeepSeek, Qwen/DashScope, Moonshot/Kimi, Zhipu GLM, and SiliconFlow
|
|
38
|
+
- Explicit model selection, `provider/model` references, and provider route lists
|
|
39
|
+
- Routing policies: `fallback`, `first`, `round_robin`, `random`, and `least_used`
|
|
40
|
+
- Vision and multimodal message passthrough with capability filtering
|
|
41
|
+
- Sync and async chat and streaming
|
|
42
|
+
- Detailed token accounting with cache hit/miss fields
|
|
43
|
+
- Optional SQLite or JSONL usage persistence
|
|
44
|
+
- Optional cost calculation from an application-owned price book
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Runtime dependencies are:
|
|
53
|
+
|
|
54
|
+
- `openai`
|
|
55
|
+
- `anthropic`
|
|
56
|
+
- `httpx`
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
There are no default providers. Pass them explicitly.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from aiya import Aiya
|
|
64
|
+
|
|
65
|
+
client = Aiya(
|
|
66
|
+
providers=[
|
|
67
|
+
{
|
|
68
|
+
"name": "deepseek",
|
|
69
|
+
"account": "billing-deepseek",
|
|
70
|
+
"type": "openai_compatible",
|
|
71
|
+
"base_url": "https://api.deepseek.com",
|
|
72
|
+
"api_key": "your-deepseek-key",
|
|
73
|
+
"models": ["deepseek-chat", "deepseek-reasoner"],
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
response = client.chat(
|
|
79
|
+
"用一句话解释什么是路由。",
|
|
80
|
+
model="deepseek/deepseek-chat",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
print(response.text)
|
|
84
|
+
print(response.usage)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> `models` is optional when the provider allows unknown models. Configured providers default to `allow_unknown_models=True`, but the provider itself must still be declared.
|
|
88
|
+
|
|
89
|
+
## Provider Configuration
|
|
90
|
+
|
|
91
|
+
Each provider accepts these common fields.
|
|
92
|
+
|
|
93
|
+
| Field | Meaning |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `name` | Unique provider name used by `provider/model` routes |
|
|
96
|
+
| `account` | Optional billing or account name recorded in usage |
|
|
97
|
+
| `type` | `openai`, `openai_compatible`, `anthropic`, or `anthropic_compatible` |
|
|
98
|
+
| `base_url` | Endpoint for compatible services |
|
|
99
|
+
| `api_key` | API key, or use `api_key_env` to read an environment variable |
|
|
100
|
+
| `models` | Optional model names or `{"id", "capabilities", ...}` mappings |
|
|
101
|
+
| `allow_unknown_models` | Allow model IDs not listed in `models`; defaults to `True` |
|
|
102
|
+
| `timeout` | SDK timeout |
|
|
103
|
+
|
|
104
|
+
### OpenAI-Compatible Examples
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
providers = [
|
|
108
|
+
{
|
|
109
|
+
"name": "deepseek",
|
|
110
|
+
"type": "openai_compatible",
|
|
111
|
+
"base_url": "https://api.deepseek.com",
|
|
112
|
+
"api_key_env": "DEEPSEEK_API_KEY",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "qwen",
|
|
116
|
+
"type": "openai_compatible",
|
|
117
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
118
|
+
"api_key_env": "DASHSCOPE_API_KEY",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "moonshot",
|
|
122
|
+
"type": "openai_compatible",
|
|
123
|
+
"base_url": "https://api.moonshot.cn/v1",
|
|
124
|
+
"api_key_env": "MOONSHOT_API_KEY",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "glm",
|
|
128
|
+
"type": "openai_compatible",
|
|
129
|
+
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
130
|
+
"api_key_env": "ZHIPU_API_KEY",
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use the `base_url` from each provider's current documentation. Aiya does not hard-code any endpoint or model name.
|
|
136
|
+
|
|
137
|
+
## Routing
|
|
138
|
+
|
|
139
|
+
Use `provider/model` to target a provider.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
response = client.chat("Hello", model="qwen/qwen-plus")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use a route list for fallback.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
response = client.chat(
|
|
149
|
+
"Hello",
|
|
150
|
+
model=["deepseek/deepseek-chat", "qwen/qwen-plus"],
|
|
151
|
+
route_policy="fallback",
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Available policies:
|
|
156
|
+
|
|
157
|
+
- `fallback` - try candidates in order
|
|
158
|
+
- `first` - use only the first candidate
|
|
159
|
+
- `round_robin` - rotate through candidates
|
|
160
|
+
- `random` - choose a random candidate
|
|
161
|
+
- `least_used` - choose the candidate with the lowest recorded token usage
|
|
162
|
+
|
|
163
|
+
The default policy is `fallback`.
|
|
164
|
+
|
|
165
|
+
## Vision and Capability Filtering
|
|
166
|
+
|
|
167
|
+
Declare capabilities in `models` and pass `require_vision=True` when a request needs image input.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from aiya import Aiya
|
|
171
|
+
|
|
172
|
+
client = Aiya(
|
|
173
|
+
providers=[
|
|
174
|
+
{
|
|
175
|
+
"name": "qwen",
|
|
176
|
+
"type": "openai_compatible",
|
|
177
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
178
|
+
"api_key": "your-key",
|
|
179
|
+
"models": [
|
|
180
|
+
{"id": "qwen-vl-plus", "capabilities": ["text", "vision"]}
|
|
181
|
+
],
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
response = client.chat(
|
|
187
|
+
[
|
|
188
|
+
{
|
|
189
|
+
"role": "user",
|
|
190
|
+
"content": [
|
|
191
|
+
{"type": "text", "text": "What is in this image?"},
|
|
192
|
+
{
|
|
193
|
+
"type": "image_url",
|
|
194
|
+
"image_url": {"url": "https://example.com/image.jpg"},
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
model="qwen/qwen-vl-plus",
|
|
200
|
+
require_vision=True,
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Usage Ledger
|
|
205
|
+
|
|
206
|
+
Every successful completion is recorded.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
response = client.chat("Hello", model="deepseek/deepseek-chat")
|
|
210
|
+
|
|
211
|
+
print(response.usage.input_tokens)
|
|
212
|
+
print(response.usage.output_tokens)
|
|
213
|
+
print(response.usage.input_cache_hit_tokens)
|
|
214
|
+
print(response.usage.input_cache_miss_tokens)
|
|
215
|
+
print(response.usage.output_cache_hit_tokens)
|
|
216
|
+
print(response.usage.output_cache_miss_tokens)
|
|
217
|
+
print(response.usage.account)
|
|
218
|
+
print(response.usage.reasoning_tokens)
|
|
219
|
+
|
|
220
|
+
print(client.ledger.total())
|
|
221
|
+
print(client.ledger.by_model())
|
|
222
|
+
print(client.ledger.by_provider())
|
|
223
|
+
print(client.ledger.by_account())
|
|
224
|
+
|
|
225
|
+
client.ledger.export_jsonl("usage.jsonl")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
For persistent accounting, pass a SQLite path.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from aiya import UsageLedger
|
|
232
|
+
|
|
233
|
+
ledger = UsageLedger(sqlite_path="data/usage.sqlite3")
|
|
234
|
+
client = Aiya(providers=providers, ledger=ledger)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Cost Calculation
|
|
238
|
+
|
|
239
|
+
Token counts are always recorded. Cost is calculated only when the application supplies a price.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from aiya import Aiya, Price, PriceBook
|
|
243
|
+
|
|
244
|
+
price_book = PriceBook(
|
|
245
|
+
{
|
|
246
|
+
"deepseek-chat": Price(
|
|
247
|
+
input_per_mtok=0.27,
|
|
248
|
+
output_per_mtok=1.10,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
client = Aiya(providers=providers, price_book=price_book)
|
|
254
|
+
response = client.chat("Hello", model="deepseek/deepseek-chat")
|
|
255
|
+
|
|
256
|
+
print(response.usage.cost_usd)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Prices are US dollars per million tokens.
|
|
260
|
+
|
|
261
|
+
## Streaming and Async
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
for chunk in client.stream("Hello", model="deepseek/deepseek-chat"):
|
|
265
|
+
print(chunk.content, end="", flush=True)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
import asyncio
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async def main():
|
|
273
|
+
response = await client.achat("Hello", model="deepseek/deepseek-chat")
|
|
274
|
+
print(response.text)
|
|
275
|
+
|
|
276
|
+
async for chunk in client.astream("Hello", model="deepseek/deepseek-chat"):
|
|
277
|
+
print(chunk.content, end="", flush=True)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
asyncio.run(main())
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Extending Providers
|
|
284
|
+
|
|
285
|
+
For a non-standard provider, subclass `aiya.BaseProvider` and implement `complete`, `acomplete`, `stream`, and `astream`.
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from aiya import Aiya, BaseProvider
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class MyProvider(BaseProvider):
|
|
292
|
+
def complete(self, request):
|
|
293
|
+
...
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Then register the instance directly.
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
client = Aiya(providers=[MyProvider(name="my-provider", models=["my-model"])])
|
|
300
|
+
```
|
aiyallm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Aiya
|
|
2
|
+
|
|
3
|
+
Aiya is a lightweight Python gateway for OpenAI/Anthropic-style language and vision models.
|
|
4
|
+
|
|
5
|
+
> Aiya does not ship a built-in model catalog and does not read API keys implicitly. Every provider, `base_url`, API key, and model is declared by the application.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Install](#install)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Provider Configuration](#provider-configuration)
|
|
13
|
+
- [Routing](#routing)
|
|
14
|
+
- [Vision and Capability Filtering](#vision-and-capability-filtering)
|
|
15
|
+
- [Usage Ledger](#usage-ledger)
|
|
16
|
+
- [Cost Calculation](#cost-calculation)
|
|
17
|
+
- [Streaming and Async](#streaming-and-async)
|
|
18
|
+
- [Extending Providers](#extending-providers)
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- OpenAI and OpenAI-compatible providers
|
|
23
|
+
- Anthropic and Anthropic-compatible providers
|
|
24
|
+
- Chinese OpenAI-compatible providers such as DeepSeek, Qwen/DashScope, Moonshot/Kimi, Zhipu GLM, and SiliconFlow
|
|
25
|
+
- Explicit model selection, `provider/model` references, and provider route lists
|
|
26
|
+
- Routing policies: `fallback`, `first`, `round_robin`, `random`, and `least_used`
|
|
27
|
+
- Vision and multimodal message passthrough with capability filtering
|
|
28
|
+
- Sync and async chat and streaming
|
|
29
|
+
- Detailed token accounting with cache hit/miss fields
|
|
30
|
+
- Optional SQLite or JSONL usage persistence
|
|
31
|
+
- Optional cost calculation from an application-owned price book
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Runtime dependencies are:
|
|
40
|
+
|
|
41
|
+
- `openai`
|
|
42
|
+
- `anthropic`
|
|
43
|
+
- `httpx`
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
There are no default providers. Pass them explicitly.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from aiya import Aiya
|
|
51
|
+
|
|
52
|
+
client = Aiya(
|
|
53
|
+
providers=[
|
|
54
|
+
{
|
|
55
|
+
"name": "deepseek",
|
|
56
|
+
"account": "billing-deepseek",
|
|
57
|
+
"type": "openai_compatible",
|
|
58
|
+
"base_url": "https://api.deepseek.com",
|
|
59
|
+
"api_key": "your-deepseek-key",
|
|
60
|
+
"models": ["deepseek-chat", "deepseek-reasoner"],
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
response = client.chat(
|
|
66
|
+
"用一句话解释什么是路由。",
|
|
67
|
+
model="deepseek/deepseek-chat",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(response.text)
|
|
71
|
+
print(response.usage)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> `models` is optional when the provider allows unknown models. Configured providers default to `allow_unknown_models=True`, but the provider itself must still be declared.
|
|
75
|
+
|
|
76
|
+
## Provider Configuration
|
|
77
|
+
|
|
78
|
+
Each provider accepts these common fields.
|
|
79
|
+
|
|
80
|
+
| Field | Meaning |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `name` | Unique provider name used by `provider/model` routes |
|
|
83
|
+
| `account` | Optional billing or account name recorded in usage |
|
|
84
|
+
| `type` | `openai`, `openai_compatible`, `anthropic`, or `anthropic_compatible` |
|
|
85
|
+
| `base_url` | Endpoint for compatible services |
|
|
86
|
+
| `api_key` | API key, or use `api_key_env` to read an environment variable |
|
|
87
|
+
| `models` | Optional model names or `{"id", "capabilities", ...}` mappings |
|
|
88
|
+
| `allow_unknown_models` | Allow model IDs not listed in `models`; defaults to `True` |
|
|
89
|
+
| `timeout` | SDK timeout |
|
|
90
|
+
|
|
91
|
+
### OpenAI-Compatible Examples
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
providers = [
|
|
95
|
+
{
|
|
96
|
+
"name": "deepseek",
|
|
97
|
+
"type": "openai_compatible",
|
|
98
|
+
"base_url": "https://api.deepseek.com",
|
|
99
|
+
"api_key_env": "DEEPSEEK_API_KEY",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "qwen",
|
|
103
|
+
"type": "openai_compatible",
|
|
104
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
105
|
+
"api_key_env": "DASHSCOPE_API_KEY",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "moonshot",
|
|
109
|
+
"type": "openai_compatible",
|
|
110
|
+
"base_url": "https://api.moonshot.cn/v1",
|
|
111
|
+
"api_key_env": "MOONSHOT_API_KEY",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "glm",
|
|
115
|
+
"type": "openai_compatible",
|
|
116
|
+
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
117
|
+
"api_key_env": "ZHIPU_API_KEY",
|
|
118
|
+
},
|
|
119
|
+
]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use the `base_url` from each provider's current documentation. Aiya does not hard-code any endpoint or model name.
|
|
123
|
+
|
|
124
|
+
## Routing
|
|
125
|
+
|
|
126
|
+
Use `provider/model` to target a provider.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
response = client.chat("Hello", model="qwen/qwen-plus")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use a route list for fallback.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
response = client.chat(
|
|
136
|
+
"Hello",
|
|
137
|
+
model=["deepseek/deepseek-chat", "qwen/qwen-plus"],
|
|
138
|
+
route_policy="fallback",
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Available policies:
|
|
143
|
+
|
|
144
|
+
- `fallback` - try candidates in order
|
|
145
|
+
- `first` - use only the first candidate
|
|
146
|
+
- `round_robin` - rotate through candidates
|
|
147
|
+
- `random` - choose a random candidate
|
|
148
|
+
- `least_used` - choose the candidate with the lowest recorded token usage
|
|
149
|
+
|
|
150
|
+
The default policy is `fallback`.
|
|
151
|
+
|
|
152
|
+
## Vision and Capability Filtering
|
|
153
|
+
|
|
154
|
+
Declare capabilities in `models` and pass `require_vision=True` when a request needs image input.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from aiya import Aiya
|
|
158
|
+
|
|
159
|
+
client = Aiya(
|
|
160
|
+
providers=[
|
|
161
|
+
{
|
|
162
|
+
"name": "qwen",
|
|
163
|
+
"type": "openai_compatible",
|
|
164
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
165
|
+
"api_key": "your-key",
|
|
166
|
+
"models": [
|
|
167
|
+
{"id": "qwen-vl-plus", "capabilities": ["text", "vision"]}
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
response = client.chat(
|
|
174
|
+
[
|
|
175
|
+
{
|
|
176
|
+
"role": "user",
|
|
177
|
+
"content": [
|
|
178
|
+
{"type": "text", "text": "What is in this image?"},
|
|
179
|
+
{
|
|
180
|
+
"type": "image_url",
|
|
181
|
+
"image_url": {"url": "https://example.com/image.jpg"},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
model="qwen/qwen-vl-plus",
|
|
187
|
+
require_vision=True,
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Usage Ledger
|
|
192
|
+
|
|
193
|
+
Every successful completion is recorded.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
response = client.chat("Hello", model="deepseek/deepseek-chat")
|
|
197
|
+
|
|
198
|
+
print(response.usage.input_tokens)
|
|
199
|
+
print(response.usage.output_tokens)
|
|
200
|
+
print(response.usage.input_cache_hit_tokens)
|
|
201
|
+
print(response.usage.input_cache_miss_tokens)
|
|
202
|
+
print(response.usage.output_cache_hit_tokens)
|
|
203
|
+
print(response.usage.output_cache_miss_tokens)
|
|
204
|
+
print(response.usage.account)
|
|
205
|
+
print(response.usage.reasoning_tokens)
|
|
206
|
+
|
|
207
|
+
print(client.ledger.total())
|
|
208
|
+
print(client.ledger.by_model())
|
|
209
|
+
print(client.ledger.by_provider())
|
|
210
|
+
print(client.ledger.by_account())
|
|
211
|
+
|
|
212
|
+
client.ledger.export_jsonl("usage.jsonl")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
For persistent accounting, pass a SQLite path.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from aiya import UsageLedger
|
|
219
|
+
|
|
220
|
+
ledger = UsageLedger(sqlite_path="data/usage.sqlite3")
|
|
221
|
+
client = Aiya(providers=providers, ledger=ledger)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Cost Calculation
|
|
225
|
+
|
|
226
|
+
Token counts are always recorded. Cost is calculated only when the application supplies a price.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from aiya import Aiya, Price, PriceBook
|
|
230
|
+
|
|
231
|
+
price_book = PriceBook(
|
|
232
|
+
{
|
|
233
|
+
"deepseek-chat": Price(
|
|
234
|
+
input_per_mtok=0.27,
|
|
235
|
+
output_per_mtok=1.10,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
client = Aiya(providers=providers, price_book=price_book)
|
|
241
|
+
response = client.chat("Hello", model="deepseek/deepseek-chat")
|
|
242
|
+
|
|
243
|
+
print(response.usage.cost_usd)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Prices are US dollars per million tokens.
|
|
247
|
+
|
|
248
|
+
## Streaming and Async
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
for chunk in client.stream("Hello", model="deepseek/deepseek-chat"):
|
|
252
|
+
print(chunk.content, end="", flush=True)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
import asyncio
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def main():
|
|
260
|
+
response = await client.achat("Hello", model="deepseek/deepseek-chat")
|
|
261
|
+
print(response.text)
|
|
262
|
+
|
|
263
|
+
async for chunk in client.astream("Hello", model="deepseek/deepseek-chat"):
|
|
264
|
+
print(chunk.content, end="", flush=True)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
asyncio.run(main())
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Extending Providers
|
|
271
|
+
|
|
272
|
+
For a non-standard provider, subclass `aiya.BaseProvider` and implement `complete`, `acomplete`, `stream`, and `astream`.
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from aiya import Aiya, BaseProvider
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class MyProvider(BaseProvider):
|
|
279
|
+
def complete(self, request):
|
|
280
|
+
...
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Then register the instance directly.
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
client = Aiya(providers=[MyProvider(name="my-provider", models=["my-model"])])
|
|
287
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aiyallm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A small, provider-agnostic gateway for OpenAI/Anthropic-style language and vision models."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"openai>=1.66",
|
|
13
|
+
"anthropic>=0.49",
|
|
14
|
+
"httpx>=0.27",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
test = [
|
|
19
|
+
"pytest>=8.3",
|
|
20
|
+
"pytest-asyncio>=0.24",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
pythonpath = ["src"]
|
|
28
|
+
testpaths = ["tests"]
|
aiyallm-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Aiya: provider-agnostic LLM and vision model gateway."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .client import Aiya
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
AiyaError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
ConfigurationError,
|
|
10
|
+
ModelNotSupportedError,
|
|
11
|
+
NoAvailableProviderError,
|
|
12
|
+
ProviderError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
)
|
|
15
|
+
from .models import Capability, ChatChunk, ChatResponse, ModelInfo, ToolCall
|
|
16
|
+
from .providers import (
|
|
17
|
+
AnthropicCompatibleProvider,
|
|
18
|
+
BaseProvider,
|
|
19
|
+
OpenAICompatibleProvider,
|
|
20
|
+
)
|
|
21
|
+
from .registry import RouteTarget
|
|
22
|
+
from .router import RoutePolicy, Router
|
|
23
|
+
from .usage import Price, PriceBook, Usage, UsageLedger
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Aiya",
|
|
27
|
+
"AiyaError",
|
|
28
|
+
"AnthropicCompatibleProvider",
|
|
29
|
+
"AuthenticationError",
|
|
30
|
+
"BaseProvider",
|
|
31
|
+
"Capability",
|
|
32
|
+
"ChatChunk",
|
|
33
|
+
"ChatResponse",
|
|
34
|
+
"ConfigurationError",
|
|
35
|
+
"ModelInfo",
|
|
36
|
+
"ModelNotSupportedError",
|
|
37
|
+
"NoAvailableProviderError",
|
|
38
|
+
"OpenAICompatibleProvider",
|
|
39
|
+
"Price",
|
|
40
|
+
"PriceBook",
|
|
41
|
+
"ProviderError",
|
|
42
|
+
"RateLimitError",
|
|
43
|
+
"RoutePolicy",
|
|
44
|
+
"RouteTarget",
|
|
45
|
+
"Router",
|
|
46
|
+
"ToolCall",
|
|
47
|
+
"Usage",
|
|
48
|
+
"UsageLedger",
|
|
49
|
+
]
|