venice-cli 0.25.0__tar.gz → 0.26.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.25.0 → venice_cli-0.26.0}/PKG-INFO +36 -6
  2. {venice_cli-0.25.0 → venice_cli-0.26.0}/README.md +35 -5
  3. venice_cli-0.26.0/src/venice/__init__.py +1 -0
  4. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_openai.py +18 -3
  5. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/code.py +38 -5
  6. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/embed.py +44 -0
  7. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/config.py +4 -0
  8. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/userconfig.py +1 -0
  9. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_code_command.py +57 -0
  10. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_embed.py +82 -1
  11. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_shared_openai.py +29 -0
  12. venice_cli-0.25.0/src/venice/__init__.py +0 -1
  13. {venice_cli-0.25.0 → venice_cli-0.26.0}/.gitignore +0 -0
  14. {venice_cli-0.25.0 → venice_cli-0.26.0}/CONTRIBUTING.md +0 -0
  15. {venice_cli-0.25.0 → venice_cli-0.26.0}/LICENSE +0 -0
  16. {venice_cli-0.25.0 → venice_cli-0.26.0}/Makefile +0 -0
  17. {venice_cli-0.25.0 → venice_cli-0.26.0}/SECURITY.md +0 -0
  18. {venice_cli-0.25.0 → venice_cli-0.26.0}/bin/venice +0 -0
  19. {venice_cli-0.25.0 → venice_cli-0.26.0}/install.sh +0 -0
  20. {venice_cli-0.25.0 → venice_cli-0.26.0}/pyproject.toml +0 -0
  21. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/__main__.py +0 -0
  22. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/audio_player.py +0 -0
  23. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/audio_post.py +0 -0
  24. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/auth.py +0 -0
  25. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/billing.py +0 -0
  26. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/cli.py +0 -0
  27. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/client.py +0 -0
  28. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/__init__.py +0 -0
  29. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_agent.py +0 -0
  30. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_audio.py +0 -0
  31. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_code.py +0 -0
  32. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_index.py +0 -0
  33. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_mcp.py +0 -0
  34. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_mcp_client.py +0 -0
  35. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_models.py +0 -0
  36. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_queue.py +0 -0
  37. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_repl.py +0 -0
  38. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_shared.py +0 -0
  39. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/balance.py +0 -0
  40. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/bg_remove.py +0 -0
  41. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/chat.py +0 -0
  42. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/config.py +0 -0
  43. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/contact_sheet.py +0 -0
  44. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/image.py +0 -0
  45. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/image_edit.py +0 -0
  46. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/index.py +0 -0
  47. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/login.py +0 -0
  48. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/master.py +0 -0
  49. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/mcp_serve.py +0 -0
  50. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/models.py +0 -0
  51. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/music.py +0 -0
  52. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/search.py +0 -0
  53. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/sfx.py +0 -0
  54. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/tts.py +0 -0
  55. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/upscale.py +0 -0
  56. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/video.py +0 -0
  57. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/image_montage.py +0 -0
  58. {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/mcp_server.py +0 -0
  59. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/__init__.py +0 -0
  60. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/_mcp_fake_server.py +0 -0
  61. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_auth.py +0 -0
  62. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_balance.py +0 -0
  63. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_bg_remove.py +0 -0
  64. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_chat.py +0 -0
  65. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_client.py +0 -0
  66. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_code.py +0 -0
  67. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_config.py +0 -0
  68. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_contact_sheet.py +0 -0
  69. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_image.py +0 -0
  70. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_image_edit.py +0 -0
  71. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_index.py +0 -0
  72. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_master.py +0 -0
  73. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_mcp_client.py +0 -0
  74. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_mcp_serve.py +0 -0
  75. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_mcp_tools.py +0 -0
  76. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_models.py +0 -0
  77. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_music.py +0 -0
  78. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_repl.py +0 -0
  79. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_search.py +0 -0
  80. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_sfx.py +0 -0
  81. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_shared.py +0 -0
  82. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_tts.py +0 -0
  83. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_upscale.py +0 -0
  84. {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_video.py +0 -0
  85. {venice_cli-0.25.0 → venice_cli-0.26.0}/uninstall.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: venice-cli
3
- Version: 0.25.0
3
+ Version: 0.26.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
@@ -731,6 +731,27 @@ The URL can also come from `$VENICE_EMBED_BASE_URL`, and a key (if the backend
731
731
  needs one) from `$VENICE_EMBED_API_KEY` — env only, never `config.json`. Both
732
732
  are config-backable per-flag via `defaults.embed.*` (see `venice config`).
733
733
 
734
+ **Self-signed backends.** A local embedder fronted by Traefik/Caddy often serves
735
+ a private or self-signed TLS cert, which the OpenAI SDK rejects
736
+ (`embed: connection error`, exit 8). Two opt-in escape hatches — **applied only
737
+ to `--embed-base-url`, never the Venice endpoint**:
738
+
739
+ ```sh
740
+ # Trust a private CA (verification stays ON -- preferred):
741
+ venice embed --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
742
+ --embed-ca-bundle /etc/ssl/my-ca.pem "hi" # or $VENICE_EMBED_CA_BUNDLE
743
+
744
+ # Disable verification entirely (self-signed, no CA handy -- prints a warning):
745
+ venice embed --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
746
+ --embed-insecure "hi"
747
+ ```
748
+
749
+ `--embed-ca-bundle` is config-backable (`defaults.embed.embed_ca_bundle`) and
750
+ reads `$VENICE_EMBED_CA_BUNDLE`. `--embed-insecure` is CLI-only by design —
751
+ turning verification off should always be an explicit, visible choice, never
752
+ something a stale env var or config file switches on. The two are mutually
753
+ exclusive, and passing either without `--embed-base-url` is an error (exit 2).
754
+
734
755
  ## Semantic search
735
756
 
736
757
  `venice index` builds a local semantic index of a project tree, and `venice
@@ -826,8 +847,14 @@ three possible ways, then executes and finally self-checks the criteria:
826
847
 
827
848
  Non-interactive with neither `--auto` nor `--plan-only` **aborts (exit 2)** before any
828
849
  model call — side-effecting work never runs unattended without an explicit opt-in.
829
- After execution a final turn reports each criterion MET/NOT MET; with `--json` that
830
- lands in the envelope and the **exit code reflects it** (0 = all met, 1 = not met).
850
+ After execution a final turn reports each criterion MET/NOT MET and ends with an
851
+ `ACCEPTANCE: PASS`/`FAIL` verdict; with `--json` the verdict lands in the envelope
852
+ (`acceptance.verdict` = `pass`/`fail`/`unknown`). The verdict parse is
853
+ case/format-tolerant and **re-prompts once** for the verdict line if the first reply
854
+ lacks it, so a correct run whose model phrased its verdict loosely still exits 0. The
855
+ **exit code reflects it**: 0 = all met (or check skipped), 1 = not met, 10 = the model
856
+ never emitted a parseable verdict even after the re-prompt (the work may still be
857
+ complete — a loud stderr warning is printed).
831
858
 
832
859
  **Tools** (path-sandboxed to the project root; mutating tools confirm unless `--auto`):
833
860
 
@@ -910,8 +937,9 @@ working directory), or a per-call `output_dir`. The API key is read the usual
910
937
  way (`$VENICE_API_KEY` or the credentials file) and is never echoed.
911
938
 
912
939
  Only stdout carries the JSON-RPC protocol; the server's own diagnostics go to
913
- stderr. `venice image-edit` exists as a CLI command, but it and the video tools
914
- are not exposed over MCP yet (tracked separately).
940
+ stderr. Video generation and image editing are exposed over MCP too: the
941
+ `venice_video` and `venice_image_edit` tools cover the same capabilities as the
942
+ `venice video` and `venice image-edit` CLI commands.
915
943
 
916
944
  The reverse direction — venice as an MCP **client**, calling *other* servers'
917
945
  tools inside `venice chat` — is [`venice chat --mcp`](#external-mcp-tools---mcp).
@@ -994,6 +1022,7 @@ Veo 3.1, Kling, Runway Gen4, LTX-2, Wan 2.7, Seedance 2.0, and more.
994
1022
  | 7 | poll timeout |
995
1023
  | 8 | network / connection error |
996
1024
  | 9 | disk write error |
1025
+ | 10 | acceptance verdict unparseable / ambiguous (`venice code`) |
997
1026
  | 130 | Ctrl-C |
998
1027
 
999
1028
  ## Audio playback caveat
@@ -1019,6 +1048,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
1019
1048
  | `VENICE_BASE_URL` | override the API base URL (testing, proxy) |
1020
1049
  | `VENICE_EMBED_BASE_URL` | `embed` alternate OpenAI-compatible endpoint (local backend) |
1021
1050
  | `VENICE_EMBED_API_KEY` | key for `VENICE_EMBED_BASE_URL` (if the backend needs one) |
1051
+ | `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for `VENICE_EMBED_BASE_URL` TLS (self-signed backend) |
1022
1052
  | `VENICE_MCP_MAX_SPEND` | `mcp-serve` auto-approve cap in USD (default `0.10`) |
1023
1053
  | `VENICE_MCP_OUTPUT_DIR` | where `mcp-serve` tools write files (default: cwd) |
1024
1054
 
@@ -1041,7 +1071,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
1041
1071
  | `venice contact-sheet DIR_OR_GLOB [--cols N] [--cell WxH] [--label] [...]` | tile images into one contact sheet (no API call) |
1042
1072
  | `venice chat MESSAGE [--system S] [--model M] [--web-search on] [...]` | one-shot chat completion (OpenAI SDK) |
1043
1073
  | `venice chat [-i] [--resume FILE]` | interactive multi-turn REPL (conversation state, `/`-commands, transcripts) |
1044
- | `venice embed [TEXT] [--from-file PATH] [--model M] [--dimensions N] [--json] [--embed-base-url URL --embed-model M]` | text embeddings (OpenAI SDK; alt/local backend) |
1074
+ | `venice embed [TEXT] [--from-file PATH] [--model M] [--dimensions N] [--json] [--embed-base-url URL --embed-model M [--embed-ca-bundle PATH \| --embed-insecure]]` | text embeddings (OpenAI SDK; alt/local backend) |
1045
1075
  | `venice index [PATH] [--model M] [...]` / `venice search QUERY [-k N] [--json]` | build / query a local semantic index of a project tree |
1046
1076
  | `venice code [TASK] [--auto\|--manual] [--plan-only] [-i] [--root DIR] [--json] [...]` | coding agent: plan → accept → edit/run a project (needs `[openai]` + tool-calling model) |
1047
1077
  | `venice mcp-serve` | run an MCP server (stdio) exposing venice tools (needs `[mcp]`) |
@@ -693,6 +693,27 @@ The URL can also come from `$VENICE_EMBED_BASE_URL`, and a key (if the backend
693
693
  needs one) from `$VENICE_EMBED_API_KEY` — env only, never `config.json`. Both
694
694
  are config-backable per-flag via `defaults.embed.*` (see `venice config`).
695
695
 
696
+ **Self-signed backends.** A local embedder fronted by Traefik/Caddy often serves
697
+ a private or self-signed TLS cert, which the OpenAI SDK rejects
698
+ (`embed: connection error`, exit 8). Two opt-in escape hatches — **applied only
699
+ to `--embed-base-url`, never the Venice endpoint**:
700
+
701
+ ```sh
702
+ # Trust a private CA (verification stays ON -- preferred):
703
+ venice embed --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
704
+ --embed-ca-bundle /etc/ssl/my-ca.pem "hi" # or $VENICE_EMBED_CA_BUNDLE
705
+
706
+ # Disable verification entirely (self-signed, no CA handy -- prints a warning):
707
+ venice embed --embed-base-url https://embed.local/v1 --embed-model bge-m3 \
708
+ --embed-insecure "hi"
709
+ ```
710
+
711
+ `--embed-ca-bundle` is config-backable (`defaults.embed.embed_ca_bundle`) and
712
+ reads `$VENICE_EMBED_CA_BUNDLE`. `--embed-insecure` is CLI-only by design —
713
+ turning verification off should always be an explicit, visible choice, never
714
+ something a stale env var or config file switches on. The two are mutually
715
+ exclusive, and passing either without `--embed-base-url` is an error (exit 2).
716
+
696
717
  ## Semantic search
697
718
 
698
719
  `venice index` builds a local semantic index of a project tree, and `venice
@@ -788,8 +809,14 @@ three possible ways, then executes and finally self-checks the criteria:
788
809
 
789
810
  Non-interactive with neither `--auto` nor `--plan-only` **aborts (exit 2)** before any
790
811
  model call — side-effecting work never runs unattended without an explicit opt-in.
791
- After execution a final turn reports each criterion MET/NOT MET; with `--json` that
792
- lands in the envelope and the **exit code reflects it** (0 = all met, 1 = not met).
812
+ After execution a final turn reports each criterion MET/NOT MET and ends with an
813
+ `ACCEPTANCE: PASS`/`FAIL` verdict; with `--json` the verdict lands in the envelope
814
+ (`acceptance.verdict` = `pass`/`fail`/`unknown`). The verdict parse is
815
+ case/format-tolerant and **re-prompts once** for the verdict line if the first reply
816
+ lacks it, so a correct run whose model phrased its verdict loosely still exits 0. The
817
+ **exit code reflects it**: 0 = all met (or check skipped), 1 = not met, 10 = the model
818
+ never emitted a parseable verdict even after the re-prompt (the work may still be
819
+ complete — a loud stderr warning is printed).
793
820
 
794
821
  **Tools** (path-sandboxed to the project root; mutating tools confirm unless `--auto`):
795
822
 
@@ -872,8 +899,9 @@ working directory), or a per-call `output_dir`. The API key is read the usual
872
899
  way (`$VENICE_API_KEY` or the credentials file) and is never echoed.
873
900
 
874
901
  Only stdout carries the JSON-RPC protocol; the server's own diagnostics go to
875
- stderr. `venice image-edit` exists as a CLI command, but it and the video tools
876
- are not exposed over MCP yet (tracked separately).
902
+ stderr. Video generation and image editing are exposed over MCP too: the
903
+ `venice_video` and `venice_image_edit` tools cover the same capabilities as the
904
+ `venice video` and `venice image-edit` CLI commands.
877
905
 
878
906
  The reverse direction — venice as an MCP **client**, calling *other* servers'
879
907
  tools inside `venice chat` — is [`venice chat --mcp`](#external-mcp-tools---mcp).
@@ -956,6 +984,7 @@ Veo 3.1, Kling, Runway Gen4, LTX-2, Wan 2.7, Seedance 2.0, and more.
956
984
  | 7 | poll timeout |
957
985
  | 8 | network / connection error |
958
986
  | 9 | disk write error |
987
+ | 10 | acceptance verdict unparseable / ambiguous (`venice code`) |
959
988
  | 130 | Ctrl-C |
960
989
 
961
990
  ## Audio playback caveat
@@ -981,6 +1010,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
981
1010
  | `VENICE_BASE_URL` | override the API base URL (testing, proxy) |
982
1011
  | `VENICE_EMBED_BASE_URL` | `embed` alternate OpenAI-compatible endpoint (local backend) |
983
1012
  | `VENICE_EMBED_API_KEY` | key for `VENICE_EMBED_BASE_URL` (if the backend needs one) |
1013
+ | `VENICE_EMBED_CA_BUNDLE` | CA bundle to trust for `VENICE_EMBED_BASE_URL` TLS (self-signed backend) |
984
1014
  | `VENICE_MCP_MAX_SPEND` | `mcp-serve` auto-approve cap in USD (default `0.10`) |
985
1015
  | `VENICE_MCP_OUTPUT_DIR` | where `mcp-serve` tools write files (default: cwd) |
986
1016
 
@@ -1003,7 +1033,7 @@ The player list (`paplay` -> `aplay` -> `ffplay` -> `mpg123` -> `play`
1003
1033
  | `venice contact-sheet DIR_OR_GLOB [--cols N] [--cell WxH] [--label] [...]` | tile images into one contact sheet (no API call) |
1004
1034
  | `venice chat MESSAGE [--system S] [--model M] [--web-search on] [...]` | one-shot chat completion (OpenAI SDK) |
1005
1035
  | `venice chat [-i] [--resume FILE]` | interactive multi-turn REPL (conversation state, `/`-commands, transcripts) |
1006
- | `venice embed [TEXT] [--from-file PATH] [--model M] [--dimensions N] [--json] [--embed-base-url URL --embed-model M]` | text embeddings (OpenAI SDK; alt/local backend) |
1036
+ | `venice embed [TEXT] [--from-file PATH] [--model M] [--dimensions N] [--json] [--embed-base-url URL --embed-model M [--embed-ca-bundle PATH \| --embed-insecure]]` | text embeddings (OpenAI SDK; alt/local backend) |
1007
1037
  | `venice index [PATH] [--model M] [...]` / `venice search QUERY [-k N] [--json]` | build / query a local semantic index of a project tree |
1008
1038
  | `venice code [TASK] [--auto\|--manual] [--plan-only] [-i] [--root DIR] [--json] [...]` | coding agent: plan → accept → edit/run a project (needs `[openai]` + tool-calling model) |
1009
1039
  | `venice mcp-serve` | run an MCP server (stdio) exposing venice tools (needs `[mcp]`) |
@@ -0,0 +1 @@
1
+ __version__ = "0.26.0"
@@ -33,17 +33,32 @@ def import_openai(label: str):
33
33
  return openai
34
34
 
35
35
 
36
- def build_openai(module, client=None, *, base_url=None, api_key=None):
36
+ def build_openai(module, client=None, *, base_url=None, api_key=None, verify=None):
37
37
  """Build an SDK client pointed at Venice, borrowing the lean client's auth.
38
38
 
39
39
  When `base_url` is given (an alternate OpenAI-compatible backend, e.g. a
40
40
  local embeddings server), use it and `api_key` directly instead of the
41
41
  Venice client -- which may then be None. Local servers usually need no key,
42
42
  so `api_key` falls back to a placeholder the SDK accepts.
43
+
44
+ `verify` overrides TLS verification for that alternate backend (a CA-bundle
45
+ path to trust a private CA, or False to disable checks for a self-signed
46
+ cert). It is opt-in and only reaches non-Venice endpoints. When set we hand
47
+ the SDK an httpx client (httpx ships transitively with the openai SDK). The
48
+ client is not explicitly closed -- fine for a one-shot CLI process that exits
49
+ right after; don't copy this into a long-lived caller without closing it.
43
50
  """
51
+ extra = {}
52
+ if verify is not None:
53
+ import httpx
54
+ extra["http_client"] = httpx.Client(verify=verify)
44
55
  if base_url is not None:
45
- return module.OpenAI(api_key=api_key or "not-needed", base_url=base_url)
46
- return module.OpenAI(api_key=client.api_key, base_url=client.base_url)
56
+ return module.OpenAI(
57
+ api_key=api_key or "not-needed", base_url=base_url, **extra
58
+ )
59
+ return module.OpenAI(
60
+ api_key=client.api_key, base_url=client.base_url, **extra
61
+ )
47
62
 
48
63
 
49
64
  def status_to_exit(module, e, label: str) -> int:
@@ -16,7 +16,11 @@ a controlling LLM identically:
16
16
  (``--auto`` -> every tool auto-approved) or manual (per-step confirm gate on the
17
17
  ``paid=True`` write/edit/run tools).
18
18
  4. **Acceptance check.** A final ``tool_choice="none"`` turn reports each criterion
19
- met/unmet; ``--json`` emits it structured and the exit code reflects it.
19
+ met/unmet and ends with an ``ACCEPTANCE: PASS``/``FAIL`` verdict. The parse is
20
+ format-tolerant and re-prompts once for the verdict line if the first reply lacks
21
+ it; ``--json`` emits the verdict structured. The exit code reflects it: 0 = pass
22
+ (or check skipped), 1 = fail, 10 = verdict unparseable even after the re-prompt
23
+ (the work may still be complete).
20
24
 
21
25
  Unlike ``venice chat --tools`` (which degrades to plain chat), ``venice code`` errors
22
26
  out on a non-tool-calling model -- coding without tools is pointless.
@@ -31,6 +35,7 @@ import contextlib
31
35
  import io
32
36
  import json
33
37
  import os
38
+ import re
34
39
  import sys
35
40
  from typing import List, Optional
36
41
 
@@ -74,6 +79,20 @@ _VERIFY_MSG = (
74
79
  "that is exactly 'ACCEPTANCE: PASS' if every criterion is met, or "
75
80
  "'ACCEPTANCE: FAIL' otherwise."
76
81
  )
82
+ _VERIFY_RETRY_MSG = (
83
+ "Your reply did not end with the required verdict line. Reply with nothing but a "
84
+ "single line that is exactly 'ACCEPTANCE: PASS' if every acceptance criterion is "
85
+ "met, or 'ACCEPTANCE: FAIL' otherwise."
86
+ )
87
+
88
+ _VERDICT_RE = re.compile(r"ACCEPTANCE:\s*(PASS|FAIL)", re.IGNORECASE)
89
+
90
+
91
+ def _parse_verdict(report: Optional[str]) -> Optional[str]:
92
+ """'pass' / 'fail' from the last ACCEPTANCE sentinel in the report, or None if
93
+ no recognizable verdict is present (case/whitespace/markdown tolerant)."""
94
+ m = _VERDICT_RE.findall(report or "")
95
+ return m[-1].lower() if m else None
77
96
 
78
97
 
79
98
  def register(subparsers) -> None:
@@ -437,18 +456,28 @@ def _run_oneshot(args, oai, openai, model, tools, system, gen_kwargs, root, task
437
456
  return _openai.status_to_exit(openai, e, "code")
438
457
 
439
458
  # --- Acceptance check ---
440
- passed = None
459
+ verdict = None # None = skipped; else 'pass' | 'fail' | 'unknown'
441
460
  report = None
442
461
  if not args.no_verify and not args.no_plan:
443
462
  messages.append({"role": "user", "content": _VERIFY_MSG})
444
463
  try:
445
464
  report = _no_tool_turn(oai, model, messages, gen_kwargs, oai_tools)
465
+ parsed = _parse_verdict(report)
466
+ if parsed is None: # re-prompt ONCE for the exact verdict line
467
+ messages.append({"role": "assistant", "content": report})
468
+ messages.append({"role": "user", "content": _VERIFY_RETRY_MSG})
469
+ retry = _no_tool_turn(oai, model, messages, gen_kwargs, oai_tools)
470
+ report = f"{report}\n{retry}" if report else retry
471
+ parsed = _parse_verdict(retry)
446
472
  except openai.OpenAIError as e:
447
473
  return _openai.status_to_exit(openai, e, "code")
448
- passed = "ACCEPTANCE: PASS" in (report or "").upper()
474
+ verdict = parsed or "unknown"
449
475
  if not args.json:
450
476
  print("\n=== Acceptance check ===", file=sys.stderr)
451
477
  print(report, file=sys.stderr)
478
+ if verdict == "unknown":
479
+ print("code: could not parse an ACCEPTANCE verdict from the model "
480
+ "(work may be complete) -- exiting 10", file=sys.stderr)
452
481
 
453
482
  if args.json:
454
483
  envelope = {
@@ -456,8 +485,12 @@ def _run_oneshot(args, oai, openai, model, tools, system, gen_kwargs, root, task
456
485
  "final": final_text,
457
486
  }
458
487
  if report is not None:
459
- envelope["acceptance"] = {"passed": passed, "report": report}
488
+ envelope["acceptance"] = {
489
+ "verdict": verdict, # pass|fail|unknown
490
+ "passed": {"pass": True, "fail": False}.get(verdict), # None when unknown
491
+ "report": report,
492
+ }
460
493
  json.dump(envelope, sys.stdout, indent=2, default=str)
461
494
  sys.stdout.write("\n")
462
495
 
463
- return 1 if passed is False else 0
496
+ return {None: 0, "pass": 0, "fail": 1, "unknown": 10}[verdict]
@@ -71,6 +71,28 @@ def register(subparsers) -> None:
71
71
  "server has its own catalog, so it is taken as given)."
72
72
  ),
73
73
  )
74
+ tls = p.add_mutually_exclusive_group()
75
+ tls.add_argument(
76
+ "--embed-ca-bundle",
77
+ dest="embed_ca_bundle",
78
+ default=None,
79
+ metavar="PATH",
80
+ help=(
81
+ "Trust this CA-bundle file when verifying the --embed-base-url TLS "
82
+ "cert (for a local backend behind a private/self-signed CA). Keeps "
83
+ "verification on; also $VENICE_EMBED_CA_BUNDLE."
84
+ ),
85
+ )
86
+ tls.add_argument(
87
+ "--embed-insecure",
88
+ dest="embed_insecure",
89
+ action="store_true",
90
+ help=(
91
+ "Disable TLS verification for --embed-base-url entirely (self-signed "
92
+ "certs). Only applies to the alternate backend, never Venice; prints "
93
+ "a warning. Prefer --embed-ca-bundle."
94
+ ),
95
+ )
74
96
  p.add_argument(
75
97
  "--dimensions",
76
98
  type=int,
@@ -154,13 +176,33 @@ def _resolve_backend(openai, args) -> tuple:
154
176
  file=sys.stderr,
155
177
  )
156
178
  return None, None, 2
179
+ # TLS override, opt-in and non-Venice only: --embed-insecure wins (the
180
+ # CLI already blocks passing both), else a CA bundle, else the SDK default.
181
+ verify = False if args.embed_insecure else (args.embed_ca_bundle or None)
182
+ if verify is False:
183
+ print(
184
+ "embed: WARNING: TLS verification disabled (--embed-insecure) "
185
+ f"for {args.embed_base_url}",
186
+ file=sys.stderr,
187
+ )
157
188
  oai = _openai.build_openai(
158
189
  openai,
159
190
  base_url=args.embed_base_url,
160
191
  api_key=os.environ.get(config.ENV_EMBED_API_KEY),
192
+ verify=verify,
161
193
  )
162
194
  return oai, model, None
163
195
 
196
+ # The Venice endpoint's TLS is not overridable -- reject the flags here so
197
+ # they can't silently no-op against Venice.
198
+ if args.embed_insecure or args.embed_ca_bundle:
199
+ print(
200
+ "embed: --embed-insecure/--embed-ca-bundle only apply with "
201
+ "--embed-base-url",
202
+ file=sys.stderr,
203
+ )
204
+ return None, None, 2
205
+
164
206
  try:
165
207
  client = build_client_from_auth()
166
208
  except auth.AuthError as e:
@@ -186,6 +228,8 @@ def _run(args) -> int:
186
228
  # apply_defaults, which only fills a dest that is still None.
187
229
  if args.embed_base_url is None:
188
230
  args.embed_base_url = os.environ.get(config.ENV_EMBED_BASE_URL)
231
+ if args.embed_ca_bundle is None:
232
+ args.embed_ca_bundle = os.environ.get(config.ENV_EMBED_CA_BUNDLE)
189
233
  userconfig.apply_defaults(args, "embed")
190
234
 
191
235
  openai = _openai.import_openai("embed")
@@ -18,6 +18,10 @@ DEFAULT_BASE_URL = "https://api.venice.ai/api/v1"
18
18
  # usually need none, and secrets don't belong in config.json).
19
19
  ENV_EMBED_BASE_URL = "VENICE_EMBED_BASE_URL"
20
20
  ENV_EMBED_API_KEY = "VENICE_EMBED_API_KEY"
21
+ # Trust a private CA when the alternate backend serves a self-signed cert. Only
22
+ # ever applied to --embed-base-url, never the Venice endpoint. (--embed-insecure
23
+ # has no env/config knob by design -- disabling verification stays a CLI choice.)
24
+ ENV_EMBED_CA_BUNDLE = "VENICE_EMBED_CA_BUNDLE"
21
25
 
22
26
  # `venice index`/`venice search` keep a semantic-search store *project-local*
23
27
  # (unlike everything above, which is user-global under ~/.config/venice). The
@@ -217,6 +217,7 @@ _COMMAND_MAP = {
217
217
  "encoding_format": ("encoding_format", str),
218
218
  "embed_base_url": ("embed_base_url", str),
219
219
  "embed_model": ("embed_model", str),
220
+ "embed_ca_bundle": ("embed_ca_bundle", str),
220
221
  },
221
222
  "index": {
222
223
  "model": ("model", str),
@@ -102,6 +102,62 @@ class TestCodeCommand(unittest.TestCase):
102
102
  _code_args(task="x", root=self.root, auto=True), seq)
103
103
  self.assertEqual(rc, 1)
104
104
 
105
+ # --- #37: the verdict parse is case/format-tolerant (no false-fail) ---
106
+ def test_acceptance_pass_loose_parse(self):
107
+ seq = [
108
+ FakeToolCompletion("plan"),
109
+ FakeToolCompletion("did the work"),
110
+ FakeToolCompletion("- works: MET\n**acceptance: pass**"), # lower + markdown
111
+ ]
112
+ rc, calls = self._run(
113
+ _code_args(task="x", root=self.root, auto=True), seq)
114
+ self.assertEqual(rc, 0)
115
+ self.assertEqual(len(calls), 3) # no re-prompt fired
116
+
117
+ # --- #37: unparseable verdict -> re-prompt once -> recovers to exit 0 ---
118
+ def test_acceptance_unknown_reprompt_recovers(self):
119
+ seq = [
120
+ FakeToolCompletion("plan"),
121
+ FakeToolCompletion("did the work"),
122
+ FakeToolCompletion("All acceptance criteria are met."), # no sentinel
123
+ FakeToolCompletion("ACCEPTANCE: PASS"), # re-prompt reply
124
+ ]
125
+ rc, calls = self._run(
126
+ _code_args(task="x", root=self.root, auto=True), seq)
127
+ self.assertEqual(rc, 0)
128
+ self.assertEqual(len(calls), 4) # extra re-prompt turn
129
+ self.assertEqual(calls[3]["tool_choice"], "none")
130
+
131
+ # --- #37: still no verdict after the re-prompt -> exit 10 + warning ---
132
+ def test_acceptance_unknown_persists_exits_10(self):
133
+ err = io.StringIO()
134
+ seq = [
135
+ FakeToolCompletion("plan"),
136
+ FakeToolCompletion("did the work"),
137
+ FakeToolCompletion("All criteria met."), # no sentinel
138
+ FakeToolCompletion("Looks good."), # still none
139
+ ]
140
+ rc, calls = self._run(
141
+ _code_args(task="x", root=self.root, auto=True), seq, stderr=err)
142
+ self.assertEqual(rc, 10)
143
+ self.assertEqual(len(calls), 4)
144
+ self.assertIn("exiting 10", err.getvalue())
145
+
146
+ def test_json_verdict_unknown(self):
147
+ out = io.StringIO()
148
+ seq = [
149
+ FakeToolCompletion("plan"),
150
+ FakeToolCompletion("did the work"),
151
+ FakeToolCompletion("All criteria met."),
152
+ FakeToolCompletion("Still looks good."),
153
+ ]
154
+ rc, _calls = self._run(
155
+ _code_args(task="x", root=self.root, auto=True, json=True), seq, stdout=out)
156
+ self.assertEqual(rc, 10)
157
+ env = json.loads(out.getvalue())
158
+ self.assertEqual(env["acceptance"]["verdict"], "unknown")
159
+ self.assertIsNone(env["acceptance"]["passed"])
160
+
105
161
  # --- fail-safe: non-TTY without --auto aborts before any model call ---
106
162
  def test_non_tty_without_auto_aborts(self):
107
163
  err = io.StringIO()
@@ -138,6 +194,7 @@ class TestCodeCommand(unittest.TestCase):
138
194
  self.assertEqual(env["plan"], "plan text")
139
195
  self.assertIn("wrote n.py", env["final"])
140
196
  self.assertTrue(env["acceptance"]["passed"])
197
+ self.assertEqual(env["acceptance"]["verdict"], "pass")
141
198
  self.assertEqual(env["root"], self.root)
142
199
 
143
200
  # --- --no-plan skips plan + verify ---
@@ -21,6 +21,7 @@ def _args(**ov):
21
21
  text=None, from_file=None, model=None,
22
22
  dimensions=None, encoding_format=None, json=False,
23
23
  embed_base_url=None, embed_model=None,
24
+ embed_ca_bundle=None, embed_insecure=False,
24
25
  )
25
26
  base.update(ov)
26
27
  return argparse.Namespace(**base)
@@ -123,7 +124,7 @@ class TestEmbed(unittest.TestCase):
123
124
  env = {
124
125
  k: v for k, v in os.environ.items()
125
126
  if k not in ("VENICE_API_KEY", "VENICE_EMBED_BASE_URL",
126
- "VENICE_EMBED_API_KEY")
127
+ "VENICE_EMBED_API_KEY", "VENICE_EMBED_CA_BUNDLE")
127
128
  }
128
129
  env.update(extra_env or {})
129
130
  with mock.patch.dict(os.environ, env, clear=True), \
@@ -332,6 +333,86 @@ class TestEmbed(unittest.TestCase):
332
333
  self.assertEqual(captured["model"], "cfg-embed")
333
334
  self.assertEqual(urlopen.call_count, 0)
334
335
 
336
+ # --- TLS override for the alternate backend (#28) ---
337
+
338
+ def test_ca_bundle_reaches_sdk_on_alt_path(self):
339
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx:
340
+ rc, built, captured, urlopen = self._run_alt(
341
+ _args(text="hi", embed_base_url="https://embed.local/v1",
342
+ embed_model="local-embed", embed_ca_bundle="/ca.pem"),
343
+ FakeEmbeddings([(0, [1.0])]),
344
+ )
345
+ self.assertEqual(rc, 0)
346
+ Hx.assert_called_once_with(verify="/ca.pem")
347
+ self.assertEqual(built["http_client"], "httpx-sentinel")
348
+
349
+ def test_insecure_disables_verification_and_warns(self):
350
+ err = io.StringIO()
351
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx:
352
+ rc, built, captured, urlopen = self._run_alt(
353
+ _args(text="hi", embed_base_url="https://embed.local/v1",
354
+ embed_model="local-embed", embed_insecure=True),
355
+ FakeEmbeddings([(0, [1.0])]), stderr=err,
356
+ )
357
+ self.assertEqual(rc, 0)
358
+ Hx.assert_called_once_with(verify=False)
359
+ self.assertEqual(built["http_client"], "httpx-sentinel")
360
+ self.assertIn("TLS verification disabled", err.getvalue())
361
+
362
+ def test_ca_bundle_from_env_on_alt_path(self):
363
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx:
364
+ rc, built, captured, urlopen = self._run_alt(
365
+ _args(text="hi", embed_base_url="https://embed.local/v1",
366
+ embed_model="local-embed"),
367
+ FakeEmbeddings([(0, [1.0])]),
368
+ extra_env={"VENICE_EMBED_CA_BUNDLE": "/env-ca.pem"},
369
+ )
370
+ self.assertEqual(rc, 0)
371
+ Hx.assert_called_once_with(verify="/env-ca.pem")
372
+
373
+ def test_ca_bundle_from_config_on_alt_path(self):
374
+ doc = {"version": 1, "mcpServers": {}, "defaults": {"embed": {
375
+ "embed_ca_bundle": "/cfg-ca.pem"}}}
376
+ with mock.patch("httpx.Client", return_value="httpx-sentinel") as Hx:
377
+ rc, built, captured, urlopen = self._run_alt(
378
+ _args(text="hi", embed_base_url="https://embed.local/v1",
379
+ embed_model="local-embed"),
380
+ FakeEmbeddings([(0, [1.0])]), doc=doc,
381
+ )
382
+ self.assertEqual(rc, 0)
383
+ Hx.assert_called_once_with(verify="/cfg-ca.pem")
384
+
385
+ def test_insecure_without_base_url_is_rejected(self):
386
+ err = io.StringIO()
387
+ rc, fake, captured = self._run(
388
+ _args(text="hi", embed_insecure=True), FakeEmbeddings([(0, [1.0])]),
389
+ stderr=err,
390
+ )
391
+ self.assertEqual(rc, 2)
392
+ self.assertEqual(fake.embeddings.create.call_count, 0)
393
+ self.assertIn("only apply with --embed-base-url", err.getvalue())
394
+
395
+ def test_ca_bundle_without_base_url_is_rejected(self):
396
+ err = io.StringIO()
397
+ rc, fake, captured = self._run(
398
+ _args(text="hi", embed_ca_bundle="/ca.pem"),
399
+ FakeEmbeddings([(0, [1.0])]), stderr=err,
400
+ )
401
+ self.assertEqual(rc, 2)
402
+ self.assertEqual(fake.embeddings.create.call_count, 0)
403
+ self.assertIn("only apply with --embed-base-url", err.getvalue())
404
+
405
+ def test_ca_bundle_and_insecure_are_mutually_exclusive(self):
406
+ from venice.commands import embed
407
+ parser = argparse.ArgumentParser()
408
+ sub = parser.add_subparsers()
409
+ embed.register(sub)
410
+ with mock.patch.object(sys, "stderr", io.StringIO()), \
411
+ self.assertRaises(SystemExit):
412
+ parser.parse_args(
413
+ ["embed", "--embed-ca-bundle", "/ca.pem", "--embed-insecure"]
414
+ )
415
+
335
416
 
336
417
  if __name__ == "__main__":
337
418
  unittest.main()
@@ -179,6 +179,35 @@ class TestBuildOpenAI(unittest.TestCase):
179
179
  stub.built, {"api_key": "not-needed", "base_url": "http://localhost:1234/v1"}
180
180
  )
181
181
 
182
+ def test_verify_ca_bundle_builds_httpx_client(self):
183
+ stub = _StubOpenAI()
184
+ with mock.patch("httpx.Client") as HttpxClient:
185
+ HttpxClient.return_value = "httpx-sentinel"
186
+ _openai.build_openai(
187
+ stub, base_url="https://embed.local/v1", verify="/ca.pem"
188
+ )
189
+ HttpxClient.assert_called_once_with(verify="/ca.pem")
190
+ self.assertEqual(stub.built["http_client"], "httpx-sentinel")
191
+ self.assertEqual(stub.built["base_url"], "https://embed.local/v1")
192
+
193
+ def test_verify_false_disables_verification(self):
194
+ stub = _StubOpenAI()
195
+ with mock.patch("httpx.Client") as HttpxClient:
196
+ HttpxClient.return_value = "httpx-sentinel"
197
+ _openai.build_openai(
198
+ stub, base_url="https://embed.local/v1", verify=False
199
+ )
200
+ HttpxClient.assert_called_once_with(verify=False)
201
+ self.assertEqual(stub.built["http_client"], "httpx-sentinel")
202
+
203
+ def test_verify_none_adds_no_http_client(self):
204
+ stub = _StubOpenAI()
205
+ # Default path must not touch httpx or pass http_client at all.
206
+ with mock.patch("httpx.Client") as HttpxClient:
207
+ _openai.build_openai(stub, base_url="http://localhost:1234/v1")
208
+ HttpxClient.assert_not_called()
209
+ self.assertNotIn("http_client", stub.built)
210
+
182
211
 
183
212
  class TestStatusToExit(unittest.TestCase):
184
213
 
@@ -1 +0,0 @@
1
- __version__ = "0.25.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