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.
- deepseek_harness_cli/__init__.py +2 -0
- deepseek_harness_cli/__main__.py +303 -0
- deepseek_harness_cli-0.2.0.dist-info/METADATA +33 -0
- deepseek_harness_cli-0.2.0.dist-info/RECORD +7 -0
- deepseek_harness_cli-0.2.0.dist-info/WHEEL +5 -0
- deepseek_harness_cli-0.2.0.dist-info/entry_points.txt +2 -0
- deepseek_harness_cli-0.2.0.dist-info/top_level.txt +1 -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 @@
|
|
|
1
|
+
deepseek_harness_cli
|