everypixel-cli 0.2.2__tar.gz → 0.3.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.
Files changed (25) hide show
  1. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/CHANGELOG.md +5 -0
  2. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/PKG-INFO +43 -3
  3. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/README.md +42 -2
  4. everypixel_cli-0.3.0/src/everypixel_cli/__init__.py +1 -0
  5. everypixel_cli-0.3.0/src/everypixel_cli/application/chat.py +192 -0
  6. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/application/services.py +69 -12
  7. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/cli.py +105 -3
  8. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/client.py +89 -17
  9. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/mcp_server.py +33 -0
  10. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/output.py +55 -0
  11. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/resources/openapi.json +567 -9
  12. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/schemas.py +147 -18
  13. everypixel_cli-0.2.2/src/everypixel_cli/__init__.py +0 -1
  14. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/.gitignore +0 -0
  15. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/LICENSE +0 -0
  16. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/pyproject.toml +0 -0
  17. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/__main__.py +0 -0
  18. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/application/__init__.py +0 -0
  19. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/application/models.py +0 -0
  20. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/application/serialization.py +0 -0
  21. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/config.py +0 -0
  22. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/errors.py +0 -0
  23. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/files.py +0 -0
  24. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/openapi.py +0 -0
  25. {everypixel_cli-0.2.2 → everypixel_cli-0.3.0}/src/everypixel_cli/resources/__init__.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-09-18
4
+
5
+ - Add GPT Image 2.5 Sunburst and quality selection for GPT Image models.
6
+ - Add GLM-5.3 chat with streaming and tool-call support.
7
+
3
8
  ## [0.2.2] - 2026-09-11
4
9
 
5
10
  - 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.2.2
3
+ Version: 0.3.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, work with audio, run visual analysis, and
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.
@@ -281,6 +290,37 @@ everypixel audio tts-create \
281
290
  --download ./outputs
282
291
  ```
283
292
 
293
+ ### Chat
294
+
295
+ ```bash
296
+ everypixel chat "Explain how a rainbow forms."
297
+ everypixel chat "Review this idea." --system "Be concise." --stream
298
+ everypixel chat "Solve this problem." --reasoning-effort high --show-reasoning
299
+ everypixel chat --input-file ./conversation.json --output-json
300
+ ```
301
+
302
+ Chat uses the same credentials as other commands and requires the client's
303
+ `chat` scope. The default model is `glm-5.3`. Each invocation sends one request;
304
+ use `--input-file` for conversation history, function tools, or other API options:
305
+
306
+ ```json
307
+ {
308
+ "messages": [
309
+ {"role": "user", "content": "What causes rainbows?"},
310
+ {"role": "assistant", "content": "Sunlight refracts and reflects in water droplets."},
311
+ {"role": "user", "content": "Why do the colors separate?"}
312
+ ],
313
+ "max_completion_tokens": 4096
314
+ }
315
+ ```
316
+
317
+ A positional prompt appends a user message to the supplied history. CLI options
318
+ override matching fields in the file. `--stream` displays text as it arrives;
319
+ JSON output returns one assembled response, including reasoning, tool calls,
320
+ and usage. Function calls are returned for the caller to handle. The MCP `chat`
321
+ tool accepts the same structured request and returns a complete response.
322
+ Use `--request-timeout` to change the chat HTTP timeout (default: 240 seconds).
323
+
284
324
  ### Visual Analysis
285
325
 
286
326
  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, work with audio, run visual analysis, and
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.
@@ -247,6 +256,37 @@ everypixel audio tts-create \
247
256
  --download ./outputs
248
257
  ```
249
258
 
259
+ ### Chat
260
+
261
+ ```bash
262
+ everypixel chat "Explain how a rainbow forms."
263
+ everypixel chat "Review this idea." --system "Be concise." --stream
264
+ everypixel chat "Solve this problem." --reasoning-effort high --show-reasoning
265
+ everypixel chat --input-file ./conversation.json --output-json
266
+ ```
267
+
268
+ Chat uses the same credentials as other commands and requires the client's
269
+ `chat` scope. The default model is `glm-5.3`. Each invocation sends one request;
270
+ use `--input-file` for conversation history, function tools, or other API options:
271
+
272
+ ```json
273
+ {
274
+ "messages": [
275
+ {"role": "user", "content": "What causes rainbows?"},
276
+ {"role": "assistant", "content": "Sunlight refracts and reflects in water droplets."},
277
+ {"role": "user", "content": "Why do the colors separate?"}
278
+ ],
279
+ "max_completion_tokens": 4096
280
+ }
281
+ ```
282
+
283
+ A positional prompt appends a user message to the supplied history. CLI options
284
+ override matching fields in the file. `--stream` displays text as it arrives;
285
+ JSON output returns one assembled response, including reasoning, tool calls,
286
+ and usage. Function calls are returned for the caller to handle. The MCP `chat`
287
+ tool accepts the same structured request and returns a complete response.
288
+ Use `--request-timeout` to change the chat HTTP timeout (default: 240 seconds).
289
+
250
290
  ### Visual Analysis
251
291
 
252
292
  Analysis commands accept HTTP(S) URLs and local files. Local files are uploaded
@@ -0,0 +1 @@
1
+ __version__ = "0.3.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())
@@ -33,6 +33,8 @@ from ..openapi import (
33
33
  validate_payload_against_operation,
34
34
  )
35
35
  from ..schemas import (
36
+ ChatRequest,
37
+ GPT_IMAGE_MODEL_QUALITIES,
36
38
  ImageEditPayload,
37
39
  ImageGeneratePayload,
38
40
  ImageUpscalePayload,
@@ -46,7 +48,9 @@ from ..schemas import (
46
48
  VideoGenerateModel,
47
49
  VideoUpscalePayload,
48
50
  normalize_grok_model,
51
+ normalize_image_quality,
49
52
  )
53
+ from .chat import ChatClientProtocol, ChatService, build_chat_payload
50
54
  from .models import (
51
55
  ExecutionOptions,
52
56
  OperationCancelled,
@@ -61,22 +65,11 @@ _VIDEO_GENERATE_ADAPTER: TypeAdapter[VideoGenerateRequest] = TypeAdapter(
61
65
  _VIDEO_EDIT_ADAPTER: TypeAdapter[VideoEditRequest] = TypeAdapter(VideoEditRequest)
62
66
 
63
67
 
64
- class EverypixelClientProtocol(Protocol):
68
+ class EverypixelClientProtocol(ChatClientProtocol, Protocol):
65
69
  """Small infrastructure seam used by application services."""
66
70
 
67
71
  base_url: str
68
72
 
69
- def request(
70
- self,
71
- method: str,
72
- path: str,
73
- *,
74
- json: dict[str, Any] | None = None,
75
- params: dict[str, Any] | None = None,
76
- files: dict[str, Any] | None = None,
77
- auth_required: bool = True,
78
- ) -> Any: ...
79
-
80
73
  def get_status(self, task_id: str) -> Any: ...
81
74
 
82
75
  def check_auth(self) -> None: ...
@@ -837,6 +830,7 @@ class ApplicationServices:
837
830
  execution: ExecutionOptions,
838
831
  lora_url: str | None = None,
839
832
  controls: dict[str, Any] | str | None = None,
833
+ quality: str | None = None,
840
834
  ) -> OperationResult:
841
835
  payload = build_image_generate_payload(
842
836
  prompt=prompt,
@@ -849,6 +843,7 @@ class ApplicationServices:
849
843
  callback_url=callback_url,
850
844
  lora_url=lora_url,
851
845
  controls=controls,
846
+ quality=quality,
852
847
  )
853
848
  return self._execute_async(
854
849
  endpoint="/v1/image_generate",
@@ -888,6 +883,7 @@ class ApplicationServices:
888
883
  callback_url: str | None,
889
884
  execution: ExecutionOptions,
890
885
  megapixel_ratio: float = 1.0,
886
+ quality: str | None = None,
891
887
  ) -> OperationResult:
892
888
  payload = build_image_edit_payload(
893
889
  prompt=prompt,
@@ -896,6 +892,7 @@ class ApplicationServices:
896
892
  image_size=image_size,
897
893
  resolution=resolution,
898
894
  megapixel_ratio=megapixel_ratio,
895
+ quality=quality,
899
896
  seed=seed,
900
897
  callback_url=callback_url,
901
898
  )
@@ -1225,6 +1222,45 @@ class ApplicationServices:
1225
1222
  fallback_extension=".mp3",
1226
1223
  )
1227
1224
 
1225
+ def execute_chat(
1226
+ self,
1227
+ *,
1228
+ prompt: str | None = None,
1229
+ system: str | None = None,
1230
+ input_file: Path | None = None,
1231
+ payload: dict[str, Any] | None = None,
1232
+ model: str | None = None,
1233
+ stream: bool | None = None,
1234
+ max_completion_tokens: int | None = None,
1235
+ temperature: float | None = None,
1236
+ top_p: float | None = None,
1237
+ reasoning_effort: str | None = None,
1238
+ request_timeout: float = 240.0,
1239
+ execution: ExecutionOptions | None = None,
1240
+ on_chunk: Callable[[dict[str, Any]], None] | None = None,
1241
+ ) -> OperationResult:
1242
+ inputs = parse_generic_payload(prompt=None, items=[], input_file=input_file)
1243
+ inputs.update(payload or {})
1244
+ request = build_chat_payload(
1245
+ payload=inputs,
1246
+ prompt=prompt,
1247
+ system=system,
1248
+ options={
1249
+ "model": model,
1250
+ "stream": stream,
1251
+ "max_completion_tokens": max_completion_tokens,
1252
+ "temperature": temperature,
1253
+ "top_p": top_p,
1254
+ "reasoning_effort": reasoning_effort,
1255
+ },
1256
+ )
1257
+ return ChatService(self._operations.client).execute(
1258
+ request,
1259
+ request_timeout=request_timeout,
1260
+ execution=execution,
1261
+ on_chunk=on_chunk,
1262
+ )
1263
+
1228
1264
  def get_task_status(
1229
1265
  self, *, task_id: str, execution: ExecutionOptions
1230
1266
  ) -> OperationResult:
@@ -1276,6 +1312,21 @@ class ApplicationServices:
1276
1312
  )
1277
1313
  if path in {"/v1/image_generate", "/v1/image_edit", "/v1/video_generate"}:
1278
1314
  payload = normalize_grok_model(payload)
1315
+ if path in {"/v1/image_generate", "/v1/image_edit"}:
1316
+ try:
1317
+ payload = normalize_image_quality(payload)
1318
+ except ValueError as exc:
1319
+ raise ValidationCLIError(str(exc)) from exc
1320
+ if (
1321
+ isinstance(payload.get("model"), str)
1322
+ and payload["model"] in GPT_IMAGE_MODEL_QUALITIES
1323
+ ):
1324
+ image_schema = (
1325
+ ImageGeneratePayload
1326
+ if path == "/v1/image_generate"
1327
+ else ImageEditPayload
1328
+ )
1329
+ image_schema.model_validate(payload, extra="ignore")
1279
1330
  if path == "/v1/video_generate" and payload.get("model") in get_args(
1280
1331
  VideoGenerateModel
1281
1332
  ):
@@ -1287,6 +1338,8 @@ class ApplicationServices:
1287
1338
  # specialized model does not know yet; validate known semantics
1288
1339
  # without rewriting or narrowing their payload.
1289
1340
  _VIDEO_EDIT_ADAPTER.validate_python(payload, extra="ignore")
1341
+ if path == "/v1/chat/completions":
1342
+ ChatRequest.model_validate(payload, extra="ignore")
1290
1343
  validate_payload_against_operation(operation, payload)
1291
1344
  if dry_run:
1292
1345
  return OperationResult(
@@ -1300,6 +1353,10 @@ class ApplicationServices:
1300
1353
  "schema_source": schema_source,
1301
1354
  }
1302
1355
  )
1356
+ if path == "/v1/chat/completions" and resolved_method == "POST":
1357
+ return ChatService(self._operations.client).execute(
1358
+ payload, execution=execution
1359
+ )
1303
1360
  return self._operations.execute(
1304
1361
  OperationRequest(
1305
1362
  endpoint=path,
@@ -48,7 +48,7 @@ from .errors import (
48
48
  normalize_exception,
49
49
  )
50
50
  from .openapi import load_schema
51
- from .output import emit_error, emit_human, emit_json
51
+ from .output import ChatRenderer, emit_error, emit_human, emit_json
52
52
 
53
53
 
54
54
  def json_output_requested(args: Sequence[str]) -> bool:
@@ -395,11 +395,20 @@ def stop_with_error(ctx: typer.Context, exc: Exception) -> NoReturn:
395
395
  raise typer.Exit(error.exit_code) from exc
396
396
 
397
397
 
398
- def run_action(ctx: typer.Context, action: Callable[[], Any]) -> None:
398
+ def run_action(
399
+ ctx: typer.Context,
400
+ action: Callable[[], Any],
401
+ *,
402
+ render: Callable[[Any], None] | None = None,
403
+ ) -> None:
399
404
  """Run a command action and handle all expected application errors."""
400
405
 
401
406
  try:
402
- finish(ctx, action())
407
+ value = action()
408
+ if render is None:
409
+ finish(ctx, value)
410
+ else:
411
+ render(value)
403
412
  except typer.Exit:
404
413
  raise
405
414
  except (
@@ -430,6 +439,83 @@ def apply_common_options(
430
439
  return runtime
431
440
 
432
441
 
442
+ @app.command("chat")
443
+ def chat(
444
+ ctx: typer.Context,
445
+ prompt: Annotated[
446
+ Optional[str], typer.Argument(help="User message to append.")
447
+ ] = None,
448
+ system: Annotated[
449
+ Optional[str], typer.Option("--system", help="System instruction.")
450
+ ] = None,
451
+ input_file: Annotated[
452
+ Optional[Path],
453
+ typer.Option(
454
+ "--input-file",
455
+ help="JSON request with messages, tools, and other chat options.",
456
+ ),
457
+ ] = None,
458
+ model: Annotated[
459
+ Optional[str], typer.Option("--model", help="Chat model (default: glm-5.3).")
460
+ ] = None,
461
+ stream: Annotated[
462
+ Optional[bool],
463
+ typer.Option("--stream/--no-stream", help="Stream the response."),
464
+ ] = None,
465
+ max_completion_tokens: Annotated[
466
+ Optional[int], typer.Option("--max-completion-tokens")
467
+ ] = None,
468
+ temperature: Annotated[Optional[float], typer.Option("--temperature")] = None,
469
+ top_p: Annotated[Optional[float], typer.Option("--top-p")] = None,
470
+ reasoning_effort: Annotated[
471
+ Optional[str], typer.Option("--reasoning-effort", help="low, high, or max.")
472
+ ] = None,
473
+ show_reasoning: Annotated[
474
+ bool,
475
+ typer.Option("--show-reasoning", help="Include reasoning in human output."),
476
+ ] = False,
477
+ request_timeout: Annotated[
478
+ float,
479
+ typer.Option(
480
+ "--request-timeout",
481
+ callback=positive_float,
482
+ help="HTTP timeout in seconds.",
483
+ ),
484
+ ] = 240.0,
485
+ output_json: OutputJsonOption = False,
486
+ jq_expr: JqOption = None,
487
+ ) -> None:
488
+ """Chat with GLM-5.3 using a prompt or a JSON conversation."""
489
+
490
+ runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
491
+ renderer = ChatRenderer(show_reasoning=show_reasoning, no_color=runtime.no_color)
492
+
493
+ def render(result: Any) -> None:
494
+ if runtime.output_json:
495
+ finish(ctx, result)
496
+ else:
497
+ renderer.finish(serialize_operation_result(result))
498
+
499
+ run_action(
500
+ ctx,
501
+ lambda: runtime.services().execute_chat(
502
+ prompt=prompt,
503
+ system=system,
504
+ input_file=input_file,
505
+ model=model,
506
+ stream=stream,
507
+ max_completion_tokens=max_completion_tokens,
508
+ temperature=temperature,
509
+ top_p=top_p,
510
+ reasoning_effort=reasoning_effort,
511
+ request_timeout=request_timeout,
512
+ execution=runtime.execution_options(),
513
+ on_chunk=None if runtime.output_json else renderer.chunk,
514
+ ),
515
+ render=render,
516
+ )
517
+
518
+
433
519
  @auth_app.command("configure")
434
520
  def auth_configure(
435
521
  ctx: typer.Context,
@@ -584,6 +670,13 @@ def image_generate(
584
670
  typer.Option("--controls", help="Recraft palette controls as JSON."),
585
671
  ] = None,
586
672
  resolution: Annotated[Optional[str], typer.Option("--resolution")] = None,
673
+ quality: Annotated[
674
+ Optional[str],
675
+ typer.Option(
676
+ "--quality",
677
+ help="GPT Image quality: low, medium, high; 2.5 also xhigh, max.",
678
+ ),
679
+ ] = None,
587
680
  seed: Annotated[int, typer.Option("--seed")] = -1,
588
681
  callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
589
682
  wait_option: WaitOption = None,
@@ -611,6 +704,7 @@ def image_generate(
611
704
  lora_url=lora_url,
612
705
  controls=controls,
613
706
  resolution=resolution,
707
+ quality=quality,
614
708
  seed=seed,
615
709
  callback_url=callback_url,
616
710
  execution=runtime.execution_options(),
@@ -630,6 +724,13 @@ def image_edit(
630
724
  size: Annotated[Optional[str], typer.Option("--size")] = None,
631
725
  resolution: Annotated[Optional[str], typer.Option("--resolution")] = None,
632
726
  megapixel_ratio: Annotated[float, typer.Option("--megapixel-ratio")] = 1.0,
727
+ quality: Annotated[
728
+ Optional[str],
729
+ typer.Option(
730
+ "--quality",
731
+ help="GPT Image quality: low, medium, high; 2.5 also xhigh, max.",
732
+ ),
733
+ ] = None,
633
734
  seed: Annotated[int, typer.Option("--seed")] = -1,
634
735
  callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
635
736
  wait_option: WaitOption = None,
@@ -655,6 +756,7 @@ def image_edit(
655
756
  image_size=size,
656
757
  resolution=resolution,
657
758
  megapixel_ratio=megapixel_ratio,
759
+ quality=quality,
658
760
  seed=seed,
659
761
  callback_url=callback_url,
660
762
  execution=runtime.execution_options(),