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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Shared UX/budget rails for spend-incurring commands.
|
|
2
|
+
|
|
3
|
+
Extracted from `image` so it, `upscale`, and `bg-remove` share one copy of the
|
|
4
|
+
estimate/confirm/status/write plumbing rather than each carrying its own. These
|
|
5
|
+
helpers take primitive args (cost, max_spend, err, byte blobs) so they stay
|
|
6
|
+
independent of any one command's argument shape.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from .. import billing
|
|
15
|
+
from ..client import VeniceAPIError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_output(arg_output: Optional[Path], default_name: str) -> Path:
|
|
19
|
+
"""Pick an output path: an explicit file, a file inside an explicit dir, or
|
|
20
|
+
`default_name` in the cwd."""
|
|
21
|
+
if arg_output is None:
|
|
22
|
+
return Path.cwd() / default_name
|
|
23
|
+
if arg_output.is_dir():
|
|
24
|
+
return arg_output / default_name
|
|
25
|
+
return arg_output
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_estimate(cost: Optional[float], label: str) -> None:
|
|
29
|
+
if cost is None:
|
|
30
|
+
print(f"Estimated cost: (unknown — {label})", file=sys.stderr)
|
|
31
|
+
else:
|
|
32
|
+
print(
|
|
33
|
+
f"Estimated cost: {billing.format_usd(cost)} ({label})",
|
|
34
|
+
file=sys.stderr,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def print_balance_and_remaining(client, cost: Optional[float], *, show: bool) -> None:
|
|
39
|
+
if not show:
|
|
40
|
+
return
|
|
41
|
+
try:
|
|
42
|
+
info = billing.fetch_balance(client)
|
|
43
|
+
except VeniceAPIError:
|
|
44
|
+
info = None
|
|
45
|
+
if not info or info.get("total") is None:
|
|
46
|
+
return
|
|
47
|
+
print(f"Balance: {billing.format_balance_breakdown(info)}", file=sys.stderr)
|
|
48
|
+
if cost is not None:
|
|
49
|
+
try:
|
|
50
|
+
remaining = float(info["total"]) - float(cost)
|
|
51
|
+
print(f"After charge: {billing.format_usd(remaining)}", file=sys.stderr)
|
|
52
|
+
except (TypeError, ValueError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def over_budget(cost: Optional[float], max_spend: Optional[float]) -> bool:
|
|
57
|
+
if max_spend is None or cost is None:
|
|
58
|
+
return False
|
|
59
|
+
try:
|
|
60
|
+
return float(cost) > float(max_spend)
|
|
61
|
+
except (TypeError, ValueError):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def confirm_or_exit(yes: bool) -> Optional[int]:
|
|
66
|
+
if yes:
|
|
67
|
+
return None
|
|
68
|
+
if not sys.stdin.isatty():
|
|
69
|
+
print("non-interactive; pass --yes to confirm the charge.", file=sys.stderr)
|
|
70
|
+
return 1
|
|
71
|
+
try:
|
|
72
|
+
ans = input("Proceed? [y/N] ").strip().lower()
|
|
73
|
+
except EOFError:
|
|
74
|
+
ans = ""
|
|
75
|
+
if ans not in ("y", "yes"):
|
|
76
|
+
print("aborted by user", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def status_to_exit(err: VeniceAPIError) -> int:
|
|
82
|
+
s = err.status
|
|
83
|
+
if s == 400:
|
|
84
|
+
return 2
|
|
85
|
+
if s == 402:
|
|
86
|
+
return 1 # insufficient balance ~ declined
|
|
87
|
+
if s == 422:
|
|
88
|
+
return 3
|
|
89
|
+
if s == 429:
|
|
90
|
+
return 4
|
|
91
|
+
if s == 503:
|
|
92
|
+
return 5
|
|
93
|
+
if 500 <= s < 600:
|
|
94
|
+
return 5
|
|
95
|
+
if s == 404:
|
|
96
|
+
return 6
|
|
97
|
+
if s == 0:
|
|
98
|
+
return 8
|
|
99
|
+
return 2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_bytes_outputs(blobs: List[bytes], paths: List[Path]) -> Optional[int]:
|
|
103
|
+
"""Write byte blobs to paths. Returns an exit code on failure, else None."""
|
|
104
|
+
for data, path in zip(blobs, paths):
|
|
105
|
+
try:
|
|
106
|
+
path.write_bytes(data)
|
|
107
|
+
except OSError as e:
|
|
108
|
+
print(f"could not write {path}: {e}", file=sys.stderr)
|
|
109
|
+
return 9
|
|
110
|
+
abs_path = path.resolve()
|
|
111
|
+
print(str(abs_path))
|
|
112
|
+
print(f"wrote {len(data)} bytes to {abs_path}", file=sys.stderr)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def post_binary_op(client, endpoint: str, body: dict, out_path: Path, label: str) -> int:
|
|
117
|
+
"""POST a JSON body to an endpoint that returns raw image bytes and write
|
|
118
|
+
them to `out_path`.
|
|
119
|
+
|
|
120
|
+
Returns an exit code (0 on success). API errors map through
|
|
121
|
+
`status_to_exit`; a JSON (non-image) 200 is treated as an unexpected
|
|
122
|
+
server response. `label` prefixes error messages (e.g. "upscale").
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
_ctype, payload = client.post_for_bytes_or_json(endpoint, body)
|
|
126
|
+
except VeniceAPIError as e:
|
|
127
|
+
print(f"{label} failed: {e}", file=sys.stderr)
|
|
128
|
+
return status_to_exit(e)
|
|
129
|
+
if isinstance(payload, (bytes, bytearray)):
|
|
130
|
+
return write_bytes_outputs([bytes(payload)], [out_path]) or 0
|
|
131
|
+
print(f"{label}: unexpected non-image response: {payload!r}", file=sys.stderr)
|
|
132
|
+
return 5
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""`venice balance` -- show current account balance and tier."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .. import auth
|
|
8
|
+
from ..billing import SPEND_ORDER, fetch_balance, format_balance_breakdown, format_usd
|
|
9
|
+
from ..client import VeniceAPIError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(subparsers) -> None:
|
|
13
|
+
p = subparsers.add_parser(
|
|
14
|
+
"balance",
|
|
15
|
+
help="Show current spendable balance (USD + DIEM credit).",
|
|
16
|
+
description=(
|
|
17
|
+
"Queries /api_keys/rate_limits. Spendable balance = USD + DIEM "
|
|
18
|
+
"(1 DIEM = $1 of purchasing power). Default: prints combined "
|
|
19
|
+
"total '$X.XX USD' to stdout. --json for raw, --verbose for "
|
|
20
|
+
"tier + breakdown + epoch. --min sets a floor: exits 1 if total "
|
|
21
|
+
"is below it (script-friendly)."
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
p.add_argument(
|
|
25
|
+
"--json",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help="Dump raw {USD, DIEM, tier, next_epoch, key_expires} to stdout.",
|
|
28
|
+
)
|
|
29
|
+
p.add_argument(
|
|
30
|
+
"--verbose",
|
|
31
|
+
"-v",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help="Print a human-readable multi-line summary.",
|
|
34
|
+
)
|
|
35
|
+
p.add_argument(
|
|
36
|
+
"--min",
|
|
37
|
+
type=float,
|
|
38
|
+
default=None,
|
|
39
|
+
metavar="USD",
|
|
40
|
+
help="Exit 1 if balance is below this USD threshold.",
|
|
41
|
+
)
|
|
42
|
+
p.set_defaults(handler=_run)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run(args) -> int:
|
|
46
|
+
try:
|
|
47
|
+
from ..client import build_client_from_auth
|
|
48
|
+
client = build_client_from_auth()
|
|
49
|
+
except auth.AuthError as e:
|
|
50
|
+
print(str(e), file=sys.stderr)
|
|
51
|
+
return 2
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
info = fetch_balance(client)
|
|
55
|
+
except VeniceAPIError as e:
|
|
56
|
+
print(f"balance: API error: {e}", file=sys.stderr)
|
|
57
|
+
if e.status == 401:
|
|
58
|
+
return 2
|
|
59
|
+
if e.status == 429:
|
|
60
|
+
return 4
|
|
61
|
+
return 5
|
|
62
|
+
|
|
63
|
+
if info is None:
|
|
64
|
+
print("balance: API returned no data block", file=sys.stderr)
|
|
65
|
+
return 5
|
|
66
|
+
|
|
67
|
+
usd = info.get("usd")
|
|
68
|
+
diem = info.get("diem")
|
|
69
|
+
total = info.get("total")
|
|
70
|
+
tier = info.get("tier")
|
|
71
|
+
next_epoch = info.get("next_epoch")
|
|
72
|
+
key_exp = info.get("key_expires")
|
|
73
|
+
|
|
74
|
+
if args.json:
|
|
75
|
+
json.dump(
|
|
76
|
+
{
|
|
77
|
+
"USD": usd,
|
|
78
|
+
"DIEM": diem,
|
|
79
|
+
"total_usd_equiv": total,
|
|
80
|
+
"tier": tier,
|
|
81
|
+
"next_epoch": next_epoch,
|
|
82
|
+
"key_expires": key_exp,
|
|
83
|
+
"spend_order": list(SPEND_ORDER),
|
|
84
|
+
"notes": {
|
|
85
|
+
"DIEM": "epoch allowance from staked DIEM; resets at next_epoch",
|
|
86
|
+
"BUNDLED_CREDITS": "monthly bundle; drains before USD cash; not exposed via inference key",
|
|
87
|
+
"VCU": "tier compute units; not exposed via inference key",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
sys.stdout,
|
|
91
|
+
indent=2,
|
|
92
|
+
)
|
|
93
|
+
sys.stdout.write("\n")
|
|
94
|
+
elif args.verbose:
|
|
95
|
+
print(f"Tier: {tier or 'unknown'}")
|
|
96
|
+
print(f"Spendable: {format_balance_breakdown(info)}")
|
|
97
|
+
if diem is not None:
|
|
98
|
+
try:
|
|
99
|
+
print(
|
|
100
|
+
f" DIEM allow.: {float(diem):.4f} "
|
|
101
|
+
f"(this epoch; resets {next_epoch or '?'})"
|
|
102
|
+
)
|
|
103
|
+
except (TypeError, ValueError):
|
|
104
|
+
print(f" DIEM allow.: {diem}")
|
|
105
|
+
print(f" monthly: (not exposed via inference key; drains before cash)")
|
|
106
|
+
print(f" USD cash: {format_usd(usd)}")
|
|
107
|
+
print(f"Spend order: {' -> '.join(SPEND_ORDER)}")
|
|
108
|
+
print(f"Key expires: {key_exp or 'never'}")
|
|
109
|
+
else:
|
|
110
|
+
print(format_usd(total))
|
|
111
|
+
|
|
112
|
+
if args.min is not None and total is not None:
|
|
113
|
+
try:
|
|
114
|
+
if float(total) < float(args.min):
|
|
115
|
+
print(
|
|
116
|
+
f"balance: total {format_usd(total)} is below floor "
|
|
117
|
+
f"{format_usd(args.min)}",
|
|
118
|
+
file=sys.stderr,
|
|
119
|
+
)
|
|
120
|
+
return 1
|
|
121
|
+
except (TypeError, ValueError):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
return 0
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""`venice bg-remove` -- strip an image's background via /image/background-remove.
|
|
2
|
+
|
|
3
|
+
Venice's generate call treats `background: transparent` as a no-op, so
|
|
4
|
+
transparent assets (e.g. rank insignia) are made opaque then run through this
|
|
5
|
+
endpoint, which returns a PNG with a transparent background. The source is
|
|
6
|
+
either a local file (sent as base64 in a JSON body) or an image URL. The
|
|
7
|
+
response is raw image/png bytes, so we use the client's bytes-or-json path.
|
|
8
|
+
Pricing is dynamic ($0.001-$10.00/call); the balance is shown and you confirm.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from .. import auth
|
|
18
|
+
from ..client import build_client_from_auth
|
|
19
|
+
from ._shared import (
|
|
20
|
+
confirm_or_exit,
|
|
21
|
+
over_budget,
|
|
22
|
+
post_binary_op,
|
|
23
|
+
print_balance_and_remaining,
|
|
24
|
+
print_estimate,
|
|
25
|
+
resolve_output,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
ENDPOINT = "/image/background-remove"
|
|
29
|
+
MAX_INPUT_BYTES = 25 * 1024 * 1024 # API limit: input file < 25 MB
|
|
30
|
+
URL_DEFAULT_NAME = "venice-nobg.png"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register(subparsers) -> None:
|
|
34
|
+
p = subparsers.add_parser(
|
|
35
|
+
"bg-remove",
|
|
36
|
+
help="Remove an image's background via /image/background-remove.",
|
|
37
|
+
description=(
|
|
38
|
+
"Returns a PNG with a transparent background. Source is a local file "
|
|
39
|
+
"(positional) or --image-url. Use for assets that need alpha, e.g. "
|
|
40
|
+
"`venice bg-remove insignia.png`. Pricing is dynamic; the balance is "
|
|
41
|
+
"shown and you confirm before the charge."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
src = p.add_mutually_exclusive_group()
|
|
45
|
+
src.add_argument("input", type=Path, nargs="?", default=None,
|
|
46
|
+
help="Image file whose background to remove.")
|
|
47
|
+
src.add_argument("--image-url", default=None, metavar="URL",
|
|
48
|
+
help="Remove the background from an image at this URL instead.")
|
|
49
|
+
p.add_argument("--output", "-o", type=Path, default=None,
|
|
50
|
+
help="Output file or directory. Default: cwd/<input>-nobg.png "
|
|
51
|
+
f"(or {URL_DEFAULT_NAME} for --image-url).")
|
|
52
|
+
p.add_argument("--yes", "-y", action="store_true")
|
|
53
|
+
p.add_argument("--dry-run", action="store_true",
|
|
54
|
+
help="Show the planned output and exit; don't call the API.")
|
|
55
|
+
p.add_argument(
|
|
56
|
+
"--max-spend",
|
|
57
|
+
type=float,
|
|
58
|
+
default=None,
|
|
59
|
+
metavar="USD",
|
|
60
|
+
help="Refuse if the estimated cost exceeds this cap. Note: bg-remove "
|
|
61
|
+
"pricing is dynamic, so no pre-charge estimate is available.",
|
|
62
|
+
)
|
|
63
|
+
p.add_argument("--no-balance", action="store_true",
|
|
64
|
+
help="Skip the upfront balance display.")
|
|
65
|
+
p.set_defaults(handler=_run)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate(args) -> Optional[int]:
|
|
69
|
+
if (args.input is None) == (args.image_url is None):
|
|
70
|
+
print("bg-remove: provide exactly one of INPUT file or --image-url",
|
|
71
|
+
file=sys.stderr)
|
|
72
|
+
return 2
|
|
73
|
+
if args.input is not None:
|
|
74
|
+
inp = args.input
|
|
75
|
+
if not inp.is_file():
|
|
76
|
+
print(f"bg-remove: input file not found: {inp}", file=sys.stderr)
|
|
77
|
+
return 2
|
|
78
|
+
size = inp.stat().st_size
|
|
79
|
+
if size == 0:
|
|
80
|
+
print(f"bg-remove: input {inp} is empty", file=sys.stderr)
|
|
81
|
+
return 2
|
|
82
|
+
if size > MAX_INPUT_BYTES:
|
|
83
|
+
print(f"bg-remove: input {inp} is {size} bytes; must be < 25 MB",
|
|
84
|
+
file=sys.stderr)
|
|
85
|
+
return 2
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _build_body(args) -> dict:
|
|
90
|
+
if args.input is not None:
|
|
91
|
+
b64 = base64.b64encode(args.input.read_bytes()).decode("ascii")
|
|
92
|
+
return {"image": b64}
|
|
93
|
+
return {"image_url": args.image_url}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run(args) -> int:
|
|
97
|
+
rc = _validate(args)
|
|
98
|
+
if rc is not None:
|
|
99
|
+
return rc
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
client = build_client_from_auth()
|
|
103
|
+
except auth.AuthError as e:
|
|
104
|
+
print(str(e), file=sys.stderr)
|
|
105
|
+
return 2
|
|
106
|
+
|
|
107
|
+
cost = None # dynamic pricing -- no reliable upfront quote
|
|
108
|
+
print_estimate(cost, "background removal; dynamic $0.001-$10.00/call")
|
|
109
|
+
print_balance_and_remaining(client, cost, show=not args.no_balance)
|
|
110
|
+
if over_budget(cost, args.max_spend): # no-op while cost is unknown
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
default_name = (
|
|
114
|
+
f"{args.input.stem}-nobg.png" if args.input is not None else URL_DEFAULT_NAME
|
|
115
|
+
)
|
|
116
|
+
out_path = resolve_output(args.output, default_name)
|
|
117
|
+
if args.dry_run:
|
|
118
|
+
print(f"would write: {out_path.resolve()}", file=sys.stderr)
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
rc = confirm_or_exit(args.yes)
|
|
122
|
+
if rc is not None:
|
|
123
|
+
return rc
|
|
124
|
+
|
|
125
|
+
body = _build_body(args)
|
|
126
|
+
return post_binary_op(client, ENDPOINT, body, out_path, "bg-remove")
|
venice/commands/chat.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""`venice chat` -- one-shot /chat/completions.
|
|
2
|
+
|
|
3
|
+
Built on the official `openai` SDK (Venice is OpenAI-compatible; the SDK is
|
|
4
|
+
lazy-imported so the rest of the stdlib-only CLI works without it). Venice's
|
|
5
|
+
own chat extensions -- web search, characters, thinking control, etc. -- are
|
|
6
|
+
surfaced as flags and passed through `extra_body={"venice_parameters": ...}`.
|
|
7
|
+
|
|
8
|
+
The free `/models?type=text` catalog GET (via the lean urllib client) is used to
|
|
9
|
+
validate `--model` and resolve a default before the paid completion call --
|
|
10
|
+
mirrors the guard pattern in `venice music`.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from .. import auth
|
|
19
|
+
from ..client import build_client_from_auth
|
|
20
|
+
from . import _models, _openai
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register(subparsers) -> None:
|
|
24
|
+
p = subparsers.add_parser(
|
|
25
|
+
"chat",
|
|
26
|
+
help="One-shot chat completion (/chat/completions).",
|
|
27
|
+
description=(
|
|
28
|
+
"Send a single message to a Venice text model and print the reply. "
|
|
29
|
+
"Reads the message from the argument, or from stdin when it is '-' "
|
|
30
|
+
"or piped. Supports Venice extensions: web search, characters, and "
|
|
31
|
+
"reasoning-model thinking control."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
p.add_argument(
|
|
35
|
+
"message",
|
|
36
|
+
nargs="?",
|
|
37
|
+
help="User message. Use '-' (or pipe stdin) to read from stdin.",
|
|
38
|
+
)
|
|
39
|
+
p.add_argument("--system", "-s", default=None, help="Optional system prompt.")
|
|
40
|
+
p.add_argument(
|
|
41
|
+
"--model",
|
|
42
|
+
"-m",
|
|
43
|
+
default=None,
|
|
44
|
+
help="Text model id (default: the catalog's 'default'-trait model).",
|
|
45
|
+
)
|
|
46
|
+
p.add_argument("--temperature", "-t", type=float, default=None)
|
|
47
|
+
p.add_argument("--max-tokens", type=int, default=None, dest="max_tokens")
|
|
48
|
+
|
|
49
|
+
stream_grp = p.add_mutually_exclusive_group()
|
|
50
|
+
stream_grp.add_argument(
|
|
51
|
+
"--stream", dest="stream", action="store_true", default=True,
|
|
52
|
+
help="Stream the reply incrementally (default).",
|
|
53
|
+
)
|
|
54
|
+
stream_grp.add_argument(
|
|
55
|
+
"--no-stream", dest="stream", action="store_false",
|
|
56
|
+
help="Wait for the full reply, then print it.",
|
|
57
|
+
)
|
|
58
|
+
p.add_argument(
|
|
59
|
+
"--json", action="store_true",
|
|
60
|
+
help="Print the raw response object (forces --no-stream).",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# --- Venice extensions -> venice_parameters ---
|
|
64
|
+
ext = p.add_argument_group("Venice extensions")
|
|
65
|
+
ext.add_argument(
|
|
66
|
+
"--web-search", choices=("auto", "on", "off"), default=None,
|
|
67
|
+
dest="web_search", help="Enable Venice web search (default: off).",
|
|
68
|
+
)
|
|
69
|
+
ext.add_argument(
|
|
70
|
+
"--web-citations", action="store_true", dest="web_citations",
|
|
71
|
+
help="Ask the model to cite web sources (with --web-search).",
|
|
72
|
+
)
|
|
73
|
+
ext.add_argument(
|
|
74
|
+
"--web-scraping", action="store_true", dest="web_scraping",
|
|
75
|
+
help="Scrape URLs in the message via Firecrawl.",
|
|
76
|
+
)
|
|
77
|
+
ext.add_argument(
|
|
78
|
+
"--character", default=None, metavar="SLUG",
|
|
79
|
+
help="Use a public Venice character (its Public ID slug).",
|
|
80
|
+
)
|
|
81
|
+
ext.add_argument(
|
|
82
|
+
"--no-venice-system-prompt", action="store_true",
|
|
83
|
+
dest="no_venice_system_prompt",
|
|
84
|
+
help="Omit Venice's supplied system prompt (default: included).",
|
|
85
|
+
)
|
|
86
|
+
ext.add_argument(
|
|
87
|
+
"--strip-thinking", action="store_true", dest="strip_thinking",
|
|
88
|
+
help="Strip <think> blocks from reasoning-model output.",
|
|
89
|
+
)
|
|
90
|
+
ext.add_argument(
|
|
91
|
+
"--no-thinking", action="store_true", dest="no_thinking",
|
|
92
|
+
help="Disable thinking on supported reasoning models.",
|
|
93
|
+
)
|
|
94
|
+
ext.add_argument(
|
|
95
|
+
"--x-search", action="store_true", dest="x_search",
|
|
96
|
+
help="Enable xAI web+X search (extra ~$0.01/search; grok models).",
|
|
97
|
+
)
|
|
98
|
+
p.set_defaults(handler=_run)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _resolve_message(args) -> Optional[str]:
|
|
102
|
+
"""Positional message, or stdin when '-' / piped. None if nothing given."""
|
|
103
|
+
msg = args.message
|
|
104
|
+
if msg == "-" or (msg is None and not sys.stdin.isatty()):
|
|
105
|
+
data = sys.stdin.read().strip()
|
|
106
|
+
return data or None
|
|
107
|
+
return msg
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _venice_parameters(args) -> dict:
|
|
111
|
+
"""Assemble venice_parameters from the extension flags (only set keys)."""
|
|
112
|
+
vp: dict = {}
|
|
113
|
+
if args.web_search is not None:
|
|
114
|
+
vp["enable_web_search"] = args.web_search
|
|
115
|
+
if args.web_citations:
|
|
116
|
+
vp["enable_web_citations"] = True
|
|
117
|
+
if args.web_scraping:
|
|
118
|
+
vp["enable_web_scraping"] = True
|
|
119
|
+
if args.character:
|
|
120
|
+
vp["character_slug"] = args.character
|
|
121
|
+
if args.no_venice_system_prompt:
|
|
122
|
+
vp["include_venice_system_prompt"] = False
|
|
123
|
+
if args.strip_thinking:
|
|
124
|
+
vp["strip_thinking_response"] = True
|
|
125
|
+
if args.no_thinking:
|
|
126
|
+
vp["disable_thinking"] = True
|
|
127
|
+
if args.x_search:
|
|
128
|
+
vp["enable_x_search"] = True
|
|
129
|
+
return vp
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_kwargs(args, model: str, message: str) -> dict:
|
|
133
|
+
messages = []
|
|
134
|
+
if args.system:
|
|
135
|
+
messages.append({"role": "system", "content": args.system})
|
|
136
|
+
messages.append({"role": "user", "content": message})
|
|
137
|
+
|
|
138
|
+
kwargs: dict = {"model": model, "messages": messages}
|
|
139
|
+
if args.temperature is not None:
|
|
140
|
+
kwargs["temperature"] = args.temperature
|
|
141
|
+
if args.max_tokens is not None:
|
|
142
|
+
kwargs["max_tokens"] = args.max_tokens
|
|
143
|
+
vp = _venice_parameters(args)
|
|
144
|
+
if vp:
|
|
145
|
+
kwargs["extra_body"] = {"venice_parameters": vp}
|
|
146
|
+
return kwargs
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _as_dict(value) -> Optional[dict]:
|
|
150
|
+
if value is None:
|
|
151
|
+
return None
|
|
152
|
+
if hasattr(value, "model_dump"):
|
|
153
|
+
return value.model_dump()
|
|
154
|
+
if isinstance(value, dict):
|
|
155
|
+
return value
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _print_citations(venice_params) -> None:
|
|
160
|
+
vp = _as_dict(venice_params)
|
|
161
|
+
if not vp:
|
|
162
|
+
return
|
|
163
|
+
cites = vp.get("web_search_citations")
|
|
164
|
+
if not isinstance(cites, list) or not cites:
|
|
165
|
+
return
|
|
166
|
+
print("\nSources:", file=sys.stderr)
|
|
167
|
+
for i, c in enumerate(cites, 1):
|
|
168
|
+
cd = _as_dict(c) or {}
|
|
169
|
+
title = cd.get("title", "")
|
|
170
|
+
url = cd.get("url", "")
|
|
171
|
+
date = cd.get("date")
|
|
172
|
+
line = f" [{i}] {title} -- {url}"
|
|
173
|
+
if date:
|
|
174
|
+
line += f" ({date})"
|
|
175
|
+
print(line, file=sys.stderr)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _print_usage(usage) -> None:
|
|
179
|
+
u = _as_dict(usage)
|
|
180
|
+
if not u:
|
|
181
|
+
return
|
|
182
|
+
pt = u.get("prompt_tokens")
|
|
183
|
+
ct = u.get("completion_tokens")
|
|
184
|
+
tt = u.get("total_tokens")
|
|
185
|
+
if tt is not None:
|
|
186
|
+
print(f"usage: prompt={pt} completion={ct} total={tt}", file=sys.stderr)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run(args) -> int:
|
|
190
|
+
message = _resolve_message(args)
|
|
191
|
+
if not message:
|
|
192
|
+
print("chat: no message (pass an argument or pipe stdin)", file=sys.stderr)
|
|
193
|
+
return 2
|
|
194
|
+
|
|
195
|
+
openai = _openai.import_openai("chat")
|
|
196
|
+
if openai is None:
|
|
197
|
+
return 2
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
client = build_client_from_auth()
|
|
201
|
+
except auth.AuthError as e:
|
|
202
|
+
print(str(e), file=sys.stderr)
|
|
203
|
+
return 2
|
|
204
|
+
|
|
205
|
+
models = _models.catalog(client, "text")
|
|
206
|
+
model, rc = _models.resolve_model(
|
|
207
|
+
args.model, models, label="chat", noun="text model"
|
|
208
|
+
)
|
|
209
|
+
if rc is not None:
|
|
210
|
+
return rc
|
|
211
|
+
|
|
212
|
+
oai = _openai.build_openai(openai, client)
|
|
213
|
+
kwargs = _build_kwargs(args, model, message)
|
|
214
|
+
|
|
215
|
+
stream = args.stream and not args.json
|
|
216
|
+
try:
|
|
217
|
+
if stream:
|
|
218
|
+
return _run_stream(oai, kwargs)
|
|
219
|
+
return _run_once(oai, kwargs, args.json)
|
|
220
|
+
except openai.OpenAIError as e:
|
|
221
|
+
return _openai.status_to_exit(openai, e, "chat")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _run_once(oai, kwargs: dict, as_json: bool) -> int:
|
|
225
|
+
resp = oai.chat.completions.create(**kwargs)
|
|
226
|
+
if as_json:
|
|
227
|
+
json.dump(resp.model_dump(), sys.stdout, indent=2, default=str)
|
|
228
|
+
sys.stdout.write("\n")
|
|
229
|
+
return 0
|
|
230
|
+
content = ""
|
|
231
|
+
if resp.choices:
|
|
232
|
+
content = resp.choices[0].message.content or ""
|
|
233
|
+
print(content)
|
|
234
|
+
_print_citations(getattr(resp, "venice_parameters", None))
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _run_stream(oai, kwargs: dict) -> int:
|
|
239
|
+
kwargs = dict(kwargs)
|
|
240
|
+
kwargs["stream"] = True
|
|
241
|
+
kwargs["stream_options"] = {"include_usage": True}
|
|
242
|
+
stream = oai.chat.completions.create(**kwargs)
|
|
243
|
+
|
|
244
|
+
citations = None
|
|
245
|
+
usage = None
|
|
246
|
+
wrote_any = False
|
|
247
|
+
for chunk in stream:
|
|
248
|
+
vp = getattr(chunk, "venice_parameters", None)
|
|
249
|
+
if vp is not None and citations is None:
|
|
250
|
+
citations = vp
|
|
251
|
+
if getattr(chunk, "usage", None):
|
|
252
|
+
usage = chunk.usage
|
|
253
|
+
if chunk.choices:
|
|
254
|
+
piece = getattr(chunk.choices[0].delta, "content", None)
|
|
255
|
+
if piece:
|
|
256
|
+
sys.stdout.write(piece)
|
|
257
|
+
sys.stdout.flush()
|
|
258
|
+
wrote_any = True
|
|
259
|
+
if wrote_any:
|
|
260
|
+
sys.stdout.write("\n")
|
|
261
|
+
_print_citations(citations)
|
|
262
|
+
_print_usage(usage)
|
|
263
|
+
return 0
|