venice-cli 0.83.2__tar.gz → 0.83.3__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 (123) hide show
  1. {venice_cli-0.83.2 → venice_cli-0.83.3}/PKG-INFO +7 -1
  2. {venice_cli-0.83.2 → venice_cli-0.83.3}/README.md +6 -0
  3. venice_cli-0.83.3/src/venice/__init__.py +1 -0
  4. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_agent.py +6 -1
  5. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_compact.py +6 -1
  6. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_openai.py +48 -0
  7. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_review.py +5 -2
  8. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/code.py +10 -0
  9. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_code_command.py +53 -0
  10. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_compact.py +17 -0
  11. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_review.py +26 -0
  12. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_shared_openai.py +35 -0
  13. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_spawn.py +22 -0
  14. venice_cli-0.83.2/src/venice/__init__.py +0 -1
  15. {venice_cli-0.83.2 → venice_cli-0.83.3}/.gitignore +0 -0
  16. {venice_cli-0.83.2 → venice_cli-0.83.3}/CONTRIBUTING.md +0 -0
  17. {venice_cli-0.83.2 → venice_cli-0.83.3}/LICENSE +0 -0
  18. {venice_cli-0.83.2 → venice_cli-0.83.3}/Makefile +0 -0
  19. {venice_cli-0.83.2 → venice_cli-0.83.3}/SECURITY.md +0 -0
  20. {venice_cli-0.83.2 → venice_cli-0.83.3}/bin/venice +0 -0
  21. {venice_cli-0.83.2 → venice_cli-0.83.3}/install.sh +0 -0
  22. {venice_cli-0.83.2 → venice_cli-0.83.3}/pyproject.toml +0 -0
  23. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/__main__.py +0 -0
  24. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/audio_player.py +0 -0
  25. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/audio_post.py +0 -0
  26. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/auth.py +0 -0
  27. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/billing.py +0 -0
  28. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/cli.py +0 -0
  29. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/client.py +0 -0
  30. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/__init__.py +0 -0
  31. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_audio.py +0 -0
  32. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_browser.py +0 -0
  33. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_code.py +0 -0
  34. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_exec.py +0 -0
  35. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_index.py +0 -0
  36. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_mailbox.py +0 -0
  37. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_mcp.py +0 -0
  38. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_mcp_client.py +0 -0
  39. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_memory.py +0 -0
  40. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_models.py +0 -0
  41. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_persona.py +0 -0
  42. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_queue.py +0 -0
  43. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_repl.py +0 -0
  44. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_session.py +0 -0
  45. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_shared.py +0 -0
  46. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/_steer.py +0 -0
  47. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/balance.py +0 -0
  48. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/bg_remove.py +0 -0
  49. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/chat.py +0 -0
  50. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/completion.py +0 -0
  51. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/config.py +0 -0
  52. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/contact_sheet.py +0 -0
  53. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/embed.py +0 -0
  54. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/image.py +0 -0
  55. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/image_edit.py +0 -0
  56. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/index.py +0 -0
  57. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/login.py +0 -0
  58. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/master.py +0 -0
  59. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/mcp_serve.py +0 -0
  60. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/memory.py +0 -0
  61. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/models.py +0 -0
  62. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/music.py +0 -0
  63. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/review.py +0 -0
  64. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/search.py +0 -0
  65. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/secret.py +0 -0
  66. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/sessions.py +0 -0
  67. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/sfx.py +0 -0
  68. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/tts.py +0 -0
  69. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/upscale.py +0 -0
  70. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/commands/video.py +0 -0
  71. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/config.py +0 -0
  72. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/image_montage.py +0 -0
  73. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/mcp_server.py +0 -0
  74. {venice_cli-0.83.2 → venice_cli-0.83.3}/src/venice/userconfig.py +0 -0
  75. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/__init__.py +0 -0
  76. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/_drive.py +0 -0
  77. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/_hygiene.py +0 -0
  78. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/_mcp_fake_server.py +0 -0
  79. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/_venice_fake_server.py +0 -0
  80. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_agent.py +0 -0
  81. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_auth.py +0 -0
  82. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_balance.py +0 -0
  83. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_bg_remove.py +0 -0
  84. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_browser.py +0 -0
  85. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_chat.py +0 -0
  86. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_cli.py +0 -0
  87. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_client.py +0 -0
  88. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_code.py +0 -0
  89. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_completion.py +0 -0
  90. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_config.py +0 -0
  91. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_contact_sheet.py +0 -0
  92. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_drive_cli.py +0 -0
  93. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_embed.py +0 -0
  94. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_exec.py +0 -0
  95. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_fake_server.py +0 -0
  96. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_image.py +0 -0
  97. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_image_edit.py +0 -0
  98. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_index.py +0 -0
  99. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_login.py +0 -0
  100. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_mailbox.py +0 -0
  101. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_master.py +0 -0
  102. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_mcp_client.py +0 -0
  103. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_mcp_serve.py +0 -0
  104. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_mcp_tools.py +0 -0
  105. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_memory.py +0 -0
  106. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_models.py +0 -0
  107. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_music.py +0 -0
  108. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_persona.py +0 -0
  109. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_planner.py +0 -0
  110. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_profiles.py +0 -0
  111. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_repl.py +0 -0
  112. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_search.py +0 -0
  113. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_secret.py +0 -0
  114. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_sessions.py +0 -0
  115. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_sfx.py +0 -0
  116. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_shared.py +0 -0
  117. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_source_hygiene.py +0 -0
  118. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_steer.py +0 -0
  119. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_tts.py +0 -0
  120. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_upscale.py +0 -0
  121. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_video.py +0 -0
  122. {venice_cli-0.83.2 → venice_cli-0.83.3}/tests/test_web_search.py +0 -0
  123. {venice_cli-0.83.2 → venice_cli-0.83.3}/uninstall.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: venice-cli
3
- Version: 0.83.2
3
+ Version: 0.83.3
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
@@ -662,6 +662,12 @@ parameters, `max-tool-calls`, the `venice code` sandbox root, and the running
662
662
  token/cost usage — so resuming restores the whole context, not just the messages.
663
663
  The API key is never written to a session.
664
664
 
665
+ `venice code` also assigns each session an opaque `prompt_cache_key`, sent as an
666
+ OpenAI-compatible routing hint so successive plan, tool-loop, and verification calls
667
+ stay on cache-affine backend infrastructure when the provider supports it. The key is
668
+ not a credential. It survives `--resume` and `/reset`; disposable subagents receive
669
+ their own keys, while compaction's deliberately fresh summary request receives none.
670
+
665
671
  One-shot `venice code "task"` runs are sessions too (they persist unless
666
672
  `--ephemeral`), so an unattended `--auto` run is resumable, inspectable, and —
667
673
  new in this release — **steerable while it runs** (see below).
@@ -622,6 +622,12 @@ parameters, `max-tool-calls`, the `venice code` sandbox root, and the running
622
622
  token/cost usage — so resuming restores the whole context, not just the messages.
623
623
  The API key is never written to a session.
624
624
 
625
+ `venice code` also assigns each session an opaque `prompt_cache_key`, sent as an
626
+ OpenAI-compatible routing hint so successive plan, tool-loop, and verification calls
627
+ stay on cache-affine backend infrastructure when the provider supports it. The key is
628
+ not a credential. It survives `--resume` and `/reset`; disposable subagents receive
629
+ their own keys, while compaction's deliberately fresh summary request receives none.
630
+
625
631
  One-shot `venice code "task"` runs are sessions too (they persist unless
626
632
  `--ephemeral`), so an unattended `--auto` run is resumable, inspectable, and —
627
633
  new in this release — **steerable while it runs** (see below).
@@ -0,0 +1 @@
1
+ __version__ = "0.83.3"
@@ -46,6 +46,7 @@ from . import _mcp
46
46
  from . import _memory
47
47
  from . import _models
48
48
  from . import _compact
49
+ from . import _openai
49
50
  from .models import MODEL_TYPES
50
51
 
51
52
 
@@ -3400,9 +3401,13 @@ def _run_disposable(
3400
3401
  {"role": "system", "content": sys_prompt},
3401
3402
  {"role": "user", "content": task},
3402
3403
  ]
3404
+ # #128: this is a fresh conversation, not a continuation of the parent. Replace
3405
+ # (rather than inherit) its routing key; every call inside this disposable loop
3406
+ # then shares affinity, while two workers cannot collide on one backend identity.
3407
+ child_kwargs = _openai.with_prompt_cache_key(base_kwargs)
3403
3408
  with _capture_stdout() as buf:
3404
3409
  run_loop(
3405
- oai, model, messages, base_kwargs, tools,
3410
+ oai, model, messages, child_kwargs, tools,
3406
3411
  max_tool_calls=max_tool_calls, yes=True, json_out=False,
3407
3412
  budget=budget, ledger=ledger,
3408
3413
  )
@@ -50,6 +50,8 @@ import time
50
50
  from dataclasses import dataclass
51
51
  from typing import List, Optional, Tuple
52
52
 
53
+ from . import _openai
54
+
53
55
  # Rough chars-per-token for English/code text. Deliberately conservative
54
56
  # (overestimate tokens) so the fallback triggers compaction a little early
55
57
  # rather than a little late; real counts from `usage` override it anyway.
@@ -315,7 +317,10 @@ def compact_messages(
315
317
  prefix, tail = split
316
318
  sys_msgs = messages[: len(messages) - len(prefix) - len(tail)]
317
319
 
318
- kwargs = dict(base_kwargs or {})
320
+ # #128: compaction is a deliberately fresh summarization conversation. Reusing
321
+ # the parent session's routing identity would mix unrelated prefixes on one cache
322
+ # affinity key; preserve all other generation/Venice parameters while stripping it.
323
+ kwargs = _openai.without_prompt_cache_key(base_kwargs)
319
324
  kwargs.pop("stream", None)
320
325
  kwargs.pop("stream_options", None)
321
326
  kwargs.pop("tools", None)
@@ -13,7 +13,55 @@ catalog, so the missing-SDK path never touches the network.
13
13
  """
14
14
  from __future__ import annotations
15
15
 
16
+ import os
16
17
  import sys
18
+ from typing import Optional
19
+
20
+
21
+ _PROMPT_CACHE_KEY = "prompt_cache_key"
22
+
23
+
24
+ def prompt_cache_key(kwargs: Optional[dict]) -> Optional[str]:
25
+ """The OpenAI-compatible cache-affinity key carried by ``extra_body``.
26
+
27
+ The project supports ``openai>=1.40``, predating the SDK's typed
28
+ ``prompt_cache_key`` argument. Keeping the wire field in ``extra_body`` makes it
29
+ available on every supported SDK version; the SDK merges it into the request's
30
+ top-level JSON object.
31
+ """
32
+ extra = (kwargs or {}).get("extra_body")
33
+ value = extra.get(_PROMPT_CACHE_KEY) if isinstance(extra, dict) else None
34
+ return value if isinstance(value, str) and value else None
35
+
36
+
37
+ def with_prompt_cache_key(kwargs: Optional[dict], key: Optional[str] = None) -> dict:
38
+ """Copy generation kwargs and set one opaque prompt-cache routing key.
39
+
40
+ A missing ``key`` mints a new conversation identity. Nested ``extra_body`` is
41
+ copied before mutation so a disposable subagent can replace its parent's key
42
+ without changing the parent session or its Venice extension parameters.
43
+ """
44
+ out = dict(kwargs or {})
45
+ extra = out.get("extra_body")
46
+ extra = dict(extra) if isinstance(extra, dict) else {}
47
+ extra[_PROMPT_CACHE_KEY] = key or f"venice-{os.urandom(16).hex()}"
48
+ out["extra_body"] = extra
49
+ return out
50
+
51
+
52
+ def without_prompt_cache_key(kwargs: Optional[dict]) -> dict:
53
+ """Copy generation kwargs without a parent conversation's affinity key."""
54
+ out = dict(kwargs or {})
55
+ extra = out.get("extra_body")
56
+ if not isinstance(extra, dict) or _PROMPT_CACHE_KEY not in extra:
57
+ return out
58
+ extra = dict(extra)
59
+ extra.pop(_PROMPT_CACHE_KEY, None)
60
+ if extra:
61
+ out["extra_body"] = extra
62
+ else:
63
+ out.pop("extra_body", None)
64
+ return out
17
65
 
18
66
 
19
67
  def import_openai(label: str):
@@ -56,7 +56,7 @@ import threading
56
56
  import time
57
57
  from typing import Dict, List, Optional, Tuple
58
58
 
59
- from . import _agent, _code, _exec
59
+ from . import _agent, _code, _exec, _openai
60
60
  from ._exec import ( # shared exec rails (#33): one gate for every git shell-out
61
61
  DEFAULT_EXEC_TIMEOUT,
62
62
  MAX_OUTPUT_CHARS,
@@ -584,6 +584,9 @@ def _retry_for_verdict(oai, model: str, report: str, base_kwargs: dict,
584
584
  it is supposed to be bounded by.
585
585
  """
586
586
  _t0 = time.monotonic()
587
+ # #128: this retry is intentionally a fresh one-shot, not a continuation of the
588
+ # author or reviewer conversation, so it must not borrow either affinity identity.
589
+ retry_kwargs = _openai.without_prompt_cache_key(base_kwargs)
587
590
  resp = oai.chat.completions.create(
588
591
  model=model,
589
592
  messages=[
@@ -591,7 +594,7 @@ def _retry_for_verdict(oai, model: str, report: str, base_kwargs: dict,
591
594
  {"role": "assistant", "content": report or ""},
592
595
  {"role": "user", "content": RETRY_MSG},
593
596
  ],
594
- **base_kwargs,
597
+ **retry_kwargs,
595
598
  )
596
599
  if ledger is not None:
597
600
  ledger.record(getattr(resp, "usage", None), seconds=time.monotonic() - _t0)
@@ -715,6 +715,16 @@ def _run(args) -> int:
715
715
  # venice_scout). `_gen_kwargs` reads only args.temperature/max_tokens -- no
716
716
  # dependency on `tools`, so the reorder is safe.
717
717
  gen_kwargs = PROFILE.build_gen_kwargs(args)
718
+ # #128: Venice can route requests carrying one stable prompt_cache_key to the
719
+ # same cache-bearing backend. Restore the saved key when one exists; old sessions
720
+ # receive a key on first resume, and fresh/ephemeral runs mint one before the plan
721
+ # turn so plan -> execute -> verify all share affinity. It rides in extra_body for
722
+ # compatibility with the project's openai>=1.40 floor and is persisted automatically
723
+ # with the session's generation kwargs.
724
+ saved_cache_key = (
725
+ _openai.prompt_cache_key(session.gen_kwargs) if session is not None else None
726
+ )
727
+ gen_kwargs = _openai.with_prompt_cache_key(gen_kwargs, saved_cache_key)
718
728
  # #52 planner slice: the session's shared dispatch record list. scout/spawn append
719
729
  # every launched dispatch to it; venice_merge (and the --json envelope) roll it up.
720
730
  dispatches = [] if planner else None
@@ -138,6 +138,10 @@ class TestCodeCommand(unittest.TestCase):
138
138
  self.assertEqual(calls[0]["tool_choice"], "none") # plan
139
139
  self.assertEqual(calls[1]["tool_choice"], "auto") # execute loop
140
140
  self.assertEqual(calls[3]["tool_choice"], "none") # acceptance check
141
+ # #128: every phase of one top-level conversation shares backend affinity.
142
+ keys = [c["extra_body"]["prompt_cache_key"] for c in calls]
143
+ self.assertTrue(keys[0].startswith("venice-"))
144
+ self.assertEqual(len(set(keys)), 1)
141
145
  # auto -> confirm=True -> the write actually happened
142
146
  with open(os.path.join(self.root, "hello.py")) as f:
143
147
  self.assertEqual(f.read(), "def hi():\n return 1\n")
@@ -505,6 +509,7 @@ class TestCodeCommand(unittest.TestCase):
505
509
  captured["root"] = root
506
510
  captured["system_reseed"] = system_reseed
507
511
  captured["ledger"] = kw.get("ledger")
512
+ captured["gen_kwargs"] = gen_kwargs
508
513
  return 0
509
514
 
510
515
  stdin = mock.MagicMock()
@@ -530,6 +535,9 @@ class TestCodeCommand(unittest.TestCase):
530
535
  # #117: the REPL is HANDED the hoisted ledger rather than building its own,
531
536
  # because the rails already mirror into that object.
532
537
  self.assertIsNotNone(captured["ledger"])
538
+ self.assertTrue(
539
+ captured["gen_kwargs"]["extra_body"]["prompt_cache_key"].startswith(
540
+ "venice-"))
533
541
 
534
542
  # --- session resume (#47) ---
535
543
  def _mk_zone(self):
@@ -612,6 +620,51 @@ class TestCodeCommand(unittest.TestCase):
612
620
  self.assertEqual(captured["root"], saved_root) # faithful restore
613
621
  self.assertEqual(captured["session_id"], sess.id)
614
622
 
623
+ def test_prompt_cache_key_survives_save_reset_and_resume(self):
624
+ zone, _session = self._mk_zone()
625
+ rc, first_calls = self._run_interactive(
626
+ _code_args(task="first", root=self.root, interactive=True, auto=True),
627
+ [FakeToolCompletion("done")], ["/exit"], zone,
628
+ )
629
+ self.assertEqual(rc, 0)
630
+ first_key = first_calls[0]["extra_body"]["prompt_cache_key"]
631
+
632
+ with mock.patch.dict(os.environ, {"VENICE_SESSIONS_DIR": zone}):
633
+ saved = _session.load(_session.most_recent("code").id, "code")
634
+ self.assertEqual(
635
+ saved.gen_kwargs["extra_body"]["prompt_cache_key"], first_key)
636
+
637
+ rc, resumed_calls = self._run_interactive(
638
+ _code_args(resume=saved.id, root=self.root, auto=True),
639
+ [FakeToolCompletion("done again")],
640
+ ["/reset", "second", "/exit"], zone,
641
+ )
642
+ self.assertEqual(rc, 0)
643
+ self.assertEqual(
644
+ resumed_calls[0]["extra_body"]["prompt_cache_key"], first_key)
645
+
646
+ def test_old_session_without_a_cache_key_is_upgraded_on_resume(self):
647
+ zone, _session = self._mk_zone()
648
+ old = _session.new_session(
649
+ "code", label="venice code", model="llama-3.3-70b", root=self.root,
650
+ gen_kwargs={"temperature": 0.4},
651
+ messages=[{"role": "system", "content": "old"}],
652
+ )
653
+ with mock.patch.dict(os.environ, {"VENICE_SESSIONS_DIR": zone}):
654
+ _session.save(old)
655
+
656
+ rc, calls = self._run_interactive(
657
+ _code_args(resume=old.id, root=self.root, auto=True),
658
+ [FakeToolCompletion("continued")], ["continue", "/exit"], zone,
659
+ )
660
+ self.assertEqual(rc, 0)
661
+ key = calls[0]["extra_body"]["prompt_cache_key"]
662
+ self.assertTrue(key.startswith("venice-"))
663
+ with mock.patch.dict(os.environ, {"VENICE_SESSIONS_DIR": zone}):
664
+ upgraded = _session.load(old.id, "code")
665
+ self.assertEqual(
666
+ upgraded.gen_kwargs["extra_body"]["prompt_cache_key"], key)
667
+
615
668
 
616
669
  class TestOneShotSteering(unittest.TestCase):
617
670
  """#78: one-shot `venice code` runs persist a steerable session + drain steers.
@@ -259,6 +259,23 @@ class TestCompactMessages(unittest.TestCase):
259
259
  self.assertEqual(calls[0]["temperature"], 0.2)
260
260
  self.assertEqual(calls[0]["max_tokens"], _compact.SUMMARY_MAX_TOKENS)
261
261
 
262
+ def test_fresh_summary_does_not_reuse_parent_cache_affinity(self):
263
+ msgs = _history(6)
264
+ fake, calls = _fake_oai()
265
+ original = {"extra_body": {
266
+ "prompt_cache_key": "parent-key",
267
+ "venice_parameters": {"include_venice_system_prompt": False},
268
+ }}
269
+ _compact.compact_messages(
270
+ fake, "m", msgs, keep_turns=2, base_kwargs=original)
271
+ self.assertNotIn("prompt_cache_key", calls[0]["extra_body"])
272
+ self.assertEqual(
273
+ calls[0]["extra_body"]["venice_parameters"],
274
+ {"include_venice_system_prompt": False},
275
+ )
276
+ self.assertEqual(
277
+ original["extra_body"]["prompt_cache_key"], "parent-key")
278
+
262
279
 
263
280
  class TestBuildSummaryPrompt(unittest.TestCase):
264
281
  def test_tool_messages_rendered_as_text(self):
@@ -696,6 +696,32 @@ class TestRunCycle(_RepoBase):
696
696
  rounds=1)
697
697
  self.assertEqual(out["verdict"], "clean")
698
698
 
699
+ def test_verdict_retry_does_not_reuse_parent_cache_affinity(self):
700
+ oai = mock.MagicMock()
701
+ oai.chat.completions.create.return_value = FakeToolCompletion(
702
+ content="REVIEW: CLEAN")
703
+
704
+ def _stub(*a, **kw):
705
+ return {"status": "ok", "tool_calls": 0, "truncated": False,
706
+ "report": "SCOPE: x\nFINDINGS: none\n(no sentinel)"}
707
+
708
+ base = {"extra_body": {
709
+ "prompt_cache_key": "parent-key",
710
+ "venice_parameters": {"include_venice_system_prompt": False},
711
+ }}
712
+ with mock.patch.object(_agent, "run_review", _stub):
713
+ out = _review.run_cycle(
714
+ oai, "m", self.collected, base, root=self.root, rounds=1)
715
+ self.assertEqual(out["verdict"], "clean")
716
+ sent = oai.chat.completions.create.call_args.kwargs
717
+ self.assertNotIn("prompt_cache_key", sent["extra_body"])
718
+ self.assertEqual(
719
+ sent["extra_body"]["venice_parameters"],
720
+ {"include_venice_system_prompt": False},
721
+ )
722
+ self.assertEqual(
723
+ base["extra_body"]["prompt_cache_key"], "parent-key")
724
+
699
725
  def test_verdict_retry_tokens_are_counted(self):
700
726
  # The retry runs outside `_run_disposable`, so nothing else records it. An
701
727
  # uncounted call is also an UNCAPPED one -- `--max-tokens` was silently not
@@ -182,6 +182,41 @@ class TestSupportsCapability(unittest.TestCase):
182
182
 
183
183
  # --- _openai ---
184
184
 
185
+
186
+ class TestPromptCacheAffinity(unittest.TestCase):
187
+ def test_mints_an_opaque_key_without_mutating_the_input(self):
188
+ original = {"temperature": 0.2}
189
+ out = _openai.with_prompt_cache_key(original)
190
+ key = _openai.prompt_cache_key(out)
191
+ self.assertIsInstance(key, str)
192
+ self.assertTrue(key.startswith("venice-"))
193
+ self.assertNotIn("extra_body", original)
194
+
195
+ def test_explicit_key_preserves_venice_parameters(self):
196
+ original = {"extra_body": {"venice_parameters": {"enable_web_search": True}}}
197
+ out = _openai.with_prompt_cache_key(original, "session-key")
198
+ self.assertEqual(_openai.prompt_cache_key(out), "session-key")
199
+ self.assertEqual(out["extra_body"]["venice_parameters"],
200
+ {"enable_web_search": True})
201
+ self.assertNotIn("prompt_cache_key", original["extra_body"])
202
+
203
+ def test_strip_keeps_other_extra_body_fields_and_does_not_mutate(self):
204
+ original = {"extra_body": {
205
+ "prompt_cache_key": "parent",
206
+ "venice_parameters": {"character_slug": "x"},
207
+ }}
208
+ out = _openai.without_prompt_cache_key(original)
209
+ self.assertIsNone(_openai.prompt_cache_key(out))
210
+ self.assertEqual(out["extra_body"],
211
+ {"venice_parameters": {"character_slug": "x"}})
212
+ self.assertEqual(_openai.prompt_cache_key(original), "parent")
213
+
214
+ def test_strip_removes_empty_extra_body(self):
215
+ out = _openai.without_prompt_cache_key(
216
+ {"temperature": 0.1, "extra_body": {"prompt_cache_key": "x"}})
217
+ self.assertEqual(out, {"temperature": 0.1})
218
+
219
+
185
220
  class _StubConnErr(Exception):
186
221
  pass
187
222
 
@@ -234,6 +234,28 @@ class TestRunScout(_ScoutBase):
234
234
  self.assertIn("SCOUT", first_msgs[0]["content"])
235
235
  self.assertEqual(first_msgs[1], {"role": "user", "content": "investigate"})
236
236
 
237
+ def test_each_disposable_conversation_replaces_the_parent_cache_key(self):
238
+ parent = {"extra_body": {
239
+ "prompt_cache_key": "parent-key",
240
+ "venice_parameters": {"include_venice_system_prompt": False},
241
+ }}
242
+ fake, calls = _fake_openai_seq([
243
+ FakeToolCompletion(_REPORT), FakeToolCompletion(_REPORT),
244
+ ])
245
+ tools = _code.read_only_tools(self.root)
246
+ _agent.run_scout(fake, "m", "one", tools, parent, max_tool_calls=4)
247
+ _agent.run_scout(fake, "m", "two", tools, parent, max_tool_calls=4)
248
+
249
+ keys = [c["extra_body"]["prompt_cache_key"] for c in calls]
250
+ self.assertNotIn("parent-key", keys)
251
+ self.assertNotEqual(keys[0], keys[1])
252
+ for call in calls:
253
+ self.assertEqual(
254
+ call["extra_body"]["venice_parameters"],
255
+ {"include_venice_system_prompt": False},
256
+ )
257
+ self.assertEqual(parent["extra_body"]["prompt_cache_key"], "parent-key")
258
+
237
259
  def test_focus_hint_threaded_into_system(self):
238
260
  seq = [FakeToolCompletion(_REPORT)]
239
261
  fake, calls = _fake_openai_seq(seq)
@@ -1 +0,0 @@
1
- __version__ = "0.83.2"
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