Research-Stack/5-Applications/tools-scripts/llm/deepseek_review_adapter.py

396 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
deepseek_review_adapter.py
==========================
Wraps `4-Infrastructure/infra/deepseek_adapter.py` with three additions for the
"verifier-of-the-verifier / fluent-bullshit defence" workflow:
(a) prefer remote `deepseek-v4-flash` by default
(b) escalate to `deepseek-v4-pro` on demand (`tier="pro"`)
(c) always include the cached project context (verifier code, findings,
MEMORY.md, etc.) using the Anthropic-compat endpoint's prompt-caching
so that re-using the context across queries costs ~50× less
Pricing (USD per 1M tokens, per https://api-docs.deepseek.com/quick_start/pricing,
fetched 2026-05-03):
deepseek-v4-flash : input cache hit $0.0028 | miss $0.14 | output $0.28
deepseek-v4-pro : input cache hit $0.003625 | miss $0.435 | output $0.87
(75 % discount through 2026/05/31)
A single review of the entire Burgers-verifier project (~20k input tokens of
context + 1k prompt + 3k response) costs ~$0.012 on first query (cache miss)
and ~$0.003 per cached re-use on v4-pro.
Each query emits a JSON receipt with token counts, model, cost, response-text
hash, and the SHA-256 of every cached file at query-time. The receipt is the
DeepSeek-side analogue of the Burgers-verifier's per-run DAG.
"""
from __future__ import annotations
import datetime
import hashlib
import json
import os
import sys
import uuid
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Optional
import requests
REPO = Path(__file__).resolve().parents[3]
RECEIPTS_DIR = REPO / "shared-data" / "artifacts" / "deepseek_review"
ENV_FILE = REPO / ".env"
# --- Pricing constants (mirrored to DeepSeekBudgetCalculator.lean) ---
PRICING_USD_PER_1M = {
"deepseek-v4-flash": {"input_cache_hit": 0.0028, "input_cache_miss": 0.14, "output": 0.28},
"deepseek-v4-pro": {"input_cache_hit": 0.003625, "input_cache_miss": 0.435, "output": 0.87},
}
ANTHROPIC_COMPAT_BASE = "https://api.deepseek.com/anthropic"
ANTHROPIC_API_VERSION = "2023-06-01"
# =============================================================================
# Result type + cost calculation (mirror of the Lean module)
# =============================================================================
@dataclass
class ReviewResult:
model: str
tier: str
response_text: str
input_tokens: int
output_tokens: int
cache_creation_input_tokens: int
cache_read_input_tokens: int
cost_usd: float
cost_breakdown: dict
cached_context_files: list
receipt_path: str | None = None
receipt_id: str = ""
def cost_for_query(model: str, fresh_input: int, cached_input: int, output: int) -> dict:
"""Compute USD cost given token counts. Mirrors Semantics.DeepSeek.queryCost."""
if model not in PRICING_USD_PER_1M:
raise ValueError(f"unknown model {model!r}; known: {list(PRICING_USD_PER_1M)}")
p = PRICING_USD_PER_1M[model]
cached_cost = (cached_input / 1_000_000.0) * p["input_cache_hit"]
fresh_cost = (fresh_input / 1_000_000.0) * p["input_cache_miss"]
output_cost = (output / 1_000_000.0) * p["output"]
return {
"cached_input_cost": cached_cost,
"fresh_input_cost": fresh_cost,
"output_cost": output_cost,
"total_usd": cached_cost + fresh_cost + output_cost,
}
def estimate_cost(model: str, fresh_input: int, cached_input: int, output: int) -> float:
return cost_for_query(model, fresh_input, cached_input, output)["total_usd"]
# =============================================================================
# .env loader (just for DEEPSEEK_API_KEY — no python-dotenv required)
# =============================================================================
def _load_api_key_from_env_file() -> Optional[str]:
if not ENV_FILE.exists():
return None
for line in ENV_FILE.read_text().splitlines():
if line.startswith("DEEPSEEK_API_KEY="):
return line.split("=", 1)[1].strip()
return None
# =============================================================================
# Project-context bundle
# =============================================================================
@dataclass
class ContextFile:
path: str
sha256: str
bytes: int
snippet_head: str # first 80 chars for human-readable receipts
def gather_project_context(paths: list[Path]) -> tuple[str, list[ContextFile]]:
"""Concatenate file contents for cache-prefix; return (context_text, file_metadata)."""
parts: list[str] = []
metadata: list[ContextFile] = []
for p in paths:
if not p.exists():
sys.stderr.write(f"WARN: cached-context file missing: {p}\n")
continue
body = p.read_bytes()
text = body.decode("utf-8", errors="replace")
rel = str(p.relative_to(REPO)) if p.is_relative_to(REPO) else str(p)
parts.append(f"\n\n===== FILE: {rel} =====\n{text}")
metadata.append(ContextFile(
path=rel,
sha256="sha256:" + hashlib.sha256(body).hexdigest(),
bytes=len(body),
snippet_head=text[:80].replace("\n", "\\n"),
))
return "".join(parts), metadata
# Default project context for Burgers-verifier work.
# Edit this list per evening to focus DeepSeek on what you want reviewed.
DEFAULT_CONTEXT_PATHS = [
REPO / "5-Applications" / "tools-scripts" / "verifier" / "burgers_verifier.py",
REPO / "5-Applications" / "tools-scripts" / "verifier" / "audit_gsp_variants.py",
REPO / "5-Applications" / "tools-scripts" / "verifier" / "run_dag.py",
REPO / "shared-data" / "artifacts" / "burgers_verifier" / "findings_2026-05-03.md",
REPO / "shared-data" / "artifacts" / "burgers_verifier" / "perceval_repro_analysis.md",
]
# =============================================================================
# Adapter
# =============================================================================
class DeepSeekReviewer:
"""
Tier-aware DeepSeek client with project-context caching and per-query receipts.
Tiers:
"flash" → deepseek-v4-flash (default; cheap)
"pro" → deepseek-v4-pro (escalate; ~3× cost)
"local" → local Ollama via existing infra/deepseek_adapter.py (free)
"""
def __init__(
self,
api_key: Optional[str] = None,
cached_context_paths: Optional[list[Path]] = None,
receipts_dir: Path = RECEIPTS_DIR,
):
self.api_key = api_key or os.getenv("DEEPSEEK_API_KEY") or _load_api_key_from_env_file()
self.cached_context_paths = cached_context_paths or DEFAULT_CONTEXT_PATHS
self.receipts_dir = receipts_dir
self.receipts_dir.mkdir(parents=True, exist_ok=True)
# Materialise context once at construction; the same prefix string is sent
# every query so DeepSeek can cache-hit on it.
self.context_text, self.context_files = gather_project_context(self.cached_context_paths)
# ------------------------------------------------------------------
# Cost-only path (no API call) — useful for dry-run and budget planning
# ------------------------------------------------------------------
def estimate_review_cost(self, prompt: str, tier: str = "flash",
expected_output_tokens: int = 3000,
first_query: bool = True) -> dict:
"""Estimate USD cost for a hypothetical review without calling the API.
Token counts use the conservative ~4-chars-per-token rule of thumb.
Use `first_query=True` for cache-miss (entire context billed at miss rate),
`False` for cache-hit (re-use within the cache TTL).
"""
model = self._tier_to_model(tier)
if model == "local":
return {"model": "local", "cost_usd": 0.0,
"note": "local Ollama; free at the margin"}
ctx_tokens = self._tokens_estimate(self.context_text)
prompt_tokens = self._tokens_estimate(prompt)
if first_query:
return cost_for_query(model, fresh_input=ctx_tokens + prompt_tokens,
cached_input=0, output=expected_output_tokens) | {"model": model}
return cost_for_query(model, fresh_input=prompt_tokens,
cached_input=ctx_tokens, output=expected_output_tokens) | {"model": model}
# ------------------------------------------------------------------
# Live review path
# ------------------------------------------------------------------
def review(
self,
prompt: str,
tier: str = "flash",
max_output_tokens: int = 4000,
receipt_label: str = "review",
) -> ReviewResult:
"""Submit a review query. Always includes the cached project context."""
model = self._tier_to_model(tier)
if model == "local":
return self._review_local(prompt, max_output_tokens, receipt_label)
if not self.api_key:
raise RuntimeError("DEEPSEEK_API_KEY missing; populate .env or pass api_key=")
# Anthropic-compat payload with prompt caching on the system block.
body = {
"model": model,
"max_tokens": max_output_tokens,
"system": [
{
"type": "text",
"text": (
"You are an adversarial reviewer for a research-stack project. "
"The cached context that follows is the project state. Read it "
"thoroughly. Then answer the user's prompt as a sceptical "
"second set of eyes — point out errors, weak claims, missing "
"evidence, fluent-but-wrong reasoning, unstated assumptions. "
"Be specific and cite file paths or line numbers when possible."
),
},
{
"type": "text",
"text": self.context_text,
"cache_control": {"type": "ephemeral"},
},
],
"messages": [{"role": "user", "content": prompt}],
}
headers = {
"x-api-key": self.api_key,
"anthropic-version": ANTHROPIC_API_VERSION,
"Content-Type": "application/json",
}
url = f"{ANTHROPIC_COMPAT_BASE}/v1/messages"
resp = requests.post(url, headers=headers, json=body, timeout=120)
resp.raise_for_status()
data = resp.json()
# Anthropic-shaped response: {"content": [{"type": "text", "text": "..."}], "usage": {...}}
text_blocks = [b.get("text", "") for b in data.get("content", []) if b.get("type") == "text"]
response_text = "\n".join(text_blocks)
usage = data.get("usage", {})
in_tokens = usage.get("input_tokens", 0)
out_tokens = usage.get("output_tokens", 0)
cache_creation = usage.get("cache_creation_input_tokens", 0)
cache_read = usage.get("cache_read_input_tokens", 0)
breakdown = cost_for_query(
model,
fresh_input=in_tokens + cache_creation, # cache_creation tokens billed at miss rate
cached_input=cache_read,
output=out_tokens,
)
result = ReviewResult(
model=model, tier=tier, response_text=response_text,
input_tokens=in_tokens, output_tokens=out_tokens,
cache_creation_input_tokens=cache_creation,
cache_read_input_tokens=cache_read,
cost_usd=breakdown["total_usd"],
cost_breakdown=breakdown,
cached_context_files=[asdict(f) for f in self.context_files],
)
self._write_receipt(result, prompt, receipt_label)
return result
# ------------------------------------------------------------------
# Internals
# ------------------------------------------------------------------
@staticmethod
def _tier_to_model(tier: str) -> str:
tier = tier.lower()
if tier == "flash":
return "deepseek-v4-flash"
if tier == "pro":
return "deepseek-v4-pro"
if tier == "local":
return "local"
raise ValueError(f"unknown tier {tier!r}; choose 'flash', 'pro', or 'local'")
@staticmethod
def _tokens_estimate(text: str) -> int:
# Conservative: ~4 chars per token (English text + code averages 3-5).
return max(1, len(text) // 4)
def _review_local(self, prompt: str, max_output_tokens: int, receipt_label: str) -> ReviewResult:
"""Falls through to the existing infra/deepseek_adapter.py path."""
sys.path.insert(0, str(REPO / "4-Infrastructure"))
from infra.deepseek_adapter import DeepSeekV4 # type: ignore[import-not-found]
client = DeepSeekV4(use_local=True)
# Same context-prefix pattern in the user message; local has no caching.
user_msg = f"{self.context_text}\n\n=== REVIEW REQUEST ===\n{prompt}"
res = client.chat([{"role": "user", "content": user_msg}], model="deepseek-r1:8b")
text = res.get("message", {}).get("content", "") if isinstance(res, dict) else ""
result = ReviewResult(
model="local:deepseek-r1:8b", tier="local", response_text=text,
input_tokens=self._tokens_estimate(user_msg),
output_tokens=self._tokens_estimate(text),
cache_creation_input_tokens=0, cache_read_input_tokens=0,
cost_usd=0.0,
cost_breakdown={"note": "local Ollama; free at the margin"},
cached_context_files=[asdict(f) for f in self.context_files],
)
self._write_receipt(result, prompt, receipt_label)
return result
def _write_receipt(self, result: ReviewResult, prompt: str, label: str) -> None:
receipt_id = f"{label}-{datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{uuid.uuid4().hex[:8]}"
receipt = {
"schema_version": "1.0",
"receipt_id": receipt_id,
"completed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
"model": result.model,
"tier": result.tier,
"prompt_sha256": "sha256:" + hashlib.sha256(prompt.encode()).hexdigest(),
"prompt_head": prompt[:200],
"response_sha256": "sha256:" + hashlib.sha256(result.response_text.encode()).hexdigest(),
"response_head": result.response_text[:200],
"tokens": {
"input": result.input_tokens,
"output": result.output_tokens,
"cache_creation": result.cache_creation_input_tokens,
"cache_read": result.cache_read_input_tokens,
},
"cost_usd": result.cost_usd,
"cost_breakdown": result.cost_breakdown,
"cached_context_files": result.cached_context_files,
}
path = self.receipts_dir / f"{receipt_id}.receipt.json"
path.write_text(json.dumps(receipt, indent=2, default=str))
result.receipt_path = str(path.relative_to(REPO))
result.receipt_id = receipt_id
# =============================================================================
# CLI
# =============================================================================
def _cli():
import argparse
ap = argparse.ArgumentParser(description="DeepSeek review wrapper with project-context caching")
ap.add_argument("prompt", nargs="?", help="review prompt (read from stdin if omitted)")
ap.add_argument("--tier", default="flash", choices=["flash", "pro", "local"])
ap.add_argument("--max-tokens", type=int, default=4000)
ap.add_argument("--label", default="review")
ap.add_argument("--dry-run", action="store_true",
help="estimate cost only; do not call API")
ap.add_argument("--first-query", action="store_true",
help="(dry-run) treat as cache-miss")
args = ap.parse_args()
prompt = args.prompt or sys.stdin.read()
reviewer = DeepSeekReviewer()
if args.dry_run:
est = reviewer.estimate_review_cost(prompt, tier=args.tier,
expected_output_tokens=args.max_tokens,
first_query=args.first_query)
print(json.dumps({"dry_run": True, "estimate": est,
"context_files": [f.path for f in reviewer.context_files]}, indent=2))
return
result = reviewer.review(prompt, tier=args.tier, max_output_tokens=args.max_tokens,
receipt_label=args.label)
print(f"\n=== model={result.model} cost=${result.cost_usd:.4f} ===\n")
print(result.response_text)
print(f"\n=== receipt: {result.receipt_path} ===")
if __name__ == "__main__":
_cli()