Skip to content

OpenRouter — Reverse-Engineering Pass & Router.Africa Comparison

Status: Reference doc / input to API design Owner: Hassan Last updated: 2026-09-18 (rev 2 — rebuilt against the OpenAPI spec) Sources: OpenRouter’s openapi.json (the authoritative source for everything in Sections 1 and 3), plus openrouter.ai/docs/* prose pages and a live GET https://openrouter.ai/api/v1/models. Bifrost column from docs.getbifrost.ai. Internal context from design-doc.md, business-model.md, plugins-and-extensibility.md.

Revision note. Rev 1 of this doc was written from the prose documentation pages only. The OpenAPI spec is substantially larger than those pages describe, and it corrected several conclusions — most importantly that OpenRouter’s multi-tenancy and governance surface is larger than Bifrost’s, not smaller. Rev 1’s §3.5 said the opposite. Corrections are marked [rev 2] throughout.

Everything below is publicly documented product surface: routing policy, API contract, pricing model, failure handling. OpenRouter’s internal architecture (infra, DB, deployment) is not public, is not documented here, and is not the useful target — per earlier discussion.

The deliverable is Section 3: a behaviour-by-behaviour comparison of OpenRouter → Router.Africa (via Bifrost) → gap. Sections 1–2 are the evidence behind it. Section 4 is the resulting build list.

Bifrost claims are from public Bifrost documentation, not from the live deployment. Several need verification against prod-bifrost-core.router.africa before they can be treated as settled — flagged inline and collected in Section 5.


  • Base URL https://openrouter.ai/api/v1. Regional hostnames exist: eu.openrouter.ai (europe) and us.openrouter.ai (us), with openrouter.ai as global — these are the enforcement points for in-region routing (§1.5).
  • Auth: Authorization: Bearer <key>. Two key classes: ordinary inference keys and management keys, which can call the admin surface but not inference. A third mechanism, RFC 8693 token exchange (POST /oauth/token), swaps a JWT from your own IdP for a ≤15-minute OpenRouter access token — workload identity without distributing long-lived keys.
  • Attribution headers (leaderboard only, no functional effect): HTTP-Referer, X-OpenRouter-Title, X-OpenRouter-Categories.
  • X-OpenRouter-Metadata: enabled opts into an openrouter_metadata object on the response (§1.4).
  • Model IDs are vendor/model slugs, with :free, :nitro, :floor variant suffixes and ~vendor/model-latest aliases that carry an alias_target in the catalog.
  • Inference endpoints — three wire formats, not one:
    • POST /chat/completions — OpenAI Chat Completions
    • POST /responses — OpenAI Responses API (stateless; previous_response_id is explicitly rejected)
    • POST /messages — Anthropic Messages, including thinking, cache_control, context_management, and a fallbacks array (max 3, model only, mutually exclusive with models)
    • Plus /embeddings, /rerank, /images, /videos, /audio/speech, /audio/transcriptions, and an alpha /api/alpha/decisions (structured scoring/classification).
  • Request body — OpenAI set plus OpenRouter extensions:
    Field What it does
    models[] Model fallback chain
    provider Provider routing preferences object (§1.2)
    plugins[] auto-router, auto-beta-router, pareto-router, fusion, web, web-fetch, file-parser, moderation, response-healing, context-compression
    session_id Sticky routing key — see §1.2
    service_tier auto|default|fast|flex|priority|scale
    user Per-end-user abuse isolation — see below
    max_tool_calls Server-tool agent step cap (default & max 30)
    stop_server_tools_when[] Stop conditions: step_count_is, has_tool_call, max_tokens_used, max_cost (dollar ceiling), finish_reason_is
    prompt_cache_key, prompt_cache_options Explicit cache control
    metadata, trace Observability tagging (≤16 pairs; trace_id/span_name/etc.)
    route Deprecated alias for provider.sort.partition
  • user is an abuse-isolation primitive, not just a label. Spec: “When a provider requires a user identity, OpenRouter folds it into the hashed identity sent upstream and never forwards it raw. If omitted, requests use an account-level identity, so provider policy blocks can affect the whole account.” This is precisely the reseller-liability containment design-doc §6 calls for.
  • Response: OpenAI-shaped, with normalised finish_reason (tool_calls|stop|length|content_filter|error) alongside raw native_finish_reason; model reports what actually ran; service_tier reports what the provider actually used; usage carries cost (§1.3).

(a) Provider routing — same model, multiple hosts. Default, no config: filter providers with outages in the last 30s, then select weighted by inverse square of price; survivors form the fallback cascade. Setting sort or order disables load balancing.

provider object (full spec list): order[], allow_fallbacks (default true), require_parameters, data_collection (allow|deny), only[], ignore[] (merges with account-level), quantizations[] (int4/int8/fp4/mxfp4/nvfp4/fp6/fp8/mxfp8/fp16/bf16/fp32/unknown), sort, preferred_min_throughput, preferred_max_latency, max_price, zdr, enforce_distillable_text.

  • sort is price | throughput | latency | exacto — a fourth mode the prose docs don’t mention. Object form {by, partition: "model"|"none"}.
  • Percentile thresholds ({p50,p75,p90,p99}) over a rolling window are soft (deprioritise); max_price is hard (request fails).
  • [rev 2] Unit trap: max_price is documented as “USD price per million tokens”, while catalog pricing is USD per token. Two different units for the same concept in the same API. Fields: prompt, completion, image, request, audio.

(b) Model fallbackmodels: [...] in priority order. Triggers on context-length errors, moderation flags, rate limits, downtime. Billed to whichever model ran.

(c) Routersopenrouter/auto and openrouter/auto-beta classify the prompt into ~30 task types and route by community spend rank within a cost_tier band (low = cost percentile [0,20), medium [20,40), high [40,60), xhigh [60,80), max [80,100]). allowed_models/excluded_models accept wildcards (up to 1024 patterns). pin_model: true reuses the previous turn’s model. A separate pareto-router computes a cost/quality Pareto frontier with max_price (USD per M input tokens) and min_coding_score, and a fusion plugin fans one prompt out to a panel of up to 8 models and has an analyst model synthesise them.

(d) [rev 2] Sticky session routing. session_id is not only an observability tag: “OpenRouter uses it as the sticky routing key, routing all requests in the session to the same provider to maximize prompt cache hits.” This is a cost-control mechanism — it keeps a conversation on the provider that already holds its cached prefix. Missed entirely in rev 1, and it interacts directly with the cached-token pricing issue in §3.3 #23.

openrouter_metadata.strategy enumerates the router that actually fired: direct, auto, free, latest, alias, fallback, pareto, bodybuilder, fusion.

No markup on inference. Revenue comes from the credit-purchase fee: 5.5% ($0.80 min) on Stripe, 5% on crypto. Refunds on unused credits within 24h; fees never refundable.

[rev 2] Catalog pricing is far richer than rev 1 recorded. PublicPricing fields, all USD per token as decimal strings, prompt and completion required:

prompt, completion, request, image, image_output, image_token, audio, audio_output, input_audio_cache, input_cache_read, input_cache_write, input_cache_write_1h, internal_reasoning, web_search, discount (fraction; price × (1 − discount)), and overrides[].

[rev 2] overrides[] is conditional pricing, and rev 1 missed it entirely. Each PricingOverride carries price keys plus conditions:

  • min_prompt_tokens — long-context pricing (a different rate once the prompt exceeds a threshold)
  • utc_start / utc_end as HHMM clock numbers, half-open, may wrap midnight — time-of-day pricing
  • utc_days[] — weekday scoping

Later applicable entries win per key; keys absent from an override inherit the base price; the top-level keys are the default-condition price. This matters for Router.Africa: a flat per-model price table cannot represent a provider that charges differently above 200k tokens or off-peak. If any chosen provider does this, the ledger either over- or under-charges.

Per-request cost in usage:

"usage": {
"prompt_tokens": 194,
"prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 100, "audio_tokens": 0, "video_tokens": 0 },
"completion_tokens": 2,
"completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0,
"accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 },
"total_tokens": 196,
"cost": 0.0012,
"cost_details": {
"upstream_inference_prompt_cost": 0.0008,
"upstream_inference_completions_cost": 0.0004,
"upstream_inference_cost": null,
"server_tool_cost": 0.0
},
"is_byok": false,
"server_tool_use_details": { "tool_calls_requested": 2, "tool_calls_executed": 2, "web_search_requests": 2 }
}

[rev 2] cost_details splits upstream cost into prompt and completion — OpenRouter already reports input and output cost separately per request, and meters server-tool execution (server_tool_cost) as its own line. Rev 1 recorded only the flat upstream_inference_cost.

Prompt caching is a separately priced token class per provider (OpenAI 1.25× write / 0.25–0.50× read; Anthropic 1.25× 5-min, 2× 1-hour write, 0.1× read; DeepSeek 1.0×/0.1×; Google input+storage / 0.25×). A cache_discount field reports the saving.

BYOK: 5% of list price, deducted from credits, after a $25k/mo (PAYG) or $200k/mo (Enterprise) free allowance. Managed through a full CRUD API (§1.5).

Data policy as a price lever: no logging by default; opting in earns a 1% usage discount.

[rev 2] Canonical error taxonomy. Beyond the {error: {code, message, metadata}} envelope (code == HTTP status), the spec defines ApiErrorType“Canonical OpenRouter error type, stable across all API formats” — surfaced as error.metadata.error_type, with 27 values:

context_length_exceeded, max_tokens_exceeded, token_limit_exceeded, string_too_long, authentication, permission_denied, payment_required, rate_limit_exceeded, provider_overloaded, provider_unavailable, invalid_request, invalid_prompt, not_found, precondition_failed, payload_too_large, unprocessable, content_policy_violation, refusal, invalid_image, image_too_large, image_too_small, unsupported_image_format, image_not_found, image_download_failed, server, timeout, unmapped.

This is the real portability contract: a client can branch on error_type without knowing which provider served the request or which of the three wire formats it used. Rev 1 treated the error contract as just a JSON shape.

Status codes on inference (spec, wider than the docs page): 400, 401, 402, 403, 404, 408, 413, 422, 429, 500, 502, 503, 524 (edge-network timeout), 529 (provider overloaded). Admin routes add 409 and 410.

openrouter_metadata (opt-in) carries: requested, strategy, region, summary, attempt, attempts[] (provider/model/status per try), is_byok, endpoints {total, available[{provider, model, selected}]}, generation_time (ms, dispatch→body-end — divide completion tokens by it for throughput), params, and pipeline[] (guardrail/plugin/server-tool stages with per-stage cost_usd).

Guardrail blocks return 403 with pipeline[] explaining which guardrail fired and why — a first-class, debuggable rejection rather than an opaque refusal.

Streaming errors keep HTTP 200 and arrive as an SSE chunk with finish_reason: "error".

Official retry policy is in the spec: exponential backoff, 3 attempts, 500ms initial / 60s max, retry on 5XX and connection errors.

Post-hoc forensics — three endpoints rev 1 under-covered:

  • GET /generation?id=total_cost, cache_discount, upstream_inference_cost, latency, generation_time, moderation_latency, native token counts, provider_name, data_region, workspace_id, and provider_responses[] — every fallback attempt with its status and latency.
  • GET /generation/content?id= — stored prompt/completion, plus a structured error with previous_errors[]: each failed upstream attempt with provider, code, message and raw body.
  • POST /generation/feedback — structured user feedback (latency, incoherence, incorrect_response, formatting, billing, api_error, other).

1.5 [rev 2] Management surface — much larger than rev 1 recorded

Section titled “1.5 [rev 2] Management surface — much larger than rev 1 recorded”

Rev 1 described this as “the Management API, i.e. /keys”. It is considerably more:

Area Endpoints What it does
Workspaces /workspaces CRUD, /workspaces/{id}/members[/add|/remove] Tenant containers under an org. Default text/image model, default provider sort, per-workspace I/O logging + sampling rate.
Workspace budgets /workspaces/{ref}/budgets/{interval} Intervals daily|weekly|monthly|lifetime. Limits must strictly decrease as the interval narrows (lifetime > monthly > weekly > daily). Returns used_usd / remaining_usd. include_byok_in_budgets is workspace-wide.
Guardrails /guardrails CRUD + /assignments/keys + /assignments/members Spend limit + reset interval; allowed/ignored models and providers; per-family ZDR (enforce_zdr_anthropic|openai|google|xai|other); training/publication opt-outs; allowed_data_regions (in-region enforcement); custom regex content filters and builtin ones (email, phone, ssn, credit-card, ip-address, secrets, person-name, address, regex-prompt-injection) each with redact|block|flag. Assignable to keys or org members.
API keys /keys CRUD, /key Per-key limit, limit_reset, expires_at, include_byok_in_limit, workspace scoping, and an external {user, api_key} object for partner-defined identities.
BYOK /byok CRUD Per-credential allowed_models, allowed_api_key_hashes, allowed_user_ids, is_fallback, is_required, is_byok_only, sort_order.
Org & SCIM /organization/members, /scim/groups, /scim/group-mappings, /scim/sync-jobs IdP group → workspace+role mapping with sync jobs.
Observability /observability/destinations CRUD 17 destination types (Langfuse, Datadog, S3, ClickHouse, Snowflake, Sentry, OTel, webhook, …), each with sampling_rate, privacy_mode, structured filter_rules, and region scoping.
Analytics /analytics/meta, /analytics/query, /activity /activity = last 30 completed UTC days by endpoint (cost, BYOK cost, tokens, requests). /analytics/query = a real query API: metrics × ≤2 dimensions × ≤20 filters × granularity minutemonth, with ordering and pagination.
Presets /presets, /presets/{slug}[/versions[/{n}]] Named, versioned bundles of model + params + system prompt. Creatable directly from a chat/messages/responses body.
Files / Containers /files, /containers/{id}/files Workspace document storage and sandbox container file access.

Also: /models is not a static catalog dump but a search APIq, 12 sort modes (including intelligence-high-to-low, coding-high-to-low, design-arena-elo-high-to-low, top-weekly), input_modalities, output_modalities, context, min_price/max_price, min_output_price/max_output_price, arch, model_authors, providers, distillable, zdr, region, model age, benchmark index ranges, and min_tool_success_rate. /models/user returns the same list already filtered by the caller’s provider preferences, privacy settings and guardrails.


  • OpenAI-compatible POST /v1/chat/completions plus provider-shaped drop-in prefixes /openai, /anthropic, /genai. Same vendor/model slug convention.
  • Per-request fallback via a fallbacks array, tried after retries on 429/500/502/503/504 are exhausted.
  • GovernanceCustomer → Team → Virtual Key, budgets at each tier (reset 1m1Y, rolling or UTC-calendar), VK-level token and request rate limits, temporary budget overrides, deny-by-default provider/model allowlists with regex, key expiry, active/inactive toggle. Driveable via /api/governance/*.
  • Typed errors: budget_exceeded → 402, token_limited/request_limited/rate_limited → 429, access_blocked/model_blocked/provider_blocked → 403, virtual_key_required → 401.
  • Cost tracking via an auto-syncing Model Catalog (~24h), per-request cost in logs, bifrost_cost_total Prometheus counter by provider/model/team/VK, and Custom Pricing overrides scoped by provider / key / virtual key.
  • VK-aware GET /v1/models.
  • Routing: weighted load balancing with manually assigned weights; automatic fallback chains ordered by weight. No price- or latency-based automatic selection; no implicit cross-provider model routing.
  • Plugins already surveyed in plugins-and-extensibility.md: semanticCache, telemetry, guardrails, otel, alerting/webhooks, MCP gateway, complexityAnalyzerConfig.

3. Comparison table — OpenRouter → Router.Africa (via Bifrost) → gap

Section titled “3. Comparison table — OpenRouter → Router.Africa (via Bifrost) → gap”

Gap column: None = Bifrost covers it · Config = values.yaml / governance API only · Build = custom code · Diverge = deliberate difference.

# OpenRouter behaviour Router.Africa via Bifrost Gap
1 OpenAI-wire /chat/completions Same, plus /openai, /anthropic, /genai prefixes None
2 Three first-class wire formats: Chat Completions, Responses, Anthropic Messages Bifrost drop-in prefixes; Responses API support unclear Verify — Responses is what Codex/Agents SDK clients speak
3 vendor/model slug IDs Same convention None
4 :free/:nitro/:floor variants Not supported Diverge — encode sorting we’re not doing in v1
5 ~vendor/model-latest aliases with alias_target in catalog Not supported Build (later) — a slug-rewrite table, not routing logic
6 Authorization: Bearer VK via Authorization: Bearer, x-api-key, x-goog-api-key, api-key, or x-bf-vk Config — confirm Bearer reaches the VK layer under our auth settings
7 Normalised finish_reason + raw native_finish_reason Not documented as provided Verify → likely Build
8 /models as a search API (12 sorts, ~20 filters) + /models/user pre-filtered by the caller’s own policy Bifrost /v1/models is VK-scoped, catalog passthrough Build — see 3.3 #20. A curated v1 catalog makes this mostly moot at launch
9 GET /key self-introspection (limit, remaining, usage daily/weekly/monthly, BYOK usage, expiry, allowed_data_regions) /api/governance/virtual-keys/{id} exists but is admin-authenticated Build — customer-callable balance/usage endpoint is table stakes
10 GET /generation?id= + /generation/content?id= incl. provider_responses[] and previous_errors[] Per-request cost in logs; no lookup-by-ID API Build — serves dispute investigation (design-doc §4)
11 session_id as sticky routing key No equivalent Build (cheap, high value) — see 3.2 #17
12 user folded into a hashed upstream identity, never forwarded raw VK identifies the customer inherently None / Config — our VK is the tenant boundary. But confirm Bifrost doesn’t forward anything customer-identifying upstream
13 POST /oauth/token RFC 8693 workload identity None Diverge — enterprise feature, explicit v1 non-goal
14 Attribution headers N/A Diverge
# OpenRouter behaviour Router.Africa via Bifrost Gap
15 Default provider LB: 30s outage filter + inverse-square-of-price weighting Weighted LB with manual weights, no price awareness Diverge (v1) — deferred per design-doc §2; with ~4 providers and non-overlapping catalogs there is little to balance
16 models[] fallback array fallbacks[], same semantics Build (thin) — accept models as an alias so ported client code works unmodified
17 Sticky session routing via session_id to maximise cache hits Not present [rev 2] Build — directly reduces provider cost on multi-turn traffic, which is exactly the agent-shaped usage the Fireworks cached-input discount was chosen for. Cheap: hash session_id → pin provider
18 Fallback triggers include moderation flags and context-length errors Bifrost falls back on 429/500/502/503/504 Verify → possible Config — a context overflow is a 400 and likely won’t trigger fallback
19 provider preferences object (14 fields) No equivalent Diverge (v1) — we’re merchant of record with a curated catalog
20 sort: price|throughput|latency|exacto, percentile soft thresholds None Diverge (v1)
21 openrouter/auto, pareto-router, fusion Bifrost complexityAnalyzerConfig is an adjacent primitive Diverge (v1) — but the primitive exists; tiered cheap/frontier routing is closer than the design doc assumes
22 stop_server_tools_when: [{type: "max_cost", max_cost_in_dollars: N}] None Diverge (v1) — but note the idea: a per-request spend ceiling the customer sets. Cheap trust-builder for a prepaid market
# OpenRouter behaviour Router.Africa via Bifrost Gap
23 Catalog pricing in USD per token, ~16 price keys Nothing customer-facing; prices live in the business-model table Build — publish a /models with KES per token (or per million — pick one, put the unit in the field name). Satisfies the design-doc §5 transparency requirement
24 pricing.overrides[]: conditional pricing on min_prompt_tokens, utc_start/utc_end, utc_days Flat per-model table assumed [rev 2] Build — and a repricing risk. A flat table cannot express long-context or time-of-day provider rates. If any v1 provider prices this way, we systematically mis-charge. Check each of OpenAI / Anthropic / Google / Fireworks for tiered or off-peak pricing before the price table is finalised.
25 max_price is USD per million tokens while catalog is per token N/A [rev 2] Note — a documented unit inconsistency in the reference API. Don’t copy it; state units in field names
26 Zero markup; revenue from 5.5% Stripe / 5% crypto top-up fee Markup on inference (14.5–23% from FX rounding), top-ups at Niobi rail cost Diverge — the core business divergence. OpenRouter monetises the payment; we monetise the token and use the payment as the wedge. A developer price-comparing raw tokens finds us more expensive; the website copy must answer that directly
27 usage.cost on every response Bifrost computes cost into logs and bifrost_cost_total; not documented as returned in the body Verify → likely Build — return usage.cost in KES plus remaining balance. Highest-value single addition for our market
28 cost_details splits upstream_inference_prompt_cost / upstream_inference_completions_cost, plus server_tool_cost [rev 2] Build — confirms business-model open item #2 (split input/output pricing) is not optional; OpenRouter’s schema is the model to copy
29 Cached tokens a separate billed class (cached_tokens, cache_write_tokens, input_cache_read, input_cache_write, input_cache_write_1h) at 0.1–2× Blended per-million rate in business-model Build — real correctness risk. Charging cache reads at full input price silently overcharges agent traffic. Note there are two write rates (5-min and 1-hour TTL) on Anthropic
30 Prepaid USD credits, decremented per request Same, in local currency. Bifrost budgets track dollars Build/Config — resolved 2026-09-20: Bifrost owns the admin-set spend cap (a USD backstop, per virtual key); Postgres owns the user-set cap in credits, enforced in the hold path. Neither enforces the other’s cap, so no conversion is needed (Decisions Log, Spend cap authority). The Custom Pricing / KES-unit option below was not adopted. Original options: Bifrost budgets in USD with a KES ledger on top, or make Bifrost’s internal unit be KES via Custom Pricing, which would make bifrost_cost_total directly usable for reconciliation
31 Hard cutoff at zero; 402 even on free models budget_exceeded402 None — Bifrost’s error already matches
32 In-flight spending budget — caps estimated cost of running requests against balance Not present Build — this is the answer to the atomic-overdraw race in design-doc §5. Reserve estimated max cost at admission, settle on completion
33 Failed requests not billed Must be an explicit ledger rule Build — interacts with #32: reserve ≠ charge
34 Per-key spend caps; workspace budgets at 4 intervals with strictly-decreasing limit validation Bifrost VK budgets, 1m1Y, calendar or rolling, with overrides None — comparable. Steal the strictly-decreasing validation rule
35 Refunds: 24h on unused credits, fees never Undecided Build (policy) — mobile-money reversals differ from card refunds; needs a written stance pre-launch
36 Continuous upstream repricing Monthly review + manual FX override; balances are currency so repricing is instant and uniform Diverge — and better for us. Keep it; the design-doc reasoning holds
37 BYOK at 5% of list Explicit non-goal (reseller model) Diverge
38 1% discount for opting into logging None Diverge — but note the shape: a privacy setting turned into a priced axis
# OpenRouter behaviour Router.Africa via Bifrost Gap
39 ApiErrorType — 27-value canonical taxonomy, stable across all three wire formats, in error.metadata.error_type Bifrost has {type, status_code, error:{message}} with ~8 governance types [rev 2] Build (shim). Rev 1 framed this as “shapes differ”. It’s bigger: the portable contract is the taxonomy, not the envelope. Either adopt OpenRouter’s error_type vocabulary or publish our own — but publish one
40 Error envelope {error:{code,message,metadata}}, code == HTTP status {type, status_code, error:{message}} Build (shim) — translate at the edge, or document ours clearly. Silently differing is the worst option
41 402 / 403 / 408 / 413 / 422 / 429 / 502 / 503 / 524 / 529 distinguished Not documented at this granularity Verify — 529 (provider overloaded) vs 503 (no provider meets requirements) is a meaningful distinction for retry logic
42 Guardrail block → 403 with pipeline[] naming the rule that fired guardrails plugin exists, not configured Config + Build — as reseller we inherit upstream ToS liability (design-doc §6); returning why something was blocked is what prevents support tickets
43 Moderation metadata: reasons[], flagged_input (≤100 chars), provider_name, model_slug Not configured Config + Build
44 openrouter_metadata with attempts[], endpoints, generation_time, per-stage cost_usd Not present Build (later) — excellent debugging surface, not launch-critical
45 Streaming errors: 200 + SSE chunk with finish_reason: "error" Not documented Verify → likely Build — a mid-stream close is indistinguishable from a network drop client-side
46 Published retry policy (exp. backoff, 3 attempts, 5XX + connection errors) Bifrost retries 429/5xx internally Build (docs) — publish ours so client authors don’t invent their own
47 Credit-derived rate limits; extra keys buy nothing VK-level token + request limits per window Config — design-doc §6 wants per-customer rate limiting; Bifrost does this natively. Set a default at VK issuance
48 Cloudflare edge + DDoS protection Traefik on a single K3s node, no WAF/CDN Build (infra) — out of API-contract scope, but a single node with no edge protection is the reliability floor
49 No published SLA / uptime / timeout Same None — matching by saying nothing is defensible for v1

3.5 [rev 2] Multi-tenancy, governance & operations — rev 1 got this backwards

Section titled “3.5 [rev 2] Multi-tenancy, governance & operations — rev 1 got this backwards”

Rev 1 concluded Bifrost’s governance was richer than OpenRouter’s “flat keys”. The spec shows OpenRouter has a full org → workspace → key hierarchy plus an orthogonal guardrail layer. Corrected:

# OpenRouter behaviour Router.Africa via Bifrost Gap
50 Org → Workspace → Key, with workspace-level default model, provider sort, logging config Customer → Team → Virtual Key None — genuinely comparable, different names
51 Management keys that cannot call inference Bifrost admin auth is separate from VK auth None
52 Guardrails as a separate, assignable policy object — attachable to a key or an org member, carrying spend limit, model/provider allowlists, ZDR, content filters, data regions Bifrost folds all of this into the VK itself Diverge — OpenRouter’s is more composable (one policy, many keys). Bifrost’s is simpler. For per-customer prepaid accounts, Bifrost’s model is a better fit; no action
53 Builtin content filters (PII, secrets, prompt-injection) with redact|block|flag Bifrost guardrails plugin, pluggable providers, not configured Config — relevant for abuse monitoring (design-doc §6)
54 allowed_data_regions + regional hostnames (eu./us.openrouter.ai) enforcing in-region routing None [rev 2] Diverge (v1), revisit. Data residency is exactly the question African enterprise buyers ask. OpenRouter has productised it. Worth knowing this exists before a first enterprise conversation
55 /analytics/query — a real query API (metrics × dimensions × filters × granularity) plus /activity Bifrost telemetry via Prometheus/bifrost_cost_total Build (later) — the dashboard needs some of this; Prometheus covers our own ops, not customer-facing analytics
56 17 observability destinations with sampling, privacy mode, filter rules Bifrost has otel, datadog, bigquery, kafka, pubsub, splunk plugins None — comparable, and ours is config-only
57 Versioned presets (model + params + system prompt, creatable from a request body) None Diverge (v1) — a genuinely good product idea; note for later
58 SCIM group → workspace/role mapping Bifrost scim (Okta, Entra, Keycloak, Zitadel, Google, OIDC) None — both have it; both irrelevant to v1
59 BYOK credential CRUD with model/key/user allowlists N/A Diverge — no BYOK

Build, ordered by whether the product works without it:

  1. usage.cost in KES on every response, plus remaining balance (#27) — the single most differentiating API-level feature for a market whose whole pitch is visible, controllable spend in local currency.
  2. Reserve-then-settle billing (#32, #33) — estimate max cost at admission, reserve it, settle actual on success, release on failure. Solves the atomic-overdraw race and don’t-bill-failures with one mechanism. Never decrement-on-admission.
  3. Cached-token pricing as a separate class, and input/output split (#28, #29) — both must exist before the first agent-shaped customer.
  4. [rev 2] Verify whether any v1 provider uses conditional pricing (#24) — long-context tiers or time-of-day rates break a flat price table. This is a pre-launch check, not a build item, but it can invalidate the business-model price sheet.
  5. Customer-facing /models with KES prices (#23) and GET /key-equivalent (#9) — the §5 transparency requirement is an API surface, not only a dashboard page.
  6. Error-contract decision (#39, #40) — adopt OpenRouter’s error_type vocabulary or publish our own, before any customer writes error handling against us.
  7. Post-hoc generation lookup (#10) — needed for the dispute investigation design-doc §4 already calls for.
  8. models accepted as an alias for fallbacks (#16) — a few lines; makes ported code work unchanged.
  9. [rev 2] Sticky session routing on session_id (#17) — cheap, and it cuts real provider cost on multi-turn traffic.

Resolved 2026-09-20 (currency unit): spend-cap authority is split (Bifrost owns the admin USD cap, Postgres owns the user cap in credits); Bifrost’s unit is not converted to KES. Original question: whether Bifrost’s internal cost unit becomes KES via Custom Pricing / pricingOverrides (#30). Cascades into #27, #31, and whether bifrost_cost_total is directly usable for reconciliation. Settle before ledger work starts.

Deliberate divergences to state in writing: no provider-preference object, no price/latency sorting, no auto/pareto/fusion routers, no BYOK, no :nitro/:floor variants, no workload-identity token exchange — all deferred per design-doc §2. And most importantly, markup on inference rather than markup on payment (#26): OpenRouter’s zero-markup stance is a consequence of its scale and isn’t available to us; the honest framing is that we charge for access, not for routing.

Ideas worth stealing later, in rough order of value: customer-set per-request cost ceilings (#22), in-region routing as a productised guarantee (#54), versioned presets (#57), a customer analytics query API (#55), and the strictly-decreasing budget-interval validation rule (#34).


Everything in the Bifrost column is from public documentation, not from prod-bifrost-core.router.africa (chart 2.1.40, app 1.5.12). Confirm against the live gateway:

  • Does the chat-completions response body include a cost field, or is cost only in logs/metrics? (#27)
  • Does Bifrost emit native_finish_reason or a raw-upstream equivalent? (#7)
  • Does Authorization: Bearer <vk> authenticate against the VK layer under our enforceAuthOnInference setting? (#6)
  • Do 400-class errors (context overflow, moderation refusal) trigger fallbacks, or only 429/5xx? (#18)
  • How are mid-stream provider failures surfaced on SSE? (#45)
  • Are 502/503/524/529-equivalents distinguished, and is provider failure state remembered across requests? (#41)
  • Does the /anthropic prefix accept Anthropic-native bodies end to end? Is there any Responses-API support? (#2)
  • Can Custom Pricing express KES rates including a separate cached-input rate, and can it express conditional rates? (#24, #29, #30)

Non-Bifrost pre-launch check: whether OpenAI, Anthropic, Google or Fireworks price any v1 model with long-context tiers or time-of-day rates (#24). Sources: each provider’s own pricing page, and OpenRouter’s GET /api/v1/models — the pricing.overrides array is populated for models that do.

The fastest way to close the Bifrost items is a scratch VK plus one provider key on the live gateway and a handful of curl calls — which also unblocks the “provider API keys not yet added” item in the repo README.


Primary (rev 2): OpenRouter openapi.json — OpenAPI 3.1, ~180 schemas, ~90 paths. This supersedes the prose pages wherever they disagree.

Secondary: openrouter.ai/docs/{quickstart, features/model-routing, guides/routing/model-fallbacks, features/provider-routing, api-reference/errors, api-reference/limits, use-cases/usage-accounting, features/prompt-caching, use-cases/byok, faq, features/provisioning-api-keys, features/latency-and-performance} · live GET https://openrouter.ai/api/v1/models.

Bifrost: docs.getbifrost.ai/features/governance · .../features/governance/routing · .../providers/custom-pricing · github.com/maximhq/bifrosttransports/README.md.