venice-cli 0.35.0__tar.gz → 0.37.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 (86) hide show
  1. {venice_cli-0.35.0 → venice_cli-0.37.0}/PKG-INFO +10 -2
  2. {venice_cli-0.35.0 → venice_cli-0.37.0}/README.md +9 -1
  3. venice_cli-0.37.0/src/venice/__init__.py +1 -0
  4. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_agent.py +65 -6
  5. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_code.py +7 -3
  6. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_mcp.py +25 -0
  7. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/chat.py +1 -0
  8. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/code.py +1 -0
  9. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_agent.py +67 -0
  10. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_chat.py +6 -4
  11. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_code.py +3 -0
  12. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_mcp_tools.py +31 -0
  13. venice_cli-0.35.0/src/venice/__init__.py +0 -1
  14. {venice_cli-0.35.0 → venice_cli-0.37.0}/.gitignore +0 -0
  15. {venice_cli-0.35.0 → venice_cli-0.37.0}/CONTRIBUTING.md +0 -0
  16. {venice_cli-0.35.0 → venice_cli-0.37.0}/LICENSE +0 -0
  17. {venice_cli-0.35.0 → venice_cli-0.37.0}/Makefile +0 -0
  18. {venice_cli-0.35.0 → venice_cli-0.37.0}/SECURITY.md +0 -0
  19. {venice_cli-0.35.0 → venice_cli-0.37.0}/bin/venice +0 -0
  20. {venice_cli-0.35.0 → venice_cli-0.37.0}/install.sh +0 -0
  21. {venice_cli-0.35.0 → venice_cli-0.37.0}/pyproject.toml +0 -0
  22. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/__main__.py +0 -0
  23. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/audio_player.py +0 -0
  24. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/audio_post.py +0 -0
  25. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/auth.py +0 -0
  26. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/billing.py +0 -0
  27. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/cli.py +0 -0
  28. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/client.py +0 -0
  29. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/__init__.py +0 -0
  30. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_audio.py +0 -0
  31. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_index.py +0 -0
  32. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_mcp_client.py +0 -0
  33. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_models.py +0 -0
  34. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_openai.py +0 -0
  35. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_queue.py +0 -0
  36. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_repl.py +0 -0
  37. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/_shared.py +0 -0
  38. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/balance.py +0 -0
  39. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/bg_remove.py +0 -0
  40. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/config.py +0 -0
  41. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/contact_sheet.py +0 -0
  42. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/embed.py +0 -0
  43. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/image.py +0 -0
  44. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/image_edit.py +0 -0
  45. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/index.py +0 -0
  46. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/login.py +0 -0
  47. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/master.py +0 -0
  48. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/mcp_serve.py +0 -0
  49. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/models.py +0 -0
  50. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/music.py +0 -0
  51. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/search.py +0 -0
  52. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/sfx.py +0 -0
  53. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/tts.py +0 -0
  54. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/upscale.py +0 -0
  55. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/commands/video.py +0 -0
  56. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/config.py +0 -0
  57. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/image_montage.py +0 -0
  58. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/mcp_server.py +0 -0
  59. {venice_cli-0.35.0 → venice_cli-0.37.0}/src/venice/userconfig.py +0 -0
  60. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/__init__.py +0 -0
  61. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/_mcp_fake_server.py +0 -0
  62. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_auth.py +0 -0
  63. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_balance.py +0 -0
  64. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_bg_remove.py +0 -0
  65. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_client.py +0 -0
  66. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_code_command.py +0 -0
  67. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_config.py +0 -0
  68. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_contact_sheet.py +0 -0
  69. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_embed.py +0 -0
  70. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_image.py +0 -0
  71. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_image_edit.py +0 -0
  72. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_index.py +0 -0
  73. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_master.py +0 -0
  74. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_mcp_client.py +0 -0
  75. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_mcp_serve.py +0 -0
  76. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_models.py +0 -0
  77. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_music.py +0 -0
  78. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_repl.py +0 -0
  79. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_search.py +0 -0
  80. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_sfx.py +0 -0
  81. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_shared.py +0 -0
  82. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_shared_openai.py +0 -0
  83. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_tts.py +0 -0
  84. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_upscale.py +0 -0
  85. {venice_cli-0.35.0 → venice_cli-0.37.0}/tests/test_video.py +0 -0
  86. {venice_cli-0.35.0 → venice_cli-0.37.0}/uninstall.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: venice-cli
3
- Version: 0.35.0
3
+ Version: 0.37.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
@@ -630,7 +630,9 @@ a read-only [semantic search](#semantic-search) over the project's local
630
630
  `venice_models`, a read-only lookup that lists model ids for a given catalog
631
631
  type (its single `type` arg — text/code/image/video/music/tts/embedding/upscale,
632
632
  or `all`) so the model can pick a valid `model` for the other tools instead of
633
- guessing. (`venice code` gets `venice_models` too.)
633
+ guessing, and `venice_model_details` (single `model` arg) which returns one
634
+ model's pricing (cost), input context/prompt-length limits, and capabilities so
635
+ the agent can budget its input. (`venice code` gets both too.)
634
636
 
635
637
  ```sh
636
638
  # One command, multiple steps: the model generates an image, then critiques it.
@@ -1050,6 +1052,12 @@ safety), it should be settable in config." Currently config-backable:
1050
1052
  Per-invocation flags (`--dry-run`, `--json`, `--resume`, `--seed`, inputs and
1051
1053
  positionals) stay CLI-only by design.
1052
1054
 
1055
+ These per-command defaults also apply when a generator runs as an **agent tool**
1056
+ inside `venice chat --tools` and `venice code` — e.g. `defaults.image.safe_mode`
1057
+ is honored when the model calls `venice_image`, not just on the `venice image`
1058
+ CLI. An explicit argument the model puts in the tool call still wins over config.
1059
+ (`venice mcp-serve` doesn't yet thread config into its wrappers.)
1060
+
1053
1061
  The **API key is never stored here** — it stays in
1054
1062
  `~/.config/venice/credentials`. Unknown keys are preserved on write, so the
1055
1063
  schema is forward-compatible.
@@ -592,7 +592,9 @@ a read-only [semantic search](#semantic-search) over the project's local
592
592
  `venice_models`, a read-only lookup that lists model ids for a given catalog
593
593
  type (its single `type` arg — text/code/image/video/music/tts/embedding/upscale,
594
594
  or `all`) so the model can pick a valid `model` for the other tools instead of
595
- guessing. (`venice code` gets `venice_models` too.)
595
+ guessing, and `venice_model_details` (single `model` arg) which returns one
596
+ model's pricing (cost), input context/prompt-length limits, and capabilities so
597
+ the agent can budget its input. (`venice code` gets both too.)
596
598
 
597
599
  ```sh
598
600
  # One command, multiple steps: the model generates an image, then critiques it.
@@ -1012,6 +1014,12 @@ safety), it should be settable in config." Currently config-backable:
1012
1014
  Per-invocation flags (`--dry-run`, `--json`, `--resume`, `--seed`, inputs and
1013
1015
  positionals) stay CLI-only by design.
1014
1016
 
1017
+ These per-command defaults also apply when a generator runs as an **agent tool**
1018
+ inside `venice chat --tools` and `venice code` — e.g. `defaults.image.safe_mode`
1019
+ is honored when the model calls `venice_image`, not just on the `venice image`
1020
+ CLI. An explicit argument the model puts in the tool call still wins over config.
1021
+ (`venice mcp-serve` doesn't yet thread config into its wrappers.)
1022
+
1015
1023
  The **API key is never stored here** — it stays in
1016
1024
  `~/.config/venice/credentials`. Unknown keys are preserved on write, so the
1017
1025
  schema is forward-compatible.
@@ -0,0 +1 @@
1
+ __version__ = "0.37.0"
@@ -24,6 +24,7 @@ and passes the combined list to :func:`run_loop`. Nothing in the loop changes.
24
24
  """
25
25
  from __future__ import annotations
26
26
 
27
+ import inspect
27
28
  import itertools
28
29
  import json
29
30
  import sys
@@ -31,6 +32,7 @@ import threading
31
32
  from dataclasses import dataclass
32
33
  from typing import Callable, Dict, List, Optional
33
34
 
35
+ from .. import userconfig
34
36
  from . import _mcp
35
37
  from .models import MODEL_TYPES
36
38
 
@@ -196,6 +198,13 @@ _MODELS_SCHEMA = _obj(
196
198
  required=["type"],
197
199
  )
198
200
 
201
+ _MODEL_DETAILS_SCHEMA = _obj(
202
+ {
203
+ "model": _p("string", "The model id to describe (e.g. from venice_models)."),
204
+ },
205
+ required=["model"],
206
+ )
207
+
199
208
  # Schema for a tool folded in ONLY via `only=` (e.g. `venice code --assets`), so it
200
209
  # is not part of chat's default advertised set. Curated subset of
201
210
  # `_mcp.image_edit_tool`; `confirm`/`max_spend`/`output_dir` omitted (loop-injected).
@@ -306,6 +315,15 @@ _BUILTINS = [
306
315
  _MODELS_SCHEMA,
307
316
  False,
308
317
  ),
318
+ (
319
+ "venice_model_details",
320
+ "model_details_tool",
321
+ "Get one model's details -- pricing (cost), input context/prompt-length "
322
+ "limits (your input budget), and capabilities -- so you can budget input "
323
+ "and confirm a model fits before using it. Read-only; not spend-gated.",
324
+ _MODEL_DETAILS_SCHEMA,
325
+ False,
326
+ ),
309
327
  (
310
328
  "project_search",
311
329
  "search_tool",
@@ -354,12 +372,20 @@ def _clean(arguments) -> dict:
354
372
  return {k: v for k, v in arguments.items() if k not in _CONTROLLED}
355
373
 
356
374
 
375
+ def _tool_section(name: str) -> str:
376
+ """Config section for a tool: `venice_image` -> `image` (matches userconfig
377
+ `_COMMAND_MAP` / the CLI command). Tools with no matching section (e.g.
378
+ `venice_models`, `project_search`) simply resolve nothing."""
379
+ return name[len("venice_"):] if name.startswith("venice_") else name
380
+
381
+
357
382
  def builtin_tools(
358
383
  client,
359
384
  *,
360
385
  max_spend: Optional[float] = None,
361
386
  output_dir: Optional[str] = None,
362
387
  only: Optional[set] = None,
388
+ config: Optional[dict] = None,
363
389
  ) -> List[Tool]:
364
390
  """Build the in-process venice tools, bound to `client`.
365
391
 
@@ -368,23 +394,56 @@ def builtin_tools(
368
394
  unknown name raises ValueError so the caller can exit 2). With `only=None` the
369
395
  set is exactly `_BUILTINS` (chat's default); passing `only=` also makes the
370
396
  `_CODE_ASSET_BUILTINS` extras (e.g. `venice_image_edit`) selectable.
397
+
398
+ `config` is a userconfig doc (issue #58): `defaults.<section>.*` values are
399
+ layered UNDER the model's tool arguments, so an explicit tool arg still wins
400
+ (precedence: model arg > config default > tool hardcoded default). Only keys
401
+ in `userconfig._COMMAND_MAP[section]` (the #57 allow-list) that the tool
402
+ function actually accepts are injected.
371
403
  """
372
404
 
373
- def _make_paid(impl):
405
+ def _config_defaults(section, impl) -> dict:
406
+ if config is None:
407
+ return {}
408
+ section_map = userconfig._COMMAND_MAP.get(section)
409
+ if not section_map:
410
+ return {}
411
+ try:
412
+ params = set(inspect.signature(impl).parameters)
413
+ except (TypeError, ValueError):
414
+ return {}
415
+ out: dict = {}
416
+ for key, (dest, coerce) in section_map.items():
417
+ if dest not in params:
418
+ continue # tool doesn't take this preference
419
+ raw = userconfig.resolve_default(section, key, config)
420
+ if raw is None:
421
+ continue
422
+ try:
423
+ out[dest] = coerce(raw)
424
+ except (TypeError, ValueError):
425
+ pass # a bad config value shouldn't break tool building
426
+ return out
427
+
428
+ def _make_paid(impl, section):
429
+ defaults = _config_defaults(section, impl)
430
+
374
431
  def invoke(arguments, *, confirm: bool = False):
375
432
  return impl(
376
433
  client,
377
434
  confirm=confirm,
378
435
  max_spend=max_spend,
379
436
  output_dir=output_dir,
380
- **_clean(arguments),
437
+ **{**defaults, **_clean(arguments)},
381
438
  )
382
439
 
383
440
  return invoke
384
441
 
385
- def _make_free(impl):
442
+ def _make_free(impl, section):
443
+ defaults = _config_defaults(section, impl)
444
+
386
445
  def invoke(arguments, *, confirm: bool = False):
387
- return impl(client, **_clean(arguments))
446
+ return impl(client, **{**defaults, **_clean(arguments)})
388
447
 
389
448
  return invoke
390
449
 
@@ -395,9 +454,9 @@ def builtin_tools(
395
454
  description=desc,
396
455
  parameters=schema,
397
456
  invoke=(
398
- _make_paid(getattr(_mcp, impl_name))
457
+ _make_paid(getattr(_mcp, impl_name), _tool_section(name))
399
458
  if paid
400
- else _make_free(getattr(_mcp, impl_name))
459
+ else _make_free(getattr(_mcp, impl_name), _tool_section(name))
401
460
  ),
402
461
  paid=paid,
403
462
  )
@@ -449,6 +449,7 @@ def code_tools(
449
449
  include_search: bool = False,
450
450
  assets: bool = False,
451
451
  max_spend=None,
452
+ config=None,
452
453
  ) -> List[_agent.Tool]:
453
454
  """Build the coding tools bound to a realpath-resolved project `root`.
454
455
 
@@ -527,10 +528,12 @@ def code_tools(
527
528
  _SEARCH_SCHEMA, search_invoke, paid=False,
528
529
  ))
529
530
 
530
- # A free model-catalog lookup so the agent can pick a valid `model` for the
531
- # asset tools instead of guessing (needs a client for the /models GET).
531
+ # Free model-catalog lookups so the agent can pick a valid `model` for the
532
+ # asset tools (and see cost/context limits) instead of guessing (need a client
533
+ # for the /models GET).
532
534
  if client is not None:
533
- tools.extend(_agent.builtin_tools(client, only={"venice_models"}))
535
+ tools.extend(_agent.builtin_tools(
536
+ client, only={"venice_models", "venice_model_details"}, config=config))
534
537
 
535
538
  if assets and client is not None:
536
539
  asset_dir = os.environ.get("VENICE_MCP_OUTPUT_DIR") or root
@@ -541,6 +544,7 @@ def code_tools(
541
544
  "venice_music", "venice_tts", "venice_upscale", "venice_bg_remove",
542
545
  "venice_video",
543
546
  },
547
+ config=config, # #58: asset tools honor defaults.<cmd>.*
544
548
  ))
545
549
  return tools
546
550
 
@@ -781,3 +781,28 @@ def models_tool(client, *, type: str) -> dict:
781
781
  "models": by_type}
782
782
  return {"status": "ok", "type": type,
783
783
  "count": len(by_type[type]), "models": by_type[type]}
784
+
785
+
786
+ def model_details_tool(client, *, model: str) -> dict:
787
+ """Details for one model id: pricing (cost), context/prompt-length limits, and
788
+ capabilities -- so an agent can budget its input and confirm a model fits
789
+ before using it. Read-only; not spend-gated. May scan the catalog by type to
790
+ locate the id.
791
+ """
792
+ if not model or not str(model).strip():
793
+ return _err("models: a model id is required")
794
+ m = _models_cmd._find_model(client, str(model).strip())
795
+ if m is None:
796
+ return _err(f"models: no model with id {model!r}")
797
+ spec = m.get("model_spec") if isinstance(m.get("model_spec"), dict) else {}
798
+ return {
799
+ "status": "ok",
800
+ "id": m.get("id"),
801
+ "type": m.get("type"),
802
+ "name": spec.get("name") or m.get("name"),
803
+ "pricing": spec.get("pricing"),
804
+ "available_context_tokens": spec.get("availableContextTokens"),
805
+ "prompt_character_limit": spec.get("promptCharacterLimit"),
806
+ "capabilities": spec.get("capabilities"),
807
+ "traits": spec.get("traits"),
808
+ }
@@ -338,6 +338,7 @@ def _tools_for(args, client, models, model):
338
338
  max_spend=args.max_spend,
339
339
  output_dir=str(args.output) if args.output else None,
340
340
  only=set(args.tool) if args.tool else None,
341
+ config=userconfig.load_config(), # #58: honor defaults.<cmd>.* in tools
341
342
  )
342
343
  except ValueError as e:
343
344
  print(f"chat: {e}", file=sys.stderr)
@@ -358,6 +358,7 @@ def _run(args) -> int:
358
358
  exec_timeout=args.exec_timeout or _code.DEFAULT_EXEC_TIMEOUT,
359
359
  include_search=True,
360
360
  assets=bool(args.assets),
361
+ config=userconfig.load_config(), # #58: honor defaults.<cmd>.* in tools
361
362
  )
362
363
  system = _system_prompt(args, root, tools)
363
364
  gen_kwargs = _gen_kwargs(args)
@@ -194,5 +194,72 @@ class TestProgress(unittest.TestCase):
194
194
  self.assertIn("· t", err.getvalue()) # per-tool-call activity line
195
195
 
196
196
 
197
+ class TestToolSection(unittest.TestCase):
198
+ def test_section_derivation(self):
199
+ self.assertEqual(_agent._tool_section("venice_image"), "image")
200
+ self.assertEqual(_agent._tool_section("venice_image_edit"), "image_edit")
201
+ self.assertEqual(_agent._tool_section("project_search"), "project_search")
202
+
203
+
204
+ class TestConfigDefaults(unittest.TestCase):
205
+ """#58: defaults.<cmd>.* are layered UNDER a tool's model-supplied args."""
206
+
207
+ def _spy(self):
208
+ captured = {}
209
+
210
+ def image_tool(client, prompt=None, *, hide_watermark=None, steps=None,
211
+ safe_mode=None, confirm=False, max_spend=None,
212
+ output_dir=None, **kw):
213
+ captured.update(hide_watermark=hide_watermark, steps=steps,
214
+ safe_mode=safe_mode)
215
+ captured.update(kw)
216
+ return {"status": "ok"}
217
+
218
+ return captured, image_tool
219
+
220
+ def _tool(self, spy, doc):
221
+ from venice.commands import _mcp
222
+ with mock.patch.object(_mcp, "image_tool", spy):
223
+ return _agent.builtin_tools(object(), config=doc,
224
+ only={"venice_image"})[0]
225
+
226
+ def test_injected_and_model_wins(self):
227
+ captured, spy = self._spy()
228
+ doc = {"defaults": {"image": {"hide_watermark": True, "steps": 40}}}
229
+ tool = self._tool(spy, doc)
230
+ tool.invoke({"prompt": "p"}) # model set no preference
231
+ self.assertIs(captured["hide_watermark"], True) # from config
232
+ self.assertEqual(captured["steps"], 40)
233
+ captured.clear()
234
+ tool.invoke({"prompt": "p", "hide_watermark": False, "steps": 5})
235
+ self.assertIs(captured["hide_watermark"], False) # explicit model arg wins
236
+ self.assertEqual(captured["steps"], 5)
237
+
238
+ def test_no_config_no_injection(self):
239
+ captured, spy = self._spy()
240
+ tool = self._tool(spy, None)
241
+ tool.invoke({"prompt": "p"})
242
+ self.assertIsNone(captured["hide_watermark"]) # tool's own default applies
243
+ self.assertIsNone(captured["steps"])
244
+
245
+ def test_only_accepted_allowlisted_keys_inject(self):
246
+ # `preset` is config-backable for image (#57) but image_tool takes no such
247
+ # param -> must NOT be injected; the accepted key still is.
248
+ captured, spy = self._spy()
249
+ doc = {"defaults": {"image": {"preset": "foo", "safe_mode": False}}}
250
+ tool = self._tool(spy, doc)
251
+ tool.invoke({"prompt": "p"})
252
+ self.assertNotIn("preset", captured)
253
+ self.assertIs(captured["safe_mode"], False)
254
+
255
+ def test_string_config_value_is_coerced(self):
256
+ captured, spy = self._spy()
257
+ doc = {"defaults": {"image": {"hide_watermark": "true", "steps": "12"}}}
258
+ tool = self._tool(spy, doc)
259
+ tool.invoke({"prompt": "p"})
260
+ self.assertIs(captured["hide_watermark"], True) # _as_bool
261
+ self.assertEqual(captured["steps"], 12) # int
262
+
263
+
197
264
  if __name__ == "__main__":
198
265
  unittest.main()
@@ -505,9 +505,10 @@ class TestChatAgent(unittest.TestCase):
505
505
  f"{t['function']['name']} leaks control kwarg {banned}",
506
506
  )
507
507
  names = {t["function"]["name"] for t in tools}
508
- self.assertEqual(len(names), 9) # media/chat + project_search + venice_models
508
+ self.assertEqual(len(names), 10) # media/chat + project_search + models(+details)
509
509
  self.assertIn("project_search", names)
510
510
  self.assertIn("venice_models", names)
511
+ self.assertIn("venice_model_details", names)
511
512
 
512
513
  def test_tool_error_surfaced_not_fatal(self):
513
514
  seq = [
@@ -637,7 +638,7 @@ class TestChatMcp(unittest.TestCase):
637
638
  names = self._tool_names(calls[0])
638
639
  self.assertIn("fs__read", names) # remote tool advertised
639
640
  self.assertIn("venice_image", names) # alongside the built-ins
640
- self.assertEqual(len(names), 10) # 9 built-ins + 1 remote
641
+ self.assertEqual(len(names), 11) # 10 built-ins + 1 remote
641
642
  self.assertEqual(attach.specs, [("fs", {"command": "srv", "args": []})])
642
643
  tool_msgs = [m for m in calls[1]["messages"] if m.get("role") == "tool"]
643
644
  self.assertIn("127.0.0.1", tool_msgs[0]["content"])
@@ -651,7 +652,7 @@ class TestChatMcp(unittest.TestCase):
651
652
  )
652
653
  self.assertEqual(rc, 0)
653
654
  attach.assert_not_called()
654
- self.assertEqual(len(self._tool_names(calls[0])), 9) # built-ins only
655
+ self.assertEqual(len(self._tool_names(calls[0])), 10) # built-ins only
655
656
 
656
657
  def test_unknown_mcp_server_exits_2_before_model(self):
657
658
  attach = mock.MagicMock()
@@ -739,8 +740,9 @@ class TestBuiltinToolsRegistry(unittest.TestCase):
739
740
  # chat's default advertisement must not grow when code gains asset tools
740
741
  from venice.commands import _agent
741
742
  names = {t.name for t in _agent.builtin_tools(object())}
742
- self.assertEqual(len(names), 9) # +venice_models (a deliberate free chat tool)
743
+ self.assertEqual(len(names), 10) # +venice_models +venice_model_details (free)
743
744
  self.assertIn("venice_models", names)
745
+ self.assertIn("venice_model_details", names)
744
746
  self.assertNotIn("venice_image_edit", names)
745
747
 
746
748
  def test_only_can_select_code_asset_extra(self):
@@ -249,11 +249,14 @@ class TestCodeFactory(unittest.TestCase):
249
249
  def test_models_tool_present_with_client(self):
250
250
  by = {t.name: t for t in _code.code_tools("/tmp", client=object())}
251
251
  self.assertIn("venice_models", by) # free model-catalog lookup for the agent
252
+ self.assertIn("venice_model_details", by) # cost/context-limit lookup
252
253
  self.assertFalse(by["venice_models"].paid) # read-only, not spend-gated
254
+ self.assertFalse(by["venice_model_details"].paid)
253
255
 
254
256
  def test_models_tool_absent_without_client(self):
255
257
  names = {t.name for t in _code.code_tools("/tmp")}
256
258
  self.assertNotIn("venice_models", names) # needs a client for the /models GET
259
+ self.assertNotIn("venice_model_details", names)
257
260
 
258
261
  def test_asset_tools_are_paid(self):
259
262
  by = {t.name: t for t in _code.code_tools("/tmp", client=object(),
@@ -526,5 +526,36 @@ class TestModelsTool(_ToolTest):
526
526
  self.assertIn("unknown type", out["message"])
527
527
 
528
528
 
529
+ class TestModelDetailsTool(_ToolTest):
530
+ def test_returns_curated_details(self):
531
+ # `text` is first in MODEL_TYPES, so _find_model matches on the first GET.
532
+ spec = {"name": "Big", "pricing": {"input": {"usd": 1.5}},
533
+ "availableContextTokens": 131072, "promptCharacterLimit": 1500,
534
+ "capabilities": {"supportsFunctionCalling": True},
535
+ "traits": ["default"]}
536
+ catalog = json.dumps(
537
+ {"data": [{"id": "big-model", "type": "text", "model_spec": spec}]}
538
+ ).encode()
539
+ with mock.patch("venice.client.urllib.request.urlopen",
540
+ _seq(FakeResp(200, catalog))), self.stdout_guard():
541
+ out = _mcp.model_details_tool(_client(), model="big-model")
542
+ self.assertEqual(out["status"], "ok")
543
+ self.assertEqual(out["id"], "big-model")
544
+ self.assertEqual(out["available_context_tokens"], 131072)
545
+ self.assertEqual(out["prompt_character_limit"], 1500)
546
+ self.assertEqual(out["pricing"], {"input": {"usd": 1.5}})
547
+ self.assertTrue(out["capabilities"]["supportsFunctionCalling"])
548
+
549
+ def test_unknown_model_errors(self):
550
+ from venice.commands.models import MODEL_TYPES
551
+ empty = json.dumps({"data": []}).encode()
552
+ resps = _seq(*[FakeResp(200, empty) for _ in MODEL_TYPES])
553
+ with mock.patch("venice.client.urllib.request.urlopen", resps), \
554
+ self.stdout_guard():
555
+ out = _mcp.model_details_tool(_client(), model="nope")
556
+ self.assertEqual(out["status"], "error")
557
+ self.assertIn("no model", out["message"])
558
+
559
+
529
560
  if __name__ == "__main__":
530
561
  unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "0.35.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