Skip to content

Wire up more than one provider

You inherited feedmill and a habit that came with it: one provider’s model for everything, billed against one subscription. That’s fine until the work stops being one shape. Onboarding a new feed’s parser is fiddly, judgment-heavy work where a frontier model earns its price. Re-running the dedupe pass over a thousand cached items, or asking “where does the sync server retry a failed pull,” is bulk reading where a cheap, fast model is the right call and the expensive one is just a tax. Locked to a single vendor, you pay the frontier rate for the cheap work or you do the hard work on the cheap model — either way you’re choosing per subscription, not per task.

OpenCode is built the other way around. It isn’t tied to a model; it talks to ~75 providers out of the Models.dev catalog, and which one runs a given turn is your call. But before you can choose, the providers have to be connected. That’s this lesson: get two of them wired up, with credentials that never touch a file you’d commit.

Add credentials first — they don’t live in config

Section titled “Add credentials first — they don’t live in config”

The keys and the configuration are two separate things, and conflating them is how secrets end up in git. Credentials are added with the /connect command from inside the TUI:

> /connect
Select a provider to connect:
› Anthropic
OpenAI
OpenRouter
Google
Groq
…75+ more
Anthropic › API key: ********************
✓ Saved. Credentials stored in ~/.local/share/opencode/auth.json

Run it again for a second provider — say OpenRouter, which fronts a wide spread of models behind one key — and you’ve got two sets of credentials on disk. The same thing works headless if you’d rather script it: opencode auth login --provider anthropic walks the same flow from the shell, prompting for the key, and opencode auth login on its own gives you the interactive picker. There’s also a --method (-m) flag that skips the method-selection step, but it takes the provider’s login-method label — which is human-readable and varies per provider (an API-key entry for one, an OAuth label for another) — so the simplest scriptable form is to pass --provider and let it prompt.

Note where those keys landed: ~/.local/share/opencode/auth.json, not your project. Confirm it whenever you want:

> opencode auth list
Anthropic api-key
OpenRouter api-key

This is the clean default, and it’s worth saying out loud — the credentials you just added are not in opencode.json and never will be. Connecting a provider this way is enough to use its models. The config file is for something else: telling OpenCode which providers and models you want surfaced, and how. You only reach for it when /connect’s defaults aren’t enough — a custom endpoint, a model the catalog doesn’t list, or a key you’d rather pull from the environment than store in auth.json.

The provider block in opencode.json is where you describe the providers you want available in this repo. For the catalog providers you just connected, you often don’t need to declare anything — /connect is sufficient. Where the block earns its place is when you want to be explicit about it living with the project: anyone who clones feedmill sees which models the work expects, even before they’ve connected their own keys.

{
"$schema": "https://opencode.ai/config.json",
"provider": {
"anthropic": {
"options": {
"apiKey": "{env:ANTHROPIC_API_KEY}"
}
},
"openrouter": {
"options": {
"apiKey": "{env:OPENROUTER_API_KEY}"
}
}
}
}

The thing to see here is {env:ANTHROPIC_API_KEY}. OpenCode substitutes that token with the value of the environment variable at load time, so the reference lives in the file and the secret lives in your shell. This is the move that lets opencode.json be committed safely — a teammate cloning feedmill gets the shape of your provider setup with none of your keys, and supplies their own through their environment. If the variable isn’t set, OpenCode substitutes an empty string rather than erroring, so a missing key surfaces as an auth failure at call time, not a parse failure at startup.

So you have two ways in, and they’re complementary rather than competing: /connect stashes a key in auth.json for quick personal use, and {env:...} in opencode.json references a key the project expects without ever holding it. For feedmill — a repo you’ll commit and maybe hand off — lean on the environment reference for anything that ships.

Project config layers on top of global, it doesn’t replace it

Section titled “Project config layers on top of global, it doesn’t replace it”

You almost certainly already have a global config at ~/.config/opencode/opencode.json — your personal defaults, the providers you reach for across every repo. The natural worry is that dropping an opencode.json into feedmill clobbers all of that. It doesn’t.

OpenCode merges the config sources rather than replacing them. Your global file and the project file are combined; where the two set the same key, the project file wins. So feedmill’s opencode.json can pin down the providers this project cares about — the parsing-grade model and the cheap bulk-read model — and quietly inherit everything else from your global config. Nothing you’ve set up machine-wide is lost by adding a per-repo file; you’re layering project specifics over a personal base, not starting from scratch.

That precedence is the whole reason a project file is the right home for feedmill’s provider choices. A decision that belongs to this codebase — these are the models this repo’s work wants — lives with the codebase and overrides your personal defaults only where it needs to, for anyone who opens the repo.

You’ve now got more than one provider connected, keys kept out of anything you’d commit, and a project config that layers cleanly over your global one. The point of all this was choice: now that feedmill can reach several models, you want to move between them mid-task without restarting. Next: switch models without leaving the session.