shortleash 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Calas Automations Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: shortleash
3
+ Version: 0.1.0
4
+ Summary: Circuit breaker for AI coding-agent runs.
5
+ Author: Calas Automations Inc.
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/calasdan-sketch/shortleash
8
+ Project-URL: Issues, https://github.com/calasdan-sketch/shortleash/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # ShortLeash — stop runaway agents
15
+
16
+ A circuit breaker for AI coding-agent runs. Wraps an agent command, tails its
17
+ telemetry in real time, and **kills the run the moment it breaches a guardrail**
18
+ — budget, subagent-spawn cap, token cap, or wall-clock timeout. Emits a
19
+ per-run receipt showing what it cost and why it stopped.
20
+
21
+ Vendors' "budgets" only warn while the meter keeps running. This one halts.
22
+
23
+ ## Install
24
+
25
+ Zero dependencies — Python 3.10+ stdlib only.
26
+
27
+ ```bash
28
+ git clone <repo> && cd shortleash
29
+ # run directly:
30
+ python3 -m shortleash run --budget 2.00 -- <command>
31
+ # or add an alias (from the repo root, no install):
32
+ alias shortleash="python3 -m shortleash"
33
+ # or install for the `shortleash` console script:
34
+ pip install .
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ # Guard a Claude Code run: kill it past $2 or 10 subagent spawns.
41
+ shortleash run --budget 2.00 --max-spawns 10 -- \
42
+ claude -p "refactor the auth module"
43
+
44
+ # All guardrails:
45
+ shortleash run --budget 5.00 --max-spawns 10 --max-tokens 500000 \
46
+ --timeout 600 --receipt ./receipt.json -- \
47
+ claude -p "migrate the database layer"
48
+ ```
49
+
50
+ ### Anomaly mode (the smart default)
51
+
52
+ Static caps punish legitimate big tasks. Anomaly mode learns instead:
53
+ every receipt is appended to `~/.shortleash/history.jsonl`, and once an
54
+ agent has 5+ past runs, the guard trips only when the live run exceeds
55
+ **3× the historical median** cost or tokens for that agent.
56
+
57
+ ```bash
58
+ shortleash run -- claude -p "migrate the database layer"
59
+ # [shortleash] anomaly=3.0x (needs 5 runs)
60
+ # ...
61
+ # STOPPED: anomaly: estimated $4.21 is 5.3x the typical $0.79 across 23 past runs
62
+ ```
63
+
64
+ A naturally large task establishes its own baseline over time; only true
65
+ runaways trip. Breached runs are excluded from baselines. Tune with
66
+ `--anomaly 5.0`, `--min-history 10`, `--profile big-refactor` (separate
67
+ baseline), or `--no-anomaly` to disable.
68
+
69
+ ### Fleet mode (`--fleet`)
70
+
71
+ Running several agents on the same machine? `--fleet` holds a **fleet
72
+ claim** on paths for the run — a lockfile for agents, merged from the
73
+ retired claimlock:
74
+
75
+ ```bash
76
+ shortleash run --fleet --budget 3 -- claude -p "refactor the api layer"
77
+ # [shortleash] fleet claim ce63a732a87b: /home/dan/project
78
+ ```
79
+
80
+ - Default claim: the current directory; override with
81
+ `--claim /path/one /path/two`.
82
+ - **Conflict = warning, never a block.** If another agent holds an
83
+ overlapping path, you get a loud stderr warning and the run proceeds
84
+ anyway — a lock must never be a deadlock.
85
+ - Released automatically on exit (normal, signal, or crash via the TTL
86
+ backstop). Tune with `--fleet-owner "dan"` and `--fleet-ttl 3600`.
87
+
88
+ ### Prompt cache optimizer
89
+
90
+ Stop the bleed *and* shrink the bill. Prompt caching discounts repeated
91
+ prefix tokens (up to 90% off) — but only when stable content sits first
92
+ and per-request variables sit last:
93
+
94
+ ```bash
95
+ shortleash cache guide # provider docs + the core rule
96
+ shortleash cache check -i prompt.txt # how cacheable is this prompt?
97
+ shortleash cache make -i prompt.txt -o prompt.cached.txt --provider anthropic
98
+ ```
99
+
100
+ `cache make` restructures your `{{variable}}`-templated prompt into
101
+ cache-optimal layout and emits integration snippets for Anthropic (explicit
102
+ `cache_control` breakpoints), OpenAI/DeepSeek (automatic prefix caching),
103
+ and Gemini (explicit context caching).
104
+
105
+ When wrapping `claude`, `--output-format stream-json --verbose` are added
106
+ automatically (the guard needs the telemetry stream). The agent's stdout
107
+ passes through untouched, so you still watch it work.
108
+
109
+ On breach the whole process tree gets SIGTERM, then SIGKILL after
110
+ `--grace-period` (default 5s). Exit code is `124` on a guard kill, otherwise
111
+ the child's exit code.
112
+
113
+ ## The receipt
114
+
115
+ Every run ends with a receipt on stderr and optionally as JSON
116
+ (`--receipt path.json`):
117
+
118
+ ```
119
+ ── shortleash receipt ──────────────────────
120
+ cost: $0.1271 (estimated)
121
+ tokens: 51,000 (in=15,000 out=4,500 cache_r=30,000 cache_w=1,500)
122
+ turns: 0 spawns: 1
123
+ duration: 0.16s exit: 124
124
+ STOPPED: budget exceeded: estimated $0.1271 > $0.05 cap
125
+ ──────────────────────────────────────────────────
126
+ ```
127
+
128
+ Mid-run cost is estimated from a fallback pricing table (clearly marked);
129
+ the final receipt prefers the agent-reported `total_cost_usd` when the run
130
+ completes. Override pricing with `--pricing pricing.json`:
131
+
132
+ ```json
133
+ {"sonnet": [3.0, 15.0], "opus": [15.0, 75.0], "haiku": [0.8, 4.0]}
134
+ ```
135
+
136
+ ## How it works
137
+
138
+ - Child runs in its own process group (`start_new_session=True`) — a kill
139
+ takes the whole tree, never your shell.
140
+ - A monitor thread parses each stream-json line (`system/init`,
141
+ `assistant` usage + `Task` tool_use spawns, `result` cost) and checks caps
142
+ after every observation.
143
+ - A watchdog thread enforces `--timeout` even if the child goes silent.
144
+ - Subagent spawns are counted from `Task` tool_use blocks in the stream.
145
+
146
+ ## Trust boundary
147
+
148
+ Be honest about what the guard can and can't see:
149
+
150
+ - **Token, budget, spawn, and anomaly caps all depend on the child
151
+ agent's own telemetry.** Token counts come from its stream-json
152
+ `usage` events; mid-run cost is estimated from those counts; the final
153
+ receipt prefers the child's reported `total_cost_usd`.
154
+ - **A misbehaving or compromised child that under-reports (or reports
155
+ negative) usage can blind those caps.** The guard clamps every
156
+ child-reported usage and cost value to ≥ 0 so negatives can't drive
157
+ totals *down*, but it cannot see spend the child simply hides.
158
+ - **Only `--timeout` is enforced independently of the child** — a
159
+ wall-clock watchdog, immune to telemetry lies.
160
+
161
+ Treat this as a guardrail against runaway cost, not a security boundary
162
+ against an adversarial agent.
163
+
164
+ ## Testing
165
+
166
+ ```bash
167
+ python3 -m unittest discover -s tests -v
168
+ ```
169
+
170
+ `sim/runaway.py` is a fake agent emitting realistic stream-json — the suite
171
+ kills it for real (budget, spawn, token, and timeout breaches) without
172
+ spending a cent.
173
+
174
+ ## Scope / limits (v0.1.0)
175
+
176
+ - Claude Code `-p` (non-interactive) is the supported path. Interactive
177
+ sessions and other agents (Codex, Cursor) are on the roadmap — the
178
+ event parser is isolated in `events.py` for exactly this.
179
+ - Cost mid-run is an estimate; treat `--budget` as a guardrail, not accounting.
@@ -0,0 +1,166 @@
1
+ # ShortLeash — stop runaway agents
2
+
3
+ A circuit breaker for AI coding-agent runs. Wraps an agent command, tails its
4
+ telemetry in real time, and **kills the run the moment it breaches a guardrail**
5
+ — budget, subagent-spawn cap, token cap, or wall-clock timeout. Emits a
6
+ per-run receipt showing what it cost and why it stopped.
7
+
8
+ Vendors' "budgets" only warn while the meter keeps running. This one halts.
9
+
10
+ ## Install
11
+
12
+ Zero dependencies — Python 3.10+ stdlib only.
13
+
14
+ ```bash
15
+ git clone <repo> && cd shortleash
16
+ # run directly:
17
+ python3 -m shortleash run --budget 2.00 -- <command>
18
+ # or add an alias (from the repo root, no install):
19
+ alias shortleash="python3 -m shortleash"
20
+ # or install for the `shortleash` console script:
21
+ pip install .
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ # Guard a Claude Code run: kill it past $2 or 10 subagent spawns.
28
+ shortleash run --budget 2.00 --max-spawns 10 -- \
29
+ claude -p "refactor the auth module"
30
+
31
+ # All guardrails:
32
+ shortleash run --budget 5.00 --max-spawns 10 --max-tokens 500000 \
33
+ --timeout 600 --receipt ./receipt.json -- \
34
+ claude -p "migrate the database layer"
35
+ ```
36
+
37
+ ### Anomaly mode (the smart default)
38
+
39
+ Static caps punish legitimate big tasks. Anomaly mode learns instead:
40
+ every receipt is appended to `~/.shortleash/history.jsonl`, and once an
41
+ agent has 5+ past runs, the guard trips only when the live run exceeds
42
+ **3× the historical median** cost or tokens for that agent.
43
+
44
+ ```bash
45
+ shortleash run -- claude -p "migrate the database layer"
46
+ # [shortleash] anomaly=3.0x (needs 5 runs)
47
+ # ...
48
+ # STOPPED: anomaly: estimated $4.21 is 5.3x the typical $0.79 across 23 past runs
49
+ ```
50
+
51
+ A naturally large task establishes its own baseline over time; only true
52
+ runaways trip. Breached runs are excluded from baselines. Tune with
53
+ `--anomaly 5.0`, `--min-history 10`, `--profile big-refactor` (separate
54
+ baseline), or `--no-anomaly` to disable.
55
+
56
+ ### Fleet mode (`--fleet`)
57
+
58
+ Running several agents on the same machine? `--fleet` holds a **fleet
59
+ claim** on paths for the run — a lockfile for agents, merged from the
60
+ retired claimlock:
61
+
62
+ ```bash
63
+ shortleash run --fleet --budget 3 -- claude -p "refactor the api layer"
64
+ # [shortleash] fleet claim ce63a732a87b: /home/dan/project
65
+ ```
66
+
67
+ - Default claim: the current directory; override with
68
+ `--claim /path/one /path/two`.
69
+ - **Conflict = warning, never a block.** If another agent holds an
70
+ overlapping path, you get a loud stderr warning and the run proceeds
71
+ anyway — a lock must never be a deadlock.
72
+ - Released automatically on exit (normal, signal, or crash via the TTL
73
+ backstop). Tune with `--fleet-owner "dan"` and `--fleet-ttl 3600`.
74
+
75
+ ### Prompt cache optimizer
76
+
77
+ Stop the bleed *and* shrink the bill. Prompt caching discounts repeated
78
+ prefix tokens (up to 90% off) — but only when stable content sits first
79
+ and per-request variables sit last:
80
+
81
+ ```bash
82
+ shortleash cache guide # provider docs + the core rule
83
+ shortleash cache check -i prompt.txt # how cacheable is this prompt?
84
+ shortleash cache make -i prompt.txt -o prompt.cached.txt --provider anthropic
85
+ ```
86
+
87
+ `cache make` restructures your `{{variable}}`-templated prompt into
88
+ cache-optimal layout and emits integration snippets for Anthropic (explicit
89
+ `cache_control` breakpoints), OpenAI/DeepSeek (automatic prefix caching),
90
+ and Gemini (explicit context caching).
91
+
92
+ When wrapping `claude`, `--output-format stream-json --verbose` are added
93
+ automatically (the guard needs the telemetry stream). The agent's stdout
94
+ passes through untouched, so you still watch it work.
95
+
96
+ On breach the whole process tree gets SIGTERM, then SIGKILL after
97
+ `--grace-period` (default 5s). Exit code is `124` on a guard kill, otherwise
98
+ the child's exit code.
99
+
100
+ ## The receipt
101
+
102
+ Every run ends with a receipt on stderr and optionally as JSON
103
+ (`--receipt path.json`):
104
+
105
+ ```
106
+ ── shortleash receipt ──────────────────────
107
+ cost: $0.1271 (estimated)
108
+ tokens: 51,000 (in=15,000 out=4,500 cache_r=30,000 cache_w=1,500)
109
+ turns: 0 spawns: 1
110
+ duration: 0.16s exit: 124
111
+ STOPPED: budget exceeded: estimated $0.1271 > $0.05 cap
112
+ ──────────────────────────────────────────────────
113
+ ```
114
+
115
+ Mid-run cost is estimated from a fallback pricing table (clearly marked);
116
+ the final receipt prefers the agent-reported `total_cost_usd` when the run
117
+ completes. Override pricing with `--pricing pricing.json`:
118
+
119
+ ```json
120
+ {"sonnet": [3.0, 15.0], "opus": [15.0, 75.0], "haiku": [0.8, 4.0]}
121
+ ```
122
+
123
+ ## How it works
124
+
125
+ - Child runs in its own process group (`start_new_session=True`) — a kill
126
+ takes the whole tree, never your shell.
127
+ - A monitor thread parses each stream-json line (`system/init`,
128
+ `assistant` usage + `Task` tool_use spawns, `result` cost) and checks caps
129
+ after every observation.
130
+ - A watchdog thread enforces `--timeout` even if the child goes silent.
131
+ - Subagent spawns are counted from `Task` tool_use blocks in the stream.
132
+
133
+ ## Trust boundary
134
+
135
+ Be honest about what the guard can and can't see:
136
+
137
+ - **Token, budget, spawn, and anomaly caps all depend on the child
138
+ agent's own telemetry.** Token counts come from its stream-json
139
+ `usage` events; mid-run cost is estimated from those counts; the final
140
+ receipt prefers the child's reported `total_cost_usd`.
141
+ - **A misbehaving or compromised child that under-reports (or reports
142
+ negative) usage can blind those caps.** The guard clamps every
143
+ child-reported usage and cost value to ≥ 0 so negatives can't drive
144
+ totals *down*, but it cannot see spend the child simply hides.
145
+ - **Only `--timeout` is enforced independently of the child** — a
146
+ wall-clock watchdog, immune to telemetry lies.
147
+
148
+ Treat this as a guardrail against runaway cost, not a security boundary
149
+ against an adversarial agent.
150
+
151
+ ## Testing
152
+
153
+ ```bash
154
+ python3 -m unittest discover -s tests -v
155
+ ```
156
+
157
+ `sim/runaway.py` is a fake agent emitting realistic stream-json — the suite
158
+ kills it for real (budget, spawn, token, and timeout breaches) without
159
+ spending a cent.
160
+
161
+ ## Scope / limits (v0.1.0)
162
+
163
+ - Claude Code `-p` (non-interactive) is the supported path. Interactive
164
+ sessions and other agents (Codex, Cursor) are on the roadmap — the
165
+ event parser is isolated in `events.py` for exactly this.
166
+ - Cost mid-run is an estimate; treat `--budget` as a guardrail, not accounting.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+ [project]
5
+ name = "shortleash"
6
+ # Single source of truth: shortleash/__init__.py defines __version__.
7
+ dynamic = ["version"]
8
+ description = "Circuit breaker for AI coding-agent runs."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{name = "Calas Automations Inc."}]
12
+ license = {text = "MIT"}
13
+
14
+ [tool.setuptools.dynamic]
15
+ version = {attr = "shortleash.__version__"}
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/calasdan-sketch/shortleash"
19
+ Issues = "https://github.com/calasdan-sketch/shortleash/issues"
20
+
21
+ [project.scripts]
22
+ shortleash = "shortleash.cli:main"
23
+
24
+ [tool.setuptools]
25
+ packages = ["shortleash"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Stop Runaway Agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,143 @@
1
+ """Cache optimizer: prompts structured for LLM prompt caching.
2
+
3
+ Prompt caching (Anthropic/OpenAI/Gemini/DeepSeek all do some form) discounts
4
+ repeated prefix tokens — up to 90% off on cache hits. But it only works if
5
+ the stable content sits FIRST and the per-request variables sit LAST.
6
+ Most hand-written prompts interleave them, silently killing the cache.
7
+
8
+ `cache make` restructures a {{variable}}-templated prompt into
9
+ cache-optimal layout and emits per-provider integration snippets.
10
+ `cache check` reports how cacheable a prompt is. `cache guide` prints
11
+ the verified provider docs.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ PLACEHOLDER_RE = re.compile(r"\{\{\s*(\w+)\s*\}\}")
19
+ # \w (not [a-zA-Z0-9_]) so unicode names like {{naïve}} are found too.
20
+
21
+ CACHE_DOCS = [
22
+ ("Anthropic",
23
+ "https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching",
24
+ "Explicit breakpoints via cache_control; up to 90% off cached tokens. "
25
+ "Put static content in early blocks, mark the breakpoint."),
26
+ ("OpenAI",
27
+ "https://platform.openai.com/docs/guides/prompt-caching",
28
+ "Automatic prefix caching (no code changes); 50% off. "
29
+ "Keep the stable prefix byte-identical across requests."),
30
+ ("Google Gemini",
31
+ "https://ai.google.dev/gemini-api/docs/caching",
32
+ "Explicit context caching API (cached_content); 75% off. "
33
+ "Cache the stable context object, pass variables per request."),
34
+ ("DeepSeek",
35
+ "https://api-docs.deepseek.com/guides/kv_cache",
36
+ "Automatic prefix caching on repeated prefixes; 50% off. "
37
+ "Stable prefix first, same as OpenAI."),
38
+ ]
39
+
40
+
41
+ def find_placeholders(text: str) -> list[str]:
42
+ seen: list[str] = []
43
+ for m in PLACEHOLDER_RE.finditer(text):
44
+ if m.group(1) not in seen:
45
+ seen.append(m.group(1))
46
+ return seen
47
+
48
+
49
+ def analyze(text: str) -> dict:
50
+ placeholders = find_placeholders(text)
51
+ static = PLACEHOLDER_RE.sub("", text)
52
+ return {
53
+ "placeholders": placeholders,
54
+ "n_placeholders": len(placeholders),
55
+ "static_chars": len(static),
56
+ "total_chars": len(text),
57
+ "static_ratio": len(static) / len(text) if text else 1.0,
58
+ }
59
+
60
+
61
+ def restructure(text: str) -> tuple[str, list[str]]:
62
+ """Return (optimized_text, notes). Static content first (cacheable
63
+ prefix), variables last (below the breakpoint)."""
64
+ placeholders = find_placeholders(text)
65
+ notes: list[str] = []
66
+ if not placeholders:
67
+ notes.append("no {{variables}} found — the whole prompt is static, "
68
+ "already fully cacheable.")
69
+ return text, notes
70
+ # Split into static segments, preserving order, dropping empties.
71
+ parts = PLACEHOLDER_RE.split(text)
72
+ statics = [p for i, p in enumerate(parts) if i % 2 == 0 and p.strip()]
73
+ static_block = "\n".join(s.rstrip() for s in statics).strip()
74
+ var_block = "\n".join("{{" + name + "}}" for name in placeholders)
75
+ optimized = (static_block
76
+ + "\n\n---\n"
77
+ "Variables (per-request — keep below the cache breakpoint):\n"
78
+ + var_block + "\n")
79
+ # Warn if a variable was buried mid-prompt (the thing we're fixing).
80
+ first_var = text.find("{{")
81
+ if first_var > len(text) // 2:
82
+ notes.append("variables were already near the end — good shape, "
83
+ "minor cleanup applied.")
84
+ else:
85
+ notes.append(f"moved {len(placeholders)} variable(s) below the "
86
+ "static prefix so the cache hits.")
87
+ notes.append("Review the rewrite: reordering can change emphasis. "
88
+ "The static text is preserved verbatim.")
89
+ return optimized, notes
90
+
91
+
92
+ def provider_snippet(provider: str) -> str:
93
+ p = provider.lower()
94
+ if p == "anthropic":
95
+ return '''Anthropic (explicit breakpoint):
96
+ import anthropic
97
+ client = anthropic.Anthropic()
98
+ # Put the STATIC part in its own block with cache_control.
99
+ # Everything after the breakpoint (your {{variables}}) is fresh each call.
100
+ msg = client.messages.create(
101
+ model="claude-opus-4-6",
102
+ max_tokens=1024,
103
+ system=[{"type": "text", "text": STATIC_PROMPT,
104
+ "cache_control": {"type": "ephemeral"}}],
105
+ messages=[{"role": "user",
106
+ "content": VARIABLES_TEXT}])
107
+ # Cached input tokens are ~90% cheaper. Breakpoints: max 4, min 1024 tokens.'''
108
+ if p == "openai":
109
+ return '''OpenAI (automatic — no code changes):
110
+ # Prefix caching is automatic on repeated prefixes (50% off).
111
+ # Just keep the STATIC part byte-identical and FIRST in messages:
112
+ messages = [{"role": "system", "content": STATIC_PROMPT}, # stable prefix
113
+ {"role": "user", "content": VARIABLES_TEXT}] # changes per call'''
114
+ if p == "gemini":
115
+ return '''Gemini (explicit context caching, 75% off):
116
+ from google import genai
117
+ client = genai.Client()
118
+ # Cache the stable context once, reuse its name per request:
119
+ cache = client.caches.create(model="gemini-2.5-flash",
120
+ config={"contents": STATIC_PROMPT,
121
+ "ttl": "3600s"})
122
+ resp = client.models.generate_content(
123
+ model="gemini-2.5-flash",
124
+ config={"cached_content": cache.name},
125
+ contents=VARIABLES_TEXT)'''
126
+ if p == "deepseek":
127
+ return '''DeepSeek (automatic prefix caching, 50% off):
128
+ # Same rule as OpenAI: stable prefix first, byte-identical across calls.
129
+ # No API changes needed — the cache engages on prefix match.'''
130
+ raise ValueError(f"unknown provider: {provider!r}")
131
+
132
+
133
+ def guide_text() -> str:
134
+ lines = ["", " PROMPT CACHING — provider guide", "",
135
+ " Cache discounts repeated prefix tokens. The rule everywhere:"]
136
+ lines.append(" STATIC content first (system prompt, docs, examples),")
137
+ lines.append(" {{variables}} last. Byte-identical prefixes hit the cache.")
138
+ lines.append("")
139
+ for name, url, note in CACHE_DOCS:
140
+ lines.append(f" {name}\n {url}\n {note}\n")
141
+ lines.append(" `shortleash cache make -i prompt.txt` restructures")
142
+ lines.append(" your prompt for this automatically.")
143
+ return "\n".join(lines)