AI Providers¶
Keen Code supports multiple AI providers through a plugin-like architecture. The provider system handles model discovery, authentication, and communication with different LLM backends.
Supported Providers¶
| Provider | ID | Authentication | Models |
|---|---|---|---|
| Anthropic | anthropic |
API Key | Claude Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5 |
| OpenAI | openai |
API Key | GPT-5.5, GPT-5.4, GPT-5.4-mini, GPT-5.3-codex |
| Codex | openai-codex |
OAuth | GPT-5.5, GPT-5.4, GPT-5.4-mini, GPT-5.3-codex |
| Google AI | googleai |
API Key | Gemini 3.1 Pro, 3.1 Flash-Lite, 3 Flash |
| Moonshot AI | moonshotai |
API Key | Kimi K2.6, K2.5, K2 Thinking, K2 Thinking Turbo |
| DeepSeek | deepseek |
API Key | DeepSeek V4 Flash, V4 Pro |
| Amazon Bedrock | amazon-bedrock |
AWS credentials | Claude Opus 4.8, Opus 4.6, Sonnet 4.6, Haiku 4.5 |
| MiniMax | minimax |
API Key | MiniMax M2.7, M2.5 |
| OpenCode Go | opencode-go |
API Key | GLM-5.1, GLM-5, Kimi K2.6, Kimi K2.5, DeepSeek V4 Pro, DeepSeek V4 Flash, MiMo-V2, MiniMax M2.7/M2.5, Qwen3 Plus/Max |
Provider Registry¶
Provider and model metadata is stored in providers/registry.yaml. This includes:
- Context window sizes
- Supported thinking efforts
- Model display names
// providers/loader.go
type Registry struct {
Providers []Provider `yaml:"providers"`
}
type Provider struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Models []Model `yaml:"models"`
}
type Model struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
ContextWindow int `yaml:"context_window"`
ThinkingEfforts []string `yaml:"thinking_efforts"`
}
Configuration¶
Global Config (~/.keen/configs.json)¶
{
"active_provider": "opencode-go",
"active_model": "kimi-k2.6",
"thinking_effort": "enabled",
"show_thinking": true,
"adversary_provider": "anthropic",
"adversary_model": "claude-sonnet-4-6",
"providers": {
"opencode-go": {
"models": ["kimi-k2.6"],
"api_key": "oc_..."
}
}
}
adversary_provider and adversary_model are set via /adversary model and control which model is used for adversarial reviews. They are independent of active_provider/active_model and can point to any configured provider.
Custom Headers¶
You can attach custom HTTP headers to a provider by adding a headers object to that provider's config in ~/.keen/configs.json. These headers are sent with every request to that provider.
{
"active_provider": "deepseek",
"active_model": "deepseek-v4-pro",
"providers": {
"deepseek": {
"models": ["deepseek-v4-pro"],
"api_key": "sk-...",
"headers": {
"x_header_1": "val1",
"x_header_2": "val2"
}
}
}
}
Notes:
- Header names and values are plain strings.
- They are set per-provider; different providers can have different headers.
- Custom headers must be added by editing the config file directly. The
/modelUI does not provide a field for them. - Applied to all clients: Anthropic, OpenAI, Codex, DeepSeek, Moonshot AI, Z.ai, MiniMax, OpenCode Go, Google AI (Genkit), and Amazon Bedrock.
Config Resolution¶
Keen loads ~/.keen/configs.json through internal/config.Loader, then builds the runtime ResolvedConfig in internal/cli/cmd/root.go.
Resolution order for the default interactive and headless paths:
1. Provider: global.active_provider
2. Model: global.active_model
3. API key: providers.{provider}.api_key_helper → providers.{provider}.api_key
For keen run --provider, the selected provider's config replaces the active provider for that invocation. If --model is omitted, Keen uses the selected provider's first configured model. The API key is still resolved through api_key_helper first, then api_key.
Authentication¶
API Key Authentication¶
Most providers use API key authentication. Keys are stored in the global config under providers.{provider}.api_key.
Instead of storing a key, a provider can define api_key_helper. Keen executes the helper locally when resolving the provider config, trims stdout, and uses that value as the in-memory API key for the current run/session. When api_key_helper is set, it always wins over api_key; api_key can be empty and Keen does not write the helper output back to ~/.keen/configs.json.
{
"active_provider": "anthropic",
"active_model": "claude-sonnet-4-6",
"providers": {
"anthropic": {
"models": ["claude-sonnet-4-6"],
"api_key": "",
"api_key_helper": "example-auth token || (example-auth login > /dev/null 2>&1 && example-auth token)"
}
}
}
Security note:
api_key_helperis executed as a shell command with the privileges of the running process. Treat it as executable code: never paste untrusted strings into this field, audit any helper script before use, and keep~/.keenpermissions strict (e.g.chmod 700 ~/.keenandchmod 600 ~/.keen/configs.json) so other local users cannot inject or read its contents.
MiniMax uses its Anthropic-compatible API. Users normally leave base_url unset. Keen uses https://api.minimax.io/anthropic, which the Anthropic SDK extends to /v1/messages.
OpenCode Go also uses API key authentication. Users normally leave base_url unset. Keen uses https://opencode.ai/zen/go/v1/ for OpenAI-compatible models and https://opencode.ai/zen/go for MiniMax models through the Anthropic SDK, which appends /v1/messages.
OAuth Authentication (OpenAI Codex)¶
OpenAI Codex uses OAuth with PKCE flow:
// internal/auth/oauth.go
type OAuthManager struct {
Store *Store
HTTPClient *http.Client
OpenBrowser BrowserOpener
}
Flow: 1. Generate PKCE verifier/challenge and state 2. Start local HTTP server on port 1455 3. Open browser to authorization URL 4. Receive callback, exchange code for tokens 5. Store refresh/access tokens
Token refresh is automatic when the access token expires.
AWS Authentication (Amazon Bedrock)¶
Amazon Bedrock uses AWS credential authentication via the AWS SDK:
// internal/config/config.go
const AuthModeAWS = "aws"
Credentials are loaded from the standard AWS credential chain (~/.aws/credentials, environment variables, IAM roles, etc.). No API key is stored in Keen's global config. An optional custom base_url can be configured to override the Bedrock endpoint.
The default AWS region is us-east-1 if none is configured in the environment.
LLM Client Architecture¶
// internal/llm/client.go
type LLMClient interface {
StreamChat(ctx context.Context, messages []Message, toolRegistry *tools.Registry) (<-chan StreamEvent, error)
Reset()
}
Three client implementations:
AnthropicClient (internal/llm/anthropic.go)¶
Direct integration with Anthropic SDK:
- Streaming via ssestream.Stream
- Tool conversion to Anthropic tool format
- Thinking budget support (low/medium/high/max)
- Cached token tracking
- MiniMax models (MiniMax-M2.7, MiniMax-M2.5) through MiniMax's Anthropic-compatible /messages endpoint
- OpenCode Go MiniMax models (minimax-m2.*) and qwen3.7-max through the Anthropic-compatible /messages endpoint
BedrockClient (internal/llm/bedrock.go)¶
AWS SDK integration for Amazon Bedrock:
- Streaming via bedrockruntime.ConverseStream
- Tool conversion to Bedrock tool format
- Reasoning content support (thinking text, signatures, redacted content)
- Prompt caching with cache points on system prompts, tools, and messages
- Cached token tracking
OpenAIResponsesClient (internal/llm/openai_responses.go)¶
OpenAI Responses API for: - OpenAI (GPT models)
OpenAICompatibleClient (internal/llm/openai.go)¶
OpenAI-compatible API for: - DeepSeek - Moonshot AI (Kimi) - Z.ai (GLM) - OpenCode Go GLM, Kimi, DeepSeek, MiMo, and OpenAI-compatible Qwen models
Handles provider-specific features like the reasoning_content extension and thinking controls for compatible providers.
GenkitClient (internal/llm/genkit.go)¶
Firebase Genkit integration for Google AI (Gemini). Currently the only provider using Genkit.
Stream Events¶
All clients emit a unified stream of events:
type StreamEvent struct {
Type StreamEventType
Content string // for Chunk, ReasoningChunk
ToolCall *ToolCall // for ToolStart, ToolEnd
Usage *TokenUsage // for Usage
Error error // for Error, Retry
Attempt int // for Retry
}
Event types:
- StreamEventTypeChunk - Text content delta
- StreamEventTypeReasoningChunk - Thinking/reasoning content
- StreamEventTypeToolStart - Tool execution begins
- StreamEventTypeToolEnd - Tool execution completes
- StreamEventTypeUsage - Token usage stats
- StreamEventTypeDone - Response complete
- StreamEventTypeError - Unrecoverable error
- StreamEventTypeRetry - Retrying after error
- StreamEventTypeIncomplete - Turn limit reached with pending state
Thinking Efforts¶
Models support different thinking effort levels:
| Provider | Efforts |
|---|---|
| Anthropic | low, medium, high, max |
| OpenAI | none, low, medium, high, xhigh |
| Google AI | low, medium, high, minimal |
| DeepSeek | off, high, max |
| Amazon Bedrock | low, medium, high, max |
| Z.ai | enabled, disabled |
| OpenCode Go DeepSeek | off, high, max |
| OpenCode Go GLM/Kimi/OpenAI-compatible Qwen | enabled, disabled |
The thinking effort is set via config and passed to the LLM client, which configures the provider's thinking parameters.
OpenCode Go thinking controls are model-family specific:
- DeepSeek sends thinking.type plus reasoning_effort for enabled efforts.
- GLM and Kimi send thinking.type.
- OpenAI-compatible Qwen sends enable_thinking.
- MiMo and MiniMax do not receive a Keen-sent thinking control; returned reasoning is still streamed when the provider exposes it.