Kynetra Conflux · Phase 0 · Launching soon
Conflux is an OpenAI-compatible gateway to a growing catalog of language models, with a single API key, unified credit-based billing, and per-key usage in one dashboard — so you stop juggling a different key, bill, and rate limit for every provider.
Conflux speaks the OpenAI chat completions format. Point your existing OpenAI client at Conflux's base URL and swap in a Conflux key — nothing else in your code has to change.
Conflux isn't publicly available yet — the snippet below is a preview of the interface it will expose at launch, not a live endpoint you can call today.
from openai import OpenAI
client = OpenAI(
base_url="https://conflux.kynetra.dev/v1",
api_key="qai_...",
)
response = client.chat.completions.create(
model="anthropic/claude-sonnet-4.5",
messages=[
{"role": "user", "content": "Say hello from Conflux."}
],
stream=True,
)
for chunk in response:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)curl https://conflux.kynetra.dev/v1/chat/completions \
-H "Authorization: Bearer qai_..." \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-sonnet-4.5",
"messages": [
{"role": "user", "content": "Say hello from Conflux."}
]
}'
Keys will be issued from the QuantumOS admin console (prefixed qai_) once
Conflux launches. At that point, conflux.kynetra.dev will be served by the
QuantumOS admin host — the same /v1/chat/completions and /v1/models
endpoints shown above. Set stream: true for server-sent-event streaming, as
shown above; omit it for a single non-streaming JSON response.
No roadmap items below — this is what the API is built to do, ahead of launch.
Standard /v1/chat/completions request/response shape, so existing OpenAI SDKs and tooling work with a base-URL and key swap.
Set stream: true for server-sent-event token deltas, or omit it for a single JSON completion.
One credit balance across every model you call — no separate accounts or invoices per provider.
Every key gets its own token-bucket limit, reported via standard RateLimit-* response headers.
Requests route through OpenRouter to a wide model catalog, with bring-your-own-key (BYOK) support for your own provider accounts, plus support for local/self-hosted models via Ollama.
Per-key call counts, token usage, and cost show up in the QuantumOS admin console as they happen.
Conflux is a thin, OpenAI-compatible layer in front of QuantumOS's existing multi-provider AI gateway — not a separate infrastructure stack. It's Phase 0: one Next.js route handler per endpoint, backed by Postgres, not a distributed global edge network.
POST /v1/chat/completions and GET /v1/models run inside the same
admin-web app as the rest of QuantumOS. The global admin-web auth middleware explicitly lets
any /v1/* path through unauthenticated, so each Conflux route owns its own bearer-key
check rather than relying on session auth. Once a request is authenticated, rate-limited, and
priced, the actual model call is delegated to the gateway's aiCall() /
aiCallStream() functions — the same call path QuantumOS's own internal AI features use.
For a given model, the gateway resolves a provider key in a fixed order: first a workspace's own bring-your-own-key (BYOK) credential for that provider, then a native platform API key from environment config if one exists for that provider, and finally OpenRouter as the universal fallback that can route almost any model without a dedicated key, via OpenRouter's own chat completions API.
The underlying gateway doesn't know about customer-facing qai_ keys, per-key rate
limits, or catalog-priced credit billing — those are entirely Conflux's own layer, added by
three small modules that sit in front of the shared gateway call:
conflux-auth.ts — resolves a qai_ bearer key to a tenant by hashing it and looking it up in qos_ai_keys.
conflux-ratelimit.ts — a Postgres-backed token bucket per key, stored in qos_conflux_rate_buckets.
ai-model-catalog.ts — the shared, cached qos_ai_models catalog both /v1/models and the credits debit read from.
This is intentionally a stopgap: the plan for Conflux beyond Phase 0 is a dedicated Rust gateway that replaces the Postgres-backed rate limiter with an in-process one — but that hasn't shipped yet, and nothing on this page describes it as though it has.
qai_-prefixed
Every /v1/chat/completions request is authenticated with a bearer key issued from
the QuantumOS admin console. GET /v1/models requires no authentication at all — it's
a public catalog listing.
Authorization: Bearer qai_<the rest of your key>
The route matches the header against /^Bearer\s+(qai_\S+)$/ — anything that isn't
exactly Bearer, one space, then a token starting with qai_ fails to
even extract a candidate key, and is treated as unauthenticated.
hashKey() the internal gateway uses, so one key row means the same thing everywhere it's checked).qos_ai_keys.key_hash, fetching id, tenant_id, status, and expires_at.status isn't exactly 'active' (e.g. revoked) → unauthenticated.expires_at is set and already in the past → unauthenticated.{ tenantId: row.tenant_id ?? row.id, keyId: row.id } — every later step (rate limiting, credits, usage rows) is scoped to that tenant and key.
qos_ai_keys doesn't yet have a distinct workspace_id column, so the
workspaceId a request resolves to is currently the same value as tenantId.
Any of the cases above — missing header, malformed header, unknown key, revoked key, or expired
key — produces the same response: 401 with detail
Invalid or missing API key and a WWW-Authenticate: Bearer
header. Conflux doesn't distinguish "wrong key" from "expired key" from "revoked key" in the
response body, so a caller can't fingerprint valid-but-inactive keys by probing.
Two endpoints today. Both are OpenAI-compatible in shape; every error response across both
uses the RFC 7807 application/problem+json body — { type, title, status,
detail?, instance? }.
GET /v1/models
No authentication required. Returns the active rows from the qos_ai_models catalog
(a shared, 5-minute in-process cache — the same cache the billing path reads from, so a model
priced here is priced identically when it's actually called). Responses are sent with
Cache-Control: public, max-age=300.
{
"object": "list",
"data": [
{
"id": "anthropic/claude-sonnet-4.5",
"object": "model",
"owned_by": "anthropic",
"context_length": 200000,
"pricing": {
"prompt": "0.000003",
"completion": "0.000015"
}
}
]
}
id is <provider>/<model_id> — except OpenRouter-catalogued
rows, whose model_id is already the real upstream id (e.g.
anthropic/claude-sonnet-4.5), so no extra openrouter/ prefix is added.
pricing.prompt / pricing.completion are per-single-token USD decimal
strings (the catalog stores per-1M-token prices; this endpoint divides them down), computed with
exact string/BigInt math so a stored 3.0000 always renders 0.000003,
never float-corrupted. A catalog read failure returns 500
with a generic {"type":"about:blank","title":"Internal Server Error","status":500} body
— no detail field, to avoid leaking query internals.
POST /v1/chat/completions
Requires the Authorization: Bearer qai_... header described above. Request order
of operations, each of which can short-circuit the response: auth → rate limit → body/JSON
validation → model lookup against the catalog → credits balance check → the actual model call.
| Field | Type | Required | Notes |
|---|---|---|---|
| model | string | yes | Must resolve against the active qos_ai_models catalog, else 400. A stray leading openrouter/ (from a pre-fix cached id) is stripped automatically before lookup. |
| messages | array | yes | Standard OpenAI-shaped message objects, non-empty, passed through unmodified. |
| tools | array | no | OpenAI-style tool/function defs. Rejected (400) together with stream: true. |
| temperature | number | no | Passed through only if it's a JS number; otherwise omitted entirely. |
| max_tokens | number | no | Same passthrough rule as temperature. |
| stream | boolean | no | true switches to the SSE response path described below. |
Non-streaming (stream omitted or false) response, 200:
{
"id": "chatcmpl-<uuid>",
"object": "chat.completion",
"created": 1735689600,
"model": "anthropic/claude-sonnet-4.5",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello from Conflux."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 12,
"completion_tokens": 5,
"total_tokens": 17
}
}
If the model returned tool calls, message.tool_calls is present alongside
content. Response headers on a successful non-streaming call include
x-conflux-cost-nanousd plus the RateLimit-* headers (see below).
| Header | Where | Meaning |
|---|---|---|
| Authorization | request | Bearer qai_... key. |
| x-conflux-cost-nanousd | 200 response | Exact cost of this one call, in nano-USD (10⁻⁹ USD), derived from the catalog's per-model price × the real token counts the provider reported — never an estimate. |
| RateLimit-Limit | every response | Bucket capacity, in whole tokens (requests). |
| RateLimit-Remaining | every response | Whole tokens left in the bucket after this call. |
| RateLimit-Reset | every response | Relative delta in seconds until the bucket refills to full capacity — not a Unix epoch timestamp. |
| Retry-After | 429 response | Seconds to wait before the bucket has at least one whole token again. |
| WWW-Authenticate | 401 response | Always Bearer. |
RateLimit-Reset here is deliberately a delta, not an absolute timestamp — a token
bucket has no natural "window boundary" instant to report the way a fixed-window limiter does,
so "seconds until full" is the more useful number for a client deciding how long to back off.
Set stream: true and Conflux responds with Content-Type: text/event-stream
instead of a single JSON body.
Each frame is a data: -prefixed line followed by a blank line, carrying a
chat.completion.chunk object:
data: {"id":"chatcmpl-<uuid>","object":"chat.completion.chunk","created":1735689600,"model":"anthropic/claude-sonnet-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"Hel"},"finish_reason":null}]}
data: {"id":"chatcmpl-<uuid>","object":"chat.completion.chunk","created":1735689600,"model":"anthropic/claude-sonnet-4.5","choices":[{"index":0,"delta":{"content":"lo"},"finish_reason":null}]}
data: {"id":"chatcmpl-<uuid>","object":"chat.completion.chunk","created":1735689600,"model":"anthropic/claude-sonnet-4.5","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
The first chunk carries delta.role: "assistant" alongside the first bit of content;
every following content chunk carries only delta.content; the final chunk carries an
empty delta and the real finish_reason. Every stream — success, upstream
failure, or client abort — always ends with a terminal data: [DONE] frame.
The gateway reads the upstream provider's own SSE response body line-by-line as bytes arrive
and forwards each parsed content delta to the response stream immediately — it isn't collected
and replayed. The response also sets X-Accel-Buffering: no to discourage
intermediary proxies from buffering it themselves.
Requesting stream: true together with a non-empty tools array is
rejected up front with 400 and detail
tools are not yet supported with stream: true. The SSE frame builder
doesn't carry tool_calls deltas today — rather than silently streaming text and
quietly dropping any tool call the model made, that combination is refused before the stream
ever opens. Non-streaming tool calls work today (see the API reference above).
If the client disconnects mid-stream, that cancellation is bridged into an
AbortController that's threaded all the way down into the upstream provider
fetch() — the upstream read is actually cut short, not left running to completion
against the provider for a response nobody is reading. An aborted call meters
zero usage — no credits are debited and no partial cost is recorded, since the abort
path never learns a token count to bill from.
Every qai_ key gets its own token bucket, currently the same defaults for every
key: 60 request capacity, refilling at 60 per minute (roughly one request/second,
averaged).
/v1/* traffic can land on any serverless instance. An in-memory limiter would let
each instance track its own count, so the effective limit would balloon to roughly
limit × instance count. Instead the bucket state lives in one table,
qos_conflux_rate_buckets (key_id, tokens_milli,
capacity, refill_per_min, updated_at), keyed by the same
keyId auth resolves. Token counts are stored ×1000 (tokens_milli) so
refill math and the 1-token-per-request decrement stay integer arithmetic — no float rounding
drift under concurrent requests.
Rate limiting is checked after authentication (it needs the resolved key id) and before the credits balance check — a caller that's already being rate-limited shouldn't also cost a credits-balance database round trip.
A request with less than one whole token available gets 429
with detail Rate limit exceeded, plus the full
RateLimit-* header set and a Retry-After header — the number of
seconds until the bucket has refilled to at least one whole token again (not until it's merely
non-negative).
/v1/models
The catalog isn't a static page — call GET /v1/models and you get the current
list of active models with per-token pricing, in the OpenAI list format. The rows below are
illustrative examples of the shape, not a live quote — check the endpoint for current prices.
| Model id | Owned by | Context | Pricing (illustrative) |
|---|---|---|---|
| anthropic/claude-sonnet-4.5 | anthropic | 200k | example only |
| openai/gpt-4o-mini | openai | 128k | example only |
| meta-llama/llama-3.1-70b | meta-llama | 128k | example only |
| … | … | … | … |
Illustrative rows only — exact model ids, context lengths, and prices are set by the live catalog at /v1/models, not by this page.
Cost is computed per call, not per request — actual input/output tokens × that model's catalog
price, the same price /v1/models publishes for that model.
The catalog stores each model's input_cost_per_1m_usd and
output_cost_per_1m_usd in qos_ai_models. After a call completes,
Conflux multiplies those catalog prices by the real prompt/completion token counts the provider
reported — using exact integer/BigInt math throughout, never a floating-point conversion of the
catalog's decimal price strings — to get a nano-USD figure. That exact figure is what
x-conflux-cost-nanousd reports on the response. It is explicitly not derived
from any internal cost-estimate heuristic — billing off an unrelated estimate table was found to
overcharge some models by roughly 100x their advertised catalog price during Conflux's build-out,
which is why billing was rewired to read the same numbers /v1/models shows you.
Balance is a flat integer counter, qos_ai_credits.balance, defaulting to
50 for a tenant that has never been billed before (the column's own default, mirrored in
code so a brand-new key's very first call is priced correctly instead of erroring). Each call's
nano-USD cost is rounded up to whole US cents, then converted to credits at roughly
1 credit ≈ 1 US cent — a call that rounds to 0 cents (a genuinely free, zero-priced catalog
model, or a call cheap enough to round down to nothing) debits 0 credits; anything above
that debits at least 1. Every metered call also writes a row into qos_ai_usage
(feature: 'conflux') so Conflux traffic shows up in the same usage dashboards as
QuantumOS's internal AI features.
Once qos_ai_credits.balance is at or below zero, calls return
402 Payment Required with detail
Insufficient credits — top up at /settings/ai/credits. That check runs
after auth, rate limiting, and model validation, but strictly before any model call goes out or
any SSE stream opens — you're never billed past your balance, and a 402 never has partial usage
attached to it.
The QuantumOS admin console sells credit packs for topping up a balance. Pack pricing is set in the platform's billing code today and is subject to change before Conflux's own general availability — treat these as illustrative of the mechanism, not a locked-in Conflux price list:
| Pack | Credits | Price (USD) |
|---|---|---|
| starter | 500 | $5 |
| growth | 2,000 | $15 |
| pro | 6,000 | $40 |
| scale | 20,000 | $120 |
We're pre-launch — pricing mechanics (credits, catalog pricing, packs) are what's described here, but exact numbers are still subject to change ahead of general availability.
Every error is application/problem+json: { type, title, status, detail?,
instance? }. instance, when present, is a request id for support
correlation — the underlying error (which can carry raw upstream/provider detail) is logged
server-side against that id and never echoed back in the response body itself.
| Status | Condition | Example detail | Notes |
|---|---|---|---|
| 400 | Body isn't valid JSON | Request body must be valid JSON | |
| 400 | Body parses but isn't a JSON object | Request body must be a JSON object | Array/primitive top-level bodies rejected. |
| 400 | Missing/empty model | model is required | |
| 400 | Missing/empty messages | messages is required | |
| 400 | stream: true with a non-empty tools | tools are not yet supported with stream: true | Checked before the SSE stream opens. |
| 400 | model not in the active catalog | Unknown model | Validated against the same catalog /v1/models serves. |
| 401 | Missing / malformed / unknown / revoked / expired key | Invalid or missing API key | WWW-Authenticate: Bearer header set. |
| 402 | Credits balance ≤ 0 | Insufficient credits — top up at /settings/ai/credits | Checked before the model call goes out. |
| 429 | Token bucket has < 1 token | Rate limit exceeded | Retry-After + RateLimit-* headers included. |
| 500 | /v1/models catalog query throws | — (no detail field) | Generic about:blank/Internal Server Error body only. |
| 502 | Upstream model call failed (non-streaming) | Upstream model call failed | Real error logged server-side; instance carries a correlatable request id. |
Streaming failures are a special case: once the SSE response's 200 status line is sent it can't
be downgraded, so an upstream failure mid-stream is surfaced as a final chunk with
finish_reason: "error" instead of an HTTP error status — followed by the terminal
data: [DONE] frame. Usage is metered at zero for that call, and the real error is
still logged server-side with a request id.
Yes — point the OpenAI Python (or any OpenAI-compatible) SDK at https://conflux.kynetra.dev/v1 with a Conflux qai_ key as the API key. The request and response shapes for /v1/chat/completions follow the OpenAI format, including streaming via stream: true.
Calls return an HTTP 402 Payment Required response (RFC 7807 application/problem+json body) until you top up. You won't be billed past your balance — the credit check runs before the model call goes out.
Yes. Set stream: true in your request and Conflux returns server-sent-event chunks in the OpenAI chat.completion.chunk format, ending with a standard [DONE] frame.
Yes — bring-your-own-key lets requests route through your own provider account instead of the shared OpenRouter pool for a given model, configured from the admin console.
Requests are logged for billing and usage purposes (token counts, cost, model, timestamps) and are visible in your own workspace's usage dashboard. We're not making broader privacy or compliance claims at this stage — ask if you need specifics for your use case.
<provider>/<model_id>, e.g. anthropic/claude-sonnet-4.5 — exactly the id field GET /v1/models returns for that model. A stray leading openrouter/ from a previously cached id (an id shape an earlier catalog version briefly advertised) is tolerated and stripped automatically before the model is resolved.
Yes, in non-streaming mode — pass an OpenAI-shaped tools array and any tool calls the model makes come back on choices[0].message.tool_calls. Combining tools with stream: true is currently rejected with a 400 (see Streaming above) rather than silently dropped mid-stream.
Yes — messages is a standard OpenAI-shaped array of any length and any mix of roles (including system), passed through to the model unmodified.
It's ignored. The route only reads model, messages, tools, temperature, max_tokens, and stream from the request body — there's no strict schema rejection of unrecognized extra fields today.
No, and that's by design — the whole point of the OpenAI-compatible shape is that the standard OpenAI SDKs (Python, Node, or any OpenAI-compatible client) work against Conflux unmodified once you swap the base URL and key, as shown in the quickstart.
Not currently. /v1/chat/completions doesn't read an idempotency-key-style header, so retrying an identical request is billed and rate-limited as an independent call. Not yet documented as a feature because it isn't implemented.
Not yet documented. Nothing in the chat-completions route or the underlying gateway call sets an explicit request timeout beyond whatever the runtime's own fetch defaults to — we'd rather say "not documented" than publish a number we can't point to in code.
No. A client disconnect is bridged into an AbortController that cuts the upstream provider call short, and the aborted call is metered at zero — no credits are debited for it.