LLM providers
The planner is provider-agnostic. Google Gemini and OpenAI are supported; configure several at once and pick one per run:
// windup.config.ts
llm: {
provider: "google", // default for runs without --llm
model: "gemini-3.1-flash-lite",
// apiKeyEnv: "GEMINI_API_KEY", // already have the key under another name? point at it
providers: {
openai: { model: "gpt-5-mini" }, // default model when --llm openai is used
// openai: { apiKeyEnv: "MY_OPENAI_KEY", baseUrl: "https://my-proxy/v1" },
},
},
npx windup run checkout # config default (google)
npx windup run checkout --llm openai # provider default model (gpt-5-mini)
npx windup run checkout --llm openai:gpt-5-nano # explicit provider:model
WINDUP_LLM=openai:gpt-5-mini npx windup run --all # same thing via env (CI)
--llmworks onrun,bench(compare providers on the same scenario) andscan(LLM-assist layer).- API keys:
GOOGLE_GENERATIVE_AI_API_KEY/OPENAI_API_KEYby default. To reuse a key your project already stores under another name, point at it withapiKeyEnv— either at thellmlevel (llm.apiKeyEnv: "GEMINI_API_KEY", applies to whichever provider has no override) or per provider (llm.providers.openai.apiKeyEnv, which wins). No need to duplicate the secret.windup doctorreports the exact variable it expects. - A wrong model name is caught as a config error, not a test failure: the provider’s 404 becomes an actionable message naming known models, the run fails with
kind: config(never retried by--retries), andwindup doctorwarns up-front when the configured model isn’t in the known-model table. baseUrl(OpenAI only) points at any OpenAI-compatible endpoint — Azure, a proxy, or a local model server.- Switching providers never invalidates the plan cache: plans are data, replays are LLM-free regardless of who planned them.
windup costsbreaks spend down by provider and by model, so alternating between LLMs keeps per-vendor spend visible.
Planning with your Claude subscription (--llm claude-code)
If you already pay for a Claude plan (Pro/Max), you can plan with it instead of buying API tokens — Windup drives the claude CLI you already have, no API key, no extra server.
Opt-in, never a default. Using a subscription to plan programmatically is a gray area not endorsed by Anthropic, and Windup does not operate it. For reliability-sensitive work (CI, shared suites) prefer
--llm googleor--llm openai. Cached replays never call any LLM, so a plan made this way still replays at $0 with nothing running.
Setup — one command
npx windup claude login # installs the claude CLI if missing, then signs it into your plan
npx windup claude status # anytime: "claude CLI: ready — you@example.com (max plan)"
windup claude login installs the Claude Code CLI (with your confirmation — never a silent global install, never in CI) and launches Anthropic’s own browser sign-in; you click authorize on your account. The desktop app and the CLI sign in separately, so having the desktop app is not enough. By hand instead: npm install -g @anthropic-ai/claude-code, then claude → /login (pick “subscription”, not an API key).
That’s it — no wrapper, no Python, no local server. Windup spawns claude in non-interactive mode for each plan (from an isolated temp dir, so it never picks up a project’s CLAUDE.md).
Several accounts — one per project (--profile)
The CLI’s login is global: one token, in one config dir, so every project plans on whichever account signed in last. If you hold a personal plan plus one per client, that means client work quietly burns your own plan. Bind each project to its own account, once:
cd ~/work/acme
npx windup claude login --profile acme # own config dir + binds this project + signs in
npx windup claude status # → confirms which account this project bills
--profile acme gives that account its own config dir (~/.claude-acme — an independent session), binds the project to it by exporting CLAUDE_CONFIG_DIR in .envrc, runs direnv allow, and then opens the sign-in. From then on, cd-ing into the project makes that account the one that plans — including the claude process Windup spawns, which inherits the environment. Repeat per project with a different name; your default ~/.claude stays untouched as the unnamed profile.
Your .envrc is never clobbered by accident: an existing file is appended to (other exports intact), re-running is a no-op, and a binding to a different profile stops and tells you to re-run with --force — which rebinds that one line and prints what it replaced. No direnv? The command prints the export to put in your shell.
npx windup claude status # which account is active here (email + plan) — no tokens spent
npx windup claude status --profile acme # check a named profile without switching to it
npx windup claude login --profile acme --force # point this project at another profile (rebinds .envrc)
npx windup claude login --force # switch the ACTIVE account (signs out first, saying whose)
npx windup claude logout --profile acme # sign that profile out (other profiles untouched)
npx windup claude logout --profile acme --remove # …and delete its config dir: profile retired
Retiring a client? npx windup claude logout --profile <name> --remove clears that profile’s credential and deletes its config dir — every other profile is untouched, and the default ~/.claude can never be removed. If the status ever reads account email not reported, the config dir holds a valid token without account metadata: sign that profile out and in again to record which account it is.
Two things worth knowing: a project’s .claude/settings.json cannot switch the account (the config dir is resolved before those settings load) — that’s why the binding lives in .envrc; and cached replays call no LLM at all, so with .windup/cache/ committed a suite runs at $0 without touching any account.
npx windup run checkout --llm claude-code # default model: claude-sonnet-4-6
npx windup run checkout --llm claude-code:claude-opus-4-6
WINDUP_LLM=claude-code npx windup run --all # via env
Optionally pin it in config so plain windup run uses it:
// windup.config.ts
llm: { provider: "claude-code", model: "claude-sonnet-4-6" },
- Cost is reported as $0 in
windup costs— the tokens are real and stay in the ledger, but they’re covered by your subscription, so Windup does not invent a per-token price for them. - If
claudeisn’t installed or logged in, the run fails fast with an actionable message (install //login), not a stack trace. - Slower to plan than a hosted API (each plan spawns the CLI’s agent — ~8–12s vs ~2–4s), but planning happens once and is cached; replays are $0 and instant regardless.
- Under the hood: there’s no JSON mode, so Windup carries the plan schema in the prompt and un-fences the reply mechanically (Ajv still validates every plan);
temperature/seedhave no CLI equivalent and aren’t sent.
Alternative: route through the claude-code-openai-wrapper (HTTP)
Instead of the CLI, you can point Windup at claude-code-openai-wrapper — a third-party, community-maintained local proxy that exposes an OpenAI-compatible endpoint over your Claude Code session. Useful if you already run it, want an HTTP boundary, or reach Claude through Bedrock/Vertex behind it. Windup uses the wrapper (instead of spawning the CLI) whenever a URL is configured:
# start the wrapper (needs Python 3.11+ and Poetry), then:
WINDUP_CLAUDE_CODE_URL=http://localhost:8000/v1 npx windup run checkout --llm claude-code
// windup.config.ts — same effect, persisted
llm: { provider: "claude-code", providers: { "claude-code": { baseUrl: "http://localhost:8000/v1" } } },
Its own client auth is off by default; set CLAUDE_CODE_API_KEY only if you enabled it. Same $0 cost, same un-fencing. A down wrapper fails fast with a message naming the URL.