Providers
Endpoints for managing AI providers (the connections to an AI service such as Anthropic, OpenAI, Google, Ollama, or Amazon Bedrock) and the models each provider exposes.
All provider routes require permission.ai.manage (superuser). A provider's
secret is sent in the key field, stored encrypted, and never returned in
clear text. The AI module must not be enabled on the tenant to read providers,
but creating a custom provider requires BYOK AI (see POST below).
GET /api/providers
List the tenant's providers, sorted by name.
Authentication: permission.ai.manage
Response:
A HAL collection under _embedded["inf:provider"]. Each provider links to its
models (inf:models). The key is never returned raw or encrypted: a provider
with a stored key serializes the masked indicator "****", and a keyless
provider serializes null.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers"
}
},
"start": 0,
"count": 3,
"total": 3,
"_embedded": {
"inf:provider": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Anthropic",
"slug": "anthropic",
"type": "anthropic",
"key": null,
"managed": true,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000002"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000002/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"name": "OpenAI",
"slug": "openai",
"type": "openai",
"key": null,
"managed": true,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000003"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000003/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000003",
"name": "OpenAI",
"slug": "openai-1",
"type": "openai",
"key": "****",
"managed": false,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
]
}
}
Provider properties:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Provider id |
name | string | Display name |
slug | string | URL-safe name, auto-derived |
type | string | Provider-driver id (anthropic, openai, google, ollama, bedrock) |
key | string | null | Masked credential indicator ("****") or null |
managed | boolean | System-managed (License Manager controlled) provider |
internal | boolean | Internal-only provider |
settings | object | null | Non-secret driver settings (e.g. Bedrock region) |
data | object | null | Driver-specific configuration |
createdAt / updatedAt | date | Timestamps |
POST /api/providers
Create a custom provider.
Authentication: permission.ai.manage
Pre-blocks: permission.ai.manage, BYOK check (server.ai.byok())
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
type | string | Yes | Provider-driver id (see GET /api/provider-types) |
key | string | No | Secret credential (API key, or a JSON envelope for Bedrock). Stored encrypted. |
settings | object | No | Non-secret driver settings |
data | object | No | Driver-specific configuration |
There is no credential object; the secret goes directly in key.
Example Request:
{
"name": "Acme OpenAI",
"type": "openai",
"key": "sk-your-api-key"
}
Response:
Responds 201 Created with the provider (key masked) and a Location header.
{
"name": "Acme OpenAI",
"type": "openai",
"key": "sk-your-api-key"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Acme OpenAI",
"type": "openai",
"key": "****",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"slug": "acme-openai",
"settings": null,
"data": null,
"internal": false,
"managed": false
}
Custom providers require BYOK AI. Without it the route answers 403, even for a
superuser.
{
"name": "Blocked Provider",
"type": "anthropic"
}
{
"error": "Forbidden",
"message": "Custom AI providers require BYOK AI"
}
GET /api/providers/{id}
Retrieve a single provider (key masked).
Authentication: permission.ai.manage
Pre-blocks: provider.lookup, permission.ai.manage
Response:
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Acme OpenAI",
"slug": "acme-openai",
"type": "openai",
"key": "****",
"managed": false,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
PUT /api/providers/{id}
Update a provider.
Authentication: permission.ai.manage
Pre-blocks: provider.lookup, permission.ai.manage
Request Body:
| Field | Type | Description |
|---|---|---|
name | string | Update display name |
type | string | Change provider type (custom providers only) |
key | string | null | Rotate the secret; null or "" clears it |
settings | object | Driver settings (merged, not replaced) |
data | object | Driver-specific configuration |
settings is always merged with the existing value (partial-update
semantics), so driver-derived metadata such as Bedrock's accessKeyIdHint
survives a region-only update.
Managed / internal providers accept only a key swap and updates to the
driver's allow-listed settings (e.g. Bedrock opts in region). Identity fields
(name, type, data) stay License-Manager controlled. A settings-only PUT
whose keys are all outside the allowlist collapses to an empty patch and
answers 403.
Example Request:
{ "key": "sk-rotated-api-key" }
Response:
Responds 200 with the updated provider.
{
"key": "sk-rotated-api-key"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001"
},
"inf:models": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Acme OpenAI",
"slug": "acme-openai",
"type": "openai",
"key": "****",
"managed": false,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
DELETE /api/providers/{id}
Delete a provider and cascade-delete its models.
Authentication: permission.ai.manage
Pre-blocks: provider.lookup, permission.ai.manage
Response:
Responds 200 with an empty body.
Deleting a provider removes all of its models. Managed providers cannot be
deleted (403).
Driver-specific payload validation
Each provider driver may apply additional validation and normalization to the
POST/PUT payload before the row is persisted. The driver receives the parsed
payload, may mutate it (e.g., to derive non-secret hints), and may throw a 400
if the shape is invalid for that driver.
Drivers without custom validation (OpenAI, Anthropic, Google, Ollama) accept any payload that satisfies the route-level Joi schema.
Amazon Bedrock
The bedrock provider expects credentials encoded as a JSON envelope in the
key field, plus a region in settings. Two envelope shapes are accepted:
Bearer token (Bedrock long-term API key):
{
"name": "AWS Bedrock",
"type": "bedrock",
"key": "{\"apiKey\":\"AWS_BEARER_TOKEN_BEDROCK_value\"}",
"settings": {
"region": "us-east-1"
}
}
AWS access keys (SigV4 authentication):
{
"name": "AWS Bedrock",
"type": "bedrock",
"key": "{\"accessKeyId\":\"AKIA…\",\"secretAccessKey\":\"…\"}",
"settings": {
"region": "us-east-1"
}
}
The driver derives a non-secret accessKeyIdHint (last 4 chars of the access
key ID) and stores it in settings for read-back UX. On PUT, settings is
merged with existing values rather than replaced, so partial updates (e.g.,
region only) preserve previously-derived metadata.
Validation rejects:
- A
keythat is not valid JSON - A JSON envelope missing both
apiKeyandaccessKeyId - A POST without
settings.region
GET /api/provider-types
Discover the provider drivers registered in the system.
Authentication: permission.ai.manage
Response:
A HAL collection of detective discoveries. Each item's value.name and
value.description are i18n message keys (e.g.
drivers:providers.openai.name) that the client resolves to the active locale.
Use the value.provider.type as the type when creating a provider.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/provider-types"
}
},
"items": [
{
"detective": "openai",
"id": "openai:0",
"value": {
"name": "drivers:providers.openai.name",
"description": "drivers:providers.openai.description",
"provider": {
"type": "openai",
"name": "drivers:providers.openai.name"
}
}
},
{
"detective": "anthropic",
"id": "anthropic:0",
"value": {
"name": "drivers:providers.anthropic.name",
"description": "drivers:providers.anthropic.description",
"provider": {
"type": "anthropic",
"name": "drivers:providers.anthropic.name"
}
}
},
{
"detective": "google",
"id": "google:0",
"value": {
"name": "drivers:providers.google.name",
"description": "drivers:providers.google.description",
"provider": {
"type": "google",
"name": "drivers:providers.google.name"
}
}
},
"… 2 more items (5 total) — omitted from docs"
],
"start": 0,
"count": 5,
"total": 5
}
GET /api/providers/{id}/models
List the models belonging to a provider.
Authentication: permission.ai.manage
Pre-blocks: provider.lookup, permission.ai.manage
Response:
A HAL collection under _embedded["inf:model"].
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"start": 0,
"count": 1,
"total": 1,
"_embedded": {
"inf:model": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models/00000000-0000-4000-8000-000000000002"
}
},
"naturalId": "claude-haiku-45",
"permissions": {},
"promptPpm": null,
"completionPpm": null,
"cacheReadPpm": null,
"cacheWriteShortPpm": null,
"cacheWriteLongPpm": null,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"providerId": "00000000-0000-4000-8000-000000000001",
"referencesId": null,
"name": "Claude Haiku 4.5",
"slug": "claude-haiku-45",
"model": "claude-haiku-4-5",
"options": null,
"chat": false,
"moderated": false,
"contextWindow": 200000,
"maxOutputTokens": null,
"instructions": null,
"description": null,
"managed": false,
"internal": false,
"deprecatedAt": null,
"web": false,
"attachments": false,
"generateImage": false,
"createEmbeddings": false,
"score": null,
"pricingTier": null,
"badges": null,
"tier": null,
"customized": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"provider": {
"permissions": {},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Anthropic",
"slug": "anthropic",
"type": "anthropic",
"key": null,
"managed": true,
"internal": false,
"settings": null,
"data": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
}
]
}
}
POST /api/providers/{id}/models
Add a model to a provider. providerId is taken from the URL.
Authentication: permission.ai.manage
Pre-blocks: permission.ai.manage, provider.lookup
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
model | string | No | Provider's model identifier (e.g. claude-opus-4) |
options | object | No | Driver-specific model options |
Response:
Responds 200 with the created model. Pricing columns (promptPpm,
completionPpm, the cache*Ppm family) and capability flags (web,
attachments, generateImage, createEmbeddings) default to null/false
and are managed separately.
{
"name": "Claude Opus 4",
"model": "claude-opus-4"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/providers/00000000-0000-4000-8000-000000000001/models"
}
},
"naturalId": "claude-opus-4",
"permissions": {},
"promptPpm": null,
"completionPpm": null,
"cacheReadPpm": null,
"cacheWriteShortPpm": null,
"cacheWriteLongPpm": null,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"web": false,
"attachments": false,
"generateImage": false,
"createEmbeddings": false,
"customized": false,
"name": "Claude Opus 4",
"model": "claude-opus-4",
"providerId": "00000000-0000-4000-8000-000000000001",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"slug": "claude-opus-4",
"options": null,
"instructions": null,
"moderated": false,
"chat": false,
"internal": false,
"contextWindow": 200000,
"description": null,
"referencesId": null,
"deprecatedAt": null,
"managed": false,
"score": null,
"pricingTier": null,
"badges": null,
"maxOutputTokens": null,
"tier": null
}