venice-cli 0.14.0__py3-none-any.whl
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.
- venice/__init__.py +1 -0
- venice/__main__.py +6 -0
- venice/audio_player.py +59 -0
- venice/audio_post.py +274 -0
- venice/auth.py +101 -0
- venice/billing.py +129 -0
- venice/cli.py +31 -0
- venice/client.py +258 -0
- venice/commands/__init__.py +22 -0
- venice/commands/_audio.py +96 -0
- venice/commands/_models.py +83 -0
- venice/commands/_openai.py +62 -0
- venice/commands/_queue.py +87 -0
- venice/commands/_shared.py +132 -0
- venice/commands/balance.py +124 -0
- venice/commands/bg_remove.py +126 -0
- venice/commands/chat.py +263 -0
- venice/commands/contact_sheet.py +59 -0
- venice/commands/embed.py +157 -0
- venice/commands/image.py +657 -0
- venice/commands/login.py +29 -0
- venice/commands/master.py +42 -0
- venice/commands/models.py +174 -0
- venice/commands/music.py +316 -0
- venice/commands/sfx.py +199 -0
- venice/commands/tts.py +362 -0
- venice/commands/upscale.py +177 -0
- venice/commands/video.py +298 -0
- venice/config.py +19 -0
- venice/image_montage.py +230 -0
- venice_cli-0.14.0.dist-info/METADATA +654 -0
- venice_cli-0.14.0.dist-info/RECORD +35 -0
- venice_cli-0.14.0.dist-info/WHEEL +4 -0
- venice_cli-0.14.0.dist-info/entry_points.txt +2 -0
- venice_cli-0.14.0.dist-info/licenses/LICENSE +21 -0
venice/client.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Thin Venice.ai HTTP client built on urllib. No third-party deps.
|
|
2
|
+
|
|
3
|
+
Returns dicts for JSON responses, bytes for binary (audio/image).
|
|
4
|
+
Maps non-2xx to VeniceAPIError with status, URL, and a body excerpt.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any, Callable, Optional, Tuple, Union
|
|
15
|
+
|
|
16
|
+
from . import config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VeniceAPIError(Exception):
|
|
20
|
+
"""HTTP-level error from the Venice API.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
status: HTTP status code (0 if connection failed pre-response).
|
|
24
|
+
url: final request URL.
|
|
25
|
+
body: excerpt of the response body (first ~2 KB), for debugging.
|
|
26
|
+
code: Venice API error code (e.g. INSUFFICIENT_BALANCE), if parseable.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, status: int, url: str, body: str, code: Optional[str] = None):
|
|
30
|
+
self.status = status
|
|
31
|
+
self.url = url
|
|
32
|
+
self.body = body
|
|
33
|
+
self.code = code
|
|
34
|
+
msg = f"HTTP {status} from {url}"
|
|
35
|
+
if code:
|
|
36
|
+
msg += f" [{code}]"
|
|
37
|
+
if body:
|
|
38
|
+
msg += f"\n body: {body[:500]}"
|
|
39
|
+
super().__init__(msg)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
ResponseType = Union[dict, bytes]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VeniceClient:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str,
|
|
49
|
+
base_url: Optional[str] = None,
|
|
50
|
+
timeout: float = 60.0,
|
|
51
|
+
user_agent: str = "venice-cli/0.1",
|
|
52
|
+
):
|
|
53
|
+
if not api_key:
|
|
54
|
+
raise ValueError("api_key is required")
|
|
55
|
+
self.api_key = api_key
|
|
56
|
+
self.base_url = (base_url or config.DEFAULT_BASE_URL).rstrip("/")
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
self.user_agent = user_agent
|
|
59
|
+
|
|
60
|
+
def request(
|
|
61
|
+
self,
|
|
62
|
+
method: str,
|
|
63
|
+
path: str,
|
|
64
|
+
*,
|
|
65
|
+
json_body: Optional[dict] = None,
|
|
66
|
+
params: Optional[dict] = None,
|
|
67
|
+
) -> Tuple[int, str, bytes]:
|
|
68
|
+
url = self.base_url + path
|
|
69
|
+
if params:
|
|
70
|
+
url += "?" + urllib.parse.urlencode(params, doseq=True)
|
|
71
|
+
|
|
72
|
+
headers = {
|
|
73
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
74
|
+
"Accept": "application/json, audio/*, image/*, video/*",
|
|
75
|
+
"User-Agent": self.user_agent,
|
|
76
|
+
}
|
|
77
|
+
data: Optional[bytes] = None
|
|
78
|
+
if json_body is not None:
|
|
79
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
80
|
+
headers["Content-Type"] = "application/json"
|
|
81
|
+
|
|
82
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
83
|
+
try:
|
|
84
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
85
|
+
body = resp.read()
|
|
86
|
+
ctype = resp.headers.get("Content-Type", "")
|
|
87
|
+
status = getattr(resp, "status", 200)
|
|
88
|
+
return status, ctype, body
|
|
89
|
+
except urllib.error.HTTPError as e:
|
|
90
|
+
err_body = b""
|
|
91
|
+
try:
|
|
92
|
+
err_body = e.read()
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
err_ctype = ""
|
|
96
|
+
try:
|
|
97
|
+
err_ctype = e.headers.get("Content-Type", "")
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
self._raise_api_error(e.code, url, err_body, err_ctype)
|
|
101
|
+
except urllib.error.URLError as e:
|
|
102
|
+
raise VeniceAPIError(0, url, f"connection error: {e.reason}") from None
|
|
103
|
+
|
|
104
|
+
def post_json(self, path: str, body: dict) -> dict:
|
|
105
|
+
status, ctype, raw = self.request("POST", path, json_body=body)
|
|
106
|
+
return self._decode_json(status, path, ctype, raw)
|
|
107
|
+
|
|
108
|
+
def get_json(self, path: str, params: Optional[dict] = None) -> dict:
|
|
109
|
+
status, ctype, raw = self.request("GET", path, params=params)
|
|
110
|
+
return self._decode_json(status, path, ctype, raw)
|
|
111
|
+
|
|
112
|
+
def post_for_bytes_or_json(
|
|
113
|
+
self, path: str, body: dict
|
|
114
|
+
) -> Tuple[str, ResponseType]:
|
|
115
|
+
"""For endpoints that may return JSON (in-progress) OR binary (done).
|
|
116
|
+
|
|
117
|
+
Used by /audio/retrieve. Returns (content_type, payload):
|
|
118
|
+
- ("audio/mpeg", b"...") on completion
|
|
119
|
+
- ("application/json", {...}) while still processing
|
|
120
|
+
"""
|
|
121
|
+
status, ctype, raw = self.request("POST", path, json_body=body)
|
|
122
|
+
ct_low = (ctype or "").lower()
|
|
123
|
+
if ct_low.startswith("application/json"):
|
|
124
|
+
return ctype, (json.loads(raw.decode("utf-8")) if raw else {})
|
|
125
|
+
if (
|
|
126
|
+
ct_low.startswith("audio/")
|
|
127
|
+
or ct_low.startswith("image/")
|
|
128
|
+
or ct_low.startswith("video/")
|
|
129
|
+
):
|
|
130
|
+
return ctype, raw
|
|
131
|
+
return ctype, raw
|
|
132
|
+
|
|
133
|
+
def poll_retrieve(
|
|
134
|
+
self,
|
|
135
|
+
path: str,
|
|
136
|
+
body: dict,
|
|
137
|
+
*,
|
|
138
|
+
interval: float = config.SFX_POLL_INTERVAL_SEC,
|
|
139
|
+
max_wait: float = config.SFX_POLL_MAX_WAIT_SEC,
|
|
140
|
+
on_tick: Optional[Callable[[dict], None]] = None,
|
|
141
|
+
terminal_statuses: Tuple[str, ...] = (),
|
|
142
|
+
) -> Tuple[str, ResponseType]:
|
|
143
|
+
"""Poll an async endpoint that switches content-type on completion.
|
|
144
|
+
|
|
145
|
+
On success returns (content_type, payload):
|
|
146
|
+
- (ctype, bytes) when the endpoint streams the finished media, or
|
|
147
|
+
- (ctype, dict) when the JSON `status` is in `terminal_statuses`
|
|
148
|
+
(e.g. video's "COMPLETED" -- the media is fetched separately from a
|
|
149
|
+
download_url). Audio callers leave `terminal_statuses` empty and
|
|
150
|
+
always get bytes.
|
|
151
|
+
|
|
152
|
+
Raises VeniceAPIError on terminal HTTP errors or an unexpected status.
|
|
153
|
+
Raises TimeoutError if max_wait elapses while still PROCESSING.
|
|
154
|
+
"""
|
|
155
|
+
deadline = time.monotonic() + max_wait
|
|
156
|
+
while True:
|
|
157
|
+
ctype, payload = self.post_for_bytes_or_json(path, body)
|
|
158
|
+
if isinstance(payload, (bytes, bytearray)):
|
|
159
|
+
return ctype, bytes(payload)
|
|
160
|
+
if not isinstance(payload, dict):
|
|
161
|
+
raise VeniceAPIError(
|
|
162
|
+
0, path, f"unexpected payload type from {path}: {type(payload).__name__}"
|
|
163
|
+
)
|
|
164
|
+
status = payload.get("status")
|
|
165
|
+
if status and status in terminal_statuses:
|
|
166
|
+
return ctype, payload
|
|
167
|
+
if status and status != "PROCESSING":
|
|
168
|
+
raise VeniceAPIError(
|
|
169
|
+
0, path, f"unexpected status: {payload!r}"
|
|
170
|
+
)
|
|
171
|
+
if on_tick:
|
|
172
|
+
try:
|
|
173
|
+
on_tick(payload)
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
if time.monotonic() >= deadline:
|
|
177
|
+
raise TimeoutError(
|
|
178
|
+
f"not ready after {max_wait}s "
|
|
179
|
+
f"(last status: {status!r})"
|
|
180
|
+
)
|
|
181
|
+
time.sleep(interval)
|
|
182
|
+
|
|
183
|
+
def get_url_bytes(self, url: str) -> Tuple[str, bytes]:
|
|
184
|
+
"""Fetch an arbitrary URL's bytes with a plain GET (no auth header).
|
|
185
|
+
|
|
186
|
+
For presigned download URLs (e.g. a video's download_url), which carry
|
|
187
|
+
their own auth in the query string and must not receive our Bearer token.
|
|
188
|
+
Returns (content_type, bytes); maps HTTP/URL errors to VeniceAPIError.
|
|
189
|
+
"""
|
|
190
|
+
req = urllib.request.Request(
|
|
191
|
+
url, headers={"User-Agent": self.user_agent}, method="GET"
|
|
192
|
+
)
|
|
193
|
+
try:
|
|
194
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
195
|
+
return resp.headers.get("Content-Type", ""), resp.read()
|
|
196
|
+
except urllib.error.HTTPError as e:
|
|
197
|
+
err_body = b""
|
|
198
|
+
try:
|
|
199
|
+
err_body = e.read()
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
self._raise_api_error(e.code, url, err_body, "")
|
|
203
|
+
except urllib.error.URLError as e:
|
|
204
|
+
raise VeniceAPIError(0, url, f"connection error: {e.reason}") from None
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _decode_json(status: int, path: str, ctype: str, raw: bytes) -> dict:
|
|
208
|
+
if not raw:
|
|
209
|
+
return {}
|
|
210
|
+
try:
|
|
211
|
+
return json.loads(raw.decode("utf-8"))
|
|
212
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
213
|
+
raise VeniceAPIError(
|
|
214
|
+
status, path, f"non-JSON response ({ctype}): {e}"
|
|
215
|
+
) from None
|
|
216
|
+
|
|
217
|
+
def get_balance(self) -> Optional[dict]:
|
|
218
|
+
"""Fetch current balance + tier via /api_keys/rate_limits.
|
|
219
|
+
|
|
220
|
+
Returns the parsed `data` block (with balances, apiTier, nextEpochBegins,
|
|
221
|
+
rateLimits) or None if the call fails. Best-effort; callers should
|
|
222
|
+
treat None as "balance unavailable, continue".
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
doc = self.get_json("/api_keys/rate_limits")
|
|
226
|
+
except VeniceAPIError:
|
|
227
|
+
return None
|
|
228
|
+
data = doc.get("data") if isinstance(doc, dict) else None
|
|
229
|
+
return data if isinstance(data, dict) else None
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _raise_api_error(status: int, url: str, body: bytes, ctype: str):
|
|
233
|
+
excerpt = ""
|
|
234
|
+
code: Optional[str] = None
|
|
235
|
+
try:
|
|
236
|
+
text = body.decode("utf-8", errors="replace")
|
|
237
|
+
excerpt = text[:2048]
|
|
238
|
+
if (ctype or "").lower().startswith("application/json"):
|
|
239
|
+
doc: Any = json.loads(text)
|
|
240
|
+
if isinstance(doc, dict):
|
|
241
|
+
code = doc.get("code")
|
|
242
|
+
if not code and isinstance(doc.get("error"), dict):
|
|
243
|
+
code = doc["error"].get("code")
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
raise VeniceAPIError(status, url, excerpt, code=code)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build_client_from_auth():
|
|
250
|
+
"""Construct a VeniceClient using env-var or file credentials.
|
|
251
|
+
|
|
252
|
+
Raises auth.AuthError if no key is available. Honors $VENICE_BASE_URL.
|
|
253
|
+
"""
|
|
254
|
+
from . import auth
|
|
255
|
+
|
|
256
|
+
key = auth.load_key()
|
|
257
|
+
base = os.environ.get(config.ENV_BASE_URL) or config.DEFAULT_BASE_URL
|
|
258
|
+
return VeniceClient(api_key=key, base_url=base)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Single import point. Adding a subcommand = one import + one tuple entry."""
|
|
2
|
+
from . import balance, bg_remove, chat, contact_sheet, embed, image, login, master, models, music, sfx, tts, upscale, video
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def register_all(subparsers) -> None:
|
|
6
|
+
login.register(subparsers)
|
|
7
|
+
balance.register(subparsers)
|
|
8
|
+
models.register(subparsers)
|
|
9
|
+
sfx.register(subparsers)
|
|
10
|
+
sfx.register_status(subparsers)
|
|
11
|
+
music.register(subparsers)
|
|
12
|
+
music.register_status(subparsers)
|
|
13
|
+
video.register(subparsers)
|
|
14
|
+
video.register_status(subparsers)
|
|
15
|
+
chat.register(subparsers)
|
|
16
|
+
tts.register(subparsers)
|
|
17
|
+
image.register(subparsers)
|
|
18
|
+
upscale.register(subparsers)
|
|
19
|
+
bg_remove.register(subparsers)
|
|
20
|
+
embed.register(subparsers)
|
|
21
|
+
master.register(subparsers)
|
|
22
|
+
contact_sheet.register(subparsers)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Audio-specific glue over the shared async-queue engine (`_queue`).
|
|
2
|
+
|
|
3
|
+
`sfx` and `music` share this: the endpoint-agnostic quote/queue/poll plumbing
|
|
4
|
+
lives in `_queue`; here we keep the audio content-type map and the
|
|
5
|
+
audio-specialized `retrieve_and_save` (optional post-processing + playback).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from .. import audio_player
|
|
15
|
+
from ..client import VeniceAPIError
|
|
16
|
+
from . import _queue
|
|
17
|
+
|
|
18
|
+
EXT_BY_CTYPE = {
|
|
19
|
+
"audio/mpeg": ".mp3",
|
|
20
|
+
"audio/mp3": ".mp3",
|
|
21
|
+
"audio/wav": ".wav",
|
|
22
|
+
"audio/wave": ".wav",
|
|
23
|
+
"audio/x-wav": ".wav",
|
|
24
|
+
"audio/flac": ".flac",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ext_for(ctype: str) -> Tuple[str, bool]:
|
|
29
|
+
return _queue.ext_for(ctype, EXT_BY_CTYPE)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def retrieve_and_save(
|
|
33
|
+
client,
|
|
34
|
+
model: str,
|
|
35
|
+
queue_id: str,
|
|
36
|
+
out_arg: Optional[Path],
|
|
37
|
+
poll_interval: float,
|
|
38
|
+
max_wait: float,
|
|
39
|
+
no_cleanup: bool,
|
|
40
|
+
want_play: Optional[bool],
|
|
41
|
+
*,
|
|
42
|
+
name_prefix: str,
|
|
43
|
+
retry_hint: str,
|
|
44
|
+
post_process: Optional[Callable[[Path], int]] = None,
|
|
45
|
+
) -> int:
|
|
46
|
+
body = {"model": model, "queue_id": queue_id}
|
|
47
|
+
start = time.monotonic()
|
|
48
|
+
try:
|
|
49
|
+
ctype, audio = client.poll_retrieve(
|
|
50
|
+
"/audio/retrieve",
|
|
51
|
+
body,
|
|
52
|
+
interval=poll_interval,
|
|
53
|
+
max_wait=max_wait,
|
|
54
|
+
on_tick=_queue.progress_tick(start),
|
|
55
|
+
)
|
|
56
|
+
except VeniceAPIError as e:
|
|
57
|
+
sys.stderr.write("\n")
|
|
58
|
+
print(f"retrieve failed: {e}", file=sys.stderr)
|
|
59
|
+
return _queue.status_to_exit(e)
|
|
60
|
+
except TimeoutError as e:
|
|
61
|
+
sys.stderr.write("\n")
|
|
62
|
+
print(f"{e}; check later with: {retry_hint}", file=sys.stderr)
|
|
63
|
+
return 7
|
|
64
|
+
sys.stderr.write("\n")
|
|
65
|
+
|
|
66
|
+
ext, unknown = ext_for(ctype)
|
|
67
|
+
if unknown:
|
|
68
|
+
print(f"warning: unexpected content-type {ctype!r}; saving as .bin", file=sys.stderr)
|
|
69
|
+
out_path = _queue.resolve_output_path(out_arg, queue_id, ext, prefix=name_prefix)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
out_path.write_bytes(audio)
|
|
73
|
+
except OSError as e:
|
|
74
|
+
print(f"could not write {out_path}: {e}", file=sys.stderr)
|
|
75
|
+
return 9
|
|
76
|
+
|
|
77
|
+
abs_path = out_path.resolve()
|
|
78
|
+
print(str(abs_path))
|
|
79
|
+
print(f"wrote {len(audio)} bytes to {abs_path}", file=sys.stderr)
|
|
80
|
+
|
|
81
|
+
if not no_cleanup:
|
|
82
|
+
try:
|
|
83
|
+
client.post_json("/audio/complete", {"model": model, "queue_id": queue_id})
|
|
84
|
+
except VeniceAPIError as e:
|
|
85
|
+
print(f"warning: cleanup call failed: {e}", file=sys.stderr)
|
|
86
|
+
|
|
87
|
+
post_rc = 0
|
|
88
|
+
if post_process is not None:
|
|
89
|
+
post_rc = post_process(out_path)
|
|
90
|
+
|
|
91
|
+
should_play = want_play
|
|
92
|
+
if should_play is None:
|
|
93
|
+
should_play = sys.stdout.isatty() and audio_player.has_player()
|
|
94
|
+
if should_play:
|
|
95
|
+
audio_player.play(out_path)
|
|
96
|
+
return post_rc
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Shared model-catalog resolution for commands that pick a model by id.
|
|
2
|
+
|
|
3
|
+
Extracted from `chat` so it, `embed`, and `video` share one copy of the free
|
|
4
|
+
`/models?type=...` GET plus the default-trait/validation logic rather than each
|
|
5
|
+
carrying its own. These helpers take primitive args (a model type, the requested
|
|
6
|
+
id, a label and noun for messages) so they stay independent of any one command's
|
|
7
|
+
argument shape.
|
|
8
|
+
|
|
9
|
+
The catalog GET is free, so commands call it before the paid request to validate
|
|
10
|
+
`--model` and resolve a default without spending.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from typing import List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from ..client import VeniceAPIError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def catalog(client, model_type: str) -> Optional[List[dict]]:
|
|
21
|
+
"""Fetch the model catalog for `model_type` ("text", "embedding", "video").
|
|
22
|
+
|
|
23
|
+
None if the (free) GET is unavailable, which leaves the caller unable to
|
|
24
|
+
validate or pick a default.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
doc = client.get_json("/models", params={"type": model_type})
|
|
28
|
+
except VeniceAPIError:
|
|
29
|
+
return None
|
|
30
|
+
data = doc.get("data") if isinstance(doc, dict) else None
|
|
31
|
+
return list(data) if isinstance(data, list) else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_model(models: List[dict]) -> Optional[str]:
|
|
35
|
+
"""The id of the first model advertising the 'default' trait, if any."""
|
|
36
|
+
for m in models:
|
|
37
|
+
spec = m.get("model_spec") if isinstance(m, dict) else None
|
|
38
|
+
traits = spec.get("traits") if isinstance(spec, dict) else None
|
|
39
|
+
if isinstance(traits, list) and "default" in traits:
|
|
40
|
+
return m.get("id")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_model(
|
|
45
|
+
requested: Optional[str],
|
|
46
|
+
models: Optional[List[dict]],
|
|
47
|
+
*,
|
|
48
|
+
label: str,
|
|
49
|
+
noun: str,
|
|
50
|
+
) -> Tuple[Optional[str], Optional[int]]:
|
|
51
|
+
"""Validate `requested` against the catalog, or pick the default.
|
|
52
|
+
|
|
53
|
+
Returns (model_id, exit_code). exit_code is None on success. `label` prefixes
|
|
54
|
+
error messages (e.g. "chat") and `noun` names the model kind in them (e.g.
|
|
55
|
+
"text model").
|
|
56
|
+
"""
|
|
57
|
+
if models is None:
|
|
58
|
+
# Catalog unavailable: can't validate or pick a default.
|
|
59
|
+
if requested:
|
|
60
|
+
return requested, None
|
|
61
|
+
print(
|
|
62
|
+
f"{label}: could not fetch the model catalog; pass --model explicitly",
|
|
63
|
+
file=sys.stderr,
|
|
64
|
+
)
|
|
65
|
+
return None, 2
|
|
66
|
+
|
|
67
|
+
ids = [m.get("id") for m in models if isinstance(m, dict) and m.get("id")]
|
|
68
|
+
if requested:
|
|
69
|
+
if requested in ids:
|
|
70
|
+
return requested, None
|
|
71
|
+
print(f"{label}: unknown {noun} {requested!r}", file=sys.stderr)
|
|
72
|
+
print("available: " + ", ".join(ids), file=sys.stderr)
|
|
73
|
+
return None, 6
|
|
74
|
+
|
|
75
|
+
default = default_model(models)
|
|
76
|
+
if default:
|
|
77
|
+
return default, None
|
|
78
|
+
print(
|
|
79
|
+
f"{label}: no default {noun} advertised; pass --model. "
|
|
80
|
+
"available: " + ", ".join(ids),
|
|
81
|
+
file=sys.stderr,
|
|
82
|
+
)
|
|
83
|
+
return None, 6
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared `openai` SDK plumbing for Venice's OpenAI-compatible endpoints.
|
|
2
|
+
|
|
3
|
+
Extracted from `chat` so it and `embed` share one copy of the lazy-import probe,
|
|
4
|
+
the SDK client construction, and the exception -> exit-code mapping rather than
|
|
5
|
+
each carrying its own. These helpers take primitive args (a label for messages,
|
|
6
|
+
the imported module, an exception) so they stay independent of any one command's
|
|
7
|
+
argument shape.
|
|
8
|
+
|
|
9
|
+
The SDK is imported lazily -- the rest of the CLI is stdlib-only, so a missing
|
|
10
|
+
`openai` must degrade to a hint and exit 2 rather than break `venice --help`.
|
|
11
|
+
Callers probe the import *first*, before building a client or fetching a
|
|
12
|
+
catalog, so the missing-SDK path never touches the network.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def import_openai(label: str):
|
|
20
|
+
"""Import the openai SDK lazily. None (after printing a hint) if absent.
|
|
21
|
+
|
|
22
|
+
`label` names the command in the hint (e.g. "chat").
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
import openai
|
|
26
|
+
except ImportError:
|
|
27
|
+
print(
|
|
28
|
+
f"venice {label} needs the openai package: "
|
|
29
|
+
'pip install "venice-cli[openai]" (or: pip install openai)',
|
|
30
|
+
file=sys.stderr,
|
|
31
|
+
)
|
|
32
|
+
return None
|
|
33
|
+
return openai
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_openai(module, client):
|
|
37
|
+
"""Build an SDK client pointed at Venice, borrowing the lean client's auth."""
|
|
38
|
+
return module.OpenAI(api_key=client.api_key, base_url=client.base_url)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def status_to_exit(module, e, label: str) -> int:
|
|
42
|
+
"""Map an openai SDK exception to a venice exit code.
|
|
43
|
+
|
|
44
|
+
`module` is the imported openai module (for its exception types) and `label`
|
|
45
|
+
prefixes the message (e.g. "chat").
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(e, module.APIConnectionError):
|
|
48
|
+
print(f"{label}: connection error: {e}", file=sys.stderr)
|
|
49
|
+
return 8
|
|
50
|
+
status = getattr(e, "status_code", None)
|
|
51
|
+
print(f"{label}: API error: {e}", file=sys.stderr)
|
|
52
|
+
if status == 401:
|
|
53
|
+
return 2
|
|
54
|
+
if status == 404:
|
|
55
|
+
return 6
|
|
56
|
+
if status == 429:
|
|
57
|
+
return 4
|
|
58
|
+
if isinstance(status, int) and 500 <= status < 600:
|
|
59
|
+
return 5
|
|
60
|
+
if isinstance(status, int) and 400 <= status < 500:
|
|
61
|
+
return 2
|
|
62
|
+
return 5
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Shared async-queue engine for Venice's quote -> queue -> poll -> retrieve
|
|
2
|
+
-> complete endpoints.
|
|
3
|
+
|
|
4
|
+
Endpoint-agnostic primitives factored out of `_audio` so audio (`sfx`/`music`)
|
|
5
|
+
and video can share one copy of the plumbing. Callers pass primitive args
|
|
6
|
+
(model, queue_id, paths, an extension map) so these helpers stay independent of
|
|
7
|
+
any one command's argument shape.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from .. import auth
|
|
17
|
+
from ..client import VeniceAPIError, build_client_from_auth
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_client():
|
|
21
|
+
try:
|
|
22
|
+
return build_client_from_auth(), 0
|
|
23
|
+
except auth.AuthError as e:
|
|
24
|
+
print(str(e), file=sys.stderr)
|
|
25
|
+
return None, 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ext_for(ctype: str, ext_map: dict, default: str = ".bin") -> Tuple[str, bool]:
|
|
29
|
+
"""Map a response content-type to a file extension.
|
|
30
|
+
|
|
31
|
+
Returns (ext, unknown): `unknown` is True when the type wasn't in the map,
|
|
32
|
+
in which case `default` is returned so the caller can warn.
|
|
33
|
+
"""
|
|
34
|
+
base = (ctype or "").split(";", 1)[0].strip().lower()
|
|
35
|
+
if base in ext_map:
|
|
36
|
+
return ext_map[base], False
|
|
37
|
+
return default, True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_output_path(
|
|
41
|
+
arg_output: Optional[Path], queue_id: str, ext: str, *, prefix: str
|
|
42
|
+
) -> Path:
|
|
43
|
+
short = (queue_id or "unknown")[:8]
|
|
44
|
+
default_name = f"{prefix}-{short}{ext}"
|
|
45
|
+
if arg_output is None:
|
|
46
|
+
return Path.cwd() / default_name
|
|
47
|
+
if arg_output.is_dir():
|
|
48
|
+
return arg_output / default_name
|
|
49
|
+
return arg_output
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def status_to_exit(err: VeniceAPIError) -> int:
|
|
53
|
+
s = err.status
|
|
54
|
+
if s == 422:
|
|
55
|
+
return 3
|
|
56
|
+
if s == 429:
|
|
57
|
+
return 4
|
|
58
|
+
if 500 <= s < 600:
|
|
59
|
+
return 5
|
|
60
|
+
if s == 404:
|
|
61
|
+
return 6
|
|
62
|
+
if s == 0:
|
|
63
|
+
return 8
|
|
64
|
+
return 2
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def progress_tick(start: float):
|
|
68
|
+
def _on(payload: dict) -> None:
|
|
69
|
+
avg_ms = payload.get("average_execution_time") or 0
|
|
70
|
+
elapsed_ms = payload.get("execution_duration") or 0
|
|
71
|
+
try:
|
|
72
|
+
avg_s = float(avg_ms) / 1000.0
|
|
73
|
+
except (TypeError, ValueError):
|
|
74
|
+
avg_s = 0.0
|
|
75
|
+
try:
|
|
76
|
+
el_s = float(elapsed_ms) / 1000.0
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
el_s = 0.0
|
|
79
|
+
wall = time.monotonic() - start
|
|
80
|
+
if avg_s > 0:
|
|
81
|
+
sys.stderr.write(
|
|
82
|
+
f"\r[wall {wall:5.1f}s | server {el_s:5.1f}s / ~{avg_s:5.1f}s] processing..."
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
sys.stderr.write(f"\r[wall {wall:5.1f}s] processing...")
|
|
86
|
+
sys.stderr.flush()
|
|
87
|
+
return _on
|