# LLM providers

`tf generate` and the generate screen inside `tf onboard` call one backend. The default is the Claude Code CLI. No API key.

Set the backend in `.env` or pass `--provider` / `--model` for a single run.

| `TF_LLM_PROVIDER` | Key | Default model |
|---|---|---|
| `claude-code` (default) | none; uses your `claude` login | the CLI’s own default. `TF_LLM_MODEL` is passed as `claude --model` |
| `anthropic` | `ANTHROPIC_API_KEY` | `claude-sonnet-4-6` |
| `openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-sonnet-4.6` |
| `openai` | `OPENAI_API_KEY` | `gpt-4o` |

```bash
# one-off, without editing .env
tf generate --provider anthropic --model claude-sonnet-4-6
```

Direct API calls use temperature `0.8` and a token cap (8,000 for a full bundle, so four ideas with two or three variants fit). Claude Code gets a 240 second timeout.

## Claude Code subprocess

`tf` runs:

```
claude -p <prompt> --output-format json --permission-mode dontAsk --tools "" --disallowedTools "*,mcp__*" --strict-mcp-config --max-turns 1 --no-session-persistence
```

`--tools ""` removes built-in tools. `--allowedTools` only pre-approves tools, so it is not used as the control. Scraped tweets, articles, and Notion pages sit inside `<reference_data>`, and a closing fence typed inside that text is stripped. The subprocess cannot run shell commands or edit files.

If `claude` is not on `PATH`, the error tells you to install Claude Code or set `TF_LLM_PROVIDER` and a key.

Claude Code has to be logged in on that machine, including a server you cron from. `tf` does not ship a Claude credential.

## Output shape

The model must return one JSON object. Zod checks `persona` (handle, identity, lane, voice rules, hottest take, building), `contentPillars`, `replyTargets`, `cadence`, `strategy`, and either `ideas[]` (each with `variants[]` of `text`, optional `aim` and `note`) or a legacy `drafts[]`. A flat `drafts` array is always written back after scoring, with `text`, `score`, `signal`, and optional `note`. Prose or a broken object throws. Re-run, or switch model.

Scores are computed in-process after the model returns text. tweet-factory does not execute the X ranking code. It multiplies editorial strengths by the published weights in `src/rank/weights.ts` (x-algorithm `cb45b55`). The same module builds the system prompt, so the prompt and the scorer share one table. The narrative write-up is `X_ALGORITHM_BOOST_PLAYBOOK.md`. When your own posts have enough impressions, `src/rank/calibrate.ts` scales reply, quote, repost, and like strengths from `tweets.db`. Bookmarks stay diagnostic. The formula is on [how drafts are scored](how-tweet-factory-scores-drafts.md).

An optional org skill at `~/.claude/skills/twitter-algorithm-optimizer/` can replace that playbook when a person runs the `/tweet-factory` Claude Code skill. `tf generate` always uses the weights compiled into the binary.

> **Verify.** `tf generate --n 1 --json` prints a JSON object with a `drafts` array, or fails with a clear provider error (missing `claude`, missing API key). Fix that error before treating generation as set up.
