Mechanics live in config and the @broject/providers bindings. This
skill is the contract reference — providers are configured, never
hardcoded.
What a provider is
A named entry in bro.config.json providers — { type, …connection }.
Consumers pick by name; the name carries no semantics. The type
(provider kind) fixes the connection shape and which surfaces the entry
can serve.
{
"providers": {
// One API host, many models — each model declares its wire.
"orcarouter": { "type": "api", "baseUrl": "https://api.orcarouter.ai",
"apiKeyCommand": "secret-tool lookup service typesafe.ai",
"models": {
"typesafe/jev-1.13": "systemone",
"acme/cheap-chat": "openai-compat",
"acme/auto-x": null // inferred
} },
"kilo-cli": { "type": "acp", "command": "kilo --acp",
"model": "typesafe/jev-1.13" },
"local": { "type": "cli", "command": "devin -p --prompt-file {promptFile}",
"model": "devin-1" }
}
}
Kinds and surfaces
| kind | call (judge) | spawn (fleet) |
|---|---|---|
api |
by model wire — systemone typed, openai-compat prose |
— |
acp |
typed on a jev-family model, else prose |
yes |
cli |
prose — stdout parsed | yes — loop.agent template contract |
- An
apientry is a host + auth + amodelsallowlist —{ "<model-id>": "systemone" | "openai-compat" | { "wire": … } | null }.null/absent infers the wire:jev-family ids →systemone, everything else →openai-compat.modelpins the default;judge.modelor a per-call override must name an allowed id — anything else is a config error listing the allowlist. cli.commandis a shell template:{promptFile}expands to the quoted path of a file holding the prompt (appended as the last arg when absent);{model}expands to the resolved effective model, quoted. The process runs and exits, stdout is the answer. A model override the template can't consume —--model/profile/judge.modelon a command with no{model}— is a loud error, not a relabel: provenance never claims a model the worker didn't run.- An
acpentry's call grade is decided by the served model —typesafe/jev-*(or barejev-*) is typed transport; anything else is prose and stampsprovider:<name>:proseindecidedBy. - A
clientry's optionalmodelsis a shell template emitting one model id per line — the catalog surface, e.g."models": "opencode models". Absent = no enumerable catalog; a pinnedmodelstill renders.
bro providers models [name…] — the catalog
Renders every provider's model list as <name>/<model-id> — the
uniform provider/model vocabulary hosts' pickers share (claude's
/bro:models command wraps exactly this call). Source per kind:
api— themodelsallowlist keys (config-declared, offline).acp—session/new's modelconfigOptions, enumerated live over the protocol: the same list the agent's own model picker serves.cli— themodelstemplate's stdout lines, plus the pinnedmodeldeduped (a pinned model always renders — a provider never prints silently empty).
Named providers error loudly (providers.<name> is not configured);
a failed provider warns and never stops the rest.
Consumer wiring
- Judge:
judge.provider: '<name>'+judge.model: '<allowed-id>'(+judge.fallback: '<name>'). The resolved model's wire picks the adapter —systemone→ typed judgments,openai-compat→ prose. A prose-grade answer is never counted as a calibrated typed judgment —decidedBysays which. Legacyjudge.{baseUrl,model,apiKeyEnv}/judge.llmsynthesize anonymous single-modelapientries with a deprecation warning. - Fleet:
agents.<backend>.provider: '<name>'orfleet.profiles.<p>.provider— the provider supplies what runs inside the backend's spawn; provenance ridesBRO_AGENT_PROVIDER/BRO_AGENT_MODELinto the registry entry andbro fleetoutput.
Non-negotiables
- No defaults, no hardcoding. Absent
providers= provider behavior off, never a silent vendor. A consumer naming a missing entry errors at use —providers.<name> is not configured— never falls through. - Secrets ride env var names or lookup commands.
apiKeyEnvnames the variable (SCREAMING_SNAKE);apiKeyCommandnames a secret-store lookup (secret-tool lookup …,pass show …,op read …) whose stdout is the key. The key value never lands in config or error output — a command smuggling one inline is rejected. - Retired kinds fail the config.
type: "systemone"or"openai-compat"as a providertypeis a hard error naming the migration — move the entry totype: "api"+modelsmap. - Surface errors are config errors. Asking an
apientry to spawn, or a non-call kind to judge, fails at resolution naming kind + surface. bro doctorreports the registry — entries (name, kind, default/served models), unsetapiKeyEnvvars, and dangling/surface-mismatched consumer refs, as warn rows that never block the exit code.
Policy
- A new vendor is a registry entry, not a new connector — kinds
exist for connection-shape differences only (the union is closed at
three); protocol differences inside an HTTP host are
modelsmap entries, not kinds. - Diagnose with
bro doctorbefore touching provider config — a warn row there is the failure a spawn/decide would hit, said early.