venice-cli 0.77.0__tar.gz → 0.78.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.
- {venice_cli-0.77.0 → venice_cli-0.78.0}/PKG-INFO +13 -5
- {venice_cli-0.77.0 → venice_cli-0.78.0}/README.md +10 -2
- {venice_cli-0.77.0 → venice_cli-0.78.0}/pyproject.toml +7 -1
- venice_cli-0.78.0/src/venice/__init__.py +1 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/cli.py +11 -1
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_agent.py +136 -10
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_repl.py +55 -34
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/config.py +8 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/_drive.py +9 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_agent.py +194 -0
- venice_cli-0.78.0/tests/test_cli.py +59 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_code_command.py +20 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_drive_cli.py +65 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_repl.py +128 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_sessions.py +33 -0
- venice_cli-0.77.0/src/venice/__init__.py +0 -1
- {venice_cli-0.77.0 → venice_cli-0.78.0}/.gitignore +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/CONTRIBUTING.md +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/LICENSE +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/Makefile +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/SECURITY.md +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/bin/venice +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/install.sh +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/__main__.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/audio_player.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/audio_post.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/auth.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/billing.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/client.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/__init__.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_audio.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_browser.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_code.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_compact.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_exec.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_index.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_mailbox.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_mcp.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_mcp_client.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_memory.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_models.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_openai.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_persona.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_queue.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_review.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_session.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_shared.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/_steer.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/balance.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/bg_remove.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/chat.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/code.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/completion.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/config.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/contact_sheet.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/embed.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/image.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/image_edit.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/index.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/login.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/master.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/mcp_serve.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/memory.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/models.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/music.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/review.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/search.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/secret.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/sessions.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/sfx.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/tts.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/upscale.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/commands/video.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/image_montage.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/mcp_server.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/src/venice/userconfig.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/__init__.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/_mcp_fake_server.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/_venice_fake_server.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_auth.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_balance.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_bg_remove.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_browser.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_chat.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_client.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_code.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_compact.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_completion.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_config.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_contact_sheet.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_embed.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_exec.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_fake_server.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_image.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_image_edit.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_index.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_login.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_mailbox.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_master.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_mcp_client.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_mcp_serve.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_mcp_tools.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_memory.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_models.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_music.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_persona.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_planner.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_profiles.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_review.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_search.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_secret.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_sfx.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_shared.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_shared_openai.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_spawn.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_steer.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_tts.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_upscale.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_video.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/tests/test_web_search.py +0 -0
- {venice_cli-0.77.0 → venice_cli-0.78.0}/uninstall.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: venice-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.78.0
|
|
4
4
|
Summary: A stdlib-only Python CLI for the Venice.ai API: audio, video, images, chat, and embeddings.
|
|
5
5
|
Project-URL: Homepage, https://github.com/gobha-me/venice-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/gobha-me/venice-cli
|
|
@@ -28,10 +28,10 @@ Classifier: Topic :: Multimedia :: Sound/Audio
|
|
|
28
28
|
Classifier: Topic :: Utilities
|
|
29
29
|
Requires-Python: >=3.9
|
|
30
30
|
Provides-Extra: all
|
|
31
|
-
Requires-Dist: mcp
|
|
31
|
+
Requires-Dist: mcp<2,>=1.2; (python_version >= '3.10') and extra == 'all'
|
|
32
32
|
Requires-Dist: openai>=1.40; extra == 'all'
|
|
33
33
|
Provides-Extra: mcp
|
|
34
|
-
Requires-Dist: mcp
|
|
34
|
+
Requires-Dist: mcp<2,>=1.2; (python_version >= '3.10') and extra == 'mcp'
|
|
35
35
|
Provides-Extra: openai
|
|
36
36
|
Requires-Dist: openai>=1.40; extra == 'openai'
|
|
37
37
|
Provides-Extra: test
|
|
@@ -614,7 +614,10 @@ message, keeping the last `N` turns verbatim),
|
|
|
614
614
|
`/cost` (this session's estimated spend so far; `--session-max-spend` adds a
|
|
615
615
|
cap), `/usage` (a token + cost breakdown for the session, keeping the
|
|
616
616
|
cache-read/cache-write/uncached input split distinct so cache-heavy sessions
|
|
617
|
-
cost out correctly
|
|
617
|
+
cost out correctly — the split and the hit rate report `n/a` when the model's
|
|
618
|
+
usage block carried no cache fields at all, so a printed `0.0%` always means the
|
|
619
|
+
provider reported a real zero rather than that nobody looked — plus a
|
|
620
|
+
**wall-clock** row — total, turn count and average
|
|
618
621
|
for the time the CLI kept you waiting, measured from submitting a turn to
|
|
619
622
|
getting the prompt back, so thinking time at the prompt is never counted; it
|
|
620
623
|
accumulates across `--resume`), `/reset` (clear history, keep the system prompt),
|
|
@@ -624,7 +627,11 @@ accumulates across `--resume`), `/reset` (clear history, keep the system prompt)
|
|
|
624
627
|
`git commit`, optionally pre-seeded with `text`),
|
|
625
628
|
`/help`, and `/exit` (or `/quit`, or Ctrl-D). Ctrl-C aborts the current turn
|
|
626
629
|
without ending the session (while a tool-loop turn is running it first pauses to
|
|
627
|
-
steer — see [Mid-run steering](#mid-run-steering-venice-sessions-send))
|
|
630
|
+
steer — see [Mid-run steering](#mid-run-steering-venice-sessions-send)); at the
|
|
631
|
+
prompt it just discards the half-typed line and re-prompts. However you leave —
|
|
632
|
+
`/exit`, Ctrl-D, or a Ctrl-C that lands somewhere unexpected — the session is
|
|
633
|
+
flushed to disk first, so slash-command edits like `/model` and `/system` are
|
|
634
|
+
never lost to the way you quit. Tab completes slash-commands (and model ids after
|
|
628
635
|
`/model `, persona names after `/persona `). At a per-tool confirmation prompt, `a` accepts that call **and**
|
|
629
636
|
auto-accepts the rest of the run. `--max-tool-calls 0` runs until the model
|
|
630
637
|
stops on its own (instead of capping at the default and asking to continue).
|
|
@@ -1896,6 +1903,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
|
|
|
1896
1903
|
| `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for a self-signed embedding backend (`embed`, `index`, `search`, and the `project_search` agent tool) |
|
|
1897
1904
|
| `VENICE_MCP_MAX_SPEND` | `mcp-serve` auto-approve cap in USD (default `0.10`) |
|
|
1898
1905
|
| `VENICE_MCP_OUTPUT_DIR` | where `mcp-serve` tools write files (default: cwd) |
|
|
1906
|
+
| `VENICE_USAGE_RAW` | set to `1`/`true`/`yes`/`on` to echo each API response's raw `usage` block to **stderr** as one `usage-raw: {...}` line (diagnostic; stdout stays machine-readable) |
|
|
1899
1907
|
|
|
1900
1908
|
## Commands at a glance
|
|
1901
1909
|
|
|
@@ -574,7 +574,10 @@ message, keeping the last `N` turns verbatim),
|
|
|
574
574
|
`/cost` (this session's estimated spend so far; `--session-max-spend` adds a
|
|
575
575
|
cap), `/usage` (a token + cost breakdown for the session, keeping the
|
|
576
576
|
cache-read/cache-write/uncached input split distinct so cache-heavy sessions
|
|
577
|
-
cost out correctly
|
|
577
|
+
cost out correctly — the split and the hit rate report `n/a` when the model's
|
|
578
|
+
usage block carried no cache fields at all, so a printed `0.0%` always means the
|
|
579
|
+
provider reported a real zero rather than that nobody looked — plus a
|
|
580
|
+
**wall-clock** row — total, turn count and average
|
|
578
581
|
for the time the CLI kept you waiting, measured from submitting a turn to
|
|
579
582
|
getting the prompt back, so thinking time at the prompt is never counted; it
|
|
580
583
|
accumulates across `--resume`), `/reset` (clear history, keep the system prompt),
|
|
@@ -584,7 +587,11 @@ accumulates across `--resume`), `/reset` (clear history, keep the system prompt)
|
|
|
584
587
|
`git commit`, optionally pre-seeded with `text`),
|
|
585
588
|
`/help`, and `/exit` (or `/quit`, or Ctrl-D). Ctrl-C aborts the current turn
|
|
586
589
|
without ending the session (while a tool-loop turn is running it first pauses to
|
|
587
|
-
steer — see [Mid-run steering](#mid-run-steering-venice-sessions-send))
|
|
590
|
+
steer — see [Mid-run steering](#mid-run-steering-venice-sessions-send)); at the
|
|
591
|
+
prompt it just discards the half-typed line and re-prompts. However you leave —
|
|
592
|
+
`/exit`, Ctrl-D, or a Ctrl-C that lands somewhere unexpected — the session is
|
|
593
|
+
flushed to disk first, so slash-command edits like `/model` and `/system` are
|
|
594
|
+
never lost to the way you quit. Tab completes slash-commands (and model ids after
|
|
588
595
|
`/model `, persona names after `/persona `). At a per-tool confirmation prompt, `a` accepts that call **and**
|
|
589
596
|
auto-accepts the rest of the run. `--max-tool-calls 0` runs until the model
|
|
590
597
|
stops on its own (instead of capping at the default and asking to continue).
|
|
@@ -1856,6 +1863,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
|
|
|
1856
1863
|
| `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for a self-signed embedding backend (`embed`, `index`, `search`, and the `project_search` agent tool) |
|
|
1857
1864
|
| `VENICE_MCP_MAX_SPEND` | `mcp-serve` auto-approve cap in USD (default `0.10`) |
|
|
1858
1865
|
| `VENICE_MCP_OUTPUT_DIR` | where `mcp-serve` tools write files (default: cwd) |
|
|
1866
|
+
| `VENICE_USAGE_RAW` | set to `1`/`true`/`yes`/`on` to echo each API response's raw `usage` block to **stderr** as one `usage-raw: {...}` line (diagnostic; stdout stays machine-readable) |
|
|
1859
1867
|
|
|
1860
1868
|
## Commands at a glance
|
|
1861
1869
|
|
|
@@ -51,7 +51,13 @@ openai = ["openai>=1.40"]
|
|
|
51
51
|
# The SDK requires Python >=3.10, so the dep is gated by an environment marker:
|
|
52
52
|
# on 3.9 the extra resolves to nothing (the CLI stays 3.9-compatible; only this
|
|
53
53
|
# one optional command needs 3.10+). Imported lazily, like the openai extra.
|
|
54
|
-
|
|
54
|
+
# Upper-bounded as of 2026-07-29 (#95): mcp 2.0.0, published 2026-07-28, deletes
|
|
55
|
+
# `mcp.server.fastmcp` -- `mcp.server` now exposes `mcpserver` instead. The old
|
|
56
|
+
# unbounded `mcp>=1.2` turned that into a red CI on every branch within the hour,
|
|
57
|
+
# on 3.10-3.13 only (2.0 declares requires-python >=3.10, so the 3.9 job kept
|
|
58
|
+
# resolving 1.x and stayed green). 1.29.0 is the current 1.x and still ships
|
|
59
|
+
# FastMCP. Lift this when `mcp_serve.py` is ported to the 2.x server API.
|
|
60
|
+
mcp = ["mcp>=1.2,<2; python_version>='3.10'"]
|
|
55
61
|
# Test-only: `pexpect` drives the real CLI on a pty in tests/test_drive_cli.py.
|
|
56
62
|
# Deliberately NOT in `all` -- `all` is a *runtime* install, and the CI `package`
|
|
57
63
|
# job's "a base install pulls in nothing third-party" proof must stay meaningful.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.78.0"
|
|
@@ -24,7 +24,17 @@ def main(argv=None) -> int:
|
|
|
24
24
|
if handler is None:
|
|
25
25
|
parser.print_help(sys.stderr)
|
|
26
26
|
return 2
|
|
27
|
-
|
|
27
|
+
try:
|
|
28
|
+
return int(handler(args) or 0)
|
|
29
|
+
except KeyboardInterrupt:
|
|
30
|
+
# The net under the per-command Ctrl-C handlers (#92). Each of those returns 130
|
|
31
|
+
# long before the exception could reach here, so this only ever catches what they
|
|
32
|
+
# miss -- and what they miss used to be a raw traceback. Deliberately silent: the
|
|
33
|
+
# command that owns the interrupt has already printed its own notice, and this
|
|
34
|
+
# must not print a second one over it. 130 is what CPython exited with anyway,
|
|
35
|
+
# so no exit code changes; see the table in the README.
|
|
36
|
+
print(file=sys.stderr) # newline past a half-drawn prompt
|
|
37
|
+
return 130
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
if __name__ == "__main__":
|
|
@@ -28,12 +28,16 @@ import contextlib
|
|
|
28
28
|
import io
|
|
29
29
|
import itertools
|
|
30
30
|
import json
|
|
31
|
+
import os
|
|
31
32
|
import sys
|
|
32
33
|
import threading
|
|
33
34
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
34
35
|
from dataclasses import dataclass
|
|
35
36
|
from typing import Callable, Dict, List, NamedTuple, Optional, Tuple
|
|
36
37
|
|
|
38
|
+
# Aliased: a bare `config` would be shadowed by the `config=None` keyword argument
|
|
39
|
+
# that `browser_tools` (and its callers) already take further down this module.
|
|
40
|
+
from .. import config as _config
|
|
37
41
|
from .. import userconfig
|
|
38
42
|
from . import _exec
|
|
39
43
|
from . import _mcp
|
|
@@ -212,6 +216,70 @@ def _detail(usage: dict, section: str, key: str):
|
|
|
212
216
|
return None
|
|
213
217
|
|
|
214
218
|
|
|
219
|
+
#: The cache sub-fields we recognize inside ``prompt_tokens_details``, in priority
|
|
220
|
+
#: order. `reference/venice-openapi.yaml` documents exactly two (`cached_tokens`,
|
|
221
|
+
#: `cache_creation_input_tokens`); `cache_write_tokens` is OBSERVED on live
|
|
222
|
+
#: kimi/glm/deepseek responses but undocumented, so it is read as a fallback and
|
|
223
|
+
#: must never take precedence over the documented name. Deliberately NOT widened
|
|
224
|
+
#: to the Anthropic/DeepSeek-native top-level aliases (`cache_read_input_tokens`,
|
|
225
|
+
#: `prompt_cache_hit_tokens`): Venice never emits them, and an additive top-level
|
|
226
|
+
#: block would hit the clamp in `record()` -- truncating the write bucket to 0
|
|
227
|
+
#: while `CostLedger`'s "reported" flag swore it was measured, which is a NEW lie.
|
|
228
|
+
#: `_cache_tokens`'s three-state is the honest degradation for a rename (#98).
|
|
229
|
+
_CACHE_READ_KEYS = ("cached_tokens",)
|
|
230
|
+
_CACHE_WRITE_KEYS = ("cache_creation_input_tokens", "cache_write_tokens")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _cache_tokens(usage: dict, keys) -> Optional[int]:
|
|
234
|
+
"""A cache token count from ``prompt_tokens_details``; None when unreported (#98).
|
|
235
|
+
|
|
236
|
+
THREE-STATE ON PURPOSE, and that is the whole point of #98. `None` means "this
|
|
237
|
+
response carried no such field at all", which is NOT the same as a reported zero:
|
|
238
|
+
the spec marks `prompt_tokens_details` ``nullable: true`` and leaves it out of
|
|
239
|
+
`required` (its own glm example ships ``prompt_tokens_details: null``), so
|
|
240
|
+
coercing absence to 0 fabricates a "cache hit rate: 0.0%" that reads as a real
|
|
241
|
+
measurement. A printed 0.0% must mean the provider said zero.
|
|
242
|
+
|
|
243
|
+
Present-but-null and present-but-garbage both count as ABSENT -- a value we cannot
|
|
244
|
+
read is not a measurement (mirrors :func:`_as_int`'s "a bool is never a token
|
|
245
|
+
count" rule). :func:`_detail` is kept as-is for `reasoning_tokens`, which stays
|
|
246
|
+
deliberately two-state: no incident, and no rate hangs off it.
|
|
247
|
+
"""
|
|
248
|
+
for key in keys:
|
|
249
|
+
v = _detail(usage, "prompt_tokens_details", key)
|
|
250
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
251
|
+
return _as_int(v)
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _dump_raw_usage(usage) -> None:
|
|
256
|
+
"""Echo one API response's raw `usage` block to stderr when opted in (#98).
|
|
257
|
+
|
|
258
|
+
The ledger aggregates, and an aggregate cannot answer "was the field even there?"
|
|
259
|
+
after the fact -- nothing else persists the raw block, so diagnosing a cache
|
|
260
|
+
regression used to mean re-running the incident live.
|
|
261
|
+
|
|
262
|
+
Off unless $VENICE_USAGE_RAW is explicitly truthy -- a bare truthiness check would
|
|
263
|
+
turn this ON for ``VENICE_USAGE_RAW=0``. STDERR only: `venice code --json` and every
|
|
264
|
+
piped command must keep stdout machine-readable.
|
|
265
|
+
|
|
266
|
+
Exactly ONE `print`, so each record stays an atomic line -- this fires on subagent
|
|
267
|
+
and review ledgers too, which run under a ThreadPoolExecutor, and two writes would
|
|
268
|
+
interleave into garbage under `--parallel`. A diagnostic must never take down the
|
|
269
|
+
turn that produced it, hence the blanket serialization fallback.
|
|
270
|
+
"""
|
|
271
|
+
if os.environ.get(_config.ENV_USAGE_RAW, "").strip().lower() not in (
|
|
272
|
+
"1", "true", "yes", "on"
|
|
273
|
+
):
|
|
274
|
+
return
|
|
275
|
+
try:
|
|
276
|
+
raw = usage.model_dump() if hasattr(usage, "model_dump") else usage
|
|
277
|
+
line = json.dumps(raw, sort_keys=True, default=str)
|
|
278
|
+
except Exception: # noqa: BLE001 - a diagnostic must never break the turn
|
|
279
|
+
line = repr(usage)
|
|
280
|
+
print(f"usage-raw: {line}", file=sys.stderr)
|
|
281
|
+
|
|
282
|
+
|
|
215
283
|
def format_duration(seconds) -> str:
|
|
216
284
|
"""A compact human duration: ``4.5s``, ``2m 14s``, ``1h 03m`` (#81).
|
|
217
285
|
|
|
@@ -258,6 +326,16 @@ class CostLedger:
|
|
|
258
326
|
self.cache_write_tokens = 0
|
|
259
327
|
self.reasoning_tokens = 0
|
|
260
328
|
self.unpriced = False # saw a turn whose model price was unknown
|
|
329
|
+
# #98: sticky "at least one recorded turn carried no recognized cache-read
|
|
330
|
+
# (resp. cache-write) field", mirroring `unpriced`. These exist so a rendered
|
|
331
|
+
# 0 can be told apart from an absent one -- the buckets above cannot say
|
|
332
|
+
# "unknown", and `cache hit rate: 0.0%` was being emitted by byte-identical
|
|
333
|
+
# code for a real miss and for a response with no cache block at all. They
|
|
334
|
+
# drive DIFFERENT render sites (see `usage_report`): read-reported-but-
|
|
335
|
+
# write-absent is the normal state for most Venice models, so folding them
|
|
336
|
+
# into one marker would tag essentially every session.
|
|
337
|
+
self.cache_read_unreported = False
|
|
338
|
+
self.cache_write_unreported = False
|
|
261
339
|
# #81: the operator's wall-clock. `elapsed_seconds` is BLOCKED time only --
|
|
262
340
|
# the caller stamps the window and passes the delta, so the ledger never reads
|
|
263
341
|
# a clock and every ledger test stays a pure-function assertion. `turns` counts
|
|
@@ -299,6 +377,8 @@ class CostLedger:
|
|
|
299
377
|
"unpriced": self.unpriced,
|
|
300
378
|
"elapsed_seconds": self.elapsed_seconds, # #81
|
|
301
379
|
"turns": self.turns,
|
|
380
|
+
"cache_read_unreported": self.cache_read_unreported, # #98
|
|
381
|
+
"cache_write_unreported": self.cache_write_unreported, # #98
|
|
302
382
|
}
|
|
303
383
|
|
|
304
384
|
def restore(self, d) -> None:
|
|
@@ -328,6 +408,14 @@ class CostLedger:
|
|
|
328
408
|
self.turns += _as_int(d.get("turns"))
|
|
329
409
|
if d.get("unpriced"):
|
|
330
410
|
self.unpriced = True
|
|
411
|
+
# #98: sticky-OR like `unpriced`, and falsy-by-default so a pre-#98 envelope
|
|
412
|
+
# (which has neither key) claims "reported" rather than retroactively marking
|
|
413
|
+
# every resumed session unknown. The inaccuracy has a one-turn lifetime: the
|
|
414
|
+
# flags are sticky, so the resumed session's first `record()` sets the truth.
|
|
415
|
+
if d.get("cache_read_unreported"):
|
|
416
|
+
self.cache_read_unreported = True
|
|
417
|
+
if d.get("cache_write_unreported"):
|
|
418
|
+
self.cache_write_unreported = True
|
|
331
419
|
|
|
332
420
|
def record_turn(self, seconds) -> None:
|
|
333
421
|
"""Add one blocked window's wall-clock and count the turn (#81).
|
|
@@ -351,7 +439,19 @@ class CostLedger:
|
|
|
351
439
|
buckets are subsets of `prompt_tokens` in Venice's OpenAI-normalized
|
|
352
440
|
usage shape, so uncached input is the remainder. With no cache tokens and
|
|
353
441
|
no cache pricing this reduces exactly to the old `pt*in + ct*out`.
|
|
442
|
+
|
|
443
|
+
A cache field the response never carried is recorded as *unreported* rather
|
|
444
|
+
than as zero (#98) -- see :func:`_cache_tokens`. The arithmetic is unchanged
|
|
445
|
+
by that: every shape that used to yield 0 still yields 0.
|
|
446
|
+
|
|
447
|
+
NOT READ: the `/responses` endpoint's second usage shape (`input_tokens` /
|
|
448
|
+
`input_tokens_details.cached_tokens`). The CLI never calls it today, but if it
|
|
449
|
+
ever does this ledger silently zeroes rather than erroring.
|
|
354
450
|
"""
|
|
451
|
+
# FIRST, ahead of every bail-out below: `record(None)` dumping `usage-raw: null`
|
|
452
|
+
# is exactly the diagnostic that was missing -- it separates "the response had
|
|
453
|
+
# no usage block" from "it had one with no cache fields" (#98).
|
|
454
|
+
_dump_raw_usage(usage)
|
|
355
455
|
if usage is None:
|
|
356
456
|
return 0.0
|
|
357
457
|
if hasattr(usage, "model_dump"):
|
|
@@ -360,10 +460,17 @@ class CostLedger:
|
|
|
360
460
|
return 0.0
|
|
361
461
|
pt = _as_int(usage.get("prompt_tokens"))
|
|
362
462
|
ct = _as_int(usage.get("completion_tokens"))
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
463
|
+
# #98: None means the field was absent, which is not a reported zero. The
|
|
464
|
+
# flags are what `usage_report` needs to refuse to print a fake 0.0%; the
|
|
465
|
+
# counts fall back to 0 so the cost math below is untouched.
|
|
466
|
+
raw_read = _cache_tokens(usage, _CACHE_READ_KEYS)
|
|
467
|
+
raw_write = _cache_tokens(usage, _CACHE_WRITE_KEYS)
|
|
468
|
+
if raw_read is None:
|
|
469
|
+
self.cache_read_unreported = True
|
|
470
|
+
if raw_write is None:
|
|
471
|
+
self.cache_write_unreported = True
|
|
472
|
+
cache_read = 0 if raw_read is None else raw_read
|
|
473
|
+
cache_write = 0 if raw_write is None else raw_write
|
|
367
474
|
reasoning = _as_int(
|
|
368
475
|
_detail(usage, "completion_tokens_details", "reasoning_tokens")
|
|
369
476
|
)
|
|
@@ -449,16 +556,35 @@ class CostLedger:
|
|
|
449
556
|
if self.prompt_tokens else 0.0
|
|
450
557
|
)
|
|
451
558
|
lines = ["session usage:"]
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
559
|
+
# #98: the two flags govern DIFFERENT rows on purpose. Read-reported-but-
|
|
560
|
+
# write-absent is the normal shape for most Venice models, so a shared marker
|
|
561
|
+
# would decorate the hit rate of nearly every session with a warning about a
|
|
562
|
+
# bucket the hit rate does not even use.
|
|
563
|
+
if self.cache_read_unreported and self.cache_write_unreported:
|
|
564
|
+
# Nothing about the split is known -- don't itemize a breakdown that is
|
|
565
|
+
# entirely inferred, same reflex as "(no tokens reported)" above.
|
|
566
|
+
split = "cache breakdown not reported"
|
|
567
|
+
else:
|
|
568
|
+
read_part = ("cache-read n/a" if self.cache_read_unreported
|
|
569
|
+
else f"{self.cache_read_tokens:,} cache-read")
|
|
570
|
+
write_part = ("cache-write n/a" if self.cache_write_unreported
|
|
571
|
+
else f"{self.cache_write_tokens:,} cache-write")
|
|
572
|
+
# NOTE: with a side unreported, `uncached` is an UPPER BOUND and the terms
|
|
573
|
+
# no longer sum to the input total -- the n/a shows where the slack went.
|
|
574
|
+
split = f"{uncached:,} uncached + {read_part} + {write_part}"
|
|
575
|
+
lines.append(f" input {self.prompt_tokens:>10,} tok ({split})")
|
|
457
576
|
out = f" output {self.completion_tokens:>10,} tok"
|
|
458
577
|
if self.reasoning_tokens:
|
|
459
578
|
out += f" (incl. {self.reasoning_tokens:,} reasoning)"
|
|
460
579
|
lines.append(out)
|
|
461
|
-
|
|
580
|
+
if not self.cache_read_unreported:
|
|
581
|
+
lines.append(f" cache hit rate: {hit:.1f}%")
|
|
582
|
+
elif self.cache_read_tokens:
|
|
583
|
+
# Some turns measured, some didn't: the rate is real but understated
|
|
584
|
+
# against a prompt total that includes turns it could not see.
|
|
585
|
+
lines.append(f" cache hit rate: {hit:.1f}% [partially unreported]")
|
|
586
|
+
else:
|
|
587
|
+
lines.append(" cache hit rate: n/a (no cache fields reported)")
|
|
462
588
|
if self.unpriced and self.total == 0.0:
|
|
463
589
|
lines.append(" cost: (model rate unknown)")
|
|
464
590
|
else:
|
|
@@ -700,42 +700,63 @@ def run(args, oai, openai, client, models, model, initial=None, *,
|
|
|
700
700
|
label=label, auto=state["yes"], session=active,
|
|
701
701
|
ephemeral=ephemeral)
|
|
702
702
|
|
|
703
|
-
#
|
|
704
|
-
|
|
705
|
-
|
|
703
|
+
# The session flush lives in exactly one place -- the `finally` below -- so that
|
|
704
|
+
# no exit path can skip it (#92). It used to be hand-copied into the ^D handler
|
|
705
|
+
# and the /exit branch, and the ^D copy was skippable: a SIGINT delivered *inside*
|
|
706
|
+
# an `except` block is not caught by a sibling clause of the same `try`, so the
|
|
707
|
+
# `except KeyboardInterrupt` here only ever guarded `input()`, never the recovery
|
|
708
|
+
# code after it. ^D-then-^C (or a lone ^C hitting the readline EOF/SIGINT race)
|
|
709
|
+
# landed between the newline and the save, and the save never ran. Same structural
|
|
710
|
+
# move as #86's ledger `_finish`: one call, in a `finally`, unskippable.
|
|
711
|
+
try:
|
|
712
|
+
# An explicit message (`venice chat -i "hello"`) becomes the first turn.
|
|
713
|
+
if initial:
|
|
714
|
+
_do_turn(oai, openai, chat, initial, messages, gen_kwargs, state, args)
|
|
706
715
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
_autosave(state, messages, gen_kwargs) # flush /model,/system-only edits
|
|
713
|
-
return 0
|
|
714
|
-
except KeyboardInterrupt:
|
|
715
|
-
print(file=sys.stderr) # ^C at the prompt: discard the line, re-prompt
|
|
716
|
-
continue
|
|
717
|
-
line = line.strip()
|
|
718
|
-
if not line:
|
|
719
|
-
continue
|
|
720
|
-
if line.startswith("/"):
|
|
721
|
-
# /paste and /edit compose a multi-line turn, then submit it like a
|
|
722
|
-
# normal message. They're handled here (not in _dispatch_slash, which
|
|
723
|
-
# lacks `openai`/`chat`) so they can call _do_turn directly. (#65)
|
|
724
|
-
cmd, _, rest = line[1:].partition(" ")
|
|
725
|
-
if cmd.lower() in ("paste", "edit"):
|
|
726
|
-
text = (_read_paste_block() if cmd.lower() == "paste"
|
|
727
|
-
else _compose_in_editor(rest.strip()))
|
|
728
|
-
if text:
|
|
729
|
-
_do_turn(oai, openai, chat, text, messages, gen_kwargs,
|
|
730
|
-
state, args)
|
|
731
|
-
continue
|
|
732
|
-
if _dispatch_slash(
|
|
733
|
-
line, messages, state, args, models, oai=oai, gen_kwargs=gen_kwargs
|
|
734
|
-
) == "exit":
|
|
735
|
-
_autosave(state, messages, gen_kwargs)
|
|
716
|
+
while True:
|
|
717
|
+
try:
|
|
718
|
+
line = input(_PROMPT)
|
|
719
|
+
except EOFError:
|
|
720
|
+
print(file=sys.stderr) # newline after ^D
|
|
736
721
|
return 0
|
|
737
|
-
|
|
738
|
-
|
|
722
|
+
except KeyboardInterrupt:
|
|
723
|
+
print(file=sys.stderr) # ^C at the prompt: discard the line, re-prompt
|
|
724
|
+
continue
|
|
725
|
+
line = line.strip()
|
|
726
|
+
if not line:
|
|
727
|
+
continue
|
|
728
|
+
if line.startswith("/"):
|
|
729
|
+
# /paste and /edit compose a multi-line turn, then submit it like a
|
|
730
|
+
# normal message. They're handled here (not in _dispatch_slash, which
|
|
731
|
+
# lacks `openai`/`chat`) so they can call _do_turn directly. (#65)
|
|
732
|
+
cmd, _, rest = line[1:].partition(" ")
|
|
733
|
+
if cmd.lower() in ("paste", "edit"):
|
|
734
|
+
text = (_read_paste_block() if cmd.lower() == "paste"
|
|
735
|
+
else _compose_in_editor(rest.strip()))
|
|
736
|
+
if text:
|
|
737
|
+
_do_turn(oai, openai, chat, text, messages, gen_kwargs,
|
|
738
|
+
state, args)
|
|
739
|
+
continue
|
|
740
|
+
if _dispatch_slash(
|
|
741
|
+
line, messages, state, args, models, oai=oai, gen_kwargs=gen_kwargs
|
|
742
|
+
) == "exit":
|
|
743
|
+
return 0
|
|
744
|
+
continue
|
|
745
|
+
_do_turn(oai, openai, chat, line, messages, gen_kwargs, state, args)
|
|
746
|
+
except KeyboardInterrupt:
|
|
747
|
+
# Whatever the inner clauses didn't already handle: a ^C inside the ^D
|
|
748
|
+
# teardown, or during a slash-command's own API call. The per-turn abort
|
|
749
|
+
# (`_turn`, above) and the at-the-prompt re-prompt fire first and never reach
|
|
750
|
+
# here, so this is always "something unexpected got interrupted" -- end the
|
|
751
|
+
# session, the way every other command's Ctrl-C handler does. 130 is what
|
|
752
|
+
# CPython already exited with; this just makes it deliberate and quiet.
|
|
753
|
+
print(f"\n{label}: aborted", file=sys.stderr)
|
|
754
|
+
return 130
|
|
755
|
+
finally:
|
|
756
|
+
# Also runs on an unexpected crash, which is new: a session that died mid-run
|
|
757
|
+
# is now persisted rather than lost. `_autosave` no-ops without an active
|
|
758
|
+
# session (--ephemeral) and swallows disk errors, so this can't itself fail.
|
|
759
|
+
_autosave(state, messages, gen_kwargs)
|
|
739
760
|
|
|
740
761
|
|
|
741
762
|
def _banner(model, tools_on, resume, messages, *, label="venice chat",
|
|
@@ -69,6 +69,14 @@ MEMORY_SUBDIR = "memory" # under INDEX_DIRNAME (.venice) for the project tier
|
|
|
69
69
|
MEMORY_FILENAME = "memory.json"
|
|
70
70
|
TASKS_FILENAME = "tasks.json"
|
|
71
71
|
|
|
72
|
+
# Opt-in verbatim dump of each API response's `usage` block to stderr (#98). The
|
|
73
|
+
# ledger aggregates, and an aggregate cannot answer "was the field even there?"
|
|
74
|
+
# after the fact -- nothing persists the raw block anywhere (`venice code --json`
|
|
75
|
+
# and the session envelope both carry only the summed `CostLedger.to_dict()`), so
|
|
76
|
+
# diagnosing a cache regression meant re-running the incident live. Diagnostic
|
|
77
|
+
# only: stderr, so a piped stdout stays machine-readable.
|
|
78
|
+
ENV_USAGE_RAW = "VENICE_USAGE_RAW"
|
|
79
|
+
|
|
72
80
|
SFX_POLL_INTERVAL_SEC = 2.0
|
|
73
81
|
SFX_POLL_MAX_WAIT_SEC = 300
|
|
74
82
|
|
|
@@ -188,6 +188,15 @@ class Driver:
|
|
|
188
188
|
def send(self, line: str) -> None:
|
|
189
189
|
self.child.sendline(line)
|
|
190
190
|
|
|
191
|
+
def send_partial(self, text: str) -> None:
|
|
192
|
+
"""Type without submitting -- no trailing newline.
|
|
193
|
+
|
|
194
|
+
For testing what happens to a line the user never sent: Ctrl-C at the
|
|
195
|
+
prompt is supposed to discard it (#92). ``send`` would submit it and turn
|
|
196
|
+
the half-typed line into a real turn.
|
|
197
|
+
"""
|
|
198
|
+
self.child.send(text)
|
|
199
|
+
|
|
191
200
|
def send_secret(self, value: str) -> None:
|
|
192
201
|
"""Type a secret, and keep it out of any failure transcript.
|
|
193
202
|
|