Skip to main content
metalworks talks to LLMs through the ChatModel protocol. You rarely construct adapters by hand — you name a model and metalworks resolves it.

Model refs

A model ref is provider: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 no model, 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:
Set 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):
Precedence: explicit 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 the claude CLI; no Node):
With the extra installed and nothing else configured (no provider key, no 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:
Honest trade-offs:
  • 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 claude CLI 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 via METALWORKS_LLM_TIMEOUT (seconds, applies to every surface — CLI, MCP, SDK) or the llm_timeout config setting:
Precedence: 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, core httpx, 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. Set GOOGLE_GENAI_USE_VERTEXAI=true and provide a project and location:
With Vertex mode on, provider inference routes to Google even when no 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:
If you set only 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.
Override explicitly by injecting a provider:
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 > the reddit floor — so an explicit choice always wins:
The selector is ON by default (sources-by-idea, #167): with 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 (in metalworks.toml / ~/.config/metalworks/metalworks.toml) tune the proactive checks. Both are on by default; set either to false to opt out:
The update check is offline-safe by design — httpx is imported lazily inside the fetch only, so import metalworks never hits the network, and any failure simply omits the update line.