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.
1. OpenRouter’s product surface
Section titled “1. OpenRouter’s product surface”1.1 API contract
Section titled “1.1 API contract”- Base URL
https://openrouter.ai/api/v1. Regional hostnames exist:eu.openrouter.ai(europe) andus.openrouter.ai(us), withopenrouter.aiasglobal— 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: enabledopts into anopenrouter_metadataobject on the response (§1.4).- Model IDs are
vendor/modelslugs, with:free,:nitro,:floorvariant suffixes and~vendor/model-latestaliases that carry analias_targetin the catalog. - Inference endpoints — three wire formats, not one:
POST /chat/completions— OpenAI Chat CompletionsPOST /responses— OpenAI Responses API (stateless;previous_response_idis explicitly rejected)POST /messages— Anthropic Messages, includingthinking,cache_control,context_management, and afallbacksarray (max 3,modelonly, mutually exclusive withmodels)- 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 providerProvider 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_idSticky routing key — see §1.2 service_tierauto|default|fast|flex|priority|scaleuserPer-end-user abuse isolation — see below max_tool_callsServer-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_isprompt_cache_key,prompt_cache_optionsExplicit cache control metadata,traceObservability tagging (≤16 pairs; trace_id/span_name/etc.) routeDeprecated alias for provider.sort.partition useris 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 rawnative_finish_reason;modelreports what actually ran;service_tierreports what the provider actually used;usagecarries cost (§1.3).
1.2 Routing policy
Section titled “1.2 Routing policy”(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.
sortisprice|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_priceis hard (request fails). - [rev 2] Unit trap:
max_priceis documented as “USD price per million tokens”, while catalogpricingis USD per token. Two different units for the same concept in the same API. Fields:prompt,completion,image,request,audio.
(b) Model fallback — models: [...] in priority order. Triggers on context-length errors, moderation flags, rate limits, downtime. Billed to whichever model ran.
(c) Routers — openrouter/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.
1.3 Pricing and billing
Section titled “1.3 Pricing and billing”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_endas HHMM clock numbers, half-open, may wrap midnight — time-of-day pricingutc_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.
1.4 Failure handling
Section titled “1.4 Failure handling”[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, andprovider_responses[]— every fallback attempt with its status and latency.GET /generation/content?id=— stored prompt/completion, plus a structurederrorwithprevious_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 minute…month, 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 API — q, 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.
2. What Bifrost gives us, per its docs
Section titled “2. What Bifrost gives us, per its docs”- OpenAI-compatible
POST /v1/chat/completionsplus provider-shaped drop-in prefixes/openai,/anthropic,/genai. Samevendor/modelslug convention. - Per-request fallback via a
fallbacksarray, tried after retries on 429/500/502/503/504 are exhausted. - Governance —
Customer → Team → Virtual Key, budgets at each tier (reset1m→1Y, 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_totalPrometheus 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.
3.1 API contract
Section titled “3.1 API contract”| # | 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 |
3.2 Routing policy
Section titled “3.2 Routing policy”| # | 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 |
3.3 Pricing & billing
Section titled “3.3 Pricing & billing”| # | 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_exceeded → 402 |
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, 1m–1Y, 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 |
3.4 Failure handling & limits
Section titled “3.4 Failure handling & limits”| # | 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 |
4. What this implies for the build
Section titled “4. What this implies for the build”Build, ordered by whether the product works without it:
usage.costin 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.- 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.
- Cached-token pricing as a separate class, and input/output split (#28, #29) — both must exist before the first agent-shaped customer.
- [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.
- Customer-facing
/modelswith KES prices (#23) andGET /key-equivalent (#9) — the §5 transparency requirement is an API surface, not only a dashboard page. - Error-contract decision (#39, #40) — adopt OpenRouter’s
error_typevocabulary or publish our own, before any customer writes error handling against us. - Post-hoc generation lookup (#10) — needed for the dispute investigation design-doc §4 already calls for.
modelsaccepted as an alias forfallbacks(#16) — a few lines; makes ported code work unchanged.- [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).
5. Open verification items
Section titled “5. Open verification items”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_reasonor a raw-upstream equivalent? (#7) - Does
Authorization: Bearer <vk>authenticate against the VK layer under ourenforceAuthOnInferencesetting? (#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
/anthropicprefix 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.
Source index
Section titled “Source index”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/bifrost → transports/README.md.