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.
- {venice_cli-0.25.0 → venice_cli-0.26.0}/PKG-INFO +36 -6
- {venice_cli-0.25.0 → venice_cli-0.26.0}/README.md +35 -5
- venice_cli-0.26.0/src/venice/__init__.py +1 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_openai.py +18 -3
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/code.py +38 -5
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/embed.py +44 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/config.py +4 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/userconfig.py +1 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_code_command.py +57 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_embed.py +82 -1
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_shared_openai.py +29 -0
- venice_cli-0.25.0/src/venice/__init__.py +0 -1
- {venice_cli-0.25.0 → venice_cli-0.26.0}/.gitignore +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/CONTRIBUTING.md +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/LICENSE +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/Makefile +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/SECURITY.md +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/bin/venice +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/install.sh +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/pyproject.toml +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/__main__.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/audio_player.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/audio_post.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/auth.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/billing.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/cli.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/client.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/__init__.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_agent.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_audio.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_code.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_index.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_mcp.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_mcp_client.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_models.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_queue.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_repl.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/_shared.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/balance.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/bg_remove.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/chat.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/config.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/contact_sheet.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/image.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/image_edit.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/index.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/login.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/master.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/mcp_serve.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/models.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/music.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/search.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/sfx.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/tts.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/upscale.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/commands/video.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/image_montage.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/src/venice/mcp_server.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/__init__.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/_mcp_fake_server.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_auth.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_balance.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_bg_remove.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_chat.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_client.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_code.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_config.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_contact_sheet.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_image.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_image_edit.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_index.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_master.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_mcp_client.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_mcp_serve.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_mcp_tools.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_models.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_music.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_repl.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_search.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_sfx.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_shared.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_tts.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_upscale.py +0 -0
- {venice_cli-0.25.0 → venice_cli-0.26.0}/tests/test_video.py +0 -0
- {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.
|
|
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
|
|
830
|
-
|
|
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.
|
|
914
|
-
|
|
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
|
|
792
|
-
|
|
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.
|
|
876
|
-
|
|
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(
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"] = {
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|