eitri
A local AI CAD agent. Describe a part in plain language; eitri models it as a parametric build123d (OpenCASCADE) model, builds it, measures it against your request, looks at it, fixes what fails, and hands you STEP / STL / 3MF / GLB files. Then keep going in a Fusion-style 3D editor in your browser, by hand or by chat: both edit the same model.
Install · Limitations · License
A real recorded session (the Wall racket holder demo, which anyone can replay without a key), played back in the editor: the plan, eitri writing the model, the failed builds, the fix, all 10 checks passing. 3 min 17 s of work in 18 s.
Every build comes back to the agent as a geometry report (bounding box, volume, every hole with diameter,
depth and through-or-blind, thinnest wall and knife edges, interferences and clearances between parts, the
model's own verify() checks) and a 4-view review image. The agent cannot "forget" to check its work:
the checks are part of the build, and a failing check is fixed in the geometry, never deleted.
Quickstart
curl -fsSL https://eitri.io/install | sh
eitri
The installer (scripts/install-remote.sh; read it first if you like) installs
uv if needed, then uv tool install --python 3.12 eitri-hardware: Python 3.12, the CAD
kernel and Claude Code (the binary inside Anthropic's claude-agent-sdk package, downloaded from PyPI at install
time; eitri does not ship it). It checks the install (eitri doctor) and tells you what to run. It downloads
about 390 MB and uses about 1 GB of disk; that takes 1 to 5 minutes depending on your connection, and the
first eitri doctor takes up to a minute more. No sudo: everything goes under your home folder.
EITRI_NO_UV_INSTALL=1 stops it from installing uv, EITRI_NO_PATH_EDIT=1 leaves your shell profile alone.
eitri opens http://127.0.0.1:8765 in your browser.
Other ways in: already have uv? uv tool install --python 3.12 eitri-hardware. From a clone:
git clone https://github.com/SiddhuYen/eitri eitri && cd eitri && ./install.sh (the same installer, pointed at
the checkout). The package is named eitri-hardware (the name eitri was taken on PyPI); the command and the
Python import are both eitri. Installed eitri 0.5.6 or older? It was called eitri-cad: run the installer again,
or uv tool uninstall eitri-cad && uv tool install --python 3.12 eitri-hardware (projects and settings are kept).
- Update:
eitri updateupgrades through uv (eitri update --checkonly looks). eitri does not look for updates on its own; see the privacy notes below. - Firmware projects:
eitri setup firmwareinstalls arduino-cli and the Arduino AVR core (--esp32adds the ESP32 core;--dry-runshows the commands first). It asks before installing arduino-cli itself.
Setting it up with an AI agent? Point it at AGENTS.md: exact commands and expected output for every step.
See it in action without a key. The home page opens with three demo projects (a 43-part V8 engine kit, a wall racket holder, a marble maze) and the real eitri sessions that made them. "Watch replay" plays a session back in the editor (the chat, every tool call, the model stepping through each revision, the checks going from red to green) with no API calls. Orbit them, edit them, export them: everything but chatting with Claude works without a key.
In your own agent: the eitri plugin
eitri also runs as one plugin inside the agent you already use (Claude Code first; Codex and Cursor too), on that agent's own subscription. In this mode eitri never calls Claude: your agent writes the model files and calls eitri's tools, so it needs no API key and costs eitri nothing.
curl -fsSL https://eitri.io/install | sh -s -- --plugin # install eitri, register the plugin, sign in to eitri.io
The plugin needs eitri 0.7.0 or newer (the release that adds eitri mcp and eitri plugin; check with
eitri --version). On 0.6.0 or older, run eitri update first: older versions have no plugin server, and the plugin's
launcher says so instead of starting.
Or, with eitri already installed: eitri plugin install (it registers the plugin in every agent app it finds) and
eitri plugin login. In Claude Code you can also add it by hand: /plugin marketplace add SiddhuYen/eitri, then
/plugin install eitri@eitri (the local tools still need eitri installed: uv tool install eitri-hardware).
The plugin brings two MCP servers and a set of skills:
- Combined workspace (the default):
eitri-combined, eitri.io's remote server (https://eitri.io/mcp). It holds a whole machine in one project: structure, wiring, motion, air and firmware, built together, with the cross-domain checks (servo torque against the arm, supply current against the servos, wire gauge against that current...). Electronics (breadboard circuits) is not part of combined projects yet: a circuit is designed with the local electronics domain, and is not checked against the combined project. It needs a personal token:eitri plugin loginopens eitri.io/account, where any GitHub sign-in can make one, and saves it in eitri's config folder (no shell file is edited). In combined mode your model files go to eitri.io, are kept there as your projects, and are used to train eitri's models unless you turn that off on eitri.io/account. - Six local single domains:
eitri(eitri mcp, on your computer): CAD, wire harnesses, mechanisms, pneumatics, firmware and electronics, with the same checks as the app. Projects go in~/eitri-projectsunless you name another folder. The viewer opens in your browser at the first build and follows every rebuild. - Skills:
designpicks the workspace: combined by default, one local domain when the task clearly fits one (a bracket, an enclosure, one harness, one sketch...). It says which one it picked.combinedruns the loop (reference → write files → build → fix until every check passes), plus one skill per local domain.
What it does compared with the app:
eitri app (eitri) |
eitri plugin | |
|---|---|---|
| Who drives | eitri's own agent, on your Anthropic API key | Your agent (Claude Code, Codex, Cursor), on its subscription |
| Domains | the six single domains | the six single domains locally, plus the combined workspace on eitri.io |
| Editor | the full browser editor with chat, direct edits, demos and replays | the same viewer and editor, following your agent's builds; you chat in your agent |
| Reminders while checks fail | the agent loop's nudges | every tool result ends with a PROJECT STATUS: what fails, what is unbuilt, what to do next |
| Network | Anthropic's API only | the local server: nothing; the combined server: eitri.io |
eitri plugin status shows what is installed, eitri plugin logout forgets the token, and eitri plugin uninstall
removes the plugin from every agent app (your projects stay). The plugin sends no telemetry.
Per agent: Claude Code gets a real plugin (eitri@eitri: skills /eitri:design, /eitri:combined, /eitri:cad
...); Codex gets both servers in ~/.codex/config.toml and the skills in ~/.agents/skills/eitri-*; Cursor gets
both servers in ~/.cursor/mcp.json (--cursor-rules <project> also adds a project rule).
Your API key, costs, privacy
eitri uses your own Anthropic API key; requests are billed to it by Anthropic. Paste it on the home page
or run eitri key set (it is checked with one free API call and stored in your user config folder,
readable only by you, never in a project). ANTHROPIC_API_KEY in the environment works too.
- Cost, measured: about $0.24 per request on average across the 22-task benchmark (range $0.07 to $1.10 per task). Long design sessions cost more: the recorded V8 engine session in the demos cost $3.21.
- API key only: eitri drives Claude Code with
--bareand your key, so a Claude.ai subscription logged in on the same machine is never used. (The plugin is the other way round: there your own agent runs on its own subscription and eitri never calls Claude.) - Local only, no telemetry. The editor, the CAD kernel and your projects stay on this computer; the server answers only this machine. The only network calls eitri makes on its own are to Anthropic's API, with your key (Claude Code runs with its telemetry, error reporting and update checks turned off). Fonts, icons and three.js are bundled; the pages fetch nothing from the internet.
- Update checks only when you ask. eitri asks pypi.org for the newest version (one GET of
https://pypi.org/pypi/eitri-hardware/json, nothing about you or your projects) only when you runeitri updateoreitri doctor --online. If you want a reminder,eitri update --auto onlets eitri make that same request when it starts, at most once a day, and print one line when there is a newer version (eitri update --auto offturns it off again).eitri setup firmwaredownloads arduino-cli and board packages from Arduino and Espressif, and it only runs when you start it.
Export for 3D printing
Print in the editor's toolbar (or eitri print <project>) turns a design, assembly or not, into the
files a slicer wants:
- Decompose: every solid part is listed; bought parts (screws, nuts, washers, bearings, inserts, anything with a metal material) are listed but left out by default, with a toggle.
- Orient: each part gets its best of six orientations, scored on its surface triangles (overhangs on curved faces count) with slender towers penalised; overhang risk is reported, and walls thinner than your threshold (default 0.8 mm, two perimeters of a 0.4 mm nozzle) are flagged with the measured value.
- Plate: parts are packed onto as many plates as your bed (default 220 x 220 mm) needs.
- Export: each part and each plate as STL and 3MF, plus one zip. Every mesh is checked before it is written (closed, manifold, consistently wound, the solid's volume) and every file is read back and checked again; a part that fails is not written, and the dialog says which and where.
On the V8 demo: 43 printable parts, 2 plates of 220 x 220 mm, all 43 written and verified in about 3 s after a 3 s plan; 12 parts are flagged with walls under 0.8 mm (the model's own report flags them too).
| Plate 1 | Plate 2 |
|---|---|
Use
eitri (no arguments) opens the local website: your projects (in ~/eitri-projects, or
eitri web <folder>), the demos, and a box to describe a part. Press Create with eitri and the
project opens in its 3D editor while eitri models it; its steps, the review renders it checks and its
replies stream into the chat beside the model. Keep talking to change it, click geometry first to point at
something, paste or drop a photo or sketch to attach it, press Stop (or Esc) to halt a turn (eitri keeps
the conversation) or New chat to start over; every direct-modeling tool works alongside. Start blank,
Import a file… (STEP, IGES, STL, OBJ, 3MF, PLY or a model .py) and Open a folder… start projects
without a prompt. Each project keeps its conversation, so it is still there after a restart.
Every page shares one layout: a top bar with where you are (eitri / wires / Quad power harness), a rail with
the six tools (CAD, Wires, Motion, Pneumatics, Firmware, Electronics; each opens that tool's projects) and your project list
at its foot, and a status bar with the tool, the core version, the project folder and the build's verdict. In a
project the model (or a domain's diagram) fills the canvas; the inspector (checks, parameters, the selection,
revisions) and the chat sit in panels docked on the right. The chat collapses to its header, the panels fold to
a strip for more room, and the Export menu at the top of the inspector lists every file the project can be
downloaded as. Dark by default, light when the system is.
The 43-part V8 demo: one prompt → eitri writes a 398-line model → all 43 checks pass (a recorded 23-minute session, replayed and compressed to 18 s).
You watch eitri work as it happens: its thinking (readable summaries; each finished thought stays as an expandable "Thought for N s"), its reply typing out, the model code streaming into a panel over the 3D view, each step with a spinner and a timer and, for a build, its stage (building → checking → rendering), and the new geometry the moment it exists, before the checks and the review render finish. The checks card at the top of the panel shows the verdict of every build and what changed since the last one ("fixed: thinnest wall", "1 warning resolved"). The CAD engine and the agent start loading when you open a project, so the first build and the first reply don't wait for them.
The terminal works too:
eitri chat ~/parts/knob # the REPL (also: eitri <folder>)
eitri chat -p "a 30 mm knob for a 6 mm D-shaft" # one shot, prints the file paths
eitri build knob.py --set diameter=35 # rebuild + report, no LLM
eitri view knob.py --section XZ:0 # extra renders, no LLM
eitri build vendor_part.step # report + review image of any existing STEP
eitri print ~/parts/knob --bed 250x210 # 3D-print export, no LLM
eitri bom ~/eitri-projects/quad # a wire-harness project's BOM and cut list, no LLM
eitri serve ~/parts/knob # just the viewer, no chat
eitri doctor # check the install
In the REPL, talk to it like Claude Code. Follow-ups edit the same model ("make the holes M5", "add a lid").
Slash commands: /show (browser viewer), /set width=45 (change a parameter and rebuild, no LLM), /undo,
/history, /params, /export stl 3mf, /view section XZ:10, /image sketch.png (attach a reference image
to your next message), /model, /notes.
Wire harnesses
eitri also designs wire harnesses and cable assemblies, with the same loop: eitri writes a parametric Python
model file, a deterministic kernel builds it, and every build returns a report of pass / warn / fail checks and a
review image that eitri has to fix its work against. Mechanical CAD is untouched; a project is a harness project when its
model file builds a Harness (it starts with from eitri.harness import *). Nothing extra to install: the kernel,
the diagram layout and the rasteriser are our own code (no WireViz, no Graphviz), in plain Python with Pillow.
from eitri.harness import *
PARAMS = dict(arm_mm=180)
def build(p):
h = Harness("Quad power")
batt = h.add(Connector("BATT", "XT60", pins=["+", "-"]))
esc = h.add(Connector("ESC1", "XT30", pins=["+", "-"]))
h.net("VBAT", current_A=20, voltage_V=14.8) # what the checks grade the wires against
h.connect(batt["+"], esc["+"], Wire(14, "RD", p.arm_mm), net="VBAT")
h.connect(esc["-"], batt["-"], Wire(14, "BK", p.arm_mm))
return h
Every build computes: connectivity (unconnected pins unless declared NC, several wires on one pin, duplicate wires,
dangling splices, nets shorted at a splice), ampacity (each wire against a chassis-wiring AWG table, FAIL over the limit,
WARN within 20%; each connector pin against its catalog rating), voltage drop (V = 2*K*I*L/CM, K = 12.9 for copper at 75 °C, per net against
max_drop_pct, summed along the worst source-to-load path) and consistency (one gauge and one colour per net, shields
terminated), plus the model's own verify(). It writes a wire-harness diagram (SVG, and the PNG review image with the failing
wires outlined red), a BOM (connectors with part numbers, crimp terminals inferred from connector type x populated pins,
wire by gauge and colour with a 10% service loop, splices, labels), a cut list (cut length, strip lengths and terminations
at both ends of every wire) and a WireViz-compatible YAML (interop only), all in out/.
In the browser a harness project opens a harness editor instead of the 3D view: click a wire to change its gauge, colour,
length or net; click a connector to rename pins, change its type from the catalog, add or remove pins; drag connectors around;
click a failing check and the wires it is about light up. Changes are direct edits (EDITS in the model file, the same path as CAD
edits) and positions are LAYOUT hints, so eitri sees your changes and carries on from them. A demo to start from:
eitri build eitri/demos/quad-power/quad_power.py (battery XT60 -> distribution splice -> 4 ESCs at 20 A + a flight controller).
eitri build harness.py # the checks, the diagram, BOM and cut list (no LLM)
eitri bom ~/eitri-projects/quad # the bill of materials; --cutlist adds the cut list, --csv prints CSV
The API, the connector catalog and three worked examples are in eitri/agent/harness_ref.md; the agent reads them as its
reference when a project is a harness (and calls harness_reference() when a blank project turns out to be one).
Mechanisms, pneumatics and firmware
Three more domains work the same way as harnesses. eitri writes a parametric model file, a deterministic kernel checks every
build, and the browser opens a domain editor instead of the 3D view. A project's domain comes from its model file's import
(from eitri.mechanism import *, from eitri.pneumatics import *, from eitri.firmware import *). Each domain knows only its own
model. The agent reads that domain's reference (eitri/agent/<domain>_ref.md) and rules, and gets a <domain>_reference() tool for
a blank project that turns out to be one. Edits made in the editor are EDITS and positions are LAYOUT hints, as for CAD and harnesses.
Mechanisms are links, joints (revolute or prismatic, with limits), actuators (stall torque, speed, travel, mass), point masses and targets. Every build checks:
- Reach. Each target is reachable within the joint limits (bounded inverse kinematics), to the target's tolerance.
- Torque. The static gravity load on each joint at its worst sampled pose is graded against the actuator's rated torque. The rated torque is estimated as half the stall torque when the datasheet gives none.
- Speed and limits. Required joint speeds are within the actuator's, and the joint limits fit inside its travel.
- Mass. A mass rollup per link and in total.
It writes a joint/torque table, a mass table and a kinematic diagram (SVG and PNG). The editor has a parameter panel and the
kinematic diagram, where you click a joint to edit its limits or actuator. The demo is a pan-tilt camera mount. Its heavy
configuration fails the tilt torque check on purpose. The reference has a 3-DOF arm and the pan-tilt.
from eitri.mechanism import *
def build(p):
m = Mechanism("Pan-tilt")
m.link("base", fixed=True)
m.link("yoke", length_mm=30, mass_g=12, direction=(0, 0, 1))
m.link("cradle", length_mm=35, mass_g=10)
servo = Actuator("SG90", stall_kgcm=1.8, speed_deg_s=500, travel_deg=180, mass_g=9)
m.joint("pan", "base", "yoke", axis=(0, 0, 1), at=(0, 0, 0), limit_deg=(-90, 90), actuator=servo)
m.joint("tilt", "yoke", "cradle", axis=(0, -1, 0), limit_deg=(-30, 60), actuator=servo)
m.mass("camera", "cradle", 90, at=(35, 0, 0))
m.target("ahead", (35, 0, 30))
return m
Pneumatics are ISO 1219 circuits: supply, FRL, valves (3/2, 5/2, 5/3), flow controls, cylinders (single- or double-acting), tubes and silencers, connected port to port. Every build checks:
- Force. Cylinder force at the pressure that reaches it, after a 10% friction allowance, against the load in both directions.
- Speed and flow. ISO 6358 flow (
C,b) through the whole supply and exhaust path of every stroke, against the required speed. - Air. Consumption per cycle and per minute, including dead volume in the tubes, against the supply.
- Pressure drop. The drop across each tube and component on every stroke's path, at the flow that stroke needs.
- Circuit validity. A supply exists, every cylinder chamber is fed from it through a valve and can exhaust in some valve position, every valve has P supplied and its work ports connected, and no line is dead-headed.
A component or tube without datasheet C/b is graded with a conservative estimate and the check warns. It writes a BOM
(Markdown and CSV), a cylinder table and an ISO 1219-style schematic (SVG and PNG). The editor shows the component list and the
schematic: edit a cylinder, valve or tube, and drag components around. The demo is a double-acting clamp with meter-out speed
control. It reports one warning on purpose, because its tubes carry no datasheet flow figures. The reference has a single-acting
clamp and a double-acting cylinder with speed control.
Firmware is a board (Arduino Uno, Arduino Nano, ESP32), a pinout (each pin with a direction, a role and a name), peripherals
(servo, PWM motor driver, limit switch, serial) and the source files eitri writes. Every build generates eitri_pins.h from the
pinout, so the sketch uses PIN_<NAME> constants instead of numbers. Then it checks:
- Compile. The sketch is compiled headless with arduino-cli for the board. A compile error is a failed check at its file and line.
- Pinout. Every pin exists on the board and can do its role (PWM, ADC, interrupt, input-only, forbidden ESP32 flash pins, ADC2 with Wi-Fi). No pin is assigned twice, and the Serial pins stay free while Serial is in use.
- Usage. The sources use each pin exactly as declared (
pinMode,digitalWrite,analogRead,analogWrite, servoattach), and use no pin that is not declared.
It writes the pinout table (CSV and Markdown), a pinout diagram, the header, the sketch folder and the compile log. The editor
has a pinout table you can edit (pin, direction, role, name, board), read-only code panes and the compile result. eitri never
uploads to a board. You flash it yourself from the sketch folder in out/. The demo and the reference example are a servo sweep.
arduino-cli is a system dependency of the firmware domain only. Without it, every firmware build has a failed
compiles (arduino-cli) check that says "compiler unavailable" and how to install it; the other checks still run. Install it once:
eitri setup firmware # arduino-cli (asks first), arduino:avr and Servo, then a test compile
eitri setup firmware --esp32 # also the ESP32 board package and ESP32Servo
eitri setup firmware --dry-run # only print the commands
It runs the same steps you would run by hand:
brew install arduino-cli # or: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
arduino-cli core update-index
arduino-cli core install arduino:avr # Uno and Nano
arduino-cli lib install Servo # if a sketch uses <Servo.h>
# ESP32 (optional):
arduino-cli config init # once, if there is no arduino-cli.yaml yet
arduino-cli config add board_manager.additional_urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core update-index
arduino-cli core install esp32:esp32
arduino-cli lib install ESP32Servo # if an ESP32 sketch drives a servo
eitri finds arduino-cli on PATH, at the path in EITRI_ARDUINO_CLI, or in ~/.local/bin, /opt/homebrew/bin or /usr/local/bin.
eitri build eitri/demos/pan-tilt/pan_tilt.py # 11 pass, 0 warn, 0 FAIL
eitri build eitri/demos/air-clamp/air_clamp.py # 20 pass, 1 warn, 0 FAIL
eitri build eitri/demos/servo-sweep/servo_sweep.py # 11 pass, 0 warn, 0 FAIL with arduino-cli and arduino:avr installed
eitri bom ~/eitri-projects/clamp # pneumatics: the bill of materials (--csv)
Electronics (breadboard and module level)
The sixth domain: a circuit as code. A model file that starts from eitri.electronics import * builds a Circuit: a board (Arduino
Uno, Nano, ESP32 DevKitC), supplies, regulators, breakout modules from a catalog (PCA9685, BME280, SSD1306, HC-SR04), resistors,
LEDs, diodes, capacitors, switches, MOSFETs, transistors, relays, motors, solenoids and servos, and the nets that join them. It is
breadboard and module level only: no PCB layout, no footprints, no KiCad export (the netlist CSV is the hand-off).
Every build classifies each net (a rail at a voltage, a ground, a pin a board drives, a transistor's switched low side), solves the
currents through the two-terminal parts between them, and grades the result. Series strings (a pin, a resistor and an LED to
ground; a rail, a relay coil and a MOSFET's drain) are solved in closed form; branched networks (two LEDs sharing a resistor, a
divider feeding two loads) by nodal analysis, written for eitri, with an ideal-diode iteration. The two agree on every series string. Every limit comes from a datasheet, standard or vendor page whose URL, words and fetch date
are recorded in eitri/electronics/sources.py; each check names its source, and each build writes the ones it used to
<name>.sources.md. Where sources disagree the conservative figure is used and both are quoted: the lower figure for a
limit, the higher for a draw or load (an MG996R draws 2.5 A at stall, not 1.4 A); a figure no document gives is marked ASSUMPTION. The checks:
- Current. Per GPIO (Uno / Nano 20 mA, ESP32 28 mA) and per chip (ATmega328P 200 mA); module outputs; every LED against its DC
maximum (Kingbright 5 mm catalog parts by
part=, a typical part by colour, or your LED's own figures; LEDs sharing a resistor are graded for current hogging); every resistor's I²R against its rating; nothing without a current limit (an LED with no resistor, a switch from a driven pin to ground). - Power. No rail tied to ground or fed by two supplies; every supply pin within its part's range (a BME280 on 5 V fails); the budget of every source (a board's 5V pin on USB or VIN, its 3V3 pin, a supply's rating, a regulator's rating), servos at stall; regulator dropout and junction temperature; VIN range; one common ground for everything that exchanges signals.
- Logic levels. No input above its absolute maximum (a 5 V sensor's output into an ESP32 pin fails); the driver's guaranteed VOH against the receiver's VIH; no floating inputs.
- I2C (NXP UM10204). Pull-ups present and between Rp(min) and Rp(max) for the clock and the bus capacitance; unique addresses. On an Uno or Nano, Wire's internal pull-ups to 5 V against any 3.3 V device on the bus (a warning, with the fixes).
- Inductive loads. A flyback diode across every coil a transistor or pin switches, the right way round and rated for the coil current.
- Switching. A MOSFET's gate drive against the VGS its RDS(on) is specified at (an IRF540N from a 5 V pin fails) and its heat; a transistor's base drive for saturation; relay contacts against their rating.
It writes a schematic (SVG and PNG: rails and grounds as flags, every other net routed), a BOM, a netlist (CSV and Markdown) and the
sources file. The editor shows the schematic beside a parts table and a nets table: edit a value, move a pin to another net, add a
resistor, LED, diode, capacitor or switch, drag parts. The demo eitri/demos/uno-arm-circuit is an Uno driving four MG996R servos
through a PCA9685, a status LED, an end-stop and a relay-switched gripper solenoid. Its bad configuration powers the servos from
the Uno's 5V pin and leaves out the relay's flyback diode, and fails exactly those two checks.
from eitri.electronics import *
def build(p):
c = Circuit("Status LED")
uno = c.board("UNO", "uno")
c.net("GND", uno["GND"])
r, led = c.resistor("R1", 330), c.led("LED1", part="WP7113SRD/D") # (5 V - 1.85 V) / 330 = 9.5 mA
c.net("LED_A", uno["D13"], r["1"])
c.connect(r["2"], led["A"])
c.connect(led["K"], "UNO.GND")
return c
eitri build eitri/demos/uno-arm-circuit/uno_arm_circuit.py # 31 pass, 0 warn, 0 FAIL
eitri build eitri/demos/uno-arm-circuit/uno_arm_circuit.py --config bad # 28 pass, 0 warn, 2 FAIL (on purpose)
eitri bom eitri/demos/uno-arm-circuit/uno_arm_circuit.py # the bill of materials (--csv)
The servos run from a 5 V 15 A supply: four MG996R at their 2.5 A stall and the relay coil draw 10.07 A, 67 % of it.
Direct modeling in the viewer (Fusion-style)
A new project starts as a blank design, like Fusion: eitri ~/parts/thing opens
an empty grid. Press S and click the grid to sketch on the ground plane and extrude
the first part, drop a primitive (B), or describe the part in the chat and let
eitri write it - every later edit works the same on either.
Thirteen seconds of the editor on the V8 demo: combine, mirror, align and mate faces, form bodies, holes sized from fastener tables, export to STEP / STL / 3MF / drawings / G-code.
The viewer is a modeling tool, not just a display. Every face, edge and part is pickable (hover shows what it is: "flat face, normal (0,0,1), 2361 mm²", "hole/bore Ø5", "convex edge 90°"). The toolbar is laid out like Fusion's:
Create
- Sketch (S) - click a flat face: the view turns to face it, a grid appears,
and you draw with clicks that snap to the grid, to the face's corners/midpoints
and to the sketch's own points. It is a real constrained sketcher: lines and
polylines (L), rectangles (2-corner R, centre, 3-point), circles (centre C,
2-point, 3-point), arcs (3-point A, centre), ellipses (E), polygons (G,
inscribed/circumscribed, any sides), slots (O), splines, points (P) and text (T),
plus construction geometry (X). Joins and near-level lines get Fusion's automatic
constraints (coincident, horizontal, vertical); the constraint row adds
coincident, point-on, H/V, parallel, perpendicular, tangent, equal, concentric,
collinear, midpoint, symmetric and fix; the Dimension (D) tool measures a
line, arc/circle, two points, two lines (angle) or a point and a line and makes
it driving - click a dimension label to change it. The panel shows the degrees of
freedom left ("fully constrained"); a change the constraints cannot meet is
refused with the conflicting constraint named. Drag any point or curve with
the Select tool (V) and the solver keeps the rest consistent (a corner of a
rectangle stays a corner; a dimensioned size does not change). Modify tools:
trim, extend, split, sketch fillet/chamfer, offset, mirror, rectangular/circular
pattern, project 3D edges/faces into the sketch, SVG import.
Then extrude (drag or type; outward adds, inward cuts; extents: a distance,
to the next face, to a face you click, or through all; symmetric, a
taper angle, a thin wall thickness, flip), revolve about the sketch's u/v
axis, a model axis or a straight edge you click, loft through scaled/rotated
sections at offsets (add as many as you like), sweep along edges, or a rib:
an open chain extended in-plane until it meets the part, a thickness across the
plane (sketch it on an offset plane inside the part). "More…" adds a mirror and a
pattern ("3x2 10x8" or "6 @ 360") of the profile, and a sketch-plane offset. Esc
restores your view.
A quick shape (rectangle, circle, …) that you do not edit is stored as that
shape; anything you edit is stored as the full sketch (entities, constraints,
dimensions), which the agent reads, edits (
sketch_op) and folds into code. - Hole (H) - M2…M12 clearance (close/normal/loose) or tapped (tap drill), counterbore / countersink, depth, pattern and mirror; click a flat face to place it, or a round face for a radial hole. Hole labels can be clicked to change diameters.
- Boss - a cylindrical boss or screw standoff (plain Ø + hole, heat-set insert, self-tapping, clearance; height, hole depth) standing where you click on a flat face.
- Primitive (B) - box, cylinder, sphere, torus, cone, hexagon, wedge (ramp) and coil (spring), picked from a grid of shapes and drawn to size: press on the ground grid or on a flat face, drag the footprint (live ghost and dimensions), release, move to set the height, click or Enter; type a number at any point for an exact size ("30", "24x16"), Esc cancels. A plain click places it at the panel's size. A new part, or joined to / cut from the face it is drawn on.
- Insert (W) - a screw (socket/button/countersunk/pan/hex head, any length), hex nut, washer or ball bearing from the fastener library, placed by clicking a hole: the screw's head sits on the surface with the shank down the hole, a nut or washer sits on the face, a bearing goes into the bore. They become parts (steel) the interference check watches.
- In a sketch, sweep along edges: draw the profile on a face the path leaves at a right angle, choose sweep, click the edges of the path - pipes, rails, channels.
Modify
- Push/Pull (P) - drag a face along its normal with a live ghost (0.5 mm snaps; Alt for free). Flat faces move (several selected faces move together); holes, bosses and spheres change radius.
- Surfaces - from a sketch, make a surface body instead of a solid: planar, extruded (an open chain becomes a sheet, a closed outline a tube), revolved, swept along edges or lofted through sections. Surface parts get their own actions: offset, extend (all free edges, or the edges you select; natural or tangent), trim by a plane, a face of another part or a sketch profile, split, stitch several sheets into one, patch an opening (G0, or G1 tangent to the neighbours), thicken into a solid and to solid (a closed surface). The report lists a surface's free edges and the continuity (G0 crease / G1 / G2) of its internal edges.
- Lip - a raised lip or a groove along the inner or outer edge of a wall's rim (click the rim face; width, height): enclosure lids that locate.
- Fillet (F) / Chamfer (C) - click edges (shift adds), drag or type; an end radius makes a variable fillet, a second size or an angle an unequal chamfer; double-click an edge to select its whole tangent chain (a rounded pocket's outline in one go).
- Shell (E) - click the faces to leave open, set the wall; or hollow a part.
- Combine (J) - join / cut / intersect two parts (keep the tool or not).
- Split (X) - by XY/XZ/YZ at an offset, or by a selected face's plane; keep one side or both (the other half becomes a new part).
- Mirror (I) - about XY/XZ/YZ through the origin or the part's centre, or about a face's plane; joined or as a new part.
- Pattern (N) - rectangular or circular copies of a part (joined or as
parts
name_2…). Holes and sketch features are patterned from their own timeline form. - Scale (K) - uniform or per-axis, about the part's centre.
- Parts can be renamed, hidden, isolated, deleted (Delete key), made translucent (opacity slider - see inside a housing) and given a material from their panel. Assembly drawings carry a parts list with materials and masses.
- Thread (T) - on a hole or a shaft: ISO size, length, cosmetic (reported) or modelled (a real helix from bd_warehouse).
- Move (M) - drag the gizmo arrows (R rotate, T translate).
Edit geometry (D) - the solid-modeling version of Fusion's Edit Form: the corners of the model get handles you drag directly (the orbit stays out of the way; the faces around the corner deform live as you drag), or click corners, edges or faces (a filter picks which) and drag the gizmo, or type a move. Corners and edges move anywhere and the flat faces around them follow - a face whose corners stop being coplanar becomes a smooth patch, so a box corner pulled up simply stops being a box. The build works out what each corner can do, so a handle never fails on you: a round handle moves; a diamond handle sits on a corner that cannot move by itself and instead moves the hole it belongs to, or pushes/pulls a face meeting there (a short message says which); a corner of a face with a hole in it slides within that face; a corner nothing can be done with gets no handle. A flat face can be moved freely and tilted (R on the gizmo, or type an angle), with its neighbours extending or trimming to meet it; holes and bosses translate. Delete removes the selected faces and heals the body (a fillet, a hole, a boss, a pocket - the captive floor of a blind hole comes along automatically). Shift-drag on empty space window-selects many corners/edges/faces at once; dragging a cylinder's rim pushes the face it bounds. When the kernel cannot do something, the editor says why in a plain sentence and offers to hand the job to eitri with the selection filled in. Edits are optimistic: the change stays on screen while the model rebuilds (the revision chip pulses), further edits queue behind it, and a rebuild never blocks the editor. Existing fillets are live too: drag a round-over with Push/Pull or type a new radius (chamfers: type a new size); Split face by a plane lets you push just part of a face; Separate turns a part with several disconnected solids into one part each.
Form - Fusion's sculpting environment, as a solid: Form in Create makes a smooth
body from a quad control cage (a box, sphere, cylinder or torus with the segment counts
you choose), smoothed by Catmull-Clark subdivision (1-3 levels) and converted to a real
B-spline solid (one patch per cage quad, sewn) so every other tool - fillets, holes,
booleans, drawings, STEP - works on it. Select a form part and press Edit cage: the
cage appears over the body with handles at every vertex, face centre and edge midpoint.
Click them (shift adds), drag the gizmo or type a move; Extrude face grows a bump
or a limb from a cage face (with a scale for the new face), Push moves a face along
its normal, Insert loop splits a ring of quads for more control, Crease keeps an
edge sharp through subdivision, and symmetry mirrors every edit across a plane.
Each action is a form_edit op in the timeline; the cage is stored with the model so
the agent can sculpt the same body (form, form_edit).
Assemble - Align (G): click a face on the part to move, then the face to mate with: flat-to-flat (with offset and flip) or concentric for round faces.
Sheet metal - in a sketch choose sheet metal: base flange (thickness, inner bend
radius, K-factor) to start a sheet part; select an edge of a sheet face and Flange…
(height, angle; an edge of the top face bends up, of the bottom face down; flanges chain
onto flanges), Contour flange… (a chain of bends from one edge, typed as
length@angle legs - 20@90, 15@-90 is a hat step, 20@90, 15@90 a C return; a
negative first angle starts towards the other face) or Hem…; tab and cut
sketches on a sheet face are carried into the flat pattern. The part panel shows the sheet rules and the flat area, lays out the flat
pattern beside the part (bend lines drawn, bend allowance = angle × (r + K t)) and
downloads it as DXF with the bend lines on their own layer.
Assemble - Align (G) mates faces; Joint makes an assembly joint between two parts by clicking a face on each: rigid, revolute (a pin in a hole), slider, cylindrical, pin-slot, planar or ball, with limits, as-built or mated. The Joints panel drives every joint with a slider (the part and everything jointed onto it move live), animates it between its limits, and writes the value into the timeline as a drive edit. Motion links tie two joints by a ratio. Parts can be grouped into components and moved together; Insert also places a component from a STEP file or another eitri model. Assemblies get a bill of materials (identical parts counted, CSV download) and numbered balloons on the drawing's isometric view.
Parameters & data - PARAMS values may be expressions of other parameters
(depth="width / 2", height="max(depth / 4, 6)"), and CONFIGS = {"small": {...}, "large": {...}} names variants: pick one in the Parameters panel, eitri build --config large, or ask the agent. The report gives each part's mass properties
(principal moments of inertia) when a material is set. Revisions can be
compared: the earlier revision is overlaid in red on the current model and the
panel lists which parts were added, removed or changed (volumes, bounding boxes) with
the source diff; the agent has compare_revisions too. Display: material
appearances (brass, steel, aluminium, PLA, acrylic… with studio reflections), a lens
(focal length) slider, saved named views, and Render PNG… for a high-resolution or
transparent image; eitri view --turntable 36 (or the agent's view tool) writes an
animated turntable GIF.
Inspect - Measure (L): distance with ΔX/ΔY/ΔZ, minimum distance between entities, angle between faces; snaps to vertices and midpoints. Interference lists every pair of parts that overlap, with the volume, and shows the overlapping region in red on the model. Section planes, exploded view, hole labels, ortho (O), look-at (A), seven named views.
Timeline - every direct edit is a chip along the bottom, in order. Click one to select it (it is then attached to your chat message as "feature #3"), double-click to open its Edit feature form (every parameter of the op, e.g. a pocket's width/height/position/depth, a hole's size/head/pattern), Edit sketch to reopen the feature's sketch on its face as a constrained sketch (a quick rectangle becomes four lines with coincident/H/V constraints and two dimensions) and drag its points and curves, change dimensions, add or remove constraints - each drag release and dimension change rebuilds; a hole's sketch is its circle (drag the centre to move it, the rim to resize) - and from the panel suppress, move it earlier/later, pattern it or delete it. A change that cannot build is refused with the reason and rolled back.
Meshes & formats - open an STL/OBJ/3MF/PLY mesh or an IGES file as a model
(eitri part.stl), or insert one as a component: meshes are repaired (holes filled,
normals fixed), dense scans decimated to a triangle budget (decimate on the insert,
--set decimate=20000 on a mesh model; anything over 200k triangles is reduced to 100k
automatically) and converted to a faceted solid with coplanar facets merged, IGES
surfaces are sewn into solids. Export adds OBJ and IGES. Sketches import DXF
(lines, arcs, circles, polylines with bulges, splines, ellipses; one layer if you like)
and SVG. A reference image (a photo or a scan of a napkin sketch) can be placed on a
plane at a known width to trace over.
Manufacture - Toolpaths: 2.5-D CAM for a part held top-up on a 3-axis mill -
facing, outside/inside contours with holding tabs, pockets found automatically, peck
drilling of the vertical holes, V-bit chamfers - from a tool library with feeds per
material, drawn over the model, with a time estimate and G-code for GRBL or LinuxCNC.
Cut file…: a laser/waterjet/plasma DXF or SVG of a flat part's outlines with the kerf
compensated (also of a sheet-metal flat pattern). Print prep…: the best print
orientation per part (overhang area, support estimate, height, bed contact), the parts
arranged on your bed, and the plate as STL/3MF. The agent has cam, cut_file and
print_prep tools for the same, and the terminal has eitri cam model.py,
eitri cut model.py --kerf 0.3 and eitri print-prep model.py --bed 220x220.
Export - STEP (verified), STL, 3MF, GLB and the model source, from the
toolbar menu - and an engineering drawing: front/top/right views plus an
isometric in third or first angle, ISO or ANSI dimension text, hidden lines dashed,
overall, radius and angle dimensions, centre marks and centrelines, lettered hole
callouts with a hole table, section views (section:XZ@20: cut, hatched, marked
A-A on the parent view), half sections (halfsection:XZ@0|YZ@0), detail views
(detail:front@x,y:r:2, enlarged, circled on the parent), auxiliary views
(aux:1,0,1), broken views (broken:front@30..70), notes, leader annotations,
GD&T feature control frames and datums (from the agent's drawing tool), your own
dimensions (horizontal, vertical or aligned between two points of a view, plus
chain and baseline groups; dims on the tool, --dims on the CLI), balloons
and a parts list for assemblies, and a title block with the projection symbol, as
SVG, DXF or PDF (tick "show the engineering drawing" in
the sidebar to see it live) or DXF with VISIBLE/HIDDEN/TEXT layers. The agent
makes one when you ask (drawing tool) and gets a PNG preview of the sheet back so
it can check its own layout, and eitri drawing model.py does it
from the terminal. Parts can be given a material (PLA, PETG, ABS, nylon, resin,
aluminium, steel, brass…) and a colour; the report and the parts list then show
their mass. Individual faces take a colour of their own (Colour… in the face panel,
the appearance op), kept through later edits and carried into the GLB.
Everything you do becomes a direct edit in the model file: an EDITS list with
geometric selectors (a plane by normal and position, a hole by axis and radius, an
edge by type and midpoint, a sketch by its face frame), applied after build()
on every rebuild - so your edits survive parameter changes, consecutive drags on
the same face merge into one, and the report lists them. The agent sees the same
list: it can change an edit you selected (update_edit), remove one, or - with
Fold into code - rewrite them all into proper parameters and construction
steps (pattern -> GridLocations, mirror -> mirror(...), shell -> offset(...),
revolve -> revolve(...), thread -> IsoThread, a dragged corner ->
move_vertices(shape, near=[(x, y, z)], delta=...) from the library, ...).
Stretch the plate by hand, tell eitri to continue: it reads your edits, adds the holes, then builds a hook on the circle you sketched and writes new checks for it ($0.61 of API usage, a real session; MP4).
For anything else, click the geometry and type in the chat box ("make this hole M5 and counterbore it", "this wall is too thin, 3 mm please"): the message goes to the same agent session as the terminal, with the exact geometry attached ("Selected in the viewer: face plate:f7: cylinder, radius 2.5, concave: a hole/bore, part of hole Ø5 depth 6 through, centre (20, 0, 3)"), and the agent edits the parametric model. The terminal shows browser messages too, and the revisions list lets you jump back to any build.
The model file
One short Python file, no decorators, no cache, no sidecars:
from eitri import *
PARAMS = dict(width=60.0, depth=40.0, thickness=5.0, hole_d=4.5)
def build(p):
plate = Box(p.width, p.depth, p.thickness, align=(Align.CENTER, Align.CENTER, Align.MIN))
plate = round_vertical_edges(plate, 5)
return cut_holes(plate, [(x, y) for x in (-22, 22) for y in (-12, 12)], d=p.hole_d)
def verify(parts, p, c):
c.equal("width", parts.size.X, p.width)
c.count("mounting holes", holes(parts[0], diameter=p.hole_d, through=True), 4)
build(p) returns build123d geometry (or a dict of named parts for an
assembly). verify() is where the agent encodes what you asked for; its results
show up in every report and in the viewer. anchor("UNO", at=(50, 0, 10), z=(0, 0, 1), part="base") inside build() declares a named point or frame of
the model (where a board, a connector or a probe goes); anchors are recorded with
the build and listed in its report, and a host can place things at them. You can edit the file yourself at any
time - eitri build rebuilds it. Builds run in a warm worker process (the CAD
kernel is imported once per session): a parameter change rebuilds in about half
a second, a crash or runaway model just restarts the worker, and nothing is
cached on disk.
from eitri import * gives you build123d, the measurement helpers (holes,
min_wall, min_distance, overlap_volume, ...) and the feature library:
tested helpers for the things that are easy to get subtly wrong - metric screw
clearance/tap/counterbore/countersink holes (cut_holes, screw_hole_cutter),
heat-set insert bosses and standoffs, nut pockets, slots, ribs and gussets,
embossed/debossed text, patterns, shelled boxes, two-part enclosures with lips
and screw bosses (enclosure), involute spur gears, real ISO threads, screws,
nuts, washers and ball bearings (via bd_warehouse), bearing pockets.
What the automatic checks catch
Things that went wrong during development and are now caught by the build, not by hoping the agent notices:
- Slivers and feather edges. A cable slot meeting a slanted back left a 0.28 mm sliver; the agent called it "just a corner" and deleted its own check. The wall scan now samples just inside every planar face along its edges (deterministic, not luck), knife edges under 30° are listed, and the prompt forbids weakening a check.
- STEP files that lose geometry. OCCT's STEP writer silently drops faces built on offset curves (rounded text outlines, offset sketches): a lid with embossed text came back from the file as loose faces. Export converts such parts to NURBS and re-imports the file to prove every part is still a solid with the right volume.
- Counterbores through thin plates. A full socket-head counterbore in a 5 mm
plate would cut clean through;
cut_holeskeeps 1.5 mm of floor and says so. - Interference, wrong hole sizes, missing features. Every pair of parts whose bounding boxes come close is checked for overlap and clearance (no close pair is skipped, and a part buried wholly inside another counts as a collision); holes are listed with diameter, depth and through/blind so "four M4 clearance holes" is a number, not a claim.
Measured on the benchmark plate (M2 Mac, warm): reference cadgen rebuild 0.17 s (its global daemon + content store), eitri parameter tweak 0.20 s (a per-session worker, nothing on disk), eitri agent build with analysis + 4-view render 2.2 s.
Known limitations: the wall scan is still a sample (dense, but not a proof);
min_wall and interference on large meshing gears take ~10-20 s; threads are
real helical geometry and slow to boolean; the Claude Code backend's startup
adds a few seconds per session.
Speed
Measured on an Apple-silicon Mac with the CAD engine already loaded (it starts loading when you open a project):
| What | Time |
|---|---|
| Change a parameter (no LLM) | about 0.5 s |
| Full build of a simple part: build, checks, review render | about 1 s (a small clip: 0.9 s) |
| 3D view of a new build (shown before the checks finish) | 0.1 s for small parts, 2.5 s for the 43-part V8 |
| 43-part V8 assembly, full build with interference and clearance checks | about 21 s |
| One request handled by eitri, end to end (22-task benchmark) | 89 s on average, 52 s median |
| Cost per request at API prices (same benchmark) | $0.24 on average ($0.07 to $1.10) |
Most of a request's time is the model thinking, not CAD work; the chat streams its thinking and the model code as they are written, so the screen is never blank.
Direct edits (push/pull, fillets, moves, primitives, parameter changes) never call the LLM: they go editor →
EDITS/PARAMS → kernel. Measured server-side on the 43-part V8 demo, request sent → geometry ready /
request sent → checks done, before and after the build-reuse and caching work (October 2026):
| Direct edit on the V8 | before | after |
|---|---|---|
| Fillet an edge | 8.1 s / 23.2 s | 0.8 s / 3.0 s |
| Move a face | 7.3 s / 22.4 s | 1.5 s / 3.2 s |
| Push/pull a face | 6.0 s / 26.1 s | 4.9 s / 7.2 s |
| Move a part | 7.9 s / 23.2 s | 0.6 s / 2.4 s |
| Change a parameter | 8.2 s / 23.9 s | 4.9 s / 6.6 s |
The same edits on a one-part plate: 0.3-0.5 s before, 0.2-0.4 s after. In the browser, from the mouse release to the new geometry on screen, the V8 went from about 37 s to 5.5-14 s and the plate from 1.1-1.8 s to 0.4-0.7 s, measured in a software-rendered headless browser that spends seconds loading the V8's 3,900-node view; a browser with a GPU is close to the server numbers.
Accuracy
- Geometry is exact B-rep (OpenCASCADE), not meshes: dimensions are what the code says.
- The benchmark below: 22 tasks scored by checkers that read only the produced STEP file. eitri solved 22/22; the same model driving Claude Code with the text-to-cad plugin solved 20/22 (its misses: walls it never measured). A blind judge preferred eitri 9 times, the baseline 3 times, 10 ties.
- Hole detection was cross-checked against OCCT's reference classifier: 1,490 holes and 51,474 near-surface points, no mismatches. STEP export is verified by reading the file back (every part a solid with the right volume). Print meshes are checked closed, manifold and at the solid's volume.
The benchmark was built and run by eitri's authors, each task once, and the judge was also Claude: strong evidence, not independent proof.
Limitations
- The wall-thickness check is a dense sample (about 600 rays plus samples along every edge), not a proof.
- Imported meshes stay faceted: an STL becomes a solid of flat facets, not smooth cylinders.
- Big assemblies are slower: the clearance check measures every pair of parts whose boxes come close (about 9 s of the V8's 21 s build); farther pairs are counted, not measured.
- Not included: FEA/thermal/modal simulation, generative design, PCB layout (electronics is breadboard and module level), lathe and multi-axis CAM, DWG/SAT/native Fusion or SolidWorks import, cloud sharing, photorealistic rendering, true T-splines (the Form workspace is subdivision-based). CAM is 2.5-axis; sheet-metal corner reliefs are basic.
- Needs an Anthropic API key to chat (billed to you); everything else works without one. Tested on macOS (Apple silicon); Linux should work (the same packages exist); Windows is untested.
- Print orientation tries the six axis-aligned orientations only; plate packing is shelf packing, not a nesting optimiser.
- Direct edits reuse the previous build's geometry when only the
EDITSlist changed. A model whosebuild(p)reads files outside its folder, uses randomness or the clock may be served stale geometry:EITRI_REUSE_BUILD=0turns that reuse off (EITRI_REUSE_EDITS=0the reuse of earlier edits).
Compared to text-to-cad
earthtojake/text-to-cad is the best open text-to-CAD system we know of; eitri was built by studying it. Where it falls short, and what eitri does instead:
| text-to-cad | eitri | |
|---|---|---|
| Shape | A skill pack + 68k-line Python runtime (cadgen) for other agents (Claude Code, Codex, Cursor...) |
A standalone CAD agent with its own REPL and viewer, and one plugin for other agents (Claude Code, Codex, Cursor) with the same checks; also usable as a library |
| Install | uv + per-host plugin marketplaces, pinned Python 3.13, telemetry on by default, update nags | One line (curl -fsSL https://eitri.io/install | sh, add -s -- --plugin to register the plugin in every agent app found); no telemetry, no update checks unless you ask |
| Model contract | @step/@stl/@glb decorators, parameterless models, a content-addressed store with a build daemon, freshness gates, sidecar JSON, link-vs-component rules |
PARAMS + build(p) + verify(); builds are fast enough that nothing needs caching |
| Verification | Up to the agent: write ad-hoc scripts in tmp/, remember to snapshot |
Automatic on every build: validity, holes, thinnest wall, interferences, plus the model's own checks; failures are flagged to the agent |
| Visual review | Playwright + headless Chromium snapshots | A numpy software renderer: 4-view contact sheet in ~1 s, no browser |
| Standard features | Agent recalls screw sizes etc. from memory | Tested library with ISO fastener/insert/bearing tables, gears, threads, enclosures |
| Parameter tweaks | Edit code and rebuild | /set width=45 or sliders in the viewer: no LLM call, ~0.5 s rebuild (a warm kernel worker per session, no global daemon) |
| Revisions | None (git) | Every build is a numbered revision; /undo, /history, restore from the viewer |
| Editing geometry directly | Click a face in its viewer to paste a reference like #o1.f7 into your prompt |
Fusion-style direct modeling: push/pull, a constrained sketcher (lines/arcs/circles/ellipses/polygons/slots/splines/text, constraints, driving dimensions, DOF, solve-on-drag, trim/extend/split/offset/mirror/pattern/fillet, projected edges, SVG import) + extrude/cut/revolve/loft/sweep, holes with fastener sizes, primitives, shell, combine, split, mirror, patterns, scale, threads, fillet/chamfer, align/mate, measure, a timeline with edit-feature forms and suppress/reorder, undo, materials and mass, export - all tracked as EDITS the agent can read, change and fold into code |
| Agent instructions | SKILL.md + ~2,600 lines of references | One prompt + a build123d reference whose every snippet is executed by the test suite |
| Evidence | - | A benchmark of 22 tasks scored by geometry checkers, run against both systems |
Benchmark
bench/tasks.py holds 22 requests (plate, L-bracket, two-part enclosure,
flange, spur gear, stepped shaft, phone stand, cable clip, print-in-place hinge,
knob, threaded bottle cap, heatsink, V pulley, divided tray, Raspberry Pi 4 case,
NEMA 17 motor bracket, Arduino Uno plate, embossed name tag, M8 hex bolt, slotted
bracket, 608 bearing housing, meshing gear pair); two of them also carry a
follow-up edit sent in the same session. Each has a checker
that reads only the produced STEP: dimensions, hole counts/diameters/positions,
wall thickness, interference, features. bench/run.py runs a task through
eitri or through the baseline (headless Claude Code with the text-to-cad
plugin installed, same model, same prompt) and scores the result identically.
Results (Claude Opus 5.5 at effort high for both, 2026-10-11; full tables and
per-check verdicts in bench/RESULTS.md):
| baseline (Claude Code + text-to-cad) | eitri | |
|---|---|---|
| Tasks solved (all checks pass) | 20 / 22 | 22 / 22 |
| Mean check score | 0.99 | 1.00 |
| Time per task (CLI-measured) | 228 s (median 52 s; one task 44 min) | 89 s (median 52 s) |
| Cost per task (list-price equivalent) | $0.44 | $0.24 |
| Follow-up edits in the same session | 2 / 2 | 2 / 2 |
| Blind pairwise judge (Opus, vision), 22 pairs | preferred 3 | preferred 9 (10 ties) |
| Judge mean fidelity / engineering / overall | 8.5 / 7.5 / 7.6 | 8.5 / 7.9 / 8.0 |
The baseline's two misses are walls it never measured (1.84 mm and 1.32 mm where 2 and 1.6 mm were asked) and then reported as fine. eitri's automatic report caught the same class of problem in its own work (a 0.28 mm sliver on the phone stand) and the agent rebuilt until the thinnest wall was 3.2 mm. The baseline is genuinely strong on single-turn parts; the gap is self-verification, not raw modelling (both use the same LLM). Four eitri tasks were re-run after product fixes found during the run (a STEP-writer bug and a stricter wall warning); the first attempts are listed in RESULTS.md.
How it works
eitri/
model.py the contract: PARAMS, build(p), verify(); Parts; Checker
kernel.py build a model file in a subprocess (timeouts, crash isolation), export STEP/GLB/STL/3MF
measure.py holes, bosses, min_wall (ray casting), distances, overlaps, mass
report.py the automatic report + text summary
render.py numpy rasterizer: contact sheets, sections, exploded views
lib/ the feature library (fasteners, features, enclosure, bearings, parts)
agent/ tools.py (the tool set), prompt.md, build123d_ref.md, <domain>_prompt.md / <domain>_ref.md, claude_cli.py (headless
Claude Code backend over MCP), api_backend.py (Messages API backend)
viewer/ the editor: local HTTP server + three.js page (three.js vendored)
web/ the local website (home page, projects, demos, API-key setup) and the shared assets
printexport.py 3D-print export: decompose, orient, plate, verified STL/3MF
extensions.py the plugin point: registered hooks add checks to every build (and guard what a model declared)
harness/ wire harnesses: the typed model, checks, report, diagram (SVG + PNG), BOM / cut list / WireViz YAML
mechanism/ mechanisms: links, joints, actuators; reach (IK), worst-pose torque, speed, limits, mass; kinematic diagram
pneumatics/ ISO 1219 circuits: force, ISO 6358 flow, air consumption, pressure drop, validity; schematic, BOM
firmware/ board, pinout, peripherals, sources; pin checks, arduino-cli compile (never uploads); pinout diagram
electronics/ circuits: boards, modules, passives, nets; sourced current, power, logic, I2C, flyback, gate checks; schematic, BOM, netlist
domains.py the registry of non-CAD domains (one `domain.py` per package); checks.py and diagram.py are shared by them
demos/ the bundled demo projects and their recorded sessions (agent/replay.py plays them)
config.py the API key (user config folder)
doctor.py `eitri doctor` and `eitri key`
update.py `eitri update` and the opt-in once-a-day "newer version" notice
setup_firmware.py `eitri setup firmware` (arduino-cli and the board cores)
cli.py the REPL and subcommands
bench/ tasks, runner, results
tests/ everything above, including every snippet of the build123d reference
Extensions. Extra checks can be plugged into every build without changing eitri: a function registered with
eitri.extensions.register(name, fn), named in EITRI_EXTENSIONS=module[,module:setup...], or published as an
eitri.extensions entry point gets a BuildContext (the model module, what it built, the report so far) after each
build and returns checks that join the report beside verify(); it can also return data for result.json and a
guard, a dict of counts the project remembers per model file and fails a later build for if they shrink (only
Project.release_guard(name) lowers it). See eitri/extensions.py.
Development: uv venv --python 3.12 && uv pip install -e '.[dev]' && pytest. The demos are rebuilt
from recorded projects with scripts/make_demos.py.
Releasing. Set the new version in pyproject.toml, eitri/__init__.py and EITRI_FALLBACK_TAG in
scripts/install-remote.sh (python scripts/check_dist.py after uv build checks that they agree and that the
wheel and sdist hold every package file), add a CHANGELOG entry, then push a tag vX.Y.Z. The release workflow
builds, installs the wheel on clean macOS and Ubuntu runners, runs eitri doctor, and publishes that sdist and wheel to
PyPI with trusted publishing (no token in the repository). The install workflow runs the same build and install
check on every push to main and every pull request.
License
eitri is licensed under the Functional Source License, Version 1.1, MIT Future License
(FSL-1.1-MIT): you may use, modify and redistribute it for any purpose except offering a competing
commercial product or service; each version becomes MIT-licensed two years after it is released. Bundled
third-party assets keep their own licenses: three.js (MIT, eitri/viewer/vendor/three/LICENSE), IBM Plex
Sans and JetBrains Mono (SIL OFL 1.1, eitri/web/assets/design/fonts/), Lucide icons (ISC,
eitri/web/assets/LICENSE-lucide.txt).
Metadata
Release files for eitri-hardware 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| eitri_hardware-0.7.0.tar.gz | 3.4 MB | With provenance | Details for eitri_hardware-0.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| eitri_hardware-0.7.0-py3-none-any.whl | Python 3 | none | any | Details for eitri_hardware-0.7.0-py3-none-any.whl |
Total release size: 6.7 MB
Release files / eitri_hardware-0.7.0.tar.gz
| Download URL | eitri_hardware-0.7.0.tar.gz |
|---|---|
| Size | 3.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5ed66f8aa8767cf4db09645ecf6917c96584220ea3a8768b4c3511ce7666a1c6
|
|
BLAKE2b-256 checksum How to use checksums |
7473ce4bff08fe64447b713f4fb570ad6f1925b087d035c846979055c3671b51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / eitri_hardware-0.7.0-py3-none-any.whl
| Download URL | eitri_hardware-0.7.0-py3-none-any.whl |
|---|---|
| Size | 3.3 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b8852200246b658d0cfd9295d1934d11c6c20c7c6fde2cb37971e8cc4644d2ca
|
|
BLAKE2b-256 checksum How to use checksums |
182251de7d45c437e6eb4e1a281086aa12803a0e64b5ce42c5dafd0ace135abd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for eitri-hardware 0.7.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for eitri_hardware-0.7.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.