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 <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 22:56:49 +02:00
co-authored by Cursor
parent 7aed64cae5
commit 3e43bb6ecb
9 changed files with 109 additions and 72 deletions
+8 -8
View File
@@ -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
+2 -4
View File
@@ -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
+5 -11
View File
@@ -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_<STAGE> /
# FALLBACK_MODEL_<STAGE> 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.
+13 -8
View File
@@ -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
+1 -2
View File
@@ -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
+3 -2
View File
@@ -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()
+1 -1
View File
@@ -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",
+25 -36
View File
@@ -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
+51
View File
@@ -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()