everypixel-cli 0.2.2__tar.gz → 0.4.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.
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/CHANGELOG.md +10 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/PKG-INFO +68 -6
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/README.md +67 -5
- everypixel_cli-0.4.0/src/everypixel_cli/__init__.py +1 -0
- everypixel_cli-0.4.0/src/everypixel_cli/application/chat.py +192 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/application/services.py +245 -73
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/cli.py +191 -21
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/client.py +89 -17
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/mcp_server.py +98 -19
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/output.py +55 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/resources/openapi.json +942 -54
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/schemas.py +432 -19
- everypixel_cli-0.2.2/src/everypixel_cli/__init__.py +0 -1
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/.gitignore +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/LICENSE +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/pyproject.toml +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/__main__.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/application/__init__.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/application/models.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/application/serialization.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/config.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/errors.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/files.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/openapi.py +0 -0
- {everypixel_cli-0.2.2 → everypixel_cli-0.4.0}/src/everypixel_cli/resources/__init__.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-10-07
|
|
4
|
+
|
|
5
|
+
- Add ElevenLabs speech synthesis and voice design.
|
|
6
|
+
- Add saved voice creation, listing, deletion, and Qwen cloning by voice ID.
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-09-18
|
|
9
|
+
|
|
10
|
+
- Add GPT Image 2.5 Sunburst and quality selection for GPT Image models.
|
|
11
|
+
- Add GLM-5.3 chat with streaming and tool-call support.
|
|
12
|
+
|
|
3
13
|
## [0.2.2] - 2026-09-11
|
|
4
14
|
|
|
5
15
|
- Increase the default task wait timeout to 30 minutes for long video generation.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: everypixel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Official command line interface for Everypixel API
|
|
5
5
|
Project-URL: Homepage, https://labs.everypixel.com
|
|
6
6
|
Project-URL: Repository, https://github.com/everypixel/everypixel-cli
|
|
@@ -35,8 +35,8 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
# Everypixel CLI
|
|
36
36
|
|
|
37
37
|
Official command-line interface for the [Everypixel API](https://labs.everypixel.com/).
|
|
38
|
-
Generate and edit images and videos,
|
|
39
|
-
automate Everypixel workflows from a terminal or an MCP client.
|
|
38
|
+
Generate and edit images and videos, chat with GLM-5.3, work with audio, run
|
|
39
|
+
visual analysis, and automate Everypixel workflows from a terminal or an MCP client.
|
|
40
40
|
|
|
41
41
|
The CLI is designed for both people and agents: commands use readable terminal
|
|
42
42
|
output by default and can return stable JSON for scripts and tool integrations.
|
|
@@ -181,6 +181,15 @@ to select their aspect ratio. Raster models also accept `--resolution`.
|
|
|
181
181
|
|
|
182
182
|
`--lora-url` is supported only by `zimage`.
|
|
183
183
|
|
|
184
|
+
GPT Image models are `gpt-image-2` and `gpt-image-2.5-sunburst`. Both generation
|
|
185
|
+
and editing accept `--quality low|medium|high` (default: `medium`); Sunburst also
|
|
186
|
+
supports `xhigh` and `max`. Use `--size square` with `--resolution 3k`.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
everypixel image generate "a detailed botanical illustration" \
|
|
190
|
+
--model gpt-image-2.5-sunburst --quality high --download ./outputs
|
|
191
|
+
```
|
|
192
|
+
|
|
184
193
|
Grok image models are `grok-imagine`, `grok-imagine-2`, and
|
|
185
194
|
`grok-imagine-2-low`. The `grok` alias remains supported. `grok_quality` and
|
|
186
195
|
`wan22` have been removed from the API.
|
|
@@ -254,9 +263,12 @@ They support 4K output. The default `seedvr2` supports up to 1440p, with a
|
|
|
254
263
|
| `everypixel lipsync video` | Synchronize a video with an audio track |
|
|
255
264
|
| `everypixel lipsync image` | Create a lipsync video from a still image and audio |
|
|
256
265
|
| `everypixel audio transcribe` | Transcribe speech from an audio file |
|
|
257
|
-
| `everypixel audio tts-create` | Create speech with a
|
|
258
|
-
| `everypixel audio tts-clone` | Create speech from
|
|
259
|
-
| `everypixel audio tts-
|
|
266
|
+
| `everypixel audio tts-create` | Create speech with a Qwen preset or saved ElevenLabs voice |
|
|
267
|
+
| `everypixel audio tts-clone` | Create Qwen speech from an audio sample or saved voice |
|
|
268
|
+
| `everypixel audio tts-design` | Design voice previews from a description |
|
|
269
|
+
| `everypixel audio tts-voice` | Save audio samples or a design preview as a voice |
|
|
270
|
+
| `everypixel audio tts-voices` | List presets and saved voices |
|
|
271
|
+
| `everypixel audio tts-delete` | Delete an owned saved voice |
|
|
260
272
|
|
|
261
273
|
Lipsync supports 720p; the API checks source media dimensions and duration.
|
|
262
274
|
|
|
@@ -281,6 +293,56 @@ everypixel audio tts-create \
|
|
|
281
293
|
--download ./outputs
|
|
282
294
|
```
|
|
283
295
|
|
|
296
|
+
`tts-create` lets the API choose the model when `--model` is omitted.
|
|
297
|
+
`tts-clone` and `tts-design` default to `--model qwen3`.
|
|
298
|
+
`--speaker`, `--style`, `--language`, and `--seed` are Qwen controls; `--character` applies only to Qwen voice design.
|
|
299
|
+
Qwen text is limited to 200 characters. ElevenLabs synthesis accepts up to
|
|
300
|
+
2048 characters and requires `--voice-id`. Supported synthesis models are
|
|
301
|
+
`eleven_v4`, `eleven_v4_turbo`, `eleven_v3`, `eleven_v3_conversational`,
|
|
302
|
+
`eleven_multilingual_v2`, and `eleven_flash_v2_5`. ElevenLabs design uses
|
|
303
|
+
`eleven_multilingual_ttv_v2` or `eleven_ttv_v3`, with 100–1000 characters of
|
|
304
|
+
text and a 20–1000 character `--prompt`.
|
|
305
|
+
|
|
306
|
+
Save a returned preview with `tts-voice --name "Narrator" --preview-id UUID`, or save local
|
|
307
|
+
samples with `tts-voice --name "My voice" --provider elevenlabs --audio ./sample.wav`.
|
|
308
|
+
`--audio` is repeatable for ElevenLabs; Qwen (`--provider labs`, the default)
|
|
309
|
+
requires one sample. Saving returns a voice card immediately. Use its ID with
|
|
310
|
+
`tts-create --model eleven_v3 --voice-id UUID --text "Hello"` or
|
|
311
|
+
`tts-clone --voice-id UUID --text "Hello"`. List voices with
|
|
312
|
+
`tts-voices --provider elevenlabs --offset 0 --limit 50`, and delete an owned
|
|
313
|
+
voice with `tts-delete --voice-id UUID`.
|
|
314
|
+
|
|
315
|
+
### Chat
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
everypixel chat "Explain how a rainbow forms."
|
|
319
|
+
everypixel chat "Review this idea." --system "Be concise." --stream
|
|
320
|
+
everypixel chat "Solve this problem." --reasoning-effort high --show-reasoning
|
|
321
|
+
everypixel chat --input-file ./conversation.json --output-json
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Chat uses the same credentials as other commands and requires the client's
|
|
325
|
+
`chat` scope. The default model is `glm-5.3`. Each invocation sends one request;
|
|
326
|
+
use `--input-file` for conversation history, function tools, or other API options:
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"messages": [
|
|
331
|
+
{"role": "user", "content": "What causes rainbows?"},
|
|
332
|
+
{"role": "assistant", "content": "Sunlight refracts and reflects in water droplets."},
|
|
333
|
+
{"role": "user", "content": "Why do the colors separate?"}
|
|
334
|
+
],
|
|
335
|
+
"max_completion_tokens": 4096
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
A positional prompt appends a user message to the supplied history. CLI options
|
|
340
|
+
override matching fields in the file. `--stream` displays text as it arrives;
|
|
341
|
+
JSON output returns one assembled response, including reasoning, tool calls,
|
|
342
|
+
and usage. Function calls are returned for the caller to handle. The MCP `chat`
|
|
343
|
+
tool accepts the same structured request and returns a complete response.
|
|
344
|
+
Use `--request-timeout` to change the chat HTTP timeout (default: 240 seconds).
|
|
345
|
+
|
|
284
346
|
### Visual Analysis
|
|
285
347
|
|
|
286
348
|
Analysis commands accept HTTP(S) URLs and local files. Local files are uploaded
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Everypixel CLI
|
|
2
2
|
|
|
3
3
|
Official command-line interface for the [Everypixel API](https://labs.everypixel.com/).
|
|
4
|
-
Generate and edit images and videos,
|
|
5
|
-
automate Everypixel workflows from a terminal or an MCP client.
|
|
4
|
+
Generate and edit images and videos, chat with GLM-5.3, work with audio, run
|
|
5
|
+
visual analysis, and automate Everypixel workflows from a terminal or an MCP client.
|
|
6
6
|
|
|
7
7
|
The CLI is designed for both people and agents: commands use readable terminal
|
|
8
8
|
output by default and can return stable JSON for scripts and tool integrations.
|
|
@@ -147,6 +147,15 @@ to select their aspect ratio. Raster models also accept `--resolution`.
|
|
|
147
147
|
|
|
148
148
|
`--lora-url` is supported only by `zimage`.
|
|
149
149
|
|
|
150
|
+
GPT Image models are `gpt-image-2` and `gpt-image-2.5-sunburst`. Both generation
|
|
151
|
+
and editing accept `--quality low|medium|high` (default: `medium`); Sunburst also
|
|
152
|
+
supports `xhigh` and `max`. Use `--size square` with `--resolution 3k`.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
everypixel image generate "a detailed botanical illustration" \
|
|
156
|
+
--model gpt-image-2.5-sunburst --quality high --download ./outputs
|
|
157
|
+
```
|
|
158
|
+
|
|
150
159
|
Grok image models are `grok-imagine`, `grok-imagine-2`, and
|
|
151
160
|
`grok-imagine-2-low`. The `grok` alias remains supported. `grok_quality` and
|
|
152
161
|
`wan22` have been removed from the API.
|
|
@@ -220,9 +229,12 @@ They support 4K output. The default `seedvr2` supports up to 1440p, with a
|
|
|
220
229
|
| `everypixel lipsync video` | Synchronize a video with an audio track |
|
|
221
230
|
| `everypixel lipsync image` | Create a lipsync video from a still image and audio |
|
|
222
231
|
| `everypixel audio transcribe` | Transcribe speech from an audio file |
|
|
223
|
-
| `everypixel audio tts-create` | Create speech with a
|
|
224
|
-
| `everypixel audio tts-clone` | Create speech from
|
|
225
|
-
| `everypixel audio tts-
|
|
232
|
+
| `everypixel audio tts-create` | Create speech with a Qwen preset or saved ElevenLabs voice |
|
|
233
|
+
| `everypixel audio tts-clone` | Create Qwen speech from an audio sample or saved voice |
|
|
234
|
+
| `everypixel audio tts-design` | Design voice previews from a description |
|
|
235
|
+
| `everypixel audio tts-voice` | Save audio samples or a design preview as a voice |
|
|
236
|
+
| `everypixel audio tts-voices` | List presets and saved voices |
|
|
237
|
+
| `everypixel audio tts-delete` | Delete an owned saved voice |
|
|
226
238
|
|
|
227
239
|
Lipsync supports 720p; the API checks source media dimensions and duration.
|
|
228
240
|
|
|
@@ -247,6 +259,56 @@ everypixel audio tts-create \
|
|
|
247
259
|
--download ./outputs
|
|
248
260
|
```
|
|
249
261
|
|
|
262
|
+
`tts-create` lets the API choose the model when `--model` is omitted.
|
|
263
|
+
`tts-clone` and `tts-design` default to `--model qwen3`.
|
|
264
|
+
`--speaker`, `--style`, `--language`, and `--seed` are Qwen controls; `--character` applies only to Qwen voice design.
|
|
265
|
+
Qwen text is limited to 200 characters. ElevenLabs synthesis accepts up to
|
|
266
|
+
2048 characters and requires `--voice-id`. Supported synthesis models are
|
|
267
|
+
`eleven_v4`, `eleven_v4_turbo`, `eleven_v3`, `eleven_v3_conversational`,
|
|
268
|
+
`eleven_multilingual_v2`, and `eleven_flash_v2_5`. ElevenLabs design uses
|
|
269
|
+
`eleven_multilingual_ttv_v2` or `eleven_ttv_v3`, with 100–1000 characters of
|
|
270
|
+
text and a 20–1000 character `--prompt`.
|
|
271
|
+
|
|
272
|
+
Save a returned preview with `tts-voice --name "Narrator" --preview-id UUID`, or save local
|
|
273
|
+
samples with `tts-voice --name "My voice" --provider elevenlabs --audio ./sample.wav`.
|
|
274
|
+
`--audio` is repeatable for ElevenLabs; Qwen (`--provider labs`, the default)
|
|
275
|
+
requires one sample. Saving returns a voice card immediately. Use its ID with
|
|
276
|
+
`tts-create --model eleven_v3 --voice-id UUID --text "Hello"` or
|
|
277
|
+
`tts-clone --voice-id UUID --text "Hello"`. List voices with
|
|
278
|
+
`tts-voices --provider elevenlabs --offset 0 --limit 50`, and delete an owned
|
|
279
|
+
voice with `tts-delete --voice-id UUID`.
|
|
280
|
+
|
|
281
|
+
### Chat
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
everypixel chat "Explain how a rainbow forms."
|
|
285
|
+
everypixel chat "Review this idea." --system "Be concise." --stream
|
|
286
|
+
everypixel chat "Solve this problem." --reasoning-effort high --show-reasoning
|
|
287
|
+
everypixel chat --input-file ./conversation.json --output-json
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Chat uses the same credentials as other commands and requires the client's
|
|
291
|
+
`chat` scope. The default model is `glm-5.3`. Each invocation sends one request;
|
|
292
|
+
use `--input-file` for conversation history, function tools, or other API options:
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"messages": [
|
|
297
|
+
{"role": "user", "content": "What causes rainbows?"},
|
|
298
|
+
{"role": "assistant", "content": "Sunlight refracts and reflects in water droplets."},
|
|
299
|
+
{"role": "user", "content": "Why do the colors separate?"}
|
|
300
|
+
],
|
|
301
|
+
"max_completion_tokens": 4096
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A positional prompt appends a user message to the supplied history. CLI options
|
|
306
|
+
override matching fields in the file. `--stream` displays text as it arrives;
|
|
307
|
+
JSON output returns one assembled response, including reasoning, tool calls,
|
|
308
|
+
and usage. Function calls are returned for the caller to handle. The MCP `chat`
|
|
309
|
+
tool accepts the same structured request and returns a complete response.
|
|
310
|
+
Use `--request-timeout` to change the chat HTTP timeout (default: 240 seconds).
|
|
311
|
+
|
|
250
312
|
### Visual Analysis
|
|
251
313
|
|
|
252
314
|
Analysis commands accept HTTP(S) URLs and local files. Local files are uploaded
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Chat request construction and completion assembly without presentation code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Callable, Iterator
|
|
7
|
+
from typing import Any, Protocol
|
|
8
|
+
|
|
9
|
+
from ..errors import APIResponseError, ValidationCLIError
|
|
10
|
+
from ..schemas import ChatRequest
|
|
11
|
+
from .models import ExecutionOptions, OperationResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChatClientProtocol(Protocol):
|
|
15
|
+
def request(
|
|
16
|
+
self,
|
|
17
|
+
method: str,
|
|
18
|
+
path: str,
|
|
19
|
+
*,
|
|
20
|
+
json: dict[str, Any] | None = None,
|
|
21
|
+
params: dict[str, Any] | None = None,
|
|
22
|
+
files: dict[str, Any] | None = None,
|
|
23
|
+
auth_required: bool = True,
|
|
24
|
+
timeout: float | None = None,
|
|
25
|
+
) -> Any: ...
|
|
26
|
+
|
|
27
|
+
def stream_request(
|
|
28
|
+
self, method: str, path: str, *, json: dict[str, Any], timeout: float
|
|
29
|
+
) -> Iterator[dict[str, Any]]: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_chat_payload(
|
|
33
|
+
*,
|
|
34
|
+
payload: dict[str, Any],
|
|
35
|
+
prompt: str | None,
|
|
36
|
+
system: str | None,
|
|
37
|
+
options: dict[str, Any],
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
values = {
|
|
40
|
+
**payload,
|
|
41
|
+
**{key: value for key, value in options.items() if value is not None},
|
|
42
|
+
}
|
|
43
|
+
values.setdefault("model", "glm-5.3")
|
|
44
|
+
messages = values.get("messages", [])
|
|
45
|
+
if not isinstance(messages, list):
|
|
46
|
+
raise ValidationCLIError("messages must be an array")
|
|
47
|
+
messages = list(messages)
|
|
48
|
+
if system is not None:
|
|
49
|
+
messages.insert(0, {"role": "system", "content": system})
|
|
50
|
+
if prompt is not None:
|
|
51
|
+
messages.append({"role": "user", "content": prompt})
|
|
52
|
+
values["messages"] = messages
|
|
53
|
+
if values.get("stream") and "stream_options" not in values:
|
|
54
|
+
values["stream_options"] = {"include_usage": True}
|
|
55
|
+
return ChatRequest.model_validate(values).model_dump(mode="json", exclude_none=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ChatCompletion:
|
|
59
|
+
"""Combine text, reasoning, and tool-call fragments into a completion."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self.metadata: dict[str, Any] = {}
|
|
63
|
+
self.choices: dict[int, dict[str, Any]] = {}
|
|
64
|
+
self.tools: dict[int, dict[int, dict[str, Any]]] = {}
|
|
65
|
+
|
|
66
|
+
def add(self, event: dict[str, Any]) -> None:
|
|
67
|
+
choices = event.get("choices")
|
|
68
|
+
if not isinstance(choices, list):
|
|
69
|
+
raise APIResponseError("API returned invalid chat choices")
|
|
70
|
+
self.metadata.update(
|
|
71
|
+
{key: value for key, value in event.items() if key != "choices"}
|
|
72
|
+
)
|
|
73
|
+
for item in choices:
|
|
74
|
+
if not isinstance(item, dict):
|
|
75
|
+
raise APIResponseError("API returned an invalid chat choice")
|
|
76
|
+
index = item.get("index")
|
|
77
|
+
delta = item.get("delta")
|
|
78
|
+
if type(index) is not int or index < 0 or not isinstance(delta, dict):
|
|
79
|
+
raise APIResponseError("API returned an invalid chat delta")
|
|
80
|
+
choice = self.choices.setdefault(
|
|
81
|
+
index,
|
|
82
|
+
{
|
|
83
|
+
"index": index,
|
|
84
|
+
"message": {"role": "assistant", "content": None},
|
|
85
|
+
"finish_reason": None,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
choice.update(
|
|
89
|
+
{
|
|
90
|
+
key: value
|
|
91
|
+
for key, value in item.items()
|
|
92
|
+
if key not in {"delta", "finish_reason"}
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
if item.get("finish_reason") is not None:
|
|
96
|
+
choice["finish_reason"] = item["finish_reason"]
|
|
97
|
+
message = choice["message"]
|
|
98
|
+
for key in ("content", "reasoning_content", "refusal"):
|
|
99
|
+
append_fragment(message, delta, key)
|
|
100
|
+
if "role" in delta:
|
|
101
|
+
message["role"] = delta["role"]
|
|
102
|
+
tool_calls = delta.get("tool_calls")
|
|
103
|
+
if tool_calls is not None:
|
|
104
|
+
self._add_tools(index, tool_calls)
|
|
105
|
+
|
|
106
|
+
def _add_tools(self, choice_index: int, calls: Any) -> None:
|
|
107
|
+
if not isinstance(calls, list):
|
|
108
|
+
raise APIResponseError("API returned invalid tool calls")
|
|
109
|
+
tools = self.tools.setdefault(choice_index, {})
|
|
110
|
+
for call in calls:
|
|
111
|
+
if not isinstance(call, dict):
|
|
112
|
+
raise APIResponseError("API returned an invalid tool call")
|
|
113
|
+
index = call.get("index")
|
|
114
|
+
if type(index) is not int or index < 0:
|
|
115
|
+
raise APIResponseError("API returned an invalid tool call index")
|
|
116
|
+
tool = tools.setdefault(index, {"function": {"name": "", "arguments": ""}})
|
|
117
|
+
append_fragment(tool, call, "id")
|
|
118
|
+
if "type" in call:
|
|
119
|
+
tool["type"] = call["type"]
|
|
120
|
+
function = call.get("function")
|
|
121
|
+
if function is not None:
|
|
122
|
+
if not isinstance(function, dict):
|
|
123
|
+
raise APIResponseError("API returned an invalid function call")
|
|
124
|
+
for key in ("name", "arguments"):
|
|
125
|
+
append_fragment(tool["function"], function, key)
|
|
126
|
+
|
|
127
|
+
def result(self) -> dict[str, Any]:
|
|
128
|
+
if not self.choices or any(
|
|
129
|
+
choice["finish_reason"] is None for choice in self.choices.values()
|
|
130
|
+
):
|
|
131
|
+
raise APIResponseError("API returned an unfinished chat completion")
|
|
132
|
+
for index, tools in self.tools.items():
|
|
133
|
+
self.choices[index]["message"]["tool_calls"] = [
|
|
134
|
+
tools[key] for key in sorted(tools)
|
|
135
|
+
]
|
|
136
|
+
return {
|
|
137
|
+
**self.metadata,
|
|
138
|
+
"object": "chat.completion",
|
|
139
|
+
"choices": [self.choices[index] for index in sorted(self.choices)],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def append_fragment(target: dict[str, Any], delta: dict[str, Any], key: str) -> None:
|
|
144
|
+
value = delta.get(key)
|
|
145
|
+
if value is None:
|
|
146
|
+
return
|
|
147
|
+
if not isinstance(value, str):
|
|
148
|
+
raise APIResponseError("API returned an invalid chat text fragment")
|
|
149
|
+
target[key] = (target.get(key) or "") + value
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ChatService:
|
|
153
|
+
def __init__(self, client: ChatClientProtocol) -> None:
|
|
154
|
+
self.client = client
|
|
155
|
+
|
|
156
|
+
def execute(
|
|
157
|
+
self,
|
|
158
|
+
payload: dict[str, Any],
|
|
159
|
+
*,
|
|
160
|
+
request_timeout: float = 240.0,
|
|
161
|
+
execution: ExecutionOptions | None = None,
|
|
162
|
+
on_chunk: Callable[[dict[str, Any]], None] | None = None,
|
|
163
|
+
) -> OperationResult:
|
|
164
|
+
if not math.isfinite(request_timeout) or request_timeout <= 0:
|
|
165
|
+
raise ValidationCLIError("request timeout must be a finite positive number")
|
|
166
|
+
execution = execution or ExecutionOptions()
|
|
167
|
+
execution.check_cancelled()
|
|
168
|
+
if not payload.get("stream"):
|
|
169
|
+
value = self.client.request(
|
|
170
|
+
"POST", "/v1/chat/completions", json=payload, timeout=request_timeout
|
|
171
|
+
)
|
|
172
|
+
execution.check_cancelled()
|
|
173
|
+
return OperationResult(value=value)
|
|
174
|
+
completion = ChatCompletion()
|
|
175
|
+
events = self.client.stream_request(
|
|
176
|
+
"POST", "/v1/chat/completions", json=payload, timeout=request_timeout
|
|
177
|
+
)
|
|
178
|
+
# Closing the generator also closes its HTTP response on cancellation
|
|
179
|
+
# or a rendering failure, without starting another generation.
|
|
180
|
+
try:
|
|
181
|
+
for event in events:
|
|
182
|
+
execution.check_cancelled()
|
|
183
|
+
completion.add(event)
|
|
184
|
+
if on_chunk is not None:
|
|
185
|
+
on_chunk(event)
|
|
186
|
+
execution.check_cancelled()
|
|
187
|
+
finally:
|
|
188
|
+
close = getattr(events, "close", None)
|
|
189
|
+
if close is not None:
|
|
190
|
+
close()
|
|
191
|
+
execution.check_cancelled()
|
|
192
|
+
return OperationResult(value=completion.result())
|