curl --request POST \
--url https://{host}/api/llm-gateway/admin/providers \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"base_url": "<string>",
"model_provider": "<string>",
"name": "<string>",
"api_key": "<string>",
"auth_type": "<string>",
"billing_note": "<string>",
"catalog_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"change_note": "<string>",
"connection_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credentials": "<unknown>",
"enforce_health_check": true,
"models": [
"<string>"
],
"request_timeout_secs": 123,
"settings": "<unknown>",
"stream_idle_timeout_secs": 123
}
'import requests
url = "https://{host}/api/llm-gateway/admin/providers"
payload = {
"base_url": "<string>",
"model_provider": "<string>",
"name": "<string>",
"api_key": "<string>",
"auth_type": "<string>",
"billing_note": "<string>",
"catalog_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"change_note": "<string>",
"connection_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credentials": "<unknown>",
"enforce_health_check": True,
"models": ["<string>"],
"request_timeout_secs": 123,
"settings": "<unknown>",
"stream_idle_timeout_secs": 123
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
base_url: '<string>',
model_provider: '<string>',
name: '<string>',
api_key: '<string>',
auth_type: '<string>',
billing_note: '<string>',
catalog_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
change_note: '<string>',
connection_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
credentials: '<unknown>',
enforce_health_check: true,
models: ['<string>'],
request_timeout_secs: 123,
settings: '<unknown>',
stream_idle_timeout_secs: 123
})
};
fetch('https://{host}/api/llm-gateway/admin/providers', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{host}/api/llm-gateway/admin/providers",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'base_url' => '<string>',
'model_provider' => '<string>',
'name' => '<string>',
'api_key' => '<string>',
'auth_type' => '<string>',
'billing_note' => '<string>',
'catalog_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'change_note' => '<string>',
'connection_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'credentials' => '<unknown>',
'enforce_health_check' => true,
'models' => [
'<string>'
],
'request_timeout_secs' => 123,
'settings' => '<unknown>',
'stream_idle_timeout_secs' => 123
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://{host}/api/llm-gateway/admin/providers"
payload := strings.NewReader("{\n \"base_url\": \"<string>\",\n \"model_provider\": \"<string>\",\n \"name\": \"<string>\",\n \"api_key\": \"<string>\",\n \"auth_type\": \"<string>\",\n \"billing_note\": \"<string>\",\n \"catalog_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"change_note\": \"<string>\",\n \"connection_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"credentials\": \"<unknown>\",\n \"enforce_health_check\": true,\n \"models\": [\n \"<string>\"\n ],\n \"request_timeout_secs\": 123,\n \"settings\": \"<unknown>\",\n \"stream_idle_timeout_secs\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://{host}/api/llm-gateway/admin/providers")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"base_url\": \"<string>\",\n \"model_provider\": \"<string>\",\n \"name\": \"<string>\",\n \"api_key\": \"<string>\",\n \"auth_type\": \"<string>\",\n \"billing_note\": \"<string>\",\n \"catalog_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"change_note\": \"<string>\",\n \"connection_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"credentials\": \"<unknown>\",\n \"enforce_health_check\": true,\n \"models\": [\n \"<string>\"\n ],\n \"request_timeout_secs\": 123,\n \"settings\": \"<unknown>\",\n \"stream_idle_timeout_secs\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{host}/api/llm-gateway/admin/providers")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"base_url\": \"<string>\",\n \"model_provider\": \"<string>\",\n \"name\": \"<string>\",\n \"api_key\": \"<string>\",\n \"auth_type\": \"<string>\",\n \"billing_note\": \"<string>\",\n \"catalog_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"change_note\": \"<string>\",\n \"connection_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"credentials\": \"<unknown>\",\n \"enforce_health_check\": true,\n \"models\": [\n \"<string>\"\n ],\n \"request_timeout_secs\": 123,\n \"settings\": \"<unknown>\",\n \"stream_idle_timeout_secs\": 123\n}"
response = http.request(request)
puts response.read_body{
"auth_type": "<string>",
"base_url": "<string>",
"billing_mode": "per_token",
"created_at": "2023-11-07T05:31:56Z",
"enabled": true,
"enforce_health_check": true,
"health_status": "<string>",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"model_provider": "<string>",
"model_sync_mode": "off",
"name": "<string>",
"org_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"secret_path": "<string>",
"settings": "<unknown>",
"updated_at": "2023-11-07T05:31:56Z",
"created_by_email": "<string>",
"created_by_name": "<string>",
"created_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"last_change_note": "<string>",
"last_modified_at": "2023-11-07T05:31:56Z",
"last_modified_by_email": "<string>",
"last_modified_by_name": "<string>",
"last_modified_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"billing_note": "<string>",
"billing_reason": "subscription",
"catalog_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"catalog_slug": "<string>",
"connection_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"health_checked_at": "2023-11-07T05:31:56Z",
"health_detail": "<string>",
"request_timeout_secs": 123,
"stream_idle_timeout_secs": 123
}Create a provider
curl --request POST \
--url https://{host}/api/llm-gateway/admin/providers \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"base_url": "<string>",
"model_provider": "<string>",
"name": "<string>",
"api_key": "<string>",
"auth_type": "<string>",
"billing_note": "<string>",
"catalog_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"change_note": "<string>",
"connection_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credentials": "<unknown>",
"enforce_health_check": true,
"models": [
"<string>"
],
"request_timeout_secs": 123,
"settings": "<unknown>",
"stream_idle_timeout_secs": 123
}
'import requests
url = "https://{host}/api/llm-gateway/admin/providers"
payload = {
"base_url": "<string>",
"model_provider": "<string>",
"name": "<string>",
"api_key": "<string>",
"auth_type": "<string>",
"billing_note": "<string>",
"catalog_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"change_note": "<string>",
"connection_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credentials": "<unknown>",
"enforce_health_check": True,
"models": ["<string>"],
"request_timeout_secs": 123,
"settings": "<unknown>",
"stream_idle_timeout_secs": 123
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
base_url: '<string>',
model_provider: '<string>',
name: '<string>',
api_key: '<string>',
auth_type: '<string>',
billing_note: '<string>',
catalog_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
change_note: '<string>',
connection_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
credentials: '<unknown>',
enforce_health_check: true,
models: ['<string>'],
request_timeout_secs: 123,
settings: '<unknown>',
stream_idle_timeout_secs: 123
})
};
fetch('https://{host}/api/llm-gateway/admin/providers', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{host}/api/llm-gateway/admin/providers",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'base_url' => '<string>',
'model_provider' => '<string>',
'name' => '<string>',
'api_key' => '<string>',
'auth_type' => '<string>',
'billing_note' => '<string>',
'catalog_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'change_note' => '<string>',
'connection_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'credentials' => '<unknown>',
'enforce_health_check' => true,
'models' => [
'<string>'
],
'request_timeout_secs' => 123,
'settings' => '<unknown>',
'stream_idle_timeout_secs' => 123
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://{host}/api/llm-gateway/admin/providers"
payload := strings.NewReader("{\n \"base_url\": \"<string>\",\n \"model_provider\": \"<string>\",\n \"name\": \"<string>\",\n \"api_key\": \"<string>\",\n \"auth_type\": \"<string>\",\n \"billing_note\": \"<string>\",\n \"catalog_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"change_note\": \"<string>\",\n \"connection_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"credentials\": \"<unknown>\",\n \"enforce_health_check\": true,\n \"models\": [\n \"<string>\"\n ],\n \"request_timeout_secs\": 123,\n \"settings\": \"<unknown>\",\n \"stream_idle_timeout_secs\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://{host}/api/llm-gateway/admin/providers")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"base_url\": \"<string>\",\n \"model_provider\": \"<string>\",\n \"name\": \"<string>\",\n \"api_key\": \"<string>\",\n \"auth_type\": \"<string>\",\n \"billing_note\": \"<string>\",\n \"catalog_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"change_note\": \"<string>\",\n \"connection_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"credentials\": \"<unknown>\",\n \"enforce_health_check\": true,\n \"models\": [\n \"<string>\"\n ],\n \"request_timeout_secs\": 123,\n \"settings\": \"<unknown>\",\n \"stream_idle_timeout_secs\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{host}/api/llm-gateway/admin/providers")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"base_url\": \"<string>\",\n \"model_provider\": \"<string>\",\n \"name\": \"<string>\",\n \"api_key\": \"<string>\",\n \"auth_type\": \"<string>\",\n \"billing_note\": \"<string>\",\n \"catalog_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"change_note\": \"<string>\",\n \"connection_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"credentials\": \"<unknown>\",\n \"enforce_health_check\": true,\n \"models\": [\n \"<string>\"\n ],\n \"request_timeout_secs\": 123,\n \"settings\": \"<unknown>\",\n \"stream_idle_timeout_secs\": 123\n}"
response = http.request(request)
puts response.read_body{
"auth_type": "<string>",
"base_url": "<string>",
"billing_mode": "per_token",
"created_at": "2023-11-07T05:31:56Z",
"enabled": true,
"enforce_health_check": true,
"health_status": "<string>",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"model_provider": "<string>",
"model_sync_mode": "off",
"name": "<string>",
"org_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"secret_path": "<string>",
"settings": "<unknown>",
"updated_at": "2023-11-07T05:31:56Z",
"created_by_email": "<string>",
"created_by_name": "<string>",
"created_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"last_change_note": "<string>",
"last_modified_at": "2023-11-07T05:31:56Z",
"last_modified_by_email": "<string>",
"last_modified_by_name": "<string>",
"last_modified_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"billing_note": "<string>",
"billing_reason": "subscription",
"catalog_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"catalog_slug": "<string>",
"connection_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"health_checked_at": "2023-11-07T05:31:56Z",
"health_detail": "<string>",
"request_timeout_secs": 123,
"stream_idle_timeout_secs": 123
}Authorizations
A Barndoor API key, sent as Authorization: Bearer <key>. Keys are prefixed bdai_ and are opaque — do not attempt to decode one. Interchangeable with an OAuth 2.0 access token.
Body
The provider's upstream API key. Required when connection_id is not supplied.
Whether the gateway records a per-token cost (BCP-3876). Omitted means
per_token — a provider never stops recording cost by default, and
nothing is inferred server-side. The create form suggests from the
catalog entry and the auth type; openrouter is why inference would be
wrong (it ships pricing: [] and is API-billed).
per_token, not_metered Optional free-text context for the arrangement, max
[BILLING_NOTE_MAX_LEN] chars.
How the vendor bills. Required when billing_mode is not_metered,
optional otherwise — see validate_billing_fields.
subscription, local, external, other Optional reference to a provider_catalog entry.
Optional free-text note explaining why this provider was added
(BCP-3998). Distinct from billing_note, which describes the billing
arrangement and persists; this describes one change and is replaced by
the next edit's note.
Optional reference to a connection for shared credentials.
Structured provider credentials. Preferred over api_key for
provider-specific auth schemas; never returned by any API.
Whether routing should gate on the connectivity health check.
None/omitted defaults to true (enforce). Set false to override
the gate and serve the provider even when its probe fails.
Whether this provider tracks Barndoor's model catalog (BCP-3811).
Omitted means off — a provider is never enrolled by default.
off, additive, full Upstream models to auto-create as 1:1 model mappings (alias = upstream name).
Provider-tier total-request timeout override (non-streaming).
Operator-only and None by default: the effective value is written at
the route tier (model_mappings), so this applies only if a route row
leaves it unset, and then to every such route served by this provider.
The range is fixed platform policy
(TOTAL_REQUEST_TIMEOUT_MIN_SECS..=TOTAL_REQUEST_TIMEOUT_MAX_SECS); see
[crate::proxy::timeouts].
Provider-tier stream-idle timeout override (streaming). Operator-only
and None by default; the effective value is written at the route
tier (model_mappings). The range is fixed platform policy
(STREAM_IDLE_TIMEOUT_MIN_SECS..=STREAM_IDLE_TIMEOUT_MAX_SECS); see
[crate::proxy::timeouts].
Response
The created record
Who created and last changed this provider, and why (BCP-3998).
Absent in the resolve/route queries -- the request path never reads it
-- and read with try_get, so those rows come back unattributed rather
than failing. That is indistinguishable from a row nobody has touched
since V75, which is the correct thing for a value the request path was
never given.
Whether the gateway records a per-token cost for this provider's
traffic (V69 / BCP-3876). PerToken for every provider that has not
opted in, which is the default and the back-compat value.
The request path does not read this field: billing consults
[ai_governance::CostCalculator]'s own cached not-metered set, which
is refreshed from the same query as pricing. This exists for the admin
API surface and the audit trail.
per_token, not_metered Operator override for the health gate (V46). When true (default)
routing requires health_status <> 'unhealthy'; when false the gate
is bypassed and the provider serves regardless of probe results (we
still probe and surface health). Lets an admin force traffic through a
false-positive probe.
Observed upstream reachability, recorded separately from enabled
(operator intent) so the system never overwrites the toggle (V45).
One of unverified | healthy | unhealthy. unverified is the
back-compat default and also covers provider families we don't probe
(Bedrock/Vertex/Azure, claude_oauth, codex_oauth). A provider serves traffic only
when enabled AND health_status <> 'unhealthy': a failed probe
auto-suspends it from routing (see resolve_*) without disabling it,
and it resumes once a probe records it healthy again.
Whether this provider tracks Barndoor's model catalog, and how far
(V63 / BCP-3811). Off for every provider that has not opted in,
which is the default and the back-compat value.
off, additive, full Optional free text explaining why the last change was made ("Raised for the Q3 batch job", "Vendor deprecated the old endpoint").
Admin-authored and rendered in the portal, so it is untrusted input: length-capped here, escaped on render.
When the change named by last_modified_by_* happened.
Its own column rather than the row's updated_at, because updated_at
is bumped by writes nobody is attributed for -- pairing the two would
date a named admin's edit to a background job's clock. See the V75
migration for the full reasoning.
Optional free-text context for the arrangement (max 200 chars). Human-readable only — never parsed, never aggregated.
How the vendor bills this provider. Required when billing_mode is
NotMetered, optional otherwise (V69 CHECK-enforced) — a subscription
billing overages is both per-token and a subscription. Purely
descriptive: no code branches on it, which is what lets a new reason be
added without touching the billing path.
subscription, local, external, other When the last connectivity probe ran (NULL until the first probe).
Reason for the last unhealthy or unverified probe (NULL if healthy or never probed).
Provider-tier total-request timeout override (non-streaming).
Operator-only and None by default: the effective value is written
at the route tier (model_mappings), so this only applies if a route
row somehow leaves it unset. Range
TOTAL_REQUEST_TIMEOUT_MIN_SECS..=TOTAL_REQUEST_TIMEOUT_MAX_SECS is
enforced by the CHECK constraint (see proxy::timeouts).
Provider-tier stream-idle timeout override (streaming). Operator-only
and None by default; the effective value is written at the route
tier (model_mappings). Range
STREAM_IDLE_TIMEOUT_MIN_SECS..=STREAM_IDLE_TIMEOUT_MAX_SECS is
enforced by the CHECK constraint (see proxy::timeouts).