Custom Providers
Octipus supports two custom-provider flavors for connecting to LLM endpoints that aren’t backed by a first-party provider class. Pick the one that matches the upstream wire format:
| Flavor | provider column value | Wire format | Use for |
|---|---|---|---|
| Custom OpenAI-compatible | custom-openai | OpenAI /v1/chat/completions | vLLM, Together, Groq, Fireworks, DeepInfra, internal OpenAI-shaped proxies |
| Custom Gemini-compatible | custom-gemini | Native Google Gemini (candidates[].content.parts[]) | Vertex AI, Google AI Studio (native), Gemini-fronting proxies |
| Custom Anthropic-compatible | custom-anthropic | Native Anthropic Messages (POST /v1/messages, content[] blocks) | Self-hosted / proxied Anthropic-shaped endpoints, Bedrock-style Messages gateways |
Both are stateless — configuration lives entirely on the model_config row
and is loaded per call. Add as many models against the same upstream as needed;
each row points at its own endpoint and key.
Reaching Google Vertex AI? For service-account auth (no static keys), prefer the first-class
vertexprovider below over thecustom-geminiroute. Usecustom-geminionly for an API-key or proxied Gemini endpoint.
First-class Vertex AI provider
Section titled “First-class Vertex AI provider”provider = 'vertex' is a built-in provider (not a custom flavor) that talks to
Vertex’s OpenAI-compatible endpoint and authenticates with a short-lived
OAuth2 access token minted from a service account — no static API key.
- Credentials. Store the service-account JSON in the vault as
system/vertex_service_account, or setVERTEX_SERVICE_ACCOUNT_JSON. The token is minted via the RS256 JWT-bearer grant (node:crypto, nogoogle-auth-librarydependency), cached, and refreshed ~60 s before expiry; concurrent refreshes coalesce into one mint. Only the derived bearer token ever reaches the model client. - Project / region.
VERTEX_PROJECT(orGOOGLE_CLOUD_PROJECT, or the SA’sproject_id) andVERTEX_LOCATION(defaultus-central1). - Model selection. A model is routed here by a
vertex/(orvertex_ai/) prefix — e.g.vertex/gemini-2.0-flash, which is sent to Vertex asgoogle/gemini-2.0-flash.checkHealth()mints a token to verify the credential path without a model call.
Configuration
Section titled “Configuration”A custom-provider model row uses the existing model_config columns plus a
metadata.customProvider block:
{ name: 'tpg-flash', // user-facing name (unique) modelId: 'gemini-3-flash-preview', // model id sent upstream provider: 'custom-gemini', // routes to the custom provider endpoint: 'https://api.example.com', // base URL (no trailing slash) apiKeyRef: 'tpg_api_key', // vault entry name (or 'env:VAR_NAME') metadata: { customProvider: { auth: { type: 'bearer' }, // 'bearer' | 'header' | 'query' // pathOverride: '/generate', // optional, replaces the default path // extraHeaders: { 'X-Org': 'foo' }, // optional }, },}Auth schemes
Section titled “Auth schemes”auth.type | Required fields | Wire effect |
|---|---|---|
bearer | — | Authorization: Bearer <key> |
header | headerName (e.g. x-api-key) | <headerName>: <key> |
query | paramName (e.g. key) | ?<paramName>=<key> |
API key resolution
Section titled “API key resolution”apiKeyRef is resolved in this order:
env:VAR_NAMEprefix → readsprocess.env.VAR_NAMEdirectly- Vault lookup by name (
getVault().getByName('system', apiKeyRef)) - Fallback env var:
CUSTOM_OPENAI_API_KEYorCUSTOM_GEMINI_API_KEY
Use env: for local development, vault for shared/production.
Request format (Gemini-compat only)
Section titled “Request format (Gemini-compat only)”The custom-gemini provider always sends requests in native Google Gemini wire format:
- Path (defaults to):
POST {endpoint}/v1beta/models/{modelId}:generateContent(or:streamGenerateContentfor streaming) - Override: Use
metadata.customProvider.pathOverrideto point at a different endpoint path if needed
Request body follows Google Gemini’s native structure:
{ "contents": [{ "role": "user", "parts": [{ "text": "..." }] }], "systemInstruction": { "parts": [{ "text": "..." }] }, "generationConfig": { "temperature": 0.7, "maxOutputTokens": 4096 }, "tools": [{ "functionDeclarations": [...] }]}Use for: real Google Gemini API, Vertex AI generative endpoints, or Gemini-API-compatible proxies that expect this exact wire format.
For proxies with non-standard request/response shapes, use custom-openai with a compatible endpoint instead.
Streaming
Section titled “Streaming”- OpenAI-compat: SSE via the OpenAI SDK. Standard
delta.content/delta.tool_callsdeltas. - Gemini-compat: SSE with
data: {gemini-chunk}\n\nevents. Each event contains the full chunk shape; we yield text deltas and tool-call deltas as they arrive.
Tool calling
Section titled “Tool calling”Both flavors support function/tool calling via the standard Octipus tool
schema (OpenAI-style). The Gemini-compat provider translates schemas
on the way out (functionDeclarations) and parses functionCall parts
on the way in.
What’s NOT supported (yet)
Section titled “What’s NOT supported (yet)”- Image / multi-modal input — text only for now
- Embeddings — no custom flavor implements
embed() - Batch / parallel mode (proxy-specific feature)
Local-runtime presets (one-click self-hosted models)
Section titled “Local-runtime presets (one-click self-hosted models)”Local OpenAI-compatible model servers (LM Studio, llama.cpp, vLLM, SGLang, TGI,
Ollama’s /v1) all speak the same wire format as the custom-openai provider,
so they need no new provider class — only their boilerplate (endpoint, path,
auth). src/models/providers/presets.ts supplies that, plus model
autodiscovery.
| Method | Endpoint | Auth | Purpose |
|---|---|---|---|
| GET | /api/models/presets | any user | List the known local-runtime presets. |
| POST | /api/models/discover | admin | { endpoint, apiKey? } → { models: string[], healthy: boolean } — lists the endpoint’s /models and probes health. |
Flow (setup wizard / models UI)
Section titled “Flow (setup wizard / models UI)”GET /api/models/presets→ user picks a runtime (e.g. LM Studio →http://localhost:1234/v1).POST /api/models/discoverwith the (default or edited) endpoint → autodiscovered model ids + a health flag.- For a chosen model,
buildModelConfigFromPreset(preset, modelId, endpoint)produces thecustom-openaimodel_configfields, then register it via the existingPOST /api/models.
Presets included: lmstudio, llamacpp, vllm, sglang, tgi,
ollama-openai. Discovery/health degrade gracefully (return [] / false)
when the endpoint is unreachable, so the UI can fall back to manual entry.