venice-cli 0.28.0__tar.gz → 0.30.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 (85) hide show
  1. {venice_cli-0.28.0 → venice_cli-0.30.0}/PKG-INFO +9 -7
  2. {venice_cli-0.28.0 → venice_cli-0.30.0}/README.md +8 -6
  3. venice_cli-0.30.0/src/venice/__init__.py +1 -0
  4. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_agent.py +24 -0
  5. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_code.py +2 -1
  6. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_repl.py +101 -3
  7. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/code.py +3 -2
  8. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_chat.py +7 -0
  9. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_code.py +3 -3
  10. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_code_command.py +22 -0
  11. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_repl.py +122 -0
  12. venice_cli-0.28.0/src/venice/__init__.py +0 -1
  13. {venice_cli-0.28.0 → venice_cli-0.30.0}/.gitignore +0 -0
  14. {venice_cli-0.28.0 → venice_cli-0.30.0}/CONTRIBUTING.md +0 -0
  15. {venice_cli-0.28.0 → venice_cli-0.30.0}/LICENSE +0 -0
  16. {venice_cli-0.28.0 → venice_cli-0.30.0}/Makefile +0 -0
  17. {venice_cli-0.28.0 → venice_cli-0.30.0}/SECURITY.md +0 -0
  18. {venice_cli-0.28.0 → venice_cli-0.30.0}/bin/venice +0 -0
  19. {venice_cli-0.28.0 → venice_cli-0.30.0}/install.sh +0 -0
  20. {venice_cli-0.28.0 → venice_cli-0.30.0}/pyproject.toml +0 -0
  21. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/__main__.py +0 -0
  22. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/audio_player.py +0 -0
  23. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/audio_post.py +0 -0
  24. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/auth.py +0 -0
  25. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/billing.py +0 -0
  26. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/cli.py +0 -0
  27. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/client.py +0 -0
  28. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/__init__.py +0 -0
  29. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_audio.py +0 -0
  30. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_index.py +0 -0
  31. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_mcp.py +0 -0
  32. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_mcp_client.py +0 -0
  33. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_models.py +0 -0
  34. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_openai.py +0 -0
  35. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_queue.py +0 -0
  36. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/_shared.py +0 -0
  37. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/balance.py +0 -0
  38. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/bg_remove.py +0 -0
  39. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/chat.py +0 -0
  40. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/config.py +0 -0
  41. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/contact_sheet.py +0 -0
  42. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/embed.py +0 -0
  43. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/image.py +0 -0
  44. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/image_edit.py +0 -0
  45. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/index.py +0 -0
  46. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/login.py +0 -0
  47. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/master.py +0 -0
  48. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/mcp_serve.py +0 -0
  49. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/models.py +0 -0
  50. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/music.py +0 -0
  51. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/search.py +0 -0
  52. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/sfx.py +0 -0
  53. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/tts.py +0 -0
  54. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/upscale.py +0 -0
  55. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/commands/video.py +0 -0
  56. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/config.py +0 -0
  57. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/image_montage.py +0 -0
  58. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/mcp_server.py +0 -0
  59. {venice_cli-0.28.0 → venice_cli-0.30.0}/src/venice/userconfig.py +0 -0
  60. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/__init__.py +0 -0
  61. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/_mcp_fake_server.py +0 -0
  62. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_auth.py +0 -0
  63. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_balance.py +0 -0
  64. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_bg_remove.py +0 -0
  65. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_client.py +0 -0
  66. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_config.py +0 -0
  67. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_contact_sheet.py +0 -0
  68. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_embed.py +0 -0
  69. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_image.py +0 -0
  70. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_image_edit.py +0 -0
  71. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_index.py +0 -0
  72. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_master.py +0 -0
  73. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_mcp_client.py +0 -0
  74. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_mcp_serve.py +0 -0
  75. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_mcp_tools.py +0 -0
  76. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_models.py +0 -0
  77. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_music.py +0 -0
  78. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_search.py +0 -0
  79. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_sfx.py +0 -0
  80. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_shared.py +0 -0
  81. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_shared_openai.py +0 -0
  82. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_tts.py +0 -0
  83. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_upscale.py +0 -0
  84. {venice_cli-0.28.0 → venice_cli-0.30.0}/tests/test_video.py +0 -0
  85. {venice_cli-0.28.0 → venice_cli-0.30.0}/uninstall.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: venice-cli
3
- Version: 0.28.0
3
+ Version: 0.30.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
@@ -558,10 +558,12 @@ venice chat --resume session.json
558
558
  ```
559
559
 
560
560
  In-REPL slash-commands: `/system [text]` (show/set the system prompt),
561
- `/model [name]` (switch model), `/reset` (clear history, keep the system
562
- prompt), `/save [file]` (write the transcript JSON; defaults to the `--resume`
563
- file), `/help`, and `/exit` (or `/quit`, or Ctrl-D). Ctrl-C aborts the current
564
- turn without ending the session.
561
+ `/model [name]` (switch model; with no name, show the current one and list the
562
+ catalog), `/models` (list the available models, marking the current and the
563
+ default), `/reset` (clear history, keep the system prompt), `/save [file]`
564
+ (write the transcript JSON; defaults to the `--resume` file), `/help`, and
565
+ `/exit` (or `/quit`, or Ctrl-D). Ctrl-C aborts the current turn without ending
566
+ the session. Tab completes slash-commands (and model ids after `/model `).
565
567
 
566
568
  ### Venice extensions
567
569
 
@@ -886,7 +888,7 @@ complete — a loud stderr warning is printed).
886
888
  | `write_file` | create/overwrite a file (atomic) | yes |
887
889
  | `edit_file` | replace an exact, unique string in a file | yes |
888
890
  | `run` | run a shell command (`/bin/sh -c`) at the root | yes |
889
- | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` | generate/edit images & audio into the project — **opt-in with `--assets`** | yes |
891
+ | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` / `venice_video` | generate/edit images, audio & video into the project — **opt-in with `--assets`** | yes |
890
892
 
891
893
  **Safety.** Every filesystem path is resolved and confined to the project root
892
894
  (default: cwd, or `--root` / `$VENICE_CODE_ROOT`); a path that escapes the root, names
@@ -909,7 +911,7 @@ forced cwd, timeout, and env-scrub — which is why it always confirms. git muta
909
911
  | `--root DIR` | project directory to sandbox to (default: cwd) |
910
912
  | `--max-tool-calls N` | cap tool invocations before forcing a final answer (default 25) |
911
913
  | `--exec-timeout SECS` | timeout for `run`/`git` (default 120) |
912
- | `--assets` | also expose the in-process asset-generation tools (image / image-edit / sfx / music / tts / upscale / bg-remove) so the agent can create images & audio in the project; paid — each confirms per call unless `--auto` |
914
+ | `--assets` | also expose the in-process asset-generation tools (image / image-edit / sfx / music / tts / upscale / bg-remove / video) so the agent can create images, audio & video in the project; paid — each confirms per call unless `--auto` |
913
915
  | `-i`, `--json`, `--model`, `--system` | interactive REPL · JSON envelope · model · extra system instructions |
914
916
 
915
917
  With `--assets`, generated files land in `$VENICE_MCP_OUTPUT_DIR` or, by default, under
@@ -520,10 +520,12 @@ venice chat --resume session.json
520
520
  ```
521
521
 
522
522
  In-REPL slash-commands: `/system [text]` (show/set the system prompt),
523
- `/model [name]` (switch model), `/reset` (clear history, keep the system
524
- prompt), `/save [file]` (write the transcript JSON; defaults to the `--resume`
525
- file), `/help`, and `/exit` (or `/quit`, or Ctrl-D). Ctrl-C aborts the current
526
- turn without ending the session.
523
+ `/model [name]` (switch model; with no name, show the current one and list the
524
+ catalog), `/models` (list the available models, marking the current and the
525
+ default), `/reset` (clear history, keep the system prompt), `/save [file]`
526
+ (write the transcript JSON; defaults to the `--resume` file), `/help`, and
527
+ `/exit` (or `/quit`, or Ctrl-D). Ctrl-C aborts the current turn without ending
528
+ the session. Tab completes slash-commands (and model ids after `/model `).
527
529
 
528
530
  ### Venice extensions
529
531
 
@@ -848,7 +850,7 @@ complete — a loud stderr warning is printed).
848
850
  | `write_file` | create/overwrite a file (atomic) | yes |
849
851
  | `edit_file` | replace an exact, unique string in a file | yes |
850
852
  | `run` | run a shell command (`/bin/sh -c`) at the root | yes |
851
- | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` | generate/edit images & audio into the project — **opt-in with `--assets`** | yes |
853
+ | `venice_image` / `venice_image_edit` / `venice_sfx` / `venice_music` / `venice_tts` / `venice_upscale` / `venice_bg_remove` / `venice_video` | generate/edit images, audio & video into the project — **opt-in with `--assets`** | yes |
852
854
 
853
855
  **Safety.** Every filesystem path is resolved and confined to the project root
854
856
  (default: cwd, or `--root` / `$VENICE_CODE_ROOT`); a path that escapes the root, names
@@ -871,7 +873,7 @@ forced cwd, timeout, and env-scrub — which is why it always confirms. git muta
871
873
  | `--root DIR` | project directory to sandbox to (default: cwd) |
872
874
  | `--max-tool-calls N` | cap tool invocations before forcing a final answer (default 25) |
873
875
  | `--exec-timeout SECS` | timeout for `run`/`git` (default 120) |
874
- | `--assets` | also expose the in-process asset-generation tools (image / image-edit / sfx / music / tts / upscale / bg-remove) so the agent can create images & audio in the project; paid — each confirms per call unless `--auto` |
876
+ | `--assets` | also expose the in-process asset-generation tools (image / image-edit / sfx / music / tts / upscale / bg-remove / video) so the agent can create images, audio & video in the project; paid — each confirms per call unless `--auto` |
875
877
  | `-i`, `--json`, `--model`, `--system` | interactive REPL · JSON envelope · model · extra system instructions |
876
878
 
877
879
  With `--assets`, generated files land in `$VENICE_MCP_OUTPUT_DIR` or, by default, under
@@ -0,0 +1 @@
1
+ __version__ = "0.30.0"
@@ -202,6 +202,21 @@ _IMAGE_EDIT_SCHEMA = _obj(
202
202
  required=["prompt"],
203
203
  )
204
204
 
205
+ _VIDEO_SCHEMA = _obj(
206
+ {
207
+ "prompt": _p("string", "What the video should depict."),
208
+ "model": _p("string", "Video model id (default: the catalog default)."),
209
+ "duration": _p("string", "Clip length, e.g. '5s'."),
210
+ "negative_prompt": _p("string"),
211
+ "resolution": _p("string", "Output resolution tier, e.g. 720p/1080p."),
212
+ "aspect_ratio": _p("string", "Output aspect ratio, e.g. 16:9."),
213
+ "no_audio": _p("boolean", "Generate silent video (no soundtrack)."),
214
+ "image_url": _p("string", "URL of a start/reference image (image-to-video)."),
215
+ "end_image_url": _p("string", "URL of an end frame to interpolate toward."),
216
+ },
217
+ required=["prompt"],
218
+ )
219
+
205
220
  # (tool name, `_mcp` impl attribute, description, schema, paid). The impl is
206
221
  # stored by NAME and resolved via getattr(_mcp, ...) at builtin_tools() time, so a
207
222
  # single source of truth wins and tests can patch `_mcp.<impl>`.
@@ -292,6 +307,15 @@ _CODE_ASSET_BUILTINS = [
292
307
  _IMAGE_EDIT_SCHEMA,
293
308
  True,
294
309
  ),
310
+ (
311
+ "venice_video",
312
+ "video_tool",
313
+ "Generate a short video via Venice's async video queue (blocks with a capped "
314
+ "wait; can be slow). Optionally image-to-video from image_url. Writes an .mp4 "
315
+ "and returns its path. Dynamic pricing, so it always needs confirmation.",
316
+ _VIDEO_SCHEMA,
317
+ True,
318
+ ),
295
319
  ]
296
320
 
297
321
  # Loop-controlled kwargs the model must never supply (stripped defensively).
@@ -459,7 +459,7 @@ def code_tools(
459
459
 
460
460
  When `assets` and a `client` are supplied, the in-process asset-generation tools
461
461
  (`venice_image`/`venice_image_edit`/`venice_sfx`/`venice_music`/`venice_tts`/
462
- `venice_upscale`/`venice_bg_remove`) are folded in via `_agent.builtin_tools`,
462
+ `venice_upscale`/`venice_bg_remove`/`venice_video`) are folded in via `_agent.builtin_tools`,
463
463
  so the agent can create images/audio in the project. They are paid and route
464
464
  through the same confirm/spend gate; generated files land in
465
465
  ``$VENICE_MCP_OUTPUT_DIR`` or, by default, under `root`.
@@ -534,6 +534,7 @@ def code_tools(
534
534
  only={
535
535
  "venice_image", "venice_image_edit", "venice_sfx",
536
536
  "venice_music", "venice_tts", "venice_upscale", "venice_bg_remove",
537
+ "venice_video",
537
538
  },
538
539
  ))
539
540
  return tools
@@ -37,13 +37,20 @@ _PROMPT = "you> "
37
37
  _HELP = """\
38
38
  Commands:
39
39
  /system [text] show, or set, the system prompt (reseeds the conversation)
40
- /model [name] show, or switch, the model for following turns
40
+ /model [name] switch model; with no name, show the current one and list the catalog
41
+ /models list the available models (marks the current and default)
41
42
  /reset clear the conversation (keeps the system prompt)
42
43
  /save [file] write the transcript JSON (default: the --resume file)
43
44
  /help show this help
44
45
  /exit, /quit leave the REPL
45
46
  Anything else is sent to the model as your next message."""
46
47
 
48
+ # Slash-commands, in help order -- the single source of truth for tab-completion
49
+ # (#40). Keep in sync with `_dispatch_slash` and `_HELP`.
50
+ _COMMANDS = (
51
+ "/system", "/model", "/models", "/reset", "/save", "/help", "/exit", "/quit",
52
+ )
53
+
47
54
 
48
55
  class _TranscriptError(Exception):
49
56
  """A --resume transcript file is missing or malformed. Message is printable."""
@@ -114,6 +121,91 @@ def _current_system(messages: List[dict]) -> Optional[str]:
114
121
  return None
115
122
 
116
123
 
124
+ # --------------------------------------------------------------------------- #
125
+ # Model listing (#39) + tab-completion (#40)
126
+ # --------------------------------------------------------------------------- #
127
+ def _format_model_list(models, current: Optional[str]) -> str:
128
+ """One id per line for `/models` and bare `/model`.
129
+
130
+ Marks the current model with ``*`` and the catalog's default-trait model with
131
+ a trailing ``(default)``; appends ``model_spec.name`` when present. Reuses the
132
+ already-fetched `models` list (no catalog re-fetch). Returns a printable block.
133
+ """
134
+ if not models:
135
+ return "(model catalog unavailable; pass --model or /model <id> to switch)"
136
+ default = _models.default_model(models)
137
+ lines = []
138
+ for m in models:
139
+ if not isinstance(m, dict):
140
+ continue
141
+ mid = m.get("id")
142
+ if not mid:
143
+ continue
144
+ spec = m.get("model_spec") if isinstance(m.get("model_spec"), dict) else {}
145
+ name = spec.get("name")
146
+ marker = "*" if mid == current else " "
147
+ tags = " (default)" if mid == default else ""
148
+ line = f" {marker} {mid}{tags}"
149
+ if name:
150
+ line += f" -- {name}"
151
+ lines.append(line)
152
+ return "\n".join(lines) if lines else "(no models advertised)"
153
+
154
+
155
+ def _make_completer(models, rl):
156
+ """Build a `readline` completer over the slash-commands and model ids (#40).
157
+
158
+ `rl` is the readline module (injected so the closure is unit-testable without
159
+ a real terminal). The returned ``completer(text, state)`` completes the leading
160
+ ``/command`` token, and model ids after ``/model ``; it is a no-op on non-slash
161
+ lines so ordinary prose is never auto-completed.
162
+ """
163
+ model_ids = [
164
+ m.get("id") for m in (models or [])
165
+ if isinstance(m, dict) and m.get("id")
166
+ ]
167
+
168
+ def completer(text, state):
169
+ buf = rl.get_line_buffer()
170
+ if not buf.lstrip().startswith("/"):
171
+ return None
172
+ # Empty prefix left of the token => we're completing the command word.
173
+ if not buf[: rl.get_begidx()].strip():
174
+ candidates = _COMMANDS
175
+ elif buf.lstrip().split(maxsplit=1)[0].lower() == "/model":
176
+ candidates = model_ids
177
+ else:
178
+ candidates = ()
179
+ matches = [c for c in candidates if c.startswith(text)]
180
+ return matches[state] if state < len(matches) else None
181
+
182
+ return completer
183
+
184
+
185
+ def _install_completer(rl, models, stack) -> None:
186
+ """Register the tab-completer for the session, restoring the previous one on exit.
187
+
188
+ Sets whitespace-only delimiters so a leading ``/command`` is one token, installs
189
+ the `_make_completer` closure, and binds Tab (libedit vs GNU readline). The
190
+ `stack` callback restores the prior completer + delims so we never leak into a
191
+ parent readline context (e.g. a REPL launched from another readline program).
192
+ """
193
+ prev_completer = rl.get_completer()
194
+ prev_delims = rl.get_completer_delims()
195
+
196
+ def _restore():
197
+ rl.set_completer(prev_completer)
198
+ rl.set_completer_delims(prev_delims)
199
+
200
+ stack.callback(_restore)
201
+ rl.set_completer_delims(" \t\n")
202
+ rl.set_completer(_make_completer(models, rl))
203
+ if "libedit" in (getattr(rl, "__doc__", "") or ""):
204
+ rl.parse_and_bind("bind ^I rl_complete")
205
+ else:
206
+ rl.parse_and_bind("tab: complete")
207
+
208
+
117
209
  # --------------------------------------------------------------------------- #
118
210
  # One turn
119
211
  # --------------------------------------------------------------------------- #
@@ -195,6 +287,9 @@ def _dispatch_slash(line, messages, state, args, models) -> str:
195
287
  print(f"(model -> {new})", file=sys.stderr)
196
288
  else:
197
289
  print(f"model: {state['model']}", file=sys.stderr)
290
+ print(_format_model_list(models, state["model"]), file=sys.stderr)
291
+ elif cmd == "models":
292
+ print(_format_model_list(models, state["model"]), file=sys.stderr)
198
293
  elif cmd == "save":
199
294
  target = rest or getattr(args, "resume", None)
200
295
  if not target:
@@ -231,9 +326,9 @@ def run(args, oai, openai, client, models, model, initial=None, *,
231
326
  from . import chat # lazy: chat imports this module at top (avoid a cycle)
232
327
 
233
328
  try:
234
- import readline # noqa: F401 (line editing + in-session history)
329
+ import readline as _rl # line editing + in-session history + completion
235
330
  except Exception: # pragma: no cover - platform without readline
236
- pass
331
+ _rl = None
237
332
 
238
333
  try:
239
334
  messages = _seed_messages(args)
@@ -267,6 +362,9 @@ def run(args, oai, openai, client, models, model, initial=None, *,
267
362
  tools_on = False # model lacks function calling -> plain chat
268
363
  state = {"model": model, "tools": tools, "tools_on": tools_on}
269
364
 
365
+ if _rl is not None:
366
+ _install_completer(_rl, models, stack)
367
+
270
368
  _banner(model, tools_on, getattr(args, "resume", None), messages, label=label)
271
369
 
272
370
  # An explicit message (`venice chat -i "hello"`) becomes the first turn.
@@ -171,8 +171,9 @@ def register(subparsers) -> None:
171
171
  grp.add_argument(
172
172
  "--assets", action="store_true", dest="assets", default=None,
173
173
  help="Also expose the in-process asset-generation tools (venice_image, "
174
- "image_edit, sfx, music, tts, upscale, bg_remove) so the agent can create "
175
- "images/audio in the project. Paid: each confirms per call unless --auto.",
174
+ "image_edit, sfx, music, tts, upscale, bg_remove, video) so the agent can "
175
+ "create images/audio/video in the project. Paid: each confirms per call "
176
+ "unless --auto.",
176
177
  )
177
178
 
178
179
  it = p.add_argument_group("Interactive")
@@ -760,6 +760,13 @@ class TestBuiltinToolsRegistry(unittest.TestCase):
760
760
  self.assertNotIn(banned, props)
761
761
  self.assertEqual(_agent._IMAGE_EDIT_SCHEMA.get("required"), ["prompt"])
762
762
 
763
+ def test_video_schema_excludes_controlled(self):
764
+ from venice.commands import _agent
765
+ props = _agent._VIDEO_SCHEMA["properties"]
766
+ for banned in ("confirm", "max_spend", "output_dir"):
767
+ self.assertNotIn(banned, props)
768
+ self.assertEqual(_agent._VIDEO_SCHEMA.get("required"), ["prompt"])
769
+
763
770
 
764
771
  if __name__ == "__main__":
765
772
  unittest.main()
@@ -18,7 +18,7 @@ def _tools(root, **kw):
18
18
 
19
19
  _ASSET_NAMES = {
20
20
  "venice_image", "venice_image_edit", "venice_sfx", "venice_music",
21
- "venice_tts", "venice_upscale", "venice_bg_remove",
21
+ "venice_tts", "venice_upscale", "venice_bg_remove", "venice_video",
22
22
  }
23
23
 
24
24
 
@@ -242,9 +242,9 @@ class TestCodeFactory(unittest.TestCase):
242
242
  def test_assets_present_when_enabled(self):
243
243
  names = {t.name for t in _code.code_tools("/tmp", client=object(),
244
244
  assets=True)}
245
- self.assertTrue(_ASSET_NAMES <= names) # all 7 folded in
245
+ self.assertTrue(_ASSET_NAMES <= names) # all 8 folded in
246
246
  self.assertNotIn("venice_chat", names) # excluded by design
247
- self.assertNotIn("venice_video", names) # deferred
247
+ self.assertIn("venice_video", names)
248
248
 
249
249
  def test_asset_tools_are_paid(self):
250
250
  by = {t.name: t for t in _code.code_tools("/tmp", client=object(),
@@ -111,6 +111,7 @@ class TestCodeCommand(unittest.TestCase):
111
111
  names = self._exec_tool_names(calls)
112
112
  self.assertIn("venice_image", names)
113
113
  self.assertIn("venice_image_edit", names)
114
+ self.assertIn("venice_video", names)
114
115
  self.assertNotIn("venice_chat", names) # excluded by design
115
116
 
116
117
  def test_assets_absent_by_default(self):
@@ -146,6 +147,27 @@ class TestCodeCommand(unittest.TestCase):
146
147
  # the model supplied only prompt; control kwargs are injected, not from args
147
148
  self.assertEqual(stub.call_args.kwargs.get("prompt"), "a hero sprite")
148
149
 
150
+ def test_video_asset_dispatches_with_confirm_under_auto(self):
151
+ seq = [
152
+ FakeToolCompletion("plan"),
153
+ FakeToolCompletion(tool_calls=[
154
+ _FnCall("c1", "venice_video",
155
+ json.dumps({"prompt": "a koi pond at dawn"}))]),
156
+ FakeToolCompletion("made the clip"),
157
+ FakeToolCompletion("ACCEPTANCE: PASS"),
158
+ ]
159
+ with mock.patch(
160
+ "venice.commands._mcp.video_tool",
161
+ return_value={"status": "ok", "path": "/x.mp4", "bytes": 1},
162
+ ) as stub:
163
+ rc, calls = self._run(
164
+ _code_args(task="film", root=self.root, auto=True, assets=True), seq)
165
+ self.assertEqual(rc, 0)
166
+ self.assertEqual(stub.call_count, 1)
167
+ # --auto -> confirm=True bypasses the spend gate
168
+ self.assertTrue(stub.call_args.kwargs.get("confirm"))
169
+ self.assertEqual(stub.call_args.kwargs.get("prompt"), "a koi pond at dawn")
170
+
149
171
  def test_acceptance_fail_returns_1(self):
150
172
  seq = [
151
173
  FakeToolCompletion("plan"),
@@ -27,6 +27,8 @@ from tests.test_chat import (
27
27
  _args,
28
28
  )
29
29
 
30
+ from venice.commands import _repl # noqa: E402
31
+
30
32
  _EMPTY_CFG = {"version": 1, "mcpServers": {}, "defaults": {}}
31
33
 
32
34
 
@@ -290,6 +292,126 @@ class TestRepl(unittest.TestCase):
290
292
  self.assertEqual([m["role"] for m in calls[1]["messages"]], ["user"])
291
293
  self.assertEqual(calls[1]["messages"][0]["content"], "again")
292
294
 
295
+ # ------------------------------------------------------------------ #
296
+ # #39: /models listing + bare /model listing
297
+ # ------------------------------------------------------------------ #
298
+ def test_slash_models_lists_catalog(self):
299
+ err = io.StringIO()
300
+ rc, fake, calls = _run_repl(
301
+ _args(interactive=True), [], ["/models", "/exit"], stderr=err,
302
+ )
303
+ self.assertEqual(rc, 0)
304
+ self.assertEqual(len(calls), 0) # listing never calls the model
305
+ out = err.getvalue()
306
+ self.assertIn("llama-3.3-70b", out)
307
+ self.assertIn("venice-uncensored", out)
308
+ self.assertIn("(default)", out) # default-trait model marked
309
+ self.assertIn("* llama-3.3-70b", out) # current model marked
310
+
311
+ def test_bare_model_shows_current_and_lists(self):
312
+ err = io.StringIO()
313
+ rc, fake, calls = _run_repl(
314
+ _args(interactive=True), [], ["/model", "/exit"], stderr=err,
315
+ )
316
+ self.assertEqual(rc, 0)
317
+ out = err.getvalue()
318
+ self.assertIn("model: llama-3.3-70b", out) # current still shown
319
+ self.assertIn("venice-uncensored", out) # ...plus the catalog list
320
+
321
+
322
+ class _FakeRL:
323
+ """Minimal readline stand-in: the completer needs only these two hooks."""
324
+
325
+ def __init__(self, buffer, begidx, *, doc=""):
326
+ self._buffer = buffer
327
+ self._begidx = begidx
328
+ self.__doc__ = doc
329
+
330
+ def get_line_buffer(self):
331
+ return self._buffer
332
+
333
+ def get_begidx(self):
334
+ return self._begidx
335
+
336
+
337
+ _CATALOG = [
338
+ {"id": "llama-3.3-70b", "model_spec": {"traits": ["default"]}},
339
+ {"id": "venice-uncensored", "model_spec": {"traits": []}},
340
+ ]
341
+
342
+
343
+ class TestReplCompletion(unittest.TestCase):
344
+ """#40: the readline completer (unit-tested via an injected fake rl)."""
345
+
346
+ def _complete_all(self, buffer, begidx, text):
347
+ comp = _repl._make_completer(_CATALOG, _FakeRL(buffer, begidx))
348
+ out, state = [], 0
349
+ while (m := comp(text, state)) is not None:
350
+ out.append(m)
351
+ state += 1
352
+ return out
353
+
354
+ def test_commands_include_models(self):
355
+ self.assertIn("/models", _repl._COMMANDS)
356
+
357
+ def test_completes_slash_command(self):
358
+ self.assertEqual(self._complete_all("/mo", 0, "/mo"), ["/model", "/models"])
359
+
360
+ def test_completes_model_id_after_model(self):
361
+ self.assertEqual(
362
+ self._complete_all("/model ven", 7, "ven"), ["venice-uncensored"]
363
+ )
364
+
365
+ def test_bare_model_space_lists_all_ids(self):
366
+ self.assertEqual(
367
+ self._complete_all("/model ", 7, ""),
368
+ ["llama-3.3-70b", "venice-uncensored"],
369
+ )
370
+
371
+ def test_no_completion_off_slash_lines(self):
372
+ self.assertEqual(self._complete_all("hello wor", 6, "wor"), [])
373
+
374
+ def test_other_commands_get_no_model_ids(self):
375
+ # after `/save ` we offer nothing (filename completion is deferred)
376
+ self.assertEqual(self._complete_all("/save ", 6, ""), [])
377
+
378
+ def test_completer_tolerates_empty_catalog(self):
379
+ comp = _repl._make_completer(None, _FakeRL("/model ", 7))
380
+ self.assertIsNone(comp("", 0)) # no ids, no crash
381
+
382
+ def test_install_completer_restores_on_exit(self):
383
+ class RL:
384
+ def __init__(self):
385
+ self.completer = "PREV"
386
+ self.delims = "PREVDELIMS"
387
+ self.bound = None
388
+ self.__doc__ = "" # not libedit -> "tab: complete"
389
+
390
+ def get_completer(self):
391
+ return self.completer
392
+
393
+ def get_completer_delims(self):
394
+ return self.delims
395
+
396
+ def set_completer(self, c):
397
+ self.completer = c
398
+
399
+ def set_completer_delims(self, d):
400
+ self.delims = d
401
+
402
+ def parse_and_bind(self, s):
403
+ self.bound = s
404
+
405
+ rl = RL()
406
+ with contextlib.ExitStack() as stack:
407
+ _repl._install_completer(rl, _CATALOG, stack)
408
+ self.assertTrue(callable(rl.completer)) # our completer is installed
409
+ self.assertEqual(rl.delims, " \t\n")
410
+ self.assertEqual(rl.bound, "tab: complete")
411
+ # leaving the REPL restores the prior completer + delims (no leak)
412
+ self.assertEqual(rl.completer, "PREV")
413
+ self.assertEqual(rl.delims, "PREVDELIMS")
414
+
293
415
 
294
416
  if __name__ == "__main__":
295
417
  unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "0.28.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes