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:
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"default_model_version": "1.7.0",
|
||||
"default_model_version": "1.8.0",
|
||||
"extract-pintable": {
|
||||
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
||||
"latest_version": "1784798970179642",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user