Skip to content

LLM Providers

Raven is bring-your-own-key (BYOK). You supply credentials for the LLM providers you want to use; Raven encrypts them at rest with AES-256-GCM, decrypts them in-memory per request, and forwards the call to the upstream provider. Token costs are billed by the provider directly to your account — Raven never proxies money through itself.

Supported providers, in the enum that backs llm_provider_configs.provider:

  • anthropic — Claude family (chat / RAG generation)
  • openai — GPT models (chat) and text-embedding-3-* (embeddings)
  • coherecommand-* (chat), embed-* (embeddings), rerank-* (reranker)
  • ollama — local self-hosted models (chat + embeddings, no API key)
  • google, azure_openai, custom — enum values reserved; concrete provider classes not wired yet

The supported set is enforced in two places: the Go enum in internal/model/llm_provider.go and the Python registry in ai-worker/raven_worker/providers/registry.py.

Provider matrix

ProviderDefault chat / generation modelDefault embedding modelRerankingAPI key requiredNotes
anthropicclaude-* (passed per request)— (stub: NotImplementedError)yesSupports prompt caching + memory tool in the streaming RAG path.
openaigpt-* (passed per request)text-embedding-3-small (1536 dims)yes429 rate-limit errors retried with tenacity exponential back-off (max 5 attempts).
coherecommand-* (passed per request)embed-english-v3.0 (1024 dims)rerank-english-v3.0yesDefault reranker for hybrid retrieval. Required only if filters.rerank == "cohere".
ollamamodel name passed per requestnomic-embed-text (768 dims)noTalks to a local Ollama daemon at OLLAMA_BASE_URL (default http://ollama:11434).

Provider keys are scoped per organisation, not per workspace or KB. The active model and provider are selected per request.

Configure a provider (UI)

  1. Sign in and open the workspace switcher → SettingsLLM Providers.
  2. Click Add provider, pick a type (openai / anthropic / cohere / ollama), and give it a display_name (the friendly label the dashboard shows).
  3. Paste the upstream API key. The frontend submits it to POST /api/v1/orgs/{org_id}/llm-providers; the server encrypts it with AES-256-GCM before INSERT.
  4. Tick Set as default if you want this provider used when a request does not name one explicitly. Only one provider per organisation can be the default (is_default = true on llm_provider_configs).

The UI lives in frontend/src/api/llm-providers.ts and its consuming Vue components.

Configure a provider (API)

org_admin is required for every write. The path is nested under the organisation:

http
POST /api/v1/orgs/{org_id}/llm-providers
bash
curl -X POST "https://raven.example.com/api/v1/orgs/$ORG_ID/llm-providers" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "anthropic",
    "display_name": "Production Anthropic",
    "api_key": "sk-ant-api03-...",
    "is_default": true
  }'

The full request schema (CreateLLMProviderRequest) accepts:

FieldRequiredNotes
provideryesOne of the enum values above.
display_nameyes1–255 chars. Surfaced in the UI.
api_keyyesPlaintext key. Encrypted before write; never returned.
base_urlnoOverride the upstream base URL (e.g. for Ollama, custom).
confignoFree-form JSONB for provider-specific knobs.
is_defaultnoMarks this row as the org default for its provider type.

The response (LLMProviderResponse) intentionally omits api_key_encrypted and api_key_iv — they are tagged json:"-" in internal/model/llm_provider.go. Only an api_key_hint (last few characters) is ever returned.

The other endpoints on the same group:

MethodPathRolePurpose
GET/api/v1/orgs/{org_id}/llm-providersany memberList configured providers
GET/api/v1/orgs/{org_id}/llm-providers/{provider_id}any memberFetch one provider
PUT/api/v1/orgs/{org_id}/llm-providers/{provider_id}org_adminUpdate (re-encrypts new key)
DELETE/api/v1/orgs/{org_id}/llm-providers/{provider_id}org_adminRemove a provider
PUT/api/v1/orgs/{org_id}/llm-providers/{provider_id}/defaultorg_adminMark as the org default

Routes are registered in cmd/api/main.go under the llm := api.Group("/orgs/:org_id/llm-providers") block.

Selecting a provider per workspace / KB

Provider selection happens per request, not per workspace. The flow in ai-worker/raven_worker/providers/registry.py is:

  1. The caller (RAG service, embedding service) passes (org_id, provider_name, model) to get_provider_for_request(...).
  2. The registry queries llm_provider_configs filtering on org_id, provider = $provider_name, status = 'active', ordered by is_default DESC LIMIT 1. The active default for that provider wins.
  3. The encrypted API key is decrypted with decrypt_api_key, except for providers in _KEYLESS_PROVIDERS = {"ollama"}.
  4. The matching Python provider class (AnthropicEmbeddingProvider, OpenAIEmbeddingProvider, CohereEmbeddingProvider, OllamaEmbeddingProvider) is instantiated and cached at module level keyed by (org_id, provider_name, model).

What can override per request:

  • Provider — chat requests carry a provider field. RAG explicitly threads provider_name through _stream_anthropic / OpenAI / Cohere branches.
  • Model — passed alongside provider on the request; falls back to the provider's _DEFAULT_MODEL constant.
  • base_url — read from the llm_provider_configs.base_url column on decrypt. Lets you point ollama at a non-default host or front openai/cohere traffic through a corporate proxy.

There is currently no default_provider_id foreign key on the workspaces or knowledge_bases tables — defaults are at the organisation level. If you need per-KB routing, use the routing rules subsystem (/api/v1/orgs/{org_id}/routing-rules).

Encryption

Keys are encrypted with AES-256-GCM by internal/crypto/aes.go:

go
// Encrypt encrypts plaintext using AES-256-GCM with a random nonce.
// The key must be exactly 32 bytes. Returns (ciphertext, iv/nonce, error).
func Encrypt(plaintext []byte, key []byte) ([]byte, []byte, error)
  • Algorithm: AES-256-GCM (authenticated encryption, no padding).
  • Master key: 32 bytes. Anything else returns ErrInvalidKeyLength.
  • Nonce: 12 bytes (GCM default), generated fresh per encryption via crypto/rand and stored next to the ciphertext in api_key_iv.
  • Storage layout (migrations/00011_llm_provider_configs.sql):
    • api_key_encrypted BYTEA — ciphertext including GCM tag
    • api_key_iv BYTEA — per-row nonce
    • api_key_hint VARCHAR(20) — last few characters for UI display

The master key is provided as RAVEN_ENCRYPTION_AES_KEY — a 32-byte key, conventionally a 64-character hex string. Generate one with:

bash
openssl rand -hex 32

Set it before booting the API; compose enforces presence via ${RAVEN_ENCRYPTION_AES_KEY:?... is required}. The variable is bound to config.Encryption.AESKey in internal/config/config.go:

go
_ = v.BindEnv("encryption.aes_key", "RAVEN_ENCRYPTION_AES_KEY")

Key rotation

Raven does not yet support online re-encryption. To rotate the master key:

  1. Generate the new key: openssl rand -hex 32.
  2. Export both RAVEN_ENCRYPTION_AES_KEY_OLD (current) and RAVEN_ENCRYPTION_AES_KEY (new) on a maintenance host.
  3. For each row in llm_provider_configs, Decrypt(api_key_encrypted, api_key_iv, old_key) then Encrypt(plaintext, new_key) and UPDATE the row with the new ciphertext and nonce.
  4. Restart the API and AI worker with the new key only.
  5. Have each org_admin re-test their providers via the dashboard "Test connection" button to confirm successful decryption.

Re-issuing keys at the upstream provider (Anthropic, OpenAI, Cohere) and running PUT /api/v1/orgs/{org_id}/llm-providers/{provider_id} with the new api_key is the simpler alternative if you suspect a master-key compromise.

Capabilities matrix

Capabilityanthropicopenaicohereollama
Chat / RAG streamyesyesyesyes
Embeddingsno (stub raises NotImplementedError)yes (text-embedding-3-small, 1536 dims)yes (embed-english-v3.0, 1024 dims)yes (nomic-embed-text, 768 dims)
Rerankingnonoyes (rerank-english-v3.0, default)no
Prompt cachingyes (cache_control: ephemeral on system prompt)nonono
Web search toolyes (web_search_20260209, opt-in via RAVEN_ENABLE_WEB_SEARCH=true)nonono
Memory toolyes (opt-in via RAVEN_MEMORY_DIR)nonono

The Anthropic embedding class exists only so the registry returns a consistent error instead of "unknown provider" when an Anthropic-only org asks for embeddings — pair Anthropic chat with OpenAI / Cohere / Ollama for embeddings.

Ollama (self-hosted models)

Ollama is the recommended provider for air-gapped deployments and PII-sensitive workloads — nothing leaves the host. The defaults in ai-worker/raven_worker/providers/ollama_provider.py:

python
_DEFAULT_MODEL = "nomic-embed-text"
_DEFAULT_DIMENSIONS = 768
_DEFAULT_BASE_URL = "http://ollama:11434"

To wire it up:

bash
curl -X POST "https://raven.example.com/api/v1/orgs/$ORG_ID/llm-providers" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "ollama",
    "display_name": "On-prem Ollama",
    "api_key": "unused",
    "base_url": "http://ollama:11434",
    "config": {"is_local": true}
  }'

Notes:

  • The api_key field is required by the request schema but the registry treats ollama as a member of _KEYLESS_PROVIDERS and skips the decryption step. Pass any non-empty placeholder.
  • For Raven Local (single-user desktop mode) a row is auto-seeded by migrations/00039_seed_ollama_local_provider.sql with base_url = 'http://ollama:11434/v1' and is_default = true.
  • Pull the model on the Ollama host before first use: ollama pull nomic-embed-text. The provider raises ModelNotPulledError (mapped to a friendly UI affordance) on 404.

Default provider

The Go API does not ship a fallback BYOK key — defaults are per-org rows with is_default = true. The .env.example file exposes the upstream keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, COHERE_API_KEY) only for the dev-agent and worker bootstrap scripts; production traffic goes through the encrypted llm_provider_configs rows.

The org-level default is set by:

bash
curl -X PUT "https://raven.example.com/api/v1/orgs/$ORG_ID/llm-providers/$PROVIDER_ID/default" \
  -H "Authorization: Bearer $TOKEN"

Per-workspace overrides are not modelled at the schema level today; prefer explicit provider + model on each request, or use routing rules to map traffic patterns to a specific provider.

Rate limits and retries

Two layers of retry:

  • In-providerOpenAIEmbeddingProvider.embed() uses tenacity with wait_exponential(multiplier=1, min=1, max=60) and stop_after_attempt(5), retrying only on openai.RateLimitError (HTTP 429). Anthropic and Cohere rely on the worker's task retry.
  • Async job queueinternal/queue enqueues all worker tasks with asynq.MaxRetry(c.maxRetry). The default is 5 (v.SetDefault("queue.max_retry", 5) in internal/config/config.go); override with RAVEN_QUEUE_MAX_RETRY. Webhook delivery is a deliberate exception (asynq.MaxRetry(0) — the handler manages its own retry schedule).

Synchronous RAG streaming is not retried — a transient upstream failure surfaces as an error event on the SSE stream and the client decides whether to retry.

Costs

Raven does not bill customers for LLM tokens. Anthropic, OpenAI, and Cohere bill your account directly for the keys you upload. The only Raven-side costs are:

  • The platform subscription (handled by Hyperswitch — see /guides/billing).
  • Voice usage (Deepgram / LiveKit), tracked separately in voice_usage.

Ollama is free in absolute terms — the cost is the hardware running the daemon.

Cache aggressively to cut your provider bill: the Anthropic provider adds cache_control: ephemeral on the system prompt, and the semantic response cache (KB-level, see /guides/retrieval) deduplicates near-identical questions before any token is spent.

Troubleshooting

SymptomLikely causeFix
Upstream returns 401 UnauthorizedProvider key was revoked or pasted incorrectlyPUT /llm-providers/{id} with a fresh api_key. Confirm via the upstream provider dashboard.
Upstream returns 429 Too Many RequestsPer-minute / per-day quota exhaustedOpenAI auto-retries up to 5 times. For sustained pressure, raise the quota upstream or split across orgs.
model not found from OpenAI / AnthropicModel name typo or model deprecatedCheck the upstream provider's current model list — Raven passes the string through unchanged.
ModelNotPulledError from OllamaThe named model has not been pulled on the Ollama hostollama pull <model> on the host. Verify with curl http://ollama:11434/api/tags.
Worker logs decrypt: cipher: message authentication failedMaster key changed without re-encrypting rowsRestore the previous RAVEN_ENCRYPTION_AES_KEY or follow the rotation procedure above to re-encrypt rows.
decrypt_api_key returns NoneNo active provider config matching (org_id, provider)Add one via POST /llm-providers or mark an existing row active.
Cohere rerank silently disabledcohere Python package missing, or API call failedCheck worker logs for cohere_rerank_unavailable / cohere_rerank_failed. RRF results are returned anyway.

Related