deepseek-harness-cli 0.2.0__py3-none-any.whl

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,2 @@
1
+ """dsh — command-line interface for deepseek-harness."""
2
+ __version__ = "0.2.0"
@@ -0,0 +1,303 @@
1
+ """`dsh` — command-line entry.
2
+
3
+ Subcommands:
4
+ dsh doctor Verify env + run a 1-token call against DeepSeek.
5
+ dsh chat [-r] Interactive REPL with all guards on (-r enables thinking).
6
+ dsh probe <name|all> Run one of the 12 probes from reports/probes/.
7
+ dsh validate <file> Audit a messages.json file for the 5 contract violations.
8
+ dsh estimate <file> Pre-flight cache hit estimate for a messages.json file.
9
+
10
+ The CLI wraps `deepseek_harness.DeepSeekHarness` with all 10 contract rules
11
+ defaulted ON. Pass `--unsafe` to drop guards (debugging / probe authoring only).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ try:
22
+ from dotenv import load_dotenv
23
+ load_dotenv()
24
+ except ImportError:
25
+ pass
26
+
27
+
28
+ def main(argv: list[str] | None = None) -> int:
29
+ p = argparse.ArgumentParser(
30
+ prog="dsh",
31
+ description="DeepSeek Harness — protocol-aware CLI for V4-Pro / V4-Flash.",
32
+ )
33
+ sub = p.add_subparsers(dest="cmd")
34
+
35
+ # ---- doctor ----
36
+ sp = sub.add_parser("doctor", help="Verify env + tiny live call.")
37
+ sp.add_argument("--model", default=os.getenv("DEEPSEEK_MODEL", "deepseek-v4-pro"))
38
+
39
+ # ---- chat ----
40
+ sp = sub.add_parser("chat", help="Interactive REPL with guards on.")
41
+ sp.add_argument("--model", default=os.getenv("DEEPSEEK_MODEL", "deepseek-v4-pro"))
42
+ sp.add_argument("-r", "--reasoning", action="store_true", help="Enable thinking mode.")
43
+ sp.add_argument("--max-tokens", type=int, default=4096)
44
+
45
+ # ---- probe ----
46
+ sp = sub.add_parser("probe", help="Run a probe by name or 'all'.")
47
+ sp.add_argument("name")
48
+ sp.add_argument("--n", type=int, default=None)
49
+
50
+ # ---- validate ----
51
+ sp = sub.add_parser("validate", help="Audit a messages.json file for contract violations.")
52
+ sp.add_argument("file", help="Path to a JSON file containing a list of messages.")
53
+
54
+ # ---- estimate ----
55
+ sp = sub.add_parser("estimate", help="Pre-flight cache-hit estimate.")
56
+ sp.add_argument("file", help="Path to a JSON file containing a list of messages.")
57
+ sp.add_argument("--prev", help="Optional path to previous messages.json (cache prefix).")
58
+
59
+ # ---- version ----
60
+ sub.add_parser("version", help="Print version.")
61
+
62
+ args = p.parse_args(argv)
63
+ if args.cmd is None:
64
+ p.print_help()
65
+ return 0
66
+
67
+ if args.cmd == "doctor":
68
+ return _doctor(args.model)
69
+ if args.cmd == "chat":
70
+ return _chat(args.model, args.reasoning, args.max_tokens)
71
+ if args.cmd == "probe":
72
+ return _probe(args.name, args.n)
73
+ if args.cmd == "validate":
74
+ return _validate(args.file)
75
+ if args.cmd == "estimate":
76
+ return _estimate(args.file, args.prev)
77
+ if args.cmd == "version":
78
+ from . import __version__
79
+ print(f"dsh {__version__}")
80
+ return 0
81
+ return 2
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # subcommand impls
86
+ # ---------------------------------------------------------------------------
87
+
88
+
89
+ def _check_key() -> str | None:
90
+ k = os.getenv("DEEPSEEK_API_KEY")
91
+ if not k:
92
+ print(
93
+ "[dsh] DEEPSEEK_API_KEY is not set. Get one at https://platform.deepseek.com\n"
94
+ " Then `export DEEPSEEK_API_KEY=sk-...` or put it in a .env file.",
95
+ file=sys.stderr,
96
+ )
97
+ return None
98
+ return k
99
+
100
+
101
+ def _doctor(model: str) -> int:
102
+ from rich.console import Console
103
+ from rich.table import Table
104
+
105
+ console = Console()
106
+ if not _check_key():
107
+ return 2
108
+
109
+ from deepseek_harness import DeepSeekHarness
110
+
111
+ h = DeepSeekHarness(disable_thinking_by_default=True)
112
+ try:
113
+ out = h.chat(
114
+ model=model,
115
+ messages=[{"role": "user", "content": "Reply with the literal word OK."}],
116
+ max_tokens=8,
117
+ )
118
+ except Exception as e:
119
+ console.print(f"[red]✗[/] live call FAILED: {type(e).__name__}: {e}")
120
+ return 2
121
+
122
+ t = Table(title="dsh doctor", show_header=False)
123
+ t.add_column("check", style="bold cyan")
124
+ t.add_column("value")
125
+ t.add_row("DEEPSEEK_API_KEY", "set ✓")
126
+ t.add_row("base_url", os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"))
127
+ t.add_row("model", model)
128
+ t.add_row("call.content", repr(out["message"].get("content")))
129
+ t.add_row("call.finish_reason", out["finish_reason"] or "?")
130
+ if out["usage"]:
131
+ u = out["usage"]
132
+ t.add_row("usage.prompt_tokens", str(u.get("prompt_tokens")))
133
+ t.add_row("usage.cache_hit", f"{u.get('prompt_cache_hit_tokens')}/{u.get('prompt_tokens')} = {u.get('cache_hit_rate', 0):.1%}")
134
+ t.add_row("usage.cost_usd", f"${u.get('estimated_cost_usd', 0):.8f}")
135
+ t.add_row("guards", "thinking-off ✓ · max_tokens-cap ✓ · reasoning-preserve ✓ · stream-resilient ✓")
136
+ console.print(t)
137
+ console.print("[green]✓ harness ready. Try `dsh chat`.[/]")
138
+ return 0
139
+
140
+
141
+ def _chat(model: str, reasoning: bool, max_tokens: int) -> int:
142
+ from rich.console import Console
143
+ from rich.markdown import Markdown
144
+
145
+ console = Console()
146
+ if not _check_key():
147
+ return 2
148
+
149
+ from deepseek_harness import DeepSeekHarness
150
+ from deepseek_harness.normalize import prepare_for_new_user_turn
151
+
152
+ h = DeepSeekHarness(disable_thinking_by_default=not reasoning)
153
+ history: list[dict] = []
154
+ console.print(
155
+ f"[bold cyan]dsh chat[/] · model=[yellow]{model}[/] · "
156
+ f"thinking={'on' if reasoning else 'off'} · max_tokens={max_tokens}\n"
157
+ "Commands: /clear /quit. History auto-preserves reasoning_content per spec §1.\n"
158
+ )
159
+ while True:
160
+ try:
161
+ user = console.input("[bold green]you ❯ [/] ").strip()
162
+ except (EOFError, KeyboardInterrupt):
163
+ console.print("\n[dim]bye.[/]")
164
+ return 0
165
+ if not user:
166
+ continue
167
+ if user in ("/quit", "/exit"):
168
+ return 0
169
+ if user == "/clear":
170
+ history.clear()
171
+ console.print("[dim]history cleared.[/]")
172
+ continue
173
+
174
+ history = prepare_for_new_user_turn(history)
175
+ history.append({"role": "user", "content": user})
176
+
177
+ try:
178
+ out = h.chat(
179
+ model=model,
180
+ messages=history,
181
+ max_tokens=max_tokens,
182
+ extra_body={"thinking": {"type": "enabled" if reasoning else "disabled"}},
183
+ )
184
+ except Exception as e:
185
+ console.print(f"[red]✗ {type(e).__name__}: {e}[/]")
186
+ history.pop()
187
+ continue
188
+
189
+ msg = out["message"]
190
+ history.append(msg)
191
+ if reasoning and msg.get("reasoning_content"):
192
+ console.print(f"[dim]reasoning: {msg['reasoning_content'][:200]}...[/]")
193
+ console.print(f"[bold magenta]dsh ❯[/] ", end="")
194
+ console.print(Markdown(msg.get("content") or ""))
195
+ if out["usage"]:
196
+ u = out["usage"]
197
+ console.print(
198
+ f"[dim]usage: hit={u['prompt_cache_hit_tokens']}/{u['prompt_tokens']} "
199
+ f"({u.get('cache_hit_rate', 0):.0%}) · cost=${u.get('estimated_cost_usd', 0):.6f}[/]\n"
200
+ )
201
+
202
+
203
+ def _probe(name: str, n: int | None) -> int:
204
+ repo_root = Path(__file__).resolve().parents[3]
205
+ probes_dir = repo_root / "reports" / "probes"
206
+ if not probes_dir.exists():
207
+ print(f"[dsh] cannot find probes at {probes_dir}", file=sys.stderr)
208
+ return 2
209
+ if name == "list":
210
+ for p in sorted(probes_dir.glob("probe_*.py")):
211
+ print(p.stem)
212
+ return 0
213
+ candidates = list(probes_dir.glob(f"{name}*.py")) if not name.endswith(".py") else [probes_dir / name]
214
+ if not candidates:
215
+ print(f"[dsh] no probe matches '{name}'. try `dsh probe list`.", file=sys.stderr)
216
+ return 2
217
+ target = candidates[0]
218
+ cmd = [sys.executable, str(target)]
219
+ if n is not None:
220
+ cmd.extend(["--n", str(n)])
221
+ import subprocess
222
+ return subprocess.call(cmd, cwd=repo_root)
223
+
224
+
225
+ def _validate(path: str) -> int:
226
+ """Audit a messages.json for the 5 most common contract violations."""
227
+ from rich.console import Console
228
+ console = Console()
229
+ try:
230
+ messages = json.loads(Path(path).read_text())
231
+ except Exception as e:
232
+ console.print(f"[red]✗ cannot read {path}: {e}[/]")
233
+ return 2
234
+
235
+ from deepseek_harness.reasoning import ReasoningLifecycle
236
+
237
+ issues: list[str] = []
238
+ # Rule C2: tool-call assistant message must carry reasoning_content if next msg is tool
239
+ for i, m in enumerate(messages):
240
+ if (
241
+ m.get("role") == "assistant"
242
+ and m.get("tool_calls")
243
+ and i + 1 < len(messages)
244
+ and messages[i + 1].get("role") == "tool"
245
+ and not m.get("reasoning_content")
246
+ ):
247
+ issues.append(f"#{i} C2: assistant→tool boundary missing reasoning_content (will 400 on V4 with thinking)")
248
+
249
+ # Rule C3-style sanity: warn if any role appears wrong
250
+ for i, m in enumerate(messages):
251
+ if m.get("role") not in ("system", "user", "assistant", "tool"):
252
+ issues.append(f"#{i} role: unknown role '{m.get('role')}'")
253
+
254
+ # tool_call structure
255
+ for i, m in enumerate(messages):
256
+ for tc in (m.get("tool_calls") or []):
257
+ if not tc.get("id") or not tc.get("function", {}).get("name"):
258
+ issues.append(f"#{i} tool_call: missing id or function.name")
259
+
260
+ # mid-prefix non-determinism (rough heuristic): system message containing a date pattern
261
+ if messages and messages[0].get("role") == "system":
262
+ sys_text = messages[0].get("content") or ""
263
+ import re
264
+ if re.search(r"\b20\d{2}-\d{2}-\d{2}\b", sys_text) or "current date" in sys_text.lower():
265
+ issues.append("#0 cache: system prompt contains a date pattern → invalidates prefix-cache on each new day")
266
+
267
+ if not issues:
268
+ console.print(f"[green]✓ {len(messages)} messages — no contract violations detected.[/]")
269
+ if ReasoningLifecycle.must_carry(messages):
270
+ console.print("[dim]note: last assistant has tool_calls — your next request MUST preserve reasoning_content if you used thinking mode.[/]")
271
+ return 0
272
+
273
+ console.print(f"[red]✗ {len(issues)} violation(s) detected:[/]")
274
+ for v in issues:
275
+ console.print(f" [yellow]•[/] {v}")
276
+ return 1
277
+
278
+
279
+ def _estimate(path: str, prev: str | None) -> int:
280
+ from rich.console import Console
281
+ console = Console()
282
+ try:
283
+ new_messages = json.loads(Path(path).read_text())
284
+ except Exception as e:
285
+ console.print(f"[red]✗ cannot read {path}: {e}[/]")
286
+ return 2
287
+ prev_messages = None
288
+ if prev:
289
+ try:
290
+ prev_messages = json.loads(Path(prev).read_text())
291
+ except Exception as e:
292
+ console.print(f"[red]✗ cannot read {prev}: {e}[/]")
293
+ return 2
294
+ from deepseek_harness import estimate_cache_hit
295
+ out = estimate_cache_hit(new_messages, prev_messages)
296
+ console.print(f"[bold]cache pre-flight estimate[/]")
297
+ for k, v in out.items():
298
+ console.print(f" [cyan]{k}[/]: {v}")
299
+ return 0
300
+
301
+
302
+ if __name__ == "__main__":
303
+ raise SystemExit(main())
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepseek-harness-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line companion to deepseek-harness. `dsh chat`, `dsh probe`, `dsh validate`, `dsh doctor`.
5
+ Author: Henry Zhang
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/HenryZ838978/deepseek-harness
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: deepseek-harness>=0.2.0
11
+ Requires-Dist: rich>=13.7.0
12
+ Requires-Dist: python-dotenv>=1.0.0
13
+
14
+ # `deepseek-harness-cli`
15
+
16
+ `dsh` — command-line companion to [`deepseek-harness`](https://pypi.org/project/deepseek-harness/).
17
+
18
+ ```bash
19
+ pip install deepseek-harness-cli
20
+ export DEEPSEEK_API_KEY=sk-...
21
+
22
+ dsh doctor # verify env + 1-token live call
23
+ dsh chat # interactive REPL with all guards on
24
+ dsh chat -r # enable thinking mode
25
+ dsh validate path/to/msgs.json # offline contract audit (no API call)
26
+ dsh estimate path/to/msgs.json # offline cache-hit estimate (no API call)
27
+ dsh probe probe_2 --n 3 # run a probe by name
28
+ dsh version
29
+ ```
30
+
31
+ Why use this over a vanilla OpenAI client: the harness enforces all 10 contract rules from the [DeepSeek V4 spec](https://github.com/HenryZ838978/deepseek-harness/tree/main/spec) by default — saving you from the documented `400 reasoning_content`, V8 `Invalid string length` crashes, parallel-tool-delta misalignment, and dual cache-field invisibility.
32
+
33
+ License: MIT.
@@ -0,0 +1,7 @@
1
+ deepseek_harness_cli/__init__.py,sha256=kX07lPF800xgJL7R_1p0ciINKAcA7T7JPcqxoFHc330,81
2
+ deepseek_harness_cli/__main__.py,sha256=uPun8Uoj8O7QhHpqSZ-kVSdQtFTfa-45sMZoD1YeqTI,11234
3
+ deepseek_harness_cli-0.2.0.dist-info/METADATA,sha256=QvCE559v5NAT0upEBSLlwClNjtAfeyea3RkvOdGrgfw,1429
4
+ deepseek_harness_cli-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ deepseek_harness_cli-0.2.0.dist-info/entry_points.txt,sha256=C8Pf7-54USMi9cPjDfhEHZhvwF2_ZiivKpDRB9DNqC0,59
6
+ deepseek_harness_cli-0.2.0.dist-info/top_level.txt,sha256=fH9RZ1lyVG7WOUvsiT374uHoyMKvwiR87VTAYImMWHo,21
7
+ deepseek_harness_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dsh = deepseek_harness_cli.__main__:main
@@ -0,0 +1 @@
1
+ deepseek_harness_cli