ChatModel protocol. You rarely construct
adapters by hand — you name a model and metalworks resolves it.
Model refs
A model ref isprovider:model-id or provider/model (the slash form matches
the convention used by OpenRouter, LiteLLM, and most agent runtimes):
A bare known-provider slash like
anthropic/claude-opus always routes to the
native SDK — it never silently lands on OpenRouter.
No ref? Inferred from your keys
With nomodel, the provider is taken from the first key present, in order:
Anthropic, OpenAI, Google. So Metalworks() with only OPENAI_API_KEY set uses
OpenAI. If none of those is set, a lone OPENROUTER_API_KEY is the recognized
single-key fallback — Metalworks() then talks to OpenRouter’s OpenAI-compatible
endpoint (so one key reaches many models). A native key always wins over it. And
with no key at all, if metalworks[claude-code] is installed, the claude-code
login is the ultimate fallback (the keyless floor — see below). You can also pin a
default in ~/.config/metalworks/metalworks.toml:
METALWORKS_MODEL to apply a ref to every surface — CLI, MCP server, and the
SDK — without editing config; the research run / research ideate commands also take a
--model / -m flag. Both behave like an explicit ref, so they win over the config file and
over key-order autodetection (handy when stray VERTEX_* / GOOGLE_APPLICATION_CREDENTIALS
env would otherwise hijack selection):
model= / --model ref > METALWORKS_MODEL env > config file > first
present key.
Run on your Claude Code login (no API key)
If you already use Claude Code, metalworks can run on that login with no API key — this is the keyless chat floor. Install the extra (it bundles theclaude CLI; no Node):
model ref, no
METALWORKS_MODEL), resolve_chat() falls back to the claude-code provider — the chat analogue
of how embeddings fall back to the local model. It runs through the Claude Agent SDK, using the
machine’s Claude Code session (claude login / CLAUDE_CODE_OAUTH_TOKEN). Select a tier with a ref:
- Any explicit key/ref wins. The floor only engages when nothing else is configured, so adding a
provider key later silently takes over — set
ANTHROPIC_API_KEY(etc.) to switch back. - It spawns the bundled
claudeCLI per call (~5–7s/call) — the keyless convenience path, not the fast one. A configured API key is faster for a many-call research run. - It uses your individual Claude Code subscription. Anthropic restricts offering claude.ai login in distributed products; this is intended for your own local/individual use, not for shipping a multi-user service on one subscription.
Web research, keyless too
The same extra also makes web research keyless. Normally the web stream needs an external search key (EXA_API_KEY / TAVILY_API_KEY / …); with metalworks[claude-code] and no such key, a
claude-code SearchProvider drives Claude Code’s WebSearch tool and feeds the same grounding
path — so a full report (Reddit demand and web) runs with zero keys. Any external search key
still wins.
Grounding note: native web-search citations aren’t reachable through the SDK (it drops the
Messages-API citations), so attribution is reconstructed — the model’s {url, title, snippet}
results are validated against the URLs WebSearch actually returned, and any invented URL is
dropped (no-cite-no-claim holds at the claim→URL level). It’s slower and more token-heavy than a
search API, and the binding is claim→URL rather than native span-citation — for the strongest web
grounding, set an EXA_API_KEY.
LLM call timeout (reasoning models)
Each LLM call has a per-call timeout budget, default 300s. The OpenAI/compatible path streams, so this is a read (gap-between-chunks) timeout, not a total — a reasoning model that is slow to the first token or trickles output completes as long as no single gap exceeds the budget, while a genuinely stalled stream still fails cleanly. Raise it for very-long-thinking models viaMETALWORKS_LLM_TIMEOUT (seconds, applies to every surface — CLI, MCP, SDK) or the
llm_timeout config setting:
METALWORKS_LLM_TIMEOUT env > llm_timeout config > 300. Grounded web calls keep a
higher floor.
Where Reddit data comes from
Submissions and comments come from the live Arctic Shift API by default — current data, corehttpx, no extra. Opt into a bulk/offline tier with ARCTIC_SHIFT_SOURCE: hf (aliases
parquet/arctic) reads the Hugging Face open-index/arctic Parquet mirror ([arctic] extra,
DuckDB; reads HF_TOKEN from the env to clear the public-mirror rate limit); mirror reads a
Supabase mirror ([supabase] extra). Both lag the live API.
Google via Vertex AI
The Google chat and embedding adapters can authenticate through Vertex AI (Application Default Credentials, e.g. a service account) instead of an API key. SetGOOGLE_GENAI_USE_VERTEXAI=true and provide a project and location:
GOOGLE_API_KEY is set. The project is required (VERTEX_PROJECT_ID or
GOOGLE_CLOUD_PROJECT); the location defaults to us-central1.
Any OpenAI-compatible endpoint
This is the “bring your own model” path. Any server that speaks the OpenAI chat-completions API — OpenRouter, vLLM, LM Studio, Together, Groq, a local runtime — works with no new adapter:native_structured=False routes structured calls straight to the schema-in-prompt
ladder tier, which is the safe default for endpoints whose JSON-schema support
varies. Leave it True if your endpoint enforces response_format reliably.
Fast vs main model
The research and discovery pipelines use a cheap “fast” model for triage and filtering and a capable model for synthesis and generation. Set both:model, the fast slot falls back to it. Resolve a pair directly
with metalworks.config.resolve_models(model, fast_model).
Embeddings
The pipeline embeds Reddit comments to cluster demand. You don’t configure this separately — it resolves from your environment, and never requires its own key:
So a chat-only provider (Anthropic, OpenRouter, a local LLM) just works: embeddings fall back
to the local model, downloaded once to the Hugging Face cache, then fully offline. A Google
or OpenAI key is used automatically when present (higher quality, no download).
Force a backend without editing code with
METALWORKS_EMBEDDINGS (local / openai /
google) — handy on a machine with stray GOOGLE_GENAI_USE_VERTEXAI you don’t want embeddings to
use. And if Vertex is enabled but its GOOGLE_APPLICATION_CREDENTIALS points at a missing file,
embeddings degrade to the local model instead of crashing.
Embedding vectors from different models live in incompatible spaces. metalworks stamps each
cached index with an identity and refuses to mix them — switching embedding backend on an
existing
.metalworks/ project triggers a clear EmbeddingModelMismatch rather than silently
degrading retrieval. Re-run research to rebuild the index under the new model.Sources
Which connectors a research run pulls from is resolved by a fixed precedence — explicit override > brief-aware selector > thereddit floor — so an explicit
choice always wins:
select unset and
no explicit override, a run picks its sources by the idea. It applies a
deterministic access gate — a source is only pickable if it needs no key or its
key is set — then an LLM cut over what’s reachable: the model selects the few
sources worth pulling for this brief (typically 2–5), an omitted source is dropped
(not re-appended), the non-removable reddit floor is always kept, and the pick is
capped at 6. So a consumer brief pulls community/forum sources and cuts the dev/B2B
and CMS/ATS ones; a developer-tool brief elevates Stack Exchange / GitHub. Set
select = false to opt back out to the configured [sources].enabled / reddit
default. A source the cut wants but can’t reach (no key) is reported in a pre-flight
line naming the env var to set, e.g.
Skipped (no key): producthunt — Set the PRODUCT_HUNT_TOKEN environment variable.
The selector has a non-removable floor and a blast-radius guard: when the
cut yields nothing — a brief that matched only paid sources with no keys set, OR
there is no chat model / the selection call fails — the run falls back to reddit
(or [sources].default) with a distinct caveat, never the all-reachable set and
never an empty corpus. (This guard keeps an offline / model-less run deterministic
reddit-only.) The pick, the skipped sources, and any floor caveat are surfaced on
the report’s source_selection field.
Check the resolution
metalworks preflight is the proactive, machine-readable check the skills run first: it reports
the active corpus reader, resolved chat/embedding models, installed extras, present keys, and any
setup issues, plus a cached PyPI update check. doctor renders from the same checks (a pretty
superset) and keeps --fix. The heavy research / build / distribution commands also print a
one-line banner before they run — silent when healthy, otherwise pointing you at doctor.
Update check + banner settings
Two non-secret settings (inmetalworks.toml / ~/.config/metalworks/metalworks.toml) tune the
proactive checks. Both are on by default; set either to false to opt out:
httpx is imported lazily inside the fetch only, so
import metalworks never hits the network, and any failure simply omits the update line.