AI Providers

On this page 10

Buddy's AI features are bring-your-own-key. You choose the provider and model; nothing is proxied through a third party, and no AI runs unless you configure a key.

With no key configured, every AI feature is a no-op and the dependency bot works exactly as it always has.

Quick start

export ANTHROPIC_API_KEY="sk-ant-..."

That's the whole setup — Buddy picks up the key and uses a current Claude model by default.

Supported providers

ProviderConfig valueKey environment variableDefault model
AnthropicanthropicANTHROPIC_API_KEYclaude-opus-5
OpenAIopenaiOPENAI_API_KEY(specify one)
GooglegoogleGOOGLE_API_KEY or GEMINI_API_KEY(specify one)
OpenRouteropenrouterOPENROUTER_API_KEY(specify one)
OpenAI-compatibleopenai-compatibleOPENAI_COMPATIBLE_API_KEY(specify one)

Only Anthropic has a built-in default model. For the others you must name a model — Buddy won't guess at another vendor's catalogue and silently route your requests to a model you didn't choose.

Configuration

// buddy.config.ts
import type { BuddyConfig } from '@buddysh/buddy'

const config: BuddyConfig = {
  ai: {
    provider: 'anthropic',
    model: 'opus',
    effort: 'medium',
    maxTokensPerRun: 200_000,
  },
}

export default config
OptionTypeDescriptionDefault
enabledbooleanTurn all AI features off even with a key presenttrue
providersee table aboveWhich provider to usefirst with a key
modelstringAlias or concrete model IDprovider default
effort'low' | 'medium' | 'high'Reasoning depth to requestprovider default
apiKeyEnvstringEnvironment variable holding the keyprovider default
baseUrlstringEndpoint override for gatewaysprovider default
maxTokensPerRunnumberHard ceiling on output tokens per rununlimited

apiKeyEnv is the name of an environment variable, not a key. Config validation rejects anything key-shaped so a credential can't be committed to the repository by mistake.

Model aliases

Short names resolve to current Anthropic models, so your config doesn't need editing when a new version ships:

AliasResolves to
claude, opus, claude-opusclaude-opus-5
sonnet, claude-sonnetclaude-sonnet-5
haiku, claude-haikuclaude-haiku-4-5
fable, claude-fableclaude-fable-5

Anything that isn't an alias is passed through unchanged, so a model released after this table was written works immediately.

Auto-selection

With no provider set, Buddy uses the first provider that has a key available, in the order anthropic → openai → google → openrouter. A blank or whitespace-only key counts as absent.

Per-run overrides

BUDDY_MODEL=haiku buddy scan

BUDDY_MODEL overrides the configured model for a single run — useful for testing a cheaper model without touching config.

Gateways and self-hosted endpoints

Any OpenAI-compatible endpoint works through baseUrl:

ai: {
  provider: 'openai-compatible',
  baseUrl: 'https://gateway.internal/v1',
  model: 'your-model-id',
  apiKeyEnv: 'INTERNAL_GATEWAY_KEY',
}

For Anthropic behind a corporate gateway, set baseUrl to the gateway's origin.

Budgets

maxTokensPerRun caps output tokens across a whole run. The check happens before each request, so an exhausted budget can't spend one more request's worth of tokens on its way out; further calls fail with AiBudgetExceededError.

Security

  • Keys are read from the environment and never written to config, logs, or PR bodies.
  • Everything the AI layer logs passes through a redaction filter that masks provider keys, bearer tokens, GitHub tokens, and assignment-shaped secrets. Provider errors frequently echo request headers back, so this matters in practice.
  • Buddy never sends your source to a provider unless a feature you enabled explicitly does so.

Programmatic use

import { createAiClient } from '@buddysh/buddy'

const ai = createAiClient(config)
if (!ai) {
  // No key configured — AI features are off.
  return
}

const response = await ai.complete({
  system: 'You summarize dependency changes.',
  messages: [{ role: 'user', content: 'Summarize: react 17 -> 18' }],
})

console.log(response.text, response.usage.outputTokens)

complete() returns the same normalized shape on every provider: text, toolCalls, optional parsed json, a stopReason of end | tool_use | max_tokens | refusal | other, and usage.

A refusal stop reason is reported as itself rather than folded into end — a refusal comes back as a successful response with empty content, so treating it as a normal completion would read that emptiness as a valid answer.

Suggest a change to this page

Last updated: