soldermaskdocs

Design (the model)

design.generate(prompt) turns a sentence into a structured design: a summary, a board size, components, nets, assumptions, open decisions and a list of what the model could not do. The pipeline downstream does not know which model produced it — that is what makes an honest A/B between backends possible.

design.converse(prompt) is the same call for a chat turn. It returns a Reply: the model's words, and a design only when the message changed the board — the turn schema in schema.py wraps the design schema with a reply string and makes design nullable, so a question is answered without a board being redrawn. The app's turns go through it; the command line and the evaluator go through generate().

Backends

Gemini (default)Claude
SelectSOLDERMASK_BACKEND=gemini or nothingSOLDERMASK_BACKEND=claude or --backend claude
CredentialsGEMINI_API_KEY or GOOGLE_API_KEYANTHROPIC_API_KEY, or a logged-in profile (ant auth login)
Default modelgemini-3.8-flashclaude-opus-5
HowInteractions API with the JSON schema enforcedoutput_config with the JSON schema, adaptive thinking, max_tokens 16000, --effort (default high)
Usage recordedinput / output / total tokensinput / output tokens and cost_usd at $5 / $25 per million

--model overrides the model id. The legacy SILIXON_BACKEND variable is still honoured. A missing key is reported in one line rather than a traceback:

No Gemini API key. Export GEMINI_API_KEY (or GOOGLE_API_KEY), or run with --backend claude.

If Gemini rejects a schema keyword, the schema is retried with additionalProperties dropped and the design's unsupported list gains the line schema was relaxed for this backend (additionalProperties dropped) — extra keys are no longer structurally rejected. A weakened guarantee goes where every other caveat goes.

What the model is told

The system prompt states the hard constraints:

  • 2 copper layers; the bottom layer is a ground pour, so keep GND simple.
  • Every part must sit in one of the supported packages — those are the only footprints that exist.
  • Two-terminal parts have pins 1 and 2 (on an LED, 1 is the anode). That is a position, not a pad number: which pad the anode actually is belongs to the ordered part, and LCSC numbers about half its LEDs cathode-first, so resolve.py reads it from the part's own symbol and wires 1 to whichever pad that part calls its anode. For an IC, give the pin's function name exactly as the datasheet writes it — VI, VO, GND, EN, SDA — never a pin number: the number is resolved from the KiCad symbol library, so a name is checkable and a recalled number is not. Multi-unit parts qualify names as A.+, B.-; power pins need no prefix.
  • A pre-built module — an ESP32 or other radio, a radar, a GPS, a LoRa transceiver — is kind: "module" with package FROM_PART: footprint and pin names come from the vendor's CAD data for the exact part ordered, so search must be that part number and its pins are named the way the vendor names them (IO17, 3V3, TXD). The module goes on the board; breaking it out to a pin header instead is a different board. FROM_PART is only for parts no supported package describes.
  • Prefer jellybean parts JLCPCB stocks as basic parts.
  • Decouple every IC. Every power rail needs bulk capacitance.

On radio modules: nothing downstream knows what an antenna is — the placer will not keep a module's antenna end at the board edge, and the ground pour will not stay out from under it — so a board carrying one says that in unsupported.

On choices: a real architectural fork goes in decisions with two or three options, one recommended, and the recommended one is built anyway. On honesty: unsupported is published verbatim next to the board; a board that quietly omits a requested feature is worse than one that reports the omission, and the list must not be padded with hedges either.

The prompt ends with priors — idioms measured from real open-hardware boards.

Priors

A net carries a current_a: the steady current in amps for a power or ground net, null for a signal and null when the load is not knowable from the prompt. It is the one number in a design that is etched into copper — electrical.py sizes the trace to it by IPC-2221 — so the schema asks for a null rather than a guess, and a null falls back to the measured median below.

priors.py carries numbers measured, not chosen: from 86 routed KiCad boards (86,801 trace segments) the median trace width 0.25 mm, clearance 0.20 mm, via 0.60 / 0.30 mm; from 144 schematics the component mix (24.8 % capacitors, 15.4 % resistors, …), 5.1 capacitors per IC, 100nF as the most common decoupling value and 1uF as bulk. Those numbers drive the router's design rules.

priors_blocks.json holds block idioms: for each IC symbol seen in at least four instances across at least two projects, which passives are drawn within 40 mm of it and how often (MCP1824: C:100n 83 %, C:1µ 67 %). mine_blocks.py produces it from a corpus of .kicad_sch files, using proximity on the sheet as a proxy for connectivity. The top six blocks are appended to the system prompt as what actually gets drawn alongside these parts.

The schema

The design is validated against a deliberately narrow JSON schema. The model does not invent footprints, part numbers or pad geometry; it describes intent and the pipeline resolves it against real data. Every field is documented in design.json. The search field is the important one: it is what to buy — a manufacturer part number when the model is confident it exists, otherwise a short description of the physical part — and it is searched against live stock. A net name there finds nothing and the part is dropped.

On this page