Providers
Relay ships adapters for eight providers out of the box. Each adapter
normalises that provider's native event stream into Relay's
AgentEvent. You don't need every one — pick what you have
keys/subscriptions for and disable the rest in
.relay/relay.toml.
Built-in adapters
| Name | Type | Auth methods | Stdin replies | Native install |
|---|---|---|---|---|
claude |
CLI subprocess | OAuth via claude login, API key |
✓ | npm i -g @anthropic-ai/claude-code |
codex |
CLI subprocess | API key (OPENAI_API_KEY) |
— | npm i -g @openai/codex |
antigravity |
IDE | manual | — | download from antigravity.google |
opencode |
CLI subprocess | API key | — | npm i -g opencode-ai /
brew install opencode-ai/tap/opencode |
ollama |
local HTTP | — (local) | — | winget install Ollama.Ollama /
brew install ollama / curl install |
copilot |
gh extension | OAuth via gh auth login |
— | gh extension install github/gh-copilot |
continue |
VS Code ext | extension UI | — | code --install-extension Continue.continue |
cline |
VS Code ext | extension UI | — | code --install-extension saoudrizwan.claude-dev |
Auto-install + auth
From the desktop app's Settings → Providers tab:
- [Install] spawns a terminal running the per-OS
install command for your platform (Windows:
winget/npm.cmd; macOS:brew/npm; Linux:apt-friendly install scripts). - [Sign in with browser] runs the provider's OAuth
subcommand (
claude login,gh auth login --web, etc.). - [Use API key] writes the key to
.relay/.env(gitignored) and reloads the daemon's env so spawned agents inherit it. - [Run via Ollama] opens
ollama launch <tool> --model <model>so a provider that needs cloud auth can run against a local model instead.
Probe
Every poll cycle (~1.5s) the daemon probes:
for each provider:
check PATH + extra dirs (%APPDATA%\npm, ~/.local/bin, brew prefix, etc.)
check provider-specific env vars
check IDE extension dirs for VS Code providers
→ status: available | no_key | not_found | unavailable | manualStatus drives the colour-coded tag in the UI and the orchestrator's "which providers are eligible right now" decision.
Quota source
Each adapter has a quota.Adapter paired with it. Three
detection strategies:
- Proxy header (Claude): a local HTTP proxy injects
between the CLI and the API, parses
x-anthropic-ratelimit-tokens-remaining. Authoritative. - Session file: provider writes usage to a known on-disk path. Polled.
- Declared cap + counted requests: fallback when no
API surface exists. From
.relay/relay.toml. - 429 backstop: detects exhaustion on first 429 even when no other signal works.
The UI surfaces which source is in use per provider.
Cost
internal/pricing/pricing.go maps each provider to
per-million-token rates (input + output). Updated by hand; PRs
welcome.
"claude": {3.00, 15.00}, // Sonnet tier
"codex": {5.00, 15.00},
"opencode": {3.00, 15.00},
"ollama": {0.0, 0.0},
"copilot": {0.0, 0.0}, // flat subscriptionThe orchestrator records token usage per event (from adapter
Meta fields) and shows live USD in the dashboard
footer.
Adding a new adapter
Seven steps. For a CLI-driven provider the adapter itself is ~15
lines; reference: the constructors at the bottom of
internal/adapter/stub.go (Codex, Antigravity, OpenCode,
Copilot, Continue, Cline are all built this way).
1. Add the name
// internal/adapter/interface.go
const (
ProviderClaude ProviderName = "claude"
// ...
ProviderYourTool ProviderName = "yourtool"
)2. Implement the adapter
Default path (CLI providers): compose the shared
CLIAdapter base. newCLIAdapter
handles process spawn, stdout draining, safe-pause, force-stop, and
event channel plumbing. You supply the argv builder and optional
parsers:
// internal/adapter/yourtool.go (or next to the other constructors in stub.go)
// NewYourToolAdapter creates a YourTool CLI adapter.
func NewYourToolAdapter() *CLIAdapter {
a := newCLIAdapter(ProviderYourTool, "yourtool", InjectionSystemLayer, nil)
a.buildArgs = func(opts RunOptions) []string {
args := []string{"run", "--non-interactive"}
if m := a.Model(); m != "" {
args = append(args, "--model", m)
}
if opts.SystemPromptFile != "" {
args = append(args, "--system", opts.SystemPromptFile)
}
return append(args, opts.Task)
}
return a.withTokenParser(parseStreamJSONUsage)
}Chainable extras, all optional:
.withLineParser(fn)— parse the tool's native JSONL stream into typedAgentEvents (seeparseCodexLine). Without it, stdout lines become plain text events..withTokenParser(parseStreamJSONUsage)— extract token usage for the cost meter and quota forecasting..withTools()— declare that the provider executes tools/commands.- If the tool has no system-prompt flag, prepend the contract to the
task with
prependContract(opts)(seeNewCodexAdapter).
Fallback path (non-CLI providers only): implement
the full AdapterContract interface yourself —
Capability(), Run(ctx, opts, ch),
AwaitSafePauseWindow(...), ForceStop(). Only
needed when a subprocess-per-task model doesn't fit (local HTTP APIs
like Ollama, IDE extensions). Reference:
internal/adapter/ollama.go.
Implementing StdinReplier.SendStdin(reply) is optional
but unlocks the inline-reply UI for your adapter.
3. Register
// internal/adapter/registry.go
func BuildAdapterRegistry(proxyPort int, ollamaURL, ollamaModel string) Registry {
return Registry{
// ...
ProviderYourTool: NewYourToolAdapter(),
}
}4. Add metadata
// cmd/relay/providers.go
{
Name: "yourtool",
DisplayName: "Your Tool",
Description: "What it does.",
Kind: "cli",
CanInstall: true,
CanOAuth: false,
CanAPIKey: true,
APIKeyEnvVar: "YOURTOOL_API_KEY",
APIKeyURL: "https://yourtool.example.com/keys",
InstallCmds: map[string][]installStep{
"windows": {{cmd: "npm.cmd", args: []string{"install", "-g", "yourtool"}}},
"darwin": {{cmd: "npm", args: []string{"install", "-g", "yourtool"}}},
"linux": {{cmd: "npm", args: []string{"install", "-g", "yourtool"}}},
},
},5. Add a probe arm
// cmd/relay/providers.go probeProvider
case "yourtool":
if commandExists("yourtool") { return ProbeAvailable, "yourtool CLI found" }
return ProbeNotFound, "yourtool CLI not found"6. Add a quota adapter
internal/quota/registry.go
BuildQuotaRegistry builds the provider → quota-adapter map
explicitly — there is no automatic fallback, so a
provider without an entry has no quota tracking at all. Unless your
provider exposes a real quota signal (like Claude's proxy headers), add
a RequestCountAdapter entry: it counts requests against the
declared cap from relay.toml and trips at 85%:
// internal/quota/registry.go BuildQuotaRegistry
adapter.ProviderYourTool: NewRequestCountAdapter("yourtool",
capFor(declaredCaps, adapter.ProviderYourTool, 500), 0.85),Local providers with no meaningful quota use
&NullQuotaAdapter{} (see Ollama).
7. Add config defaults
Give the provider a default enabled/cap entry in
internal/config/config.go Default():
// internal/config/config.go Default()
"yourtool": {Enabled: true, DeclaredCap: 500},and document the new key in the relay.toml template
(.relay/relay.toml).
Also touch
- Pricing (required): add per-Mtoken rates to
internal/pricing/pricing.goso the cost meter works. - Detection (optional): to make
relay detectfind already-running sessions of your provider, add a signature ininternal/detect/signatures.goand, if it stores transcripts on disk, a reader ininternal/detect/.
That's it. Ship it.
Ollama as a backend
Several providers (Claude Code, Codex, OpenCode, Cline) support
ollama launch — a built-in bridge that points them at a
local Ollama model. The desktop app surfaces this as a "Run via
Ollama" button on each provider card when ollama
is installed.
relay-ui → POST /api/ollama/launch {"provider":"claude","model":"qwen3.5"}
→ opens terminal: ollama launch claude --model qwen3.5
Use case: you have no Anthropic subscription but want to use Claude Code as your agent. Pick a local model.