Default to DeepSeek V4.1 Flash, show API cost in USD, and re-analyze after replacing BOM and netlist.

V4.1 is natively multimodal so every stage uses deepseek-flash; pricing and the UI now surface dollars instead of empty credits. KiCad netlists and neighborhood fingerprints land in the same cut so a second run can keep the project and skip unchanged ICs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 20:55:35 +02:00
co-authored by Cursor
parent d454cf75af
commit 61f85f519b
29 changed files with 1174 additions and 118 deletions
+29 -6
View File
@@ -49,8 +49,9 @@ def test_extract_pdf_text_includes_page_markers(sample_pdf: Path):
def test_vision_model_detection():
assert _is_vision_model("deepseek-v4-flash-vision-exp")
assert _is_vision_model("deepseek-flash")
assert _is_vision_model("deepseek-v4-flash")
assert not _is_vision_model("deepseek-v4-pro")
assert not _is_vision_model("deepseek-v4-flash")
def test_messages_to_openai_pdf_becomes_text(sample_pdf: Path):
@@ -329,14 +330,17 @@ def test_factory_routes_deepseek(monkeypatch):
def test_config_defaults_are_deepseek():
assert settings.provider_default == "deepseek"
assert settings.model_for_stage("validation") == settings.model_validation_deepseek
assert "vision" in settings.model_for_stage("pintable")
from backend.config import Settings
assert Settings.model_fields["provider_default"].default == "deepseek"
assert Settings.model_fields["deepseek_model"].default == "deepseek-flash"
assert Settings.model_fields["model_pintable_deepseek"].default == "deepseek-flash"
assert Settings.model_fields["model_validation_deepseek"].default == "deepseek-flash"
assert settings.provider_for_stage("pintable") == "deepseek"
def test_deepseek_pricing_positive():
cost = cost_for_entry({
pro = cost_for_entry({
"provider": "deepseek",
"model": "deepseek-v4-pro",
"input_tokens": 1_000_000,
@@ -344,5 +348,24 @@ def test_deepseek_pricing_positive():
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
})
assert cost == pytest.approx(1.32)
assert pro == pytest.approx(1.32)
flash = cost_for_entry({
"provider": "deepseek",
"model": "deepseek-flash",
"input_tokens": 1_000_000,
"output_tokens": 0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
})
assert flash == pytest.approx(0.30)
cached = cost_for_entry({
"provider": "deepseek",
"model": "deepseek-flash",
"input_tokens": 0,
"output_tokens": 0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 1_000_000,
})
assert cached == pytest.approx(0.006)
assert "default" in PRICING["deepseek"]
assert "deepseek-flash" in PRICING["deepseek"]
+116
View File
@@ -0,0 +1,116 @@
from pathlib import Path
from backend.pinscopex.parsers import detect_netlist_format, parse_netlist_any, validate_netlist
XML = """<?xml version="1.0" encoding="UTF-8"?>
<export version="E">
<components>
<comp ref="U1">
<value>MSPM0G3507</value>
<footprint>Package_QFP:LQFP-48_7x7mm_P0.5mm</footprint>
<fields>
<field name="MPN">MSPM0G3507SPTR</field>
</fields>
</comp>
<comp ref="C1">
<value>100n</value>
<footprint>Capacitor_SMD:C_0603</footprint>
</comp>
<comp ref="R1">
<value>10k</value>
<footprint>Resistor_SMD:R_0603</footprint>
</comp>
</components>
<nets>
<net code="1" name="GND">
<node ref="U1" pin="8"/>
<node ref="C1" pin="2"/>
</net>
<net code="2" name="3V3">
<node ref="U1" pin="1"/>
<node ref="C1" pin="1"/>
<node ref="R1" pin="2"/>
</net>
<net code="3" name="I2C_SDA">
<node ref="U1" pin="12"/>
<node ref="R1" pin="1"/>
</net>
</nets>
</export>
"""
SEXP = """(export (version "E")
(components
(comp (ref "U1")
(value "MSPM0G3507")
(footprint "Package_QFP:LQFP-48")
(fields
(field (name "MPN") "MSPM0G3507SPTR")
(field (name "LCSC") "C12345")
)
)
(comp (ref "C1")
(value "100n")
(footprint "C_0603")
)
)
(nets
(net (code "1") (name "GND")
(node (ref "U1") (pin "8"))
(node (ref "C1") (pin "2"))
)
(net (code "2") (name "3V3")
(node (ref "U1") (pin "1"))
(node (ref "C1") (pin "1"))
)
)
)
"""
def test_detect_kicad_xml():
assert detect_netlist_format(XML) == "kicad_xml"
assert detect_netlist_format(SEXP) == "kicad_sexp"
assert detect_netlist_format("(kicad_sch (version 20231120)") == "kicad_sch"
def test_parse_kicad_xml(tmp_path: Path):
p = tmp_path / "net.xml"
p.write_text(XML)
parts, nets, fmt = parse_netlist_any(p)
assert fmt == "kicad_xml"
assert parts["U1"].startswith("Package_QFP")
assert ("U1", "8") in nets["GND"]
assert ("C1", "1") in nets["3V3"]
assert ("R1", "1") in nets["I2C_SDA"]
assert validate_netlist(parts, nets) == []
def test_parse_kicad_sexp_and_mpn_fields(tmp_path: Path):
p = tmp_path / "net.kicad_net"
p.write_text(SEXP)
parts, nets, fmt = parse_netlist_any(p)
assert fmt == "kicad_sexp"
assert ("U1", "1") in nets["3V3"]
from backend.pinscopex.parsers_kicad import kicad_part_fields
fields = kicad_part_fields(p)
assert fields["U1"]["mpn"] == "MSPM0G3507SPTR"
assert fields["U1"]["lcsc"] == "C12345"
def test_kicad_mpn_fills_empty_bom(tmp_path: Path):
from backend.pinscopex.graph import build_graph
net = tmp_path / "net.xml"
net.write_text(XML)
bom = tmp_path / "bom.csv"
bom.write_text(
"Reference,Value,Footprint,Manufacturer Part Number\n"
"U1,MSPM0,,\nC1,100n,C_0603,\nR1,10k,R_0603,\n"
)
g = build_graph(
net, bom, tmp_path / "empty_ex", tmp_path / "empty_pat", tmp_path / "empty_mod",
)
assert g.components["U1"].mpn == "MSPM0G3507SPTR"
assert "GND" in g.nets
+91
View File
@@ -0,0 +1,91 @@
"""Reopen a finished project and replace BOM/netlist without deleting it."""
from __future__ import annotations
from fastapi.testclient import TestClient
from backend.services import projects as proj_svc
from backend.services.storage import LocalStorageBackend
_BOM_V1 = (
b"Reference,Value,Manufacturer Part Number\n"
b"U1,MCU,STM32F103C8T6\n"
)
_BOM_V2 = (
b"Reference,Value,Manufacturer Part Number\n"
b"U1,MCU,STM32F103C8T6\n"
b"R1,10k,RC0603FR-0710KL\n"
)
_NETLIST = b"""*PADS-PCB*
*PART*
U1 LQFP48
*NET*
*SIGNAL* GND
U1.1
*END*
"""
def _client(tmp_path) -> TestClient:
from backend.main import app
app.state.storage = LocalStorageBackend(tmp_path)
return TestClient(app)
def test_reopen_then_replace_bom_and_netlist(tmp_path):
client = _client(tmp_path)
meta = client.post("/api/projects", json={"name": "board"}).json()
pid = meta["id"]
resp = client.post(
f"/api/projects/{pid}/upload/bom",
files={"file": ("bom.csv", _BOM_V1, "text/csv")},
)
assert resp.status_code == 200, resp.text
resp = client.post(
f"/api/projects/{pid}/upload/netlist",
files={"file": ("netlist.asc", _NETLIST, "text/plain")},
)
assert resp.status_code == 200, resp.text
storage = client.app.state.storage
proj_svc.update_project(
storage, "local", pid,
status="complete",
total_cost_usd=1.23,
summary={"ERROR": 1, "WARNING": 0, "INFO": 0, "total": 1},
)
prefix = f"users/local/projects/{pid}"
storage.write_json(f"{prefix}/report.json", {"findings": []})
storage.write_text(f"{prefix}/api_logs.jsonl", '{"cost_usd": 1.23}\n')
reopen = client.post(f"/api/projects/{pid}/reopen")
assert reopen.status_code == 200, reopen.text
body = reopen.json()
assert body["status"] == "draft"
assert body["id"] == pid
assert body["has_bom"] is True
assert body["has_netlist"] is True
assert body["total_cost_usd"] == 1.23
assert not storage.exists(f"{prefix}/report.json")
resp = client.post(
f"/api/projects/{pid}/upload/bom",
files={"file": ("bom.csv", _BOM_V2, "text/csv")},
)
assert resp.status_code == 200, resp.text
assert resp.json()["components"] == 2
resp = client.post(
f"/api/projects/{pid}/upload/netlist",
files={"file": ("netlist.asc", _NETLIST, "text/plain")},
)
assert resp.status_code == 200, resp.text
fresh = client.get(f"/api/projects/{pid}").json()
assert fresh["id"] == pid
assert fresh["status"] == "draft"
assert fresh["has_bom"] is True
assert fresh["has_netlist"] is True
assert "RC0603FR-0710KL" in (fresh.get("component_mpns") or {}).get("passive", [])
+59
View File
@@ -0,0 +1,59 @@
from backend.pinscopex.models import (
Component,
ComponentType,
DesignGraph,
Net,
NetType,
PinConnection,
)
from backend.pinscopex.review_fingerprint import (
graph_ic_fingerprints,
skip_unchanged_ics,
)
def _g():
u1 = Component(
reference="U1", value="", footprint="",
component_type=ComponentType.IC, mpn="IC1",
pins={"1": "3V3", "2": "SDA"},
)
r1 = Component(
reference="R1", value="4k7", footprint="",
component_type=ComponentType.RESISTOR, mpn="R",
pins={"1": "SDA", "2": "3V3"},
)
return DesignGraph(
components={"U1": u1, "R1": r1},
nets={
"3V3": Net(name="3V3", net_type=NetType.POWER, pins=[
PinConnection(component_ref="U1", pin_number="1"),
PinConnection(component_ref="R1", pin_number="2"),
]),
"SDA": Net(name="SDA", net_type=NetType.SIGNAL, pins=[
PinConnection(component_ref="U1", pin_number="2"),
PinConnection(component_ref="R1", pin_number="1"),
]),
},
)
def test_fingerprint_changes_when_neighbor_added():
g = _g()
fp1 = graph_ic_fingerprints(g)["U1"]
c2 = Component(
reference="C1", value="100n", footprint="",
component_type=ComponentType.CAPACITOR, mpn="C",
pins={"1": "3V3", "2": "GND"},
)
g.components["C1"] = c2
g.nets["3V3"].pins.append(PinConnection(component_ref="C1", pin_number="1"))
fp2 = graph_ic_fingerprints(g)["U1"]
assert fp1 != fp2
def test_skip_unchanged_drops_changed_refs():
prev = {"U1": "aaa", "U2": "bbb"}
cur = {"U1": "aaa", "U2": "ccc"}
skip = skip_unchanged_ics({"U1", "U2"}, prev, cur)
assert skip == {"U1"}