vikky 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.
- vikky-0.1.0/LICENSE +21 -0
- vikky-0.1.0/PKG-INFO +326 -0
- vikky-0.1.0/README.md +305 -0
- vikky-0.1.0/pyproject.toml +32 -0
- vikky-0.1.0/src/vikky/__init__.py +98 -0
- vikky-0.1.0/src/vikky/py.typed +0 -0
vikky-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VSP AI & 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.
|
vikky-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vikky
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Vikky Platform: chat, vision, speech, embeddings, images and more with one key.
|
|
5
|
+
Keywords: vikky,vikkyverse,ai,llm,sdk,api
|
|
6
|
+
Author: VSP AI & Robotics
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: openai>=1.55.3
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Project-URL: Homepage, https://vikkyverse.com
|
|
18
|
+
Project-URL: Documentation, https://vikkyverse.com/docs
|
|
19
|
+
Project-URL: Get an API key, https://vikkyverse.com/platform
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# vikky
|
|
23
|
+
|
|
24
|
+
Python client for **Vikky**, VSP's AI API gateway. Vikky speaks the OpenAI API,
|
|
25
|
+
so this package is a thin wrapper over the official `openai` package: anything
|
|
26
|
+
in the OpenAI Python docs works here too.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install vikky
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
In Colab or Jupyter, use `%pip install vikky`.
|
|
35
|
+
|
|
36
|
+
## Get a key
|
|
37
|
+
|
|
38
|
+
Sign in to the Vikky console at https://vikkyverse.com/platform and create an API key. Keep it secret: anyone with
|
|
39
|
+
the key spends your quota.
|
|
40
|
+
|
|
41
|
+
## Set VIKKY_API_KEY
|
|
42
|
+
|
|
43
|
+
On your laptop:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export VIKKY_API_KEY="your-key"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
In Google Colab: click the key icon in the left sidebar, add a secret named
|
|
50
|
+
`VIKKY_API_KEY`, turn on "Notebook access", then run:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import os
|
|
54
|
+
from google.colab import userdata
|
|
55
|
+
|
|
56
|
+
os.environ["VIKKY_API_KEY"] = userdata.get("VIKKY_API_KEY")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Never paste the key into a notebook cell you might share.
|
|
60
|
+
|
|
61
|
+
## First call (JSON out)
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import json
|
|
65
|
+
from vikky import Vikky
|
|
66
|
+
|
|
67
|
+
client = Vikky() # reads VIKKY_API_KEY
|
|
68
|
+
|
|
69
|
+
resp = client.chat.completions.create(
|
|
70
|
+
model="vikky-chat",
|
|
71
|
+
messages=[
|
|
72
|
+
{"role": "system", "content": "Reply in JSON."},
|
|
73
|
+
{"role": "user", "content": 'List 3 planets as {"planets": [...]}'},
|
|
74
|
+
],
|
|
75
|
+
response_format={"type": "json_object"},
|
|
76
|
+
)
|
|
77
|
+
data = json.loads(resp.choices[0].message.content)
|
|
78
|
+
print(data["planets"])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
JSON mode works best when your messages say "JSON" and show the shape you want.
|
|
82
|
+
|
|
83
|
+
## Streaming
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
stream = client.chat.completions.create(
|
|
87
|
+
model="vikky-chat",
|
|
88
|
+
messages=[{"role": "user", "content": "Explain PID control in 3 lines."}],
|
|
89
|
+
stream=True,
|
|
90
|
+
)
|
|
91
|
+
for chunk in stream:
|
|
92
|
+
if chunk.choices:
|
|
93
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Models
|
|
97
|
+
|
|
98
|
+
| Model | What it does | Call it with |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `vikky-chat` | chat, JSON output, tool calling | `chat.completions.create`, or `responses.create` |
|
|
101
|
+
| `vikky-vision` | chat that also takes images | `chat.completions.create` with an `image_url` part |
|
|
102
|
+
| `vikky-embed` | embeddings | `embeddings.create` |
|
|
103
|
+
| `vikky-transcribe` | audio to text | `audio.transcriptions.create` |
|
|
104
|
+
| `vikky-speech` | text to audio | `audio.speech.create` |
|
|
105
|
+
| `vikky-image` | image generation and editing | `images.generate`, `images.edit` |
|
|
106
|
+
| `vikky-video` | video generation, async | `videos.create_and_poll` |
|
|
107
|
+
| `vikky-rerank` | rank documents against a query | `rerank` |
|
|
108
|
+
| `vikky-ocr` | text out of a document | `ocr` |
|
|
109
|
+
| `vikky-moderate` | content moderation | `moderations.create` |
|
|
110
|
+
|
|
111
|
+
Every row except the last two is a method the `openai` package already has, so
|
|
112
|
+
the OpenAI Python docs apply unchanged. `rerank` and `ocr` are not OpenAI
|
|
113
|
+
routes: they are the only two methods this package adds, and they return a
|
|
114
|
+
plain dict instead of a typed object.
|
|
115
|
+
|
|
116
|
+
## Tool calling
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
resp = client.chat.completions.create(
|
|
120
|
+
model="vikky-chat",
|
|
121
|
+
messages=[{"role": "user", "content": "Weather in Hyderabad?"}],
|
|
122
|
+
tools=[{"type": "function", "function": {
|
|
123
|
+
"name": "get_weather",
|
|
124
|
+
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
|
|
125
|
+
}}],
|
|
126
|
+
)
|
|
127
|
+
call = resp.choices[0].message.tool_calls[0]
|
|
128
|
+
print(call.function.name, call.function.arguments)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Responses API
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
resp = client.responses.create(model="vikky-chat", input="Say hello.")
|
|
135
|
+
print(resp.output_text)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Vision
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
resp = client.chat.completions.create(
|
|
142
|
+
model="vikky-vision",
|
|
143
|
+
messages=[{"role": "user", "content": [
|
|
144
|
+
{"type": "text", "text": "What is in this picture?"},
|
|
145
|
+
{"type": "image_url", "image_url": {"url": "https://example.com/arm.jpg"}},
|
|
146
|
+
]}],
|
|
147
|
+
)
|
|
148
|
+
print(resp.choices[0].message.content)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For a local image, pass a data URL as the `url`:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
import base64, pathlib
|
|
155
|
+
|
|
156
|
+
raw = base64.b64encode(pathlib.Path("arm.png").read_bytes()).decode()
|
|
157
|
+
url = f"data:image/png;base64,{raw}"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Embeddings
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
resp = client.embeddings.create(model="vikky-embed", input=["robot arm", "pizza"])
|
|
164
|
+
arm, pizza = (d.embedding for d in resp.data)
|
|
165
|
+
print(len(arm), len(pizza))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Pass a list to embed a batch in one call. Results come back in the order you
|
|
169
|
+
sent them, and `d.index` tells you which input each vector belongs to.
|
|
170
|
+
|
|
171
|
+
## Audio to text
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
with open("meeting.m4a", "rb") as f:
|
|
175
|
+
resp = client.audio.transcriptions.create(model="vikky-transcribe", file=f)
|
|
176
|
+
print(resp.text)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Text to audio
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
resp = client.audio.speech.create(
|
|
183
|
+
model="vikky-speech",
|
|
184
|
+
voice="alloy",
|
|
185
|
+
input="The arm is homed and ready.",
|
|
186
|
+
)
|
|
187
|
+
resp.write_to_file("ready.mp3")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Images
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
resp = client.images.generate(model="vikky-image", prompt="a blue robot arm on a bench", n=1)
|
|
194
|
+
item = resp.data[0]
|
|
195
|
+
print(item.url or "returned as base64") # either one, see "Saving generated media"
|
|
196
|
+
|
|
197
|
+
with open("arm.png", "rb") as f:
|
|
198
|
+
edited = client.images.edit(model="vikky-image", image=f, prompt="make the bench wooden")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Video
|
|
202
|
+
|
|
203
|
+
Video generation takes minutes, so it is a job, not a call. Submit it, wait for
|
|
204
|
+
it, then download it. Keep the id `create` gave you and download with that one.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
job = client.videos.create(model="vikky-video", prompt="a robot arm picking up a cube")
|
|
208
|
+
done = client.videos.poll(job.id, poll_interval_ms=5000)
|
|
209
|
+
assert done.status == "completed", done.error
|
|
210
|
+
client.videos.download_content(job.id).write_to_file("clip.mp4")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`client.videos.retrieve(job.id)` is the single-shot version of `poll`, if you
|
|
214
|
+
want to show `video.progress` in your own loop. Do not use
|
|
215
|
+
`videos.create_and_poll`: it only returns the polled job, and Vikky's polled id
|
|
216
|
+
cannot be downloaded from.
|
|
217
|
+
|
|
218
|
+
## Saving generated media
|
|
219
|
+
|
|
220
|
+
An image comes back as either a URL or base64, depending on what you asked
|
|
221
|
+
for. Audio and video come back as a binary response with `write_to_file`.
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
import base64, pathlib, urllib.request
|
|
225
|
+
|
|
226
|
+
item = client.images.generate(model="vikky-image", prompt="a blue cube").data[0]
|
|
227
|
+
if item.b64_json:
|
|
228
|
+
pathlib.Path("cube.png").write_bytes(base64.b64decode(item.b64_json))
|
|
229
|
+
else:
|
|
230
|
+
with urllib.request.urlopen(item.url) as r:
|
|
231
|
+
pathlib.Path("cube.png").write_bytes(r.read())
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Ask for `response_format="b64_json"` and you never have to fetch a URL at all.
|
|
235
|
+
|
|
236
|
+
**A generated file's URL is temporary.** Download it in the same run that
|
|
237
|
+
created it. Do not store the URL in a database or a notebook output and expect
|
|
238
|
+
it to still work tomorrow.
|
|
239
|
+
|
|
240
|
+
## Rerank
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
resp = client.rerank(
|
|
244
|
+
query="how do I reset the arm?",
|
|
245
|
+
documents=[
|
|
246
|
+
"Press the red button to reset the arm.",
|
|
247
|
+
"Our office is in Hyderabad.",
|
|
248
|
+
],
|
|
249
|
+
top_n=1,
|
|
250
|
+
)
|
|
251
|
+
for r in resp["results"]:
|
|
252
|
+
print(r["index"], r["relevance_score"])
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Results come back best first. `r["index"]` points back into the `documents`
|
|
256
|
+
list you sent.
|
|
257
|
+
|
|
258
|
+
## OCR
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
resp = client.ocr(document={"type": "document_url", "document_url": "https://example.com/invoice.pdf"})
|
|
262
|
+
for page in resp["pages"]:
|
|
263
|
+
print(page["markdown"])
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
For an image instead of a PDF, send
|
|
267
|
+
`{"type": "image_url", "image_url": "https://..."}`. A data URL works too.
|
|
268
|
+
|
|
269
|
+
## Moderation
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
resp = client.moderations.create(model="vikky-moderate", input="Some user text.")
|
|
273
|
+
result = resp.results[0]
|
|
274
|
+
print(result.flagged, [name for name, hit in result.categories if hit])
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Async
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
import asyncio
|
|
281
|
+
from vikky import AsyncVikky
|
|
282
|
+
|
|
283
|
+
async def main():
|
|
284
|
+
client = AsyncVikky()
|
|
285
|
+
resp = await client.chat.completions.create(
|
|
286
|
+
model="vikky-chat",
|
|
287
|
+
messages=[{"role": "user", "content": "Say hello."}],
|
|
288
|
+
)
|
|
289
|
+
print(resp.choices[0].message.content)
|
|
290
|
+
|
|
291
|
+
asyncio.run(main()) # in Colab or Jupyter, use: await main()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Every model above works on `AsyncVikky`, `rerank` and `ocr` included: same
|
|
295
|
+
arguments, awaited.
|
|
296
|
+
|
|
297
|
+
## Environment variables
|
|
298
|
+
|
|
299
|
+
| Variable | Required | Default |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| `VIKKY_API_KEY` | yes | none. `OPENAI_API_KEY` is never used. |
|
|
302
|
+
| `VIKKY_BASE_URL` | no | `https://api.vikkyverse.com/v1` |
|
|
303
|
+
|
|
304
|
+
Arguments win over environment: `Vikky(api_key=..., base_url=...)`. Every other
|
|
305
|
+
argument (`timeout`, `max_retries`, ...) goes straight to `openai.OpenAI`.
|
|
306
|
+
A missing key raises `vikky.VikkyError`.
|
|
307
|
+
|
|
308
|
+
## If Vikky is down
|
|
309
|
+
|
|
310
|
+
Write lab code so the model name comes from the environment:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
import os
|
|
314
|
+
from vikky import Vikky
|
|
315
|
+
|
|
316
|
+
MODEL = os.environ.get("VIKKY_MODEL", "vikky-chat")
|
|
317
|
+
client = Vikky()
|
|
318
|
+
resp = client.chat.completions.create(model=MODEL, messages=[...])
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Then a trainer can set `VIKKY_BASE_URL`, `VIKKY_API_KEY` and `VIKKY_MODEL` to
|
|
322
|
+
any other OpenAI-compatible endpoint, and the notebook runs unchanged.
|
|
323
|
+
|
|
324
|
+
## License
|
|
325
|
+
|
|
326
|
+
MIT
|
vikky-0.1.0/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# vikky
|
|
2
|
+
|
|
3
|
+
Python client for **Vikky**, VSP's AI API gateway. Vikky speaks the OpenAI API,
|
|
4
|
+
so this package is a thin wrapper over the official `openai` package: anything
|
|
5
|
+
in the OpenAI Python docs works here too.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install vikky
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
In Colab or Jupyter, use `%pip install vikky`.
|
|
14
|
+
|
|
15
|
+
## Get a key
|
|
16
|
+
|
|
17
|
+
Sign in to the Vikky console at https://vikkyverse.com/platform and create an API key. Keep it secret: anyone with
|
|
18
|
+
the key spends your quota.
|
|
19
|
+
|
|
20
|
+
## Set VIKKY_API_KEY
|
|
21
|
+
|
|
22
|
+
On your laptop:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
export VIKKY_API_KEY="your-key"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In Google Colab: click the key icon in the left sidebar, add a secret named
|
|
29
|
+
`VIKKY_API_KEY`, turn on "Notebook access", then run:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import os
|
|
33
|
+
from google.colab import userdata
|
|
34
|
+
|
|
35
|
+
os.environ["VIKKY_API_KEY"] = userdata.get("VIKKY_API_KEY")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Never paste the key into a notebook cell you might share.
|
|
39
|
+
|
|
40
|
+
## First call (JSON out)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import json
|
|
44
|
+
from vikky import Vikky
|
|
45
|
+
|
|
46
|
+
client = Vikky() # reads VIKKY_API_KEY
|
|
47
|
+
|
|
48
|
+
resp = client.chat.completions.create(
|
|
49
|
+
model="vikky-chat",
|
|
50
|
+
messages=[
|
|
51
|
+
{"role": "system", "content": "Reply in JSON."},
|
|
52
|
+
{"role": "user", "content": 'List 3 planets as {"planets": [...]}'},
|
|
53
|
+
],
|
|
54
|
+
response_format={"type": "json_object"},
|
|
55
|
+
)
|
|
56
|
+
data = json.loads(resp.choices[0].message.content)
|
|
57
|
+
print(data["planets"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
JSON mode works best when your messages say "JSON" and show the shape you want.
|
|
61
|
+
|
|
62
|
+
## Streaming
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
stream = client.chat.completions.create(
|
|
66
|
+
model="vikky-chat",
|
|
67
|
+
messages=[{"role": "user", "content": "Explain PID control in 3 lines."}],
|
|
68
|
+
stream=True,
|
|
69
|
+
)
|
|
70
|
+
for chunk in stream:
|
|
71
|
+
if chunk.choices:
|
|
72
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Models
|
|
76
|
+
|
|
77
|
+
| Model | What it does | Call it with |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `vikky-chat` | chat, JSON output, tool calling | `chat.completions.create`, or `responses.create` |
|
|
80
|
+
| `vikky-vision` | chat that also takes images | `chat.completions.create` with an `image_url` part |
|
|
81
|
+
| `vikky-embed` | embeddings | `embeddings.create` |
|
|
82
|
+
| `vikky-transcribe` | audio to text | `audio.transcriptions.create` |
|
|
83
|
+
| `vikky-speech` | text to audio | `audio.speech.create` |
|
|
84
|
+
| `vikky-image` | image generation and editing | `images.generate`, `images.edit` |
|
|
85
|
+
| `vikky-video` | video generation, async | `videos.create_and_poll` |
|
|
86
|
+
| `vikky-rerank` | rank documents against a query | `rerank` |
|
|
87
|
+
| `vikky-ocr` | text out of a document | `ocr` |
|
|
88
|
+
| `vikky-moderate` | content moderation | `moderations.create` |
|
|
89
|
+
|
|
90
|
+
Every row except the last two is a method the `openai` package already has, so
|
|
91
|
+
the OpenAI Python docs apply unchanged. `rerank` and `ocr` are not OpenAI
|
|
92
|
+
routes: they are the only two methods this package adds, and they return a
|
|
93
|
+
plain dict instead of a typed object.
|
|
94
|
+
|
|
95
|
+
## Tool calling
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
resp = client.chat.completions.create(
|
|
99
|
+
model="vikky-chat",
|
|
100
|
+
messages=[{"role": "user", "content": "Weather in Hyderabad?"}],
|
|
101
|
+
tools=[{"type": "function", "function": {
|
|
102
|
+
"name": "get_weather",
|
|
103
|
+
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
|
|
104
|
+
}}],
|
|
105
|
+
)
|
|
106
|
+
call = resp.choices[0].message.tool_calls[0]
|
|
107
|
+
print(call.function.name, call.function.arguments)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Responses API
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
resp = client.responses.create(model="vikky-chat", input="Say hello.")
|
|
114
|
+
print(resp.output_text)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Vision
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
resp = client.chat.completions.create(
|
|
121
|
+
model="vikky-vision",
|
|
122
|
+
messages=[{"role": "user", "content": [
|
|
123
|
+
{"type": "text", "text": "What is in this picture?"},
|
|
124
|
+
{"type": "image_url", "image_url": {"url": "https://example.com/arm.jpg"}},
|
|
125
|
+
]}],
|
|
126
|
+
)
|
|
127
|
+
print(resp.choices[0].message.content)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For a local image, pass a data URL as the `url`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import base64, pathlib
|
|
134
|
+
|
|
135
|
+
raw = base64.b64encode(pathlib.Path("arm.png").read_bytes()).decode()
|
|
136
|
+
url = f"data:image/png;base64,{raw}"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Embeddings
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
resp = client.embeddings.create(model="vikky-embed", input=["robot arm", "pizza"])
|
|
143
|
+
arm, pizza = (d.embedding for d in resp.data)
|
|
144
|
+
print(len(arm), len(pizza))
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Pass a list to embed a batch in one call. Results come back in the order you
|
|
148
|
+
sent them, and `d.index` tells you which input each vector belongs to.
|
|
149
|
+
|
|
150
|
+
## Audio to text
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
with open("meeting.m4a", "rb") as f:
|
|
154
|
+
resp = client.audio.transcriptions.create(model="vikky-transcribe", file=f)
|
|
155
|
+
print(resp.text)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Text to audio
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
resp = client.audio.speech.create(
|
|
162
|
+
model="vikky-speech",
|
|
163
|
+
voice="alloy",
|
|
164
|
+
input="The arm is homed and ready.",
|
|
165
|
+
)
|
|
166
|
+
resp.write_to_file("ready.mp3")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Images
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
resp = client.images.generate(model="vikky-image", prompt="a blue robot arm on a bench", n=1)
|
|
173
|
+
item = resp.data[0]
|
|
174
|
+
print(item.url or "returned as base64") # either one, see "Saving generated media"
|
|
175
|
+
|
|
176
|
+
with open("arm.png", "rb") as f:
|
|
177
|
+
edited = client.images.edit(model="vikky-image", image=f, prompt="make the bench wooden")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Video
|
|
181
|
+
|
|
182
|
+
Video generation takes minutes, so it is a job, not a call. Submit it, wait for
|
|
183
|
+
it, then download it. Keep the id `create` gave you and download with that one.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
job = client.videos.create(model="vikky-video", prompt="a robot arm picking up a cube")
|
|
187
|
+
done = client.videos.poll(job.id, poll_interval_ms=5000)
|
|
188
|
+
assert done.status == "completed", done.error
|
|
189
|
+
client.videos.download_content(job.id).write_to_file("clip.mp4")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`client.videos.retrieve(job.id)` is the single-shot version of `poll`, if you
|
|
193
|
+
want to show `video.progress` in your own loop. Do not use
|
|
194
|
+
`videos.create_and_poll`: it only returns the polled job, and Vikky's polled id
|
|
195
|
+
cannot be downloaded from.
|
|
196
|
+
|
|
197
|
+
## Saving generated media
|
|
198
|
+
|
|
199
|
+
An image comes back as either a URL or base64, depending on what you asked
|
|
200
|
+
for. Audio and video come back as a binary response with `write_to_file`.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import base64, pathlib, urllib.request
|
|
204
|
+
|
|
205
|
+
item = client.images.generate(model="vikky-image", prompt="a blue cube").data[0]
|
|
206
|
+
if item.b64_json:
|
|
207
|
+
pathlib.Path("cube.png").write_bytes(base64.b64decode(item.b64_json))
|
|
208
|
+
else:
|
|
209
|
+
with urllib.request.urlopen(item.url) as r:
|
|
210
|
+
pathlib.Path("cube.png").write_bytes(r.read())
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Ask for `response_format="b64_json"` and you never have to fetch a URL at all.
|
|
214
|
+
|
|
215
|
+
**A generated file's URL is temporary.** Download it in the same run that
|
|
216
|
+
created it. Do not store the URL in a database or a notebook output and expect
|
|
217
|
+
it to still work tomorrow.
|
|
218
|
+
|
|
219
|
+
## Rerank
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
resp = client.rerank(
|
|
223
|
+
query="how do I reset the arm?",
|
|
224
|
+
documents=[
|
|
225
|
+
"Press the red button to reset the arm.",
|
|
226
|
+
"Our office is in Hyderabad.",
|
|
227
|
+
],
|
|
228
|
+
top_n=1,
|
|
229
|
+
)
|
|
230
|
+
for r in resp["results"]:
|
|
231
|
+
print(r["index"], r["relevance_score"])
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Results come back best first. `r["index"]` points back into the `documents`
|
|
235
|
+
list you sent.
|
|
236
|
+
|
|
237
|
+
## OCR
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
resp = client.ocr(document={"type": "document_url", "document_url": "https://example.com/invoice.pdf"})
|
|
241
|
+
for page in resp["pages"]:
|
|
242
|
+
print(page["markdown"])
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
For an image instead of a PDF, send
|
|
246
|
+
`{"type": "image_url", "image_url": "https://..."}`. A data URL works too.
|
|
247
|
+
|
|
248
|
+
## Moderation
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
resp = client.moderations.create(model="vikky-moderate", input="Some user text.")
|
|
252
|
+
result = resp.results[0]
|
|
253
|
+
print(result.flagged, [name for name, hit in result.categories if hit])
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Async
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import asyncio
|
|
260
|
+
from vikky import AsyncVikky
|
|
261
|
+
|
|
262
|
+
async def main():
|
|
263
|
+
client = AsyncVikky()
|
|
264
|
+
resp = await client.chat.completions.create(
|
|
265
|
+
model="vikky-chat",
|
|
266
|
+
messages=[{"role": "user", "content": "Say hello."}],
|
|
267
|
+
)
|
|
268
|
+
print(resp.choices[0].message.content)
|
|
269
|
+
|
|
270
|
+
asyncio.run(main()) # in Colab or Jupyter, use: await main()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Every model above works on `AsyncVikky`, `rerank` and `ocr` included: same
|
|
274
|
+
arguments, awaited.
|
|
275
|
+
|
|
276
|
+
## Environment variables
|
|
277
|
+
|
|
278
|
+
| Variable | Required | Default |
|
|
279
|
+
|---|---|---|
|
|
280
|
+
| `VIKKY_API_KEY` | yes | none. `OPENAI_API_KEY` is never used. |
|
|
281
|
+
| `VIKKY_BASE_URL` | no | `https://api.vikkyverse.com/v1` |
|
|
282
|
+
|
|
283
|
+
Arguments win over environment: `Vikky(api_key=..., base_url=...)`. Every other
|
|
284
|
+
argument (`timeout`, `max_retries`, ...) goes straight to `openai.OpenAI`.
|
|
285
|
+
A missing key raises `vikky.VikkyError`.
|
|
286
|
+
|
|
287
|
+
## If Vikky is down
|
|
288
|
+
|
|
289
|
+
Write lab code so the model name comes from the environment:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
import os
|
|
293
|
+
from vikky import Vikky
|
|
294
|
+
|
|
295
|
+
MODEL = os.environ.get("VIKKY_MODEL", "vikky-chat")
|
|
296
|
+
client = Vikky()
|
|
297
|
+
resp = client.chat.completions.create(model=MODEL, messages=[...])
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Then a trainer can set `VIKKY_BASE_URL`, `VIKKY_API_KEY` and `VIKKY_MODEL` to
|
|
301
|
+
any other OpenAI-compatible endpoint, and the notebook runs unchanged.
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vikky"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for Vikky Platform: chat, vision, speech, embeddings, images and more with one key."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "VSP AI & Robotics" }]
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
# 1.55.3: first openai release that works with httpx 0.28 (older ones crash on `proxies`).
|
|
11
|
+
dependencies = ["openai>=1.55.3"]
|
|
12
|
+
keywords = ["vikky", "vikkyverse", "ai", "llm", "sdk", "api"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://vikkyverse.com"
|
|
24
|
+
Documentation = "https://vikkyverse.com/docs"
|
|
25
|
+
"Get an API key" = "https://vikkyverse.com/platform"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = ["pytest>=8"]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.11.14,<0.12.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Python client for Vikky, VSP's OpenAI-compatible AI API gateway.
|
|
2
|
+
|
|
3
|
+
A thin wrapper over the official ``openai`` package: only the API key and
|
|
4
|
+
base URL defaults differ. Everything else is plain ``openai``.
|
|
5
|
+
|
|
6
|
+
Chat, vision, embeddings, audio, images, video and moderation all go through
|
|
7
|
+
the methods ``openai`` already has. The two exceptions are ``rerank`` and
|
|
8
|
+
``ocr``: Vikky serves those, the OpenAI API does not, so they are the only
|
|
9
|
+
methods this package adds.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from importlib.metadata import version
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Dict, cast
|
|
17
|
+
|
|
18
|
+
import openai
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
__version__ = version("vikky")
|
|
24
|
+
__all__ = ["DEFAULT_BASE_URL", "AsyncVikky", "Vikky", "VikkyError", "__version__"]
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = "https://api.vikkyverse.com/v1"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VikkyError(openai.OpenAIError):
|
|
30
|
+
"""Raised when a Vikky client cannot be configured."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve(api_key, base_url):
|
|
34
|
+
# Never fall back to OPENAI_API_KEY: a student's OpenAI key must not go to Vikky.
|
|
35
|
+
api_key = api_key or os.environ.get("VIKKY_API_KEY")
|
|
36
|
+
if not api_key:
|
|
37
|
+
raise VikkyError("No Vikky API key. Set VIKKY_API_KEY or pass api_key=...")
|
|
38
|
+
return api_key, base_url or os.environ.get("VIKKY_BASE_URL") or DEFAULT_BASE_URL
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# /rerank and /ocr are not OpenAI routes, so openai has no method for them and no
|
|
42
|
+
# response model to parse them into. cast_to=object is openai's way of saying "give
|
|
43
|
+
# me the parsed JSON": these two return a plain dict, not a pydantic object.
|
|
44
|
+
# Everything else Vikky serves is a real OpenAI route and is not wrapped here.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Vikky(openai.OpenAI):
|
|
48
|
+
"""``openai.OpenAI`` pointed at Vikky."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, *, api_key: str | None = None, base_url: str | None = None, **kwargs):
|
|
51
|
+
api_key, base_url = _resolve(api_key, base_url)
|
|
52
|
+
super().__init__(api_key=api_key, base_url=base_url, **kwargs)
|
|
53
|
+
|
|
54
|
+
def rerank(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
query: str,
|
|
58
|
+
documents: Sequence[str],
|
|
59
|
+
model: str = "vikky-rerank",
|
|
60
|
+
**kwargs: Any,
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
"""Score documents against a query. ``POST /v1/rerank``, Cohere-shaped."""
|
|
63
|
+
body = {"model": model, "query": query, "documents": list(documents), **kwargs}
|
|
64
|
+
return cast("Dict[str, Any]", self.post("/rerank", body=body, cast_to=object))
|
|
65
|
+
|
|
66
|
+
def ocr(
|
|
67
|
+
self, *, document: Dict[str, Any], model: str = "vikky-ocr", **kwargs: Any
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""Read text out of a document or image. ``POST /v1/ocr``."""
|
|
70
|
+
body = {"model": model, "document": document, **kwargs}
|
|
71
|
+
return cast("Dict[str, Any]", self.post("/ocr", body=body, cast_to=object))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AsyncVikky(openai.AsyncOpenAI):
|
|
75
|
+
"""``openai.AsyncOpenAI`` pointed at Vikky."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, *, api_key: str | None = None, base_url: str | None = None, **kwargs):
|
|
78
|
+
api_key, base_url = _resolve(api_key, base_url)
|
|
79
|
+
super().__init__(api_key=api_key, base_url=base_url, **kwargs)
|
|
80
|
+
|
|
81
|
+
async def rerank(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
query: str,
|
|
85
|
+
documents: Sequence[str],
|
|
86
|
+
model: str = "vikky-rerank",
|
|
87
|
+
**kwargs: Any,
|
|
88
|
+
) -> Dict[str, Any]:
|
|
89
|
+
"""Score documents against a query. ``POST /v1/rerank``, Cohere-shaped."""
|
|
90
|
+
body = {"model": model, "query": query, "documents": list(documents), **kwargs}
|
|
91
|
+
return cast("Dict[str, Any]", await self.post("/rerank", body=body, cast_to=object))
|
|
92
|
+
|
|
93
|
+
async def ocr(
|
|
94
|
+
self, *, document: Dict[str, Any], model: str = "vikky-ocr", **kwargs: Any
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
"""Read text out of a document or image. ``POST /v1/ocr``."""
|
|
97
|
+
body = {"model": model, "document": document, **kwargs}
|
|
98
|
+
return cast("Dict[str, Any]", await self.post("/ocr", body=body, cast_to=object))
|
|
File without changes
|