portkey.chat.completions.create({ model, messages }). It does that job well, and the API is
clean. But it is also a separate vendor account:
- LLM routing lives behind its own API key, its own provider keys (virtual keys / Model Catalog), and its own dashboard.
- Its
_usermetadata tag scopes observability and budgets for LLM calls and nothing else — it knows what the agent spent on tokens, but not its virtual card, its inbox, its vault secrets, or its KYC status. - “Who let this agent burn $400 on GPT-5, and what else can it touch?” is answered in Portkey for tokens, and somewhere else for everything else. There is no shared accountability.
/llm primitive gives the agent the same capability —
one OpenAI-compatible endpoint, 300+ models, provider routing, fallbacks, streaming — but
rooted in one identity:
- The tenant user that makes the LLM call is the same user that owns its cards, its email inbox, its vault secrets, and its KYC.
- Whether the agent may call the model at all is decided by that user’s Account Kit at execution time — not by which key happened to be in the request header.
- Every completion lands in the same per-user activity log as everything else the agent does, and is billed in the same credits as its cards and domains.
Portkey is a trademark of its owner, used here for identification only. No endorsement or affiliation is implied.
Tested against: Portkey AI Node SDK
portkey-ai (gateway base
https://api.portkey.ai/v1, PORTKEY_GATEWAY_URL; API surface per
portkey.ai/docs, snapshot June 2026) and the Naive Node SDK
@usenaive-sdk/server against the Naive API (base
https://api.usenaive.ai/v1, docs snapshot June 2026).Version notes:- Portkey renamed Virtual Keys → AI Providers / Model Catalog. Models are now referenced as
@provider-slug/model-namein themodelfield; thevirtualKey/x-portkey-virtual-keyheader still works for backward compatibility. This guide notes both. - Naive’s
/llmis a managed wrapper over OpenRouter, so models use OpenRouter’sprovider/modelconvention (e.g.anthropic/claude-sonnet-4.6) and Naive holds the upstream key — there is no bring-your-own provider key. See gaps. - Both are evolving APIs — verify method and model names against your installed SDK version.
Concept map
| Portkey | Naive | Notes |
|---|---|---|
new Portkey({ apiKey }) | new Naive({ apiKey }) | Server-side key in both cases |
| Portkey key + provider keys (virtual keys / Model Catalog) you manage | Single Naive key; OpenRouter key held server-side | You stop holding provider keys — see gaps |
_user metadata tag — scopes LLM logs/budgets only | tenant user via naive.forUser(id) — scopes every primitive | The core consolidation win |
portkey.chat.completions.create({ model, messages }) | naive.llm.chat({ model, messages }) | Both OpenAI-shaped; response is OpenAI-compatible |
OpenAI SDK + baseURL: PORTKEY_GATEWAY_URL + createHeaders({...}) | OpenAI SDK + baseURL: …/v1/proxy/openrouter | Drop-in proxy on both — keep your existing client |
Model @openai-prod/gpt-4o (Model Catalog slug) | Model openai/gpt-5.2 (OpenRouter provider/model) | Both prefix the provider; slug namespace differs |
stream: true | naive.llm.stream(...) (or stream: true on the proxy) | SSE on both; final chunk carries usage |
Gateway Config strategy: { mode: "fallback" }, targets: […] | models: ["…", "…"] fallback chain | Naive uses OpenRouter’s ordered fallback list |
| Provider preferences inside a target / config | provider: { order, only, ignore, sort, allow_fallbacks, … } | OpenRouter provider routing |
retry: { attempts, on_status_codes } | Partial — models fallback + provider.allow_fallbacks | No standalone retry/backoff object — see gaps |
strategy: { mode: "loadbalance", targets: [{ weight }] } | Partial — provider.sort / order | No weighted/canary target splitting — see gaps |
| List models | naive.llm.models(filter) (GET /v1/llm/models, free) | Returns routable OpenRouter models |
Per-request usage/cost (Portkey logs, x-portkey-trace-id) | naive.llm.generation(id) (GET /v1/llm/generation?id=) + credits_used on the response | Cost is returned inline and logged per user |
| Virtual-key budget / rate limit | Account Kit primitives_config.llm — enable/disable + approval gating + workspace credits | Execution-time policy, per user — different shape, see gaps |
Prompt templates (portkey.prompts.completions.create) | — | Not provided — see gaps |
| Guardrails, caching (simple/semantic), conditional routing | — (approvals is the nearest governance hook) | Not provided — see gaps |
Before / after: the core path
The path that matters for almost every agent is send a chat completion, with a fallback model if the first is unavailable. Here it is on both platforms.- No provider keys to manage. In Portkey you create a virtual key / Model Catalog entry per provider and reference it. Naive holds the OpenRouter key server-side and bills you in credits — there is no upstream key to provision or rotate. (The flip side: you cannot bring your own provider key — see gaps.)
- Fallbacks are an ordered list, not a saved config. Portkey’s
config(apc-…object withstrategy/targets) becomes themodels: [...]array on the request, plus an optionalproviderobject for OpenRouter routing preferences. - Cost is inline and per-user. Portkey surfaces spend in its dashboard keyed by a
_usermetadata tag. Naive returnscredits_usedon the response and writes the event to the caller’s activity log — the same log as that user’s cards and email.
Keep your existing OpenAI client (drop-in proxy)
If you already point the OpenAI SDK at Portkey’s gateway, the migration is a two-line change: swap thebaseURL and drop createHeaders.
- The Naive proxy maps every path under
/v1/proxy/openrouter/*to OpenRouter, authenticated by your Naive key and billed in credits. - The proxy is not Account-Kit gated. When you want per-tenant policy enforcement, use the
typed
naive.llm.chat()/POST /v1/llm/chat/completionsroutes instead — that is the path the consolidation below relies on.
Minimal viable migration
The smallest swap that keeps a working agent running is just change the endpoint and the model slug. You do not need Account Kits, tenant-user fan-out, or credits top-ups to make your first call.Install the SDK and set your key
NAIVE_API_KEY (a server-side key from the dashboard).Translate model slugs
Portkey’s
@provider/model (Model Catalog) becomes OpenRouter’s provider/model. Browse the
routable set with naive.llm.models("claude") (free) to find exact ids like
anthropic/claude-sonnet-4.6 or openai/gpt-5.2.Swap the call
Replace
portkey.chat.completions.create({ model, messages }) with
naive.llm.chat({ model, messages }), or — if you use the OpenAI SDK — just point baseURL
at https://api.usenaive.ai/v1/proxy/openrouter and drop createHeaders.Move fallbacks/routing onto the request
A Portkey fallback
config becomes models: ["primary", "backup"]; provider preferences
become the provider: { … } object. No saved config object to create.Consolidate further once you’re on Naive
This is where the migration pays for itself. In Portkey, the_user metadata tag isolates LLM
observability and budgets and nothing else. On Naive, the unit of isolation is a tenant
user, and it isolates the agent’s entire footprint.
Gain #1 — one identity across primitives
- With Portkey, the agent’s LLM usage is an island tagged by metadata. With Naive,
naive.forUser(acme.id)is a single handle to LLM and cards and email and vault and KYC. - You provision a customer’s whole agent footprint from one identity, and tear it down from one place. Sibling tenants can never read each other’s completions, cards, or secrets.
Gain #2 — execution-time permission enforcement
- Whether an agent may call the model at all is policy on the Account Kit — not a key that happens to be in scope, and not a budget that lives in a separate product.
- The agent’s code is identical for every tier —
client.llm.chat({ model, … }). Whether the call runs is decided by the caller’s kit at execution time:primitives_config.llm.enabled: falsereturnsforbiddenfor that user — same line of code, no model access.requiresApprovalfreezes a sensitive sibling action (a card charge, a domain purchase) until a human approves — the API replays it only after approval.
- Move the user to a kit that disables
llmand the exact same line returnsforbidden, with no code change on your side. Portkey gates which key is used; Naive additionally gates whether execution happens at all, per user, in the same model as every other primitive.
Gain #3 — unified accountability
- Every completion for a customer lands in one per-user activity log — alongside their card, email, and vault events, not in a separate LLM dashboard:
- Spend is unified too: LLM calls bill the same credits as the
agent’s cards and domains (Naive bills OpenRouter’s returned
usage.cost× a small markup), so “what did this customer’s agent cost us this month?” is one number, not a reconciliation across Portkey, Stripe, and your own ledger.
What does not map yet
A migration guide that hides gaps is worse than none. None of the following block the core path (OpenAI-compatible completions, multi-model fallbacks, provider routing, streaming, and the drop-in proxy all map cleanly), but they are real differences. Check this list against your app before you commit.| Portkey feature | Status on Naive | Workaround |
|---|---|---|
| Bring-your-own provider keys (virtual keys / Model Catalog over your OpenAI/Anthropic/Bedrock/Azure accounts) | Not supported — Naive holds the OpenRouter key and bills credits | Use Naive credits; if you must use your own provider contract/keys, keep that traffic on Portkey or call the provider directly |
Saved Gateway Configs (pc-… objects, reused across requests, edited in the dashboard) | No saved config object | Pass models + provider inline per request, or wrap them in your own helper |
Load balancing / canary (strategy: "loadbalance", weighted targets, conditional routing) | Partial — OpenRouter provider.sort / order | Order-based fallback only; no weighted split or per-condition routing |
Retry policy (retry: { attempts, on_status_codes }, request timeouts) | Partial — models fallback + provider.allow_fallbacks | Implement client-side retry/backoff around llm.chat() |
Prompt management (portkey.prompts.completions.create({ promptID }), versioned templates) | Not provided | Manage prompts in your own code/store and pass full messages |
| Guardrails (input/output validation, PII, schema checks in the gateway) | Not provided (approvals gate primitives, not LLM content) | Validate in your app, or gate sibling actions with Approvals |
| Caching (simple + semantic response cache) | Not provided | Cache in your application layer |
| Per-key budgets & rate limits (hard spend caps / RPM per virtual key) | No per-user LLM budget cap; governance is enable/disable + approvals + workspace credits | Cap workspace credits; enforce per-user limits in your app for now |
| Observability product (analytics dashboards, OpenTelemetry export, custom metadata, trace ids) | Per-user activity log + generation(id) cost, not a full analytics suite | Query/stream logs; export to your own analytics |
| Self-hosted / hybrid gateway | Not applicable — Naive is hosted | n/a |
| Provider-specific passthrough features | OpenRouter normalizes across providers; some provider-only params may differ | Verify edge params against OpenRouter; fall back to calling the provider directly if required |
Where to go next
/llmprimitive — chat, streaming, models, and the drop-in proxy- LLM API reference — exact request/response shapes
llmSDK sub-client — typed method signatures- Credits — how LLM calls are metered and billed
- Account Kits and Approvals — the policy model that makes execution-time governance real
- Tenant users — the identity that the consolidation hangs off