From 3e43bb6ecbbf6adc4cd1338617225ce57ea7c9f6 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Thu, 10 Sep 2026 22:56:49 +0200 Subject: [PATCH] Force every LLM stage onto DeepSeek and stop Anthropic Console uploads. PROVIDER_*=anthropic and Claude fallbacks now coerce to DeepSeek, skills stay local, and default_model_version bumps without upload_skills.py. Co-authored-by: Cursor --- CLAUDE.md | 16 +++---- README.md | 6 +-- backend/.env.example | 16 ++----- backend/config.py | 21 ++++---- backend/services/extraction.py | 3 +- backend/services/llm/factory.py | 5 +- backend/skills_manifest.json | 2 +- tests/test_cost_estimator_model_aware.py | 61 ++++++++++-------------- tests/test_deepseek_only.py | 51 ++++++++++++++++++++ 9 files changed, 109 insertions(+), 72 deletions(-) create mode 100644 tests/test_deepseek_only.py diff --git a/CLAUDE.md b/CLAUDE.md index 84f71cc..33f8322 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ Three layers: | **Backend** | `backend/` | FastAPI app — async pipeline orchestration, SSE progress, project/file storage | | **Frontend** | `frontend/` | Next.js 16 app — project dashboard, pipeline progress, report viewer, derating, admin dashboard | -Plus `skills/` — extraction prompts (pintable, patterns, specs) inlined locally for DeepSeek; optional Anthropic Console Skills if you route a stage to Anthropic. +Plus `skills/` — extraction prompts (pintable, patterns, specs) inlined locally for DeepSeek. Do not upload to Anthropic Console. The pipeline stages: Parse BOM → Extract IC Pintables → Extract Simple Components → Extract Passives → DigiKey Auto-Resolve + Value Fallback → Build Graph → Direct Datasheet Review. Pipeline runs can be cancelled mid-execution via `POST /api/pipeline/{id}/cancel`. @@ -44,7 +44,7 @@ Files: `.asc` (PADS-PCB netlist; `.edn` EDIF 2.0.0 also accepted), `.csv`/`.xlsx - **Netlist as graph** — Queryable bipartite graph (components + nets) with traversal helpers - **LLM API for PDF extraction** — Forced tool calls for structured output (pintable, passive patterns, specs). Default provider is DeepSeek. - **Prompt caching** — Anthropic stamps `cache_control`; Gemini uses CachedContent; DeepSeek uses automatic prefix cache (cache-hit tokens in usage). -- **Local extraction skills** — `skills/*/SKILL.md` is inlined and `validate.py` runs in-process. Anthropic Console Skills remain optional via `scripts/upload_skills.py`. +- **Local extraction skills** — `skills/*/SKILL.md` is inlined and `validate.py` runs in-process. Never call `scripts/upload_skills.py` (Anthropic Console). - **Direct datasheet review** — The model reads the IC datasheet plus circuit neighborhood, compares to the reference application circuit, and flags issues via graph query tools (`find_connected_components`, `get_net_for_pin`, `get_pintable`). DeepSeek converts PDFs to text (and page images on the vision model). - **Datasheet page trimming** — Large PDFs are keyword-trimmed to relevant pages before sending to Claude, reducing token cost (`pypdf`) - **DigiKey fallback (exact MPN only)** — When pattern-based and direct extraction fail, DigiKey API fetches product parameters for auto-resolve. DigiKey matches only on exact MPN; fuzzy hits are rejected to avoid polluting the shared library with wrong-dielectric / wrong-voltage parts. @@ -75,7 +75,7 @@ Per-MPN IC extraction captures: For discrete/simple components: 4. **Specs** — Component specs (value, tolerance, package, voltage rating, etc.); parameters are filtered against taxonomy specs schemas -Extraction inlines **local skills** (`skills/*/SKILL.md` + `validate.py`). Anthropic Console Skills are optional when `PROVIDER_*=anthropic` and a skill_id is in `backend/skills_manifest.json`. +Extraction inlines **local skills** (`skills/*/SKILL.md` + `validate.py`) against DeepSeek. Do not use Anthropic Console Skills. ## Claude Console Skills @@ -101,7 +101,7 @@ Key taxonomy features: ## Scripts -- `scripts/upload_skills.py` — Create, update, or list Claude Console Skills. Reads/writes skill IDs to `backend/skills_manifest.json` +- `scripts/upload_skills.py` — leftover Claude Console uploader. **Do not run.** Skills are local + DeepSeek only. - `scripts/migrate_datasheets_to_library.py` — One-time migration: copy per-project datasheets to `library/datasheets/` (dry-run by default, `--apply` to execute) - `scripts/migrate_datasheets_to_blobs.py` — Migrate named-PDF datasheets into the content-addressed blobs/refs layout (dry-run by default, `--apply` to execute) - `scripts/dedup_library_datasheets.py` — Remove redundant per-MPN datasheet PDFs when a passive pattern already has a `datasheet_key` (dry-run by default, `--apply` to execute) @@ -113,9 +113,9 @@ Key taxonomy features: - **Core**: Python 3.12+, Pydantic 2.x, OpenAI SDK (DeepSeek), Anthropic SDK (optional), google-genai (optional), openpyxl, pypdf, PyMuPDF - **Backend**: FastAPI, uvicorn, sse-starlette, pydantic-settings - **Frontend**: Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS v4, shadcn/ui (Base UI), react-pdf -- **AI**: DeepSeek Chat Completions (OpenAI-compatible) with forced tool calls for extraction and agentic review. Optional Anthropic / Gemini fallbacks. +- **AI**: DeepSeek Chat Completions (OpenAI-compatible) with forced tool calls for extraction and agentic review. Do not route stages to Anthropic. - **Model**: `deepseek-flash` for extraction, review, auto-resolve, and normalize (per-stage overrides via `.env`) -- **Skills**: Local SKILL.md + validate.py (DeepSeek/Gemini); optional Anthropic Console Skills +- **Skills**: Local SKILL.md + validate.py on DeepSeek - **External APIs**: DigiKey API v4 (OAuth2) — optional datasheet auto-fetch and parameter-based auto-resolve (`DIGIKEY_CLIENT_ID`, `DIGIKEY_CLIENT_SECRET`) ## Extracted Model Versioning @@ -123,9 +123,9 @@ Key taxonomy features: All `ComponentConstraints` extracted JSON files carry a `model_version` semver field: - **Initial value** — set from `default_model_version` in `backend/skills_manifest.json` (starts at `1.0.0`) -- **Minor bump** — `default_model_version` in `skills_manifest.json` is incremented by `scripts/upload_skills.py --update`, so all new extractions after a skill update start at the new minor (e.g. `1.0.0` → `1.1.0`) +- **Minor bump** — increment `default_model_version` in `skills_manifest.json` when extraction prompts change (do **not** run `upload_skills.py`). -**Rule**: When committing or pushing changes under `skills/`, run `python3 scripts/upload_skills.py --update` before the commit/push to sync skill versions and bump `default_model_version`. +**Rule**: When committing changes under `skills/`, bump `default_model_version` locally. Never call Anthropic. ## Development Guidelines diff --git a/README.md b/README.md index a046a12..921babf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Pinscope reviews schematics the way a good senior engineer does: with the datasheets open. -This tree is adapted from [manvalan/pinscope](https://github.com/manvalan/pinscope) so the pipeline talks to the **DeepSeek API** (`deepseek-flash`, with legacy aliases still accepted) instead of requiring an Anthropic Console skill upload. Anthropic and Gemini remain optional fallbacks. +This tree is adapted from [manvalan/pinscope](https://github.com/manvalan/pinscope) so the pipeline talks to the **DeepSeek API** (`deepseek-flash`, with legacy aliases still accepted). Do not use Anthropic. Give it a netlist, a BOM, and your datasheet PDFs. It builds a graph of your design, reads each IC's datasheet, and checks the circuit around every part against what the manufacturer actually specifies — reference application, pin functions, absolute maximums, recommended operating conditions. Every finding points at the datasheet page that backs it up. @@ -55,9 +55,7 @@ cd frontend && npm install NEXT_PUBLIC_API_URL=http://127.0.0.1:18741 npm run dev -- --port 18742 --hostname 127.0.0.1 ``` -Open the frontend URL, create a project, and feed it the netlist and BOM from `simple_project/`. Datasheets are fetched automatically (LCSC, TI, optional DigiKey); you can still drop in PDFs by hand. Fetched PDFs and extracted pin tables land in the **Library** (sidebar) and are reused on later projects. Everything runs locally against your own key; projects and the extraction library live in `data/`. - -Anthropic Console Skills (`python3 scripts/upload_skills.py --update`) are optional and only needed if you set `PROVIDER_DEFAULT=anthropic`. +Open the frontend URL, create a project, and feed it the netlist and BOM from `simple_project/`. Datasheets are fetched automatically (LCSC, TI, optional DigiKey); you can still drop in PDFs by hand. Fetched PDFs and extracted pin tables land in the **Library** (sidebar) and are reused on later projects. Everything runs locally against your own DeepSeek key; projects and the extraction library live in `data/`. Skills are local `skills/*/SKILL.md` — do not run `scripts/upload_skills.py`. ## Docker diff --git a/backend/.env.example b/backend/.env.example index 31cc0a4..67ca3ce 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -26,17 +26,12 @@ DEEPSEEK_REASONING_EFFORT=high # -- AI provider routing ----------------------------------------------------- # Default provider for every stage; per-stage env vars override. -# Valid values: deepseek | anthropic | gemini +# Valid values: deepseek | gemini (anthropic is ignored and coerced to deepseek) PROVIDER_DEFAULT=deepseek # PROVIDER_VALIDATION=deepseek # PROVIDER_AUTO_RESOLVE=deepseek -# -- Anthropic (optional fallback) ------------------------------------------- -# ANTHROPIC_API_KEY=sk-ant-... -# ANTHROPIC_MODEL=claude-sonnet-4-6 -# MODEL_PINTABLE= -# MODEL_PATTERN= -# MODEL_VALIDATION= +# Anthropic is not used. Do not set ANTHROPIC_API_KEY. # -- Gemini (optional) ------------------------------------------------------- # GEMINI_API_KEY= @@ -44,10 +39,9 @@ PROVIDER_DEFAULT=deepseek # MODEL_VALIDATION_GEMINI= # -- Per-stage fallback ------------------------------------------------------ -# If set, the stage retries once with FALLBACK_PROVIDER_ / -# FALLBACK_MODEL_ when the primary provider raises. -# FALLBACK_PROVIDER_VALIDATION=anthropic -# FALLBACK_MODEL_VALIDATION=claude-sonnet-4-6 +# If set, the stage retries once. Anthropic is ignored (DeepSeek only). +# FALLBACK_PROVIDER_VALIDATION=deepseek +# FALLBACK_MODEL_VALIDATION=deepseek-flash # -- Storage ----------------------------------------------------------------- # Set GCS_BUCKET to store projects/library in Google Cloud Storage. diff --git a/backend/config.py b/backend/config.py index 1149e1b..e2a6cd9 100644 --- a/backend/config.py +++ b/backend/config.py @@ -220,9 +220,16 @@ class Settings(BaseSettings): return bool(self.email_sender and self.email_frontend_url) def provider_for_stage(self, stage: str) -> str: - """Return the LLM provider name for a pipeline stage.""" + """Return the LLM provider name for a pipeline stage. + + Anthropic is never used: any ``PROVIDER_*=anthropic`` override is + coerced to DeepSeek. + """ override = getattr(self, f"provider_{stage}", "") - return override or self.provider_default + name = override or self.provider_default + if name == "anthropic": + return "deepseek" + return name def model_for_stage(self, stage: str) -> str: """Return the model for a pipeline stage, provider-aware. @@ -247,7 +254,7 @@ class Settings(BaseSettings): when the primary provider raises. """ fb_provider = getattr(self, f"fallback_provider_{stage}", "") - if not fb_provider: + if not fb_provider or fb_provider == "anthropic": return None fb_model = getattr(self, f"fallback_model_{stage}", "") if not fb_model: @@ -269,15 +276,13 @@ class Settings(BaseSettings): def has_llm_credentials(self) -> bool: """True if the configured default provider has an API key.""" name = self.provider_default + if name == "anthropic": + name = "deepseek" if name == "deepseek": return bool(self.deepseek_api_key) if name == "gemini": return bool(self.gemini_api_key) - if name == "anthropic": - return bool(self.anthropic_api_key) - return bool( - self.deepseek_api_key or self.anthropic_api_key or self.gemini_api_key - ) + return bool(self.deepseek_api_key) def get_skill_or_none(self, name: str) -> tuple[str | None, str | None]: """Return (skill_id, version) or (None, None) if the Anthropic diff --git a/backend/services/extraction.py b/backend/services/extraction.py index 1bb9cd3..671e4ae 100644 --- a/backend/services/extraction.py +++ b/backend/services/extraction.py @@ -5,8 +5,7 @@ Ports the extraction steps from run_pipeline.py to async: - extract_pattern: Passive MPN pattern - extract_specs: Component specs (discrete, connectors, crystals, etc.) -Skills (SKILL.md + validate.py) run locally for DeepSeek/Gemini. Anthropic -can still use Console Skills when a skill_id is in skills_manifest.json. +Skills (SKILL.md + validate.py) run locally against DeepSeek. Do not use Anthropic Console Skills. """ from __future__ import annotations diff --git a/backend/services/llm/factory.py b/backend/services/llm/factory.py index b91ce83..8f90427 100644 --- a/backend/services/llm/factory.py +++ b/backend/services/llm/factory.py @@ -24,8 +24,9 @@ def get_provider_by_name(name: str) -> LLMProvider: from backend.services.llm.deepseek_provider import DeepSeekProvider return DeepSeekProvider() if name == "anthropic": - from backend.services.llm.anthropic_provider import AnthropicProvider - return AnthropicProvider() + log.warning("Anthropic is disabled — using DeepSeek instead") + from backend.services.llm.deepseek_provider import DeepSeekProvider + return DeepSeekProvider() if name == "gemini": from backend.services.llm.gemini_provider import GeminiProvider return GeminiProvider() diff --git a/backend/skills_manifest.json b/backend/skills_manifest.json index 7e5670a..cefc9b4 100644 --- a/backend/skills_manifest.json +++ b/backend/skills_manifest.json @@ -1,5 +1,5 @@ { - "default_model_version": "1.7.0", + "default_model_version": "1.8.0", "extract-pintable": { "skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY", "latest_version": "1784798970179642", diff --git a/tests/test_cost_estimator_model_aware.py b/tests/test_cost_estimator_model_aware.py index a50e790..dfa7ec1 100644 --- a/tests/test_cost_estimator_model_aware.py +++ b/tests/test_cost_estimator_model_aware.py @@ -39,6 +39,7 @@ def restore_settings(): "model_pattern", "model_pattern_gemini", "model_pattern_deepseek", "model_specs", "model_specs_gemini", "model_specs_deepseek", "model_auto_resolve", "model_auto_resolve_gemini", "model_auto_resolve_deepseek", + "fallback_provider_validation", "fallback_model_validation", ] snapshot = {f: getattr(settings, f) for f in fields if hasattr(settings, f)} yield @@ -47,54 +48,42 @@ def restore_settings(): def test_review_cost_changes_with_validation_model(restore_settings): - """Routing validation to Sonnet vs Haiku should produce different - per-IC review costs — and Haiku should be cheaper than Sonnet.""" - settings.provider_validation = "anthropic" - - settings.model_validation = "claude-sonnet-4-6" - sonnet_cost = estimate_stage_cost_usd("review") - - settings.model_validation = "claude-haiku-4-5" - haiku_cost = estimate_stage_cost_usd("review") - - assert sonnet_cost > 0 - assert haiku_cost > 0 - # Haiku is ~3× cheaper than Sonnet on input ($1 vs $3) and 3× on - # output ($5 vs $15). The blended ratio with cache_read should - # land Haiku at <50% of Sonnet's cost — wide enough margin to be - # robust to baseline tweaks. - assert haiku_cost < sonnet_cost * 0.6 - - -def test_review_cost_changes_with_validation_provider(restore_settings): - """Flipping PROVIDER_VALIDATION between deepseek and anthropic must - swap the rate table the estimator pulls from.""" + """Routing validation to two DeepSeek models should produce different + per-IC review costs.""" settings.provider_validation = "deepseek" + settings.model_validation_deepseek = "deepseek-v4-pro" - deepseek_cost = estimate_stage_cost_usd("review") + pro_cost = estimate_stage_cost_usd("review") + settings.model_validation_deepseek = "deepseek-flash" + flash_cost = estimate_stage_cost_usd("review") + + assert pro_cost > 0 + assert flash_cost > 0 + assert pro_cost != flash_cost + + +def test_review_cost_stays_on_deepseek_table(restore_settings): + """PROVIDER_VALIDATION=anthropic must still price DeepSeek, not Claude.""" settings.provider_validation = "anthropic" + settings.model_validation_deepseek = "deepseek-flash" settings.model_validation = "claude-sonnet-4-6" - anthropic_cost = estimate_stage_cost_usd("review") - - assert deepseek_cost > 0 - assert anthropic_cost > 0 - assert abs(deepseek_cost - anthropic_cost) > 0.01, ( - f"expected materially different costs, got " - f"deepseek={deepseek_cost!r} anthropic={anthropic_cost!r}" - ) + cost = estimate_stage_cost_usd("review") + assert cost > 0 + settings.provider_validation = "deepseek" + ds_cost = estimate_stage_cost_usd("review") + assert cost == pytest.approx(ds_cost, rel=1e-9) def test_unknown_model_falls_back_to_default_rate(restore_settings): """A model not in PRICING[provider] should price against PRICING[provider]['default'], not crash.""" - settings.provider_validation = "anthropic" - settings.model_validation = "claude-totally-made-up-2099" + settings.provider_validation = "deepseek" + settings.model_validation_deepseek = "deepseek-totally-made-up-2099" cost = estimate_stage_cost_usd("review") - # Same baseline against PRICING['anthropic']['default'] - settings.model_validation = "" # forces anthropic_model fallback - settings.anthropic_model = "claude-totally-made-up-2099" + settings.model_validation_deepseek = "" + settings.deepseek_model = "deepseek-totally-made-up-2099" cost_via_global_default = estimate_stage_cost_usd("review") assert cost > 0 diff --git a/tests/test_deepseek_only.py b/tests/test_deepseek_only.py new file mode 100644 index 0000000..f830071 --- /dev/null +++ b/tests/test_deepseek_only.py @@ -0,0 +1,51 @@ +"""Pinscope LLM routing is DeepSeek only. + +Favor: every pipeline stage uses DeepSeek even if PROVIDER_* is set to +anthropic; model_for_stage stays on deepseek-flash. +Against: anthropic fallback is ignored; get_provider_by_name('anthropic') +does not construct the Anthropic SDK client. +""" + +from __future__ import annotations + +import pytest + +from backend.config import settings +from backend.services.llm.factory import get_provider, get_provider_by_name + + +@pytest.fixture +def restore_routing(): + snap = { + "provider_default": settings.provider_default, + "provider_validation": settings.provider_validation, + "fallback_provider_validation": settings.fallback_provider_validation, + "fallback_model_validation": settings.fallback_model_validation, + } + yield + for k, v in snap.items(): + setattr(settings, k, v) + get_provider_by_name.cache_clear() + + +def test_stage_stays_deepseek_when_env_says_anthropic(restore_routing): + settings.provider_validation = "anthropic" + assert settings.provider_for_stage("validation") == "deepseek" + get_provider_by_name.cache_clear() + assert get_provider("validation").name == "deepseek" + assert "deepseek" in settings.model_for_stage("validation") + + +def test_anthropic_fallback_is_not_used(restore_routing): + settings.fallback_provider_validation = "anthropic" + settings.fallback_model_validation = "claude-sonnet-4-6" + assert settings.fallback_for_stage("validation") is None + + +def test_get_provider_by_name_does_not_load_anthropic(restore_routing): + get_provider_by_name.cache_clear() + try: + p = get_provider_by_name("anthropic") + assert p.name == "deepseek" + finally: + get_provider_by_name.cache_clear()