AI API
Org-scoped AI provider management and a pass-through proxy: configure provider credentials once per org, then call any provider's native API through Kapable without shipping provider keys to clients.
The AI module is scope-gated: an sk_live_ key with the right
scope gets 200 (verified live — it's one of the surfaces in the
SDK's golden-path smoke test).
Providers
| Method | Path | SDK (client.ai) |
|---|---|---|
| GET | /v1/providers | listProviders |
| GET | /v1/providers/{id} | getProvider |
| POST | /v1/providers/{id}/configure | configureProvider |
| DELETE | /v1/providers/{id}/configure | removeProviderConfig |
Provider proxy
POST /v1/proxy/{provider}/{path} forwards the request to the
configured provider with the org's stored credentials. The SDK returns the raw
Response so you keep full control over streaming bodies.
A freshly provisioned org can call client.ai.proxy('openrouter', …)
(or POST /v1/proxy/openrouter/* directly) with no setup — the
platform funds a default key for that provider, so the call just works. This
default is provider-specific, by design: only providers the
platform funds a key for get one (currently just OpenRouter); calling any other
provider without first configuring your own key still returns 400 No API
key configured for provider. Configuring your own key for a provider
(configureProvider) always overrides the platform default for that
provider.
SDK Examples
// List providers and their configuration state
const { providers } = await client.ai.listProviders();
// Point the provider at a strongbox secret holding the org's upstream key —
// the raw key never travels through this SDK (store it via client.secrets first)
await client.ai.configureProvider('anthropic', { key_name: 'anthropic-api-key' });
// Call the provider's native API through the proxy
const res = await client.ai.proxy('anthropic', '/v1/messages', {
body: {
model: 'claude-sonnet-4-6',
max_tokens: 256,
messages: [{ role: 'user', content: 'Hello from Kapable' }],
},
});
const completion = await res.json();
let providers = client.ai().list_providers(ListProvidersParams::default()).await?;
let payload = serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Hello from Kapable"}]
});
let res = client.ai().proxy("anthropic", "/v1/messages", ProxyRequest {
body: Some(serde_json::to_vec(&payload)?),
..ProxyRequest::post()
}).await?;
Text-to-speech (M10)
A dedicated endpoint for spoken-summary generation — the counterpart to Artifacts' audio summaries. Distinct from the generic provider proxy above: this owns ElevenLabs key resolution so callers never touch the raw upstream API or strongbox directly.
| Method | Path | SDK (client.ai) |
|---|---|---|
| POST | /v1/ai/tts | tts |
| GET | /v1/ai/admin/tts-settings | getTtsSettings |
| PUT | /v1/ai/admin/tts-settings | putTtsSettings |
| DELETE | /v1/ai/admin/tts-settings | deleteTtsSettings |
Key resolution: if the org has stored its own ElevenLabs key
(PUT /v1/ai/admin/tts-settings, admin/owner only — encrypted at rest,
never echoed back), it synthesizes with no metering. Otherwise the
platform's own key synthesizes and a usage event (characters synthesized) is recorded.
text is capped at ~5000 characters per call (422 if exceeded); voice
is optional (org default, else a platform default).
// BYO org key (admin/owner only) — no metering once set.
await client.ai.putTtsSettings({ api_key: process.env.ELEVENLABS_API_KEY });
// Synthesize — returns raw audio/mpeg bytes.
const bytes = await client.ai.tts({ text: "Relate's fifty-fourth tick shipped…" });
// Attach to an artifact version (base64-encode first).
const b64 = Buffer.from(bytes).toString('base64');
await client.artifacts.attachAudio('relate-tick-54', 1, { data: b64 });
let bytes = client.ai().tts(TtsRequest { text: "…".into(), voice: None }).await?;
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
client.artifacts().attach_audio("relate-tick-54", 1, AudioAttach {
data: b64, content_type: None, duration_secs: None, generated_via: None,
}).await?;