<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ModelPlane</title>
    <description>The latest articles on DEV Community by ModelPlane (@modelplane).</description>
    <link>https://dev.to/modelplane</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066690%2Fcbd71875-aaa2-4b68-b0c8-eb62500acc04.png</url>
      <title>DEV Community: ModelPlane</title>
      <link>https://dev.to/modelplane</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/modelplane"/>
    <language>en</language>
    <item>
      <title>Embedding Groups: Dimension-Aware Routing for /v1/embeddings</title>
      <dc:creator>ModelPlane</dc:creator>
      <pubDate>Sun, 09 Aug 2026 02:22:59 +0000</pubDate>
      <link>https://dev.to/modelplane/embedding-groups-dimension-aware-routing-for-v1embeddings-372b</link>
      <guid>https://dev.to/modelplane/embedding-groups-dimension-aware-routing-for-v1embeddings-372b</guid>
      <description>&lt;h1&gt;
  
  
  Embedding Groups: Dimension-Aware Routing for /v1/embeddings
&lt;/h1&gt;

&lt;p&gt;If you're building on embeddings, you already know the pain: you pick a provider, bake &lt;code&gt;model="text-embedding-3-large"&lt;/code&gt; into your code, and pray the pricing and latency stay acceptable. The moment you want to switch or add a fallback, you discover the dirty secret of embedding vectors — &lt;strong&gt;they're meaningless unless every vector in your store has the same dimension&lt;/strong&gt;. A 3072-dimension vector from one model and a 1536-dimension vector from another can't be compared, clustered, or searched. Your vector database will happily store both, and then your similarity queries return garbage.&lt;/p&gt;

&lt;p&gt;ModelPlane's embedding groups solve this by treating dimension as a first-class routing constraint. The gateway validates that every backend in an embedding model group produces vectors with the same dimension — at configuration time, before you route a single request, and at runtime, so a provider change can't silently corrupt your index.&lt;/p&gt;

&lt;h2&gt;
  
  
  The abstraction: model groups, not model IDs
&lt;/h2&gt;

&lt;p&gt;The core ModelPlane abstraction is the &lt;a href="https://modelplane.dev/blog/model-groups-explained/" rel="noopener noreferrer"&gt;model group&lt;/a&gt;: &lt;code&gt;request.model&lt;/code&gt; is a name you control, not a provider model ID. You define a group like &lt;code&gt;prod-embeddings&lt;/code&gt;, point it at one or more backends, and your application code never changes when you swap providers.&lt;/p&gt;

&lt;p&gt;For chat and completion models, a model group can contain wildly different backends — &lt;code&gt;gpt-4o&lt;/code&gt; and &lt;code&gt;claude-3-5-sonnet&lt;/code&gt; have different tokenizers, different pricing, different latency profiles, but they all produce text. The routing engine doesn't care about the output shape; it just needs to get a string back.&lt;/p&gt;

&lt;p&gt;Embeddings break that assumption. The output of an embedding call is a vector, and vectors are only useful when they share a dimensional space. You can't fall back from a 3072-dimension model to a 1536-dimension model without silently breaking every downstream consumer of those vectors.&lt;/p&gt;

&lt;p&gt;This is why embedding groups are dimension-aware. The gateway enforces a contract that your code can't — that every backend in a group produces vectors your vector store can actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why dimension mismatch is a silent production killer
&lt;/h2&gt;

&lt;p&gt;Let's be concrete about the failure mode. You have a RAG pipeline. You chunk documents, embed them with &lt;code&gt;text-embedding-3-large&lt;/code&gt; (3072 dimensions), and store them in Pinecone or pgvector. Your app queries with the same model, gets a 3072-dimension query vector, and everything works.&lt;/p&gt;

&lt;p&gt;Now your embedding provider has an outage. You have a fallback configured — good engineering instinct. The fallback is &lt;code&gt;text-embedding-3-small&lt;/code&gt; (1536 dimensions). The gateway routes your query to the fallback, returns a 1536-dimension vector, and your vector store... accepts it. Pinecone doesn't reject vectors with the wrong dimension. pgvector doesn't either. Your similarity search now compares 3072-dimension stored vectors against a 1536-dimension query vector.&lt;/p&gt;

&lt;p&gt;The results are nonsense. Not obviously broken — just subtly wrong rankings, missing relevant documents, and a production incident that takes hours to diagnose because the error messages are unhelpful or nonexistent.&lt;/p&gt;

&lt;p&gt;This is the kind of bug that doesn't show up in staging. Staging has one provider. Production has fallbacks. And fallbacks without dimension validation are landmines.&lt;/p&gt;

&lt;h2&gt;
  
  
  How embedding groups enforce the contract
&lt;/h2&gt;

&lt;p&gt;ModelPlane's routing engine, &lt;code&gt;tryTargetsRecursively&lt;/code&gt;, walks a target tree according to a &lt;a href="https://modelplane.dev/blog/routing-strategies-explained/" rel="noopener noreferrer"&gt;routing strategy&lt;/a&gt;: single, fallback, loadbalance, or conditional. For embedding groups, the gateway adds a dimension check on top of that tree.&lt;/p&gt;

&lt;p&gt;At configuration time, when you create or update an embedding model group, the gateway validates that all targets resolve to models with the same embedding dimension. It uses the provider catalog's model metadata to check this before the group is saved. If you try to mix a 3072-dimension model with a 1536-dimension model, the gateway rejects the configuration.&lt;/p&gt;

&lt;p&gt;At runtime, the gateway verifies the dimension of each embedding response against the group's expected dimension. If a provider returns a vector with the wrong dimension — whether due to a model change, a provider-side error, or a misconfigured override — the gateway treats it as a failed target and advances to the next backend in the fallback chain.&lt;/p&gt;

&lt;p&gt;This means your fallback chain is safe by construction. You can configure &lt;code&gt;prod-embeddings&lt;/code&gt; with &lt;code&gt;text-embedding-3-large&lt;/code&gt; as primary and &lt;code&gt;text-embedding-3-small&lt;/code&gt; as fallback — but the gateway will reject that group at creation time because the dimensions don't match. You'll need to pick backends that actually produce compatible vectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can build with dimension-aware groups
&lt;/h2&gt;

&lt;p&gt;The constraint isn't a limitation — it's what makes the abstraction useful. Here's what becomes possible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;True embedding HA.&lt;/strong&gt; Pick two or more providers that offer models with the same dimension. For example, OpenAI's &lt;code&gt;text-embedding-3-large&lt;/code&gt; (3072) and a compatible model from another provider. Configure them in a fallback group. When one provider has an outage or rate-limits you, the gateway routes to the next, and every vector that comes back is guaranteed to be queryable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load-balanced embedding pipelines.&lt;/strong&gt; If you have a high-volume ingestion pipeline, spread embedding requests across multiple backends with weighted load balancing. The gateway distributes traffic, and you never have to worry about a fraction of your vectors being incompatible with the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional routing by request metadata.&lt;/strong&gt; Route embedding requests based on customer tier, region, or feature flag — but only among backends that produce the same dimension. The dimension check applies regardless of which strategy mode you use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider migration without re-embedding.&lt;/strong&gt; Want to move from one embedding provider to another? If they offer same-dimension models, you can run both in a load-balanced group, migrate traffic gradually, and re-embed your corpus at your own pace. The gateway ensures every new vector is compatible with your existing store.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;Here's what an embedding group looks like in practice. First, create the group via the ModelPlane API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://modelplane.dev/api/model-groups &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$MODELPLANE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "prod-embeddings",
    "routing_config": {
      "strategy": { "mode": "fallback" },
      "targets": [
        { "kv_ref": "cred:user:openai", "override_params": { "model": "text-embedding-3-large" } },
        { "kv_ref": "cred:user:voyage", "override_params": { "model": "voyage-3-large" } }
      ]
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;text-embedding-3-large&lt;/code&gt; and &lt;code&gt;voyage-3-large&lt;/code&gt; have the same dimension, the group is created. If not, the gateway returns a validation error telling you the dimensions don't match.&lt;/p&gt;

&lt;p&gt;Now your application code is trivial. Point your OpenAI client at ModelPlane and use the group name as the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://modelplane.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-modelplane-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# This routes to prod-embeddings, which handles fallback and dimension validation
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The vector database is the new hotness, but only if your vectors are compatible.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dimension: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your code doesn't know which provider answered, doesn't know about fallbacks, and doesn't need to check dimensions. The gateway handles all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dimension check is the feature
&lt;/h2&gt;

&lt;p&gt;Most routing gateways treat embeddings as an afterthought — just another endpoint to proxy. ModelPlane treats them as a distinct workload with a distinct contract. The dimension check isn't a validation nicety; it's the difference between a fallback that saves you from an outage and a fallback that silently corrupts your production data.&lt;/p&gt;

&lt;p&gt;Think about what your vector store actually needs. It needs every vector to have the same dimension. It needs that invariant to hold across provider outages, model deprecations, and traffic spikes. Your application code can't enforce that invariant — it's too far from the routing decision. Your vector store can't enforce it — it accepts whatever you give it. The only place to enforce it is at the routing layer, where the gateway sees every request and every response.&lt;/p&gt;

&lt;p&gt;That's what embedding groups do. They make dimension compatibility a property of the route, not a hope about the provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond embeddings: the same principle applies
&lt;/h2&gt;

&lt;p&gt;The dimension check is a specific instance of a general principle: &lt;strong&gt;the routing layer should enforce the contracts your application depends on&lt;/strong&gt;. For chat models, that means thinking normalization — one &lt;code&gt;thinking&lt;/code&gt; parameter, encoded per provider, so reasoning output comes back in a consistent shape. For system prompts, it means injection at the router, so safety and formatting instructions live with the route, not copy-pasted into every client. For embeddings, it means dimension validation, so your vector store never receives a vector it can't use.&lt;/p&gt;

&lt;p&gt;ModelPlane's &lt;a href="https://modelplane.dev/providers/" rel="noopener noreferrer"&gt;provider catalog&lt;/a&gt; covers the major embedding providers, and the same routing engine that handles your chat traffic handles your embedding traffic. One endpoint, one API key, one dashboard for usage and billing across all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a free account
&lt;/h2&gt;

&lt;p&gt;If you're building on embeddings, you know the pain of provider lock-in and the fear of silent corruption. Embedding groups give you the safety of a validated contract and the flexibility of routing — without changing your application code.&lt;/p&gt;

&lt;p&gt;Create your first embedding group — free $5 credits, no card required. Point your OpenAI client at &lt;code&gt;https://modelplane.dev/v1&lt;/code&gt;, define a model group, and let the gateway handle the rest.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series on building production-grade AI infrastructure with ModelPlane. Check out the rest:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/one-endpoint-every-model/" rel="noopener noreferrer"&gt;One endpoint, every model: why we built ModelPlane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/routing-strategies-explained/" rel="noopener noreferrer"&gt;Routing strategies, explained: fallback, load-balance, conditional&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/model-groups-explained/" rel="noopener noreferrer"&gt;Model groups: the one abstraction that decouples your app from providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/llm-high-availability-playbook/" rel="noopener noreferrer"&gt;High availability for LLM apps: a fallback playbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/coding-plan-routing/" rel="noopener noreferrer"&gt;Stop paying twice: route your coding-plan quota into production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/price-aware-routing/" rel="noopener noreferrer"&gt;Price-aware routing: cut your LLM bill without changing models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/credits-usage-billing/" rel="noopener noreferrer"&gt;Credits, usage &amp;amp; billing, explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/provider-catalog/" rel="noopener noreferrer"&gt;1600+ models, one API: the ModelPlane provider catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/global-and-china-routing/" rel="noopener noreferrer"&gt;One gateway, two regions: routing to global and China models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/orgs-workspaces/" rel="noopener noreferrer"&gt;ModelPlane for teams: orgs, workspaces &amp;amp; shared keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/in-product-assistant/" rel="noopener noreferrer"&gt;Meet the assistant: an AI helper that lives in your gateway dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/inside-the-routing-engine/" rel="noopener noreferrer"&gt;Inside the ModelPlane routing engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/unified-thinking-parameter/" rel="noopener noreferrer"&gt;One thinking parameter, every model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/system-prompt-injection/" rel="noopener noreferrer"&gt;The system prompt belongs at the router, not in your app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/embedding-groups-dimension-routing/" rel="noopener noreferrer"&gt;Embedding groups: dimension-aware routing for /v1/embeddings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/concurrency-control-gateway/" rel="noopener noreferrer"&gt;Concurrency control: rate limiting and queueing at the gateway&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/multimodal-routing/" rel="noopener noreferrer"&gt;Multimodal routing: images, audio, and video through one endpoint&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>engineering</category>
      <category>product</category>
      <category>embedding</category>
    </item>
    <item>
      <title>Multimodal-aware routing: vision requests find vision-capable backends</title>
      <dc:creator>ModelPlane</dc:creator>
      <pubDate>Sun, 09 Aug 2026 02:22:58 +0000</pubDate>
      <link>https://dev.to/modelplane/multimodal-aware-routing-vision-requests-find-vision-capable-backends-1mf0</link>
      <guid>https://dev.to/modelplane/multimodal-aware-routing-vision-requests-find-vision-capable-backends-1mf0</guid>
      <description>&lt;p&gt;Your app sends a text-only request. It also sends an image-with-a-question request. Right now, both go to the same model — usually the most expensive one that can handle both. That's the wrong unit of integration.&lt;/p&gt;

&lt;p&gt;The provider is the wrong unit of integration. The model group is the right one. And once you think in model groups, a question you couldn't answer before becomes obvious: &lt;em&gt;why does a text-only request need to hit a vision-capable backend at all?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Multimodal-aware routing means your model group filters its targets by the request's modalities. Text requests go to a cheap text tier. Image requests skip incompatible backends and land on a vision-capable one. You get lower cost, fewer failed requests, and no code changes in your app.&lt;/p&gt;

&lt;p&gt;Here's how to build it with ModelPlane.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: one model for every modality is expensive and fragile
&lt;/h2&gt;

&lt;p&gt;Most LLM apps are multimodal in practice even when they don't think of themselves that way. A support bot sends text. A document analyzer sends images. A coding assistant sends screenshots of error messages. A data pipeline sends charts.&lt;/p&gt;

&lt;p&gt;If you hard-code &lt;code&gt;model="gpt-4o"&lt;/code&gt; in your client, every one of those requests — text and image alike — goes to the same backend. That's the default, and it's wrong for two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, cost.&lt;/strong&gt; Vision-capable flagship models are priced at a premium over text-only tiers. A text-only request that gets routed to a vision model is paying for capability it doesn't use. When 80% of your traffic is text, that's a tax on the majority of your requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, fragility.&lt;/strong&gt; Not every model accepts images. If you route an image request to a text-only model, you get a 400 error — or worse, the model silently ignores the image and answers from text alone. You've shipped a bug you can't see.&lt;/p&gt;

&lt;p&gt;The fix isn't to write modality-checking logic in every service. It's to make the routing layer aware of what the request contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model groups: the abstraction that makes this possible
&lt;/h2&gt;

&lt;p&gt;A model group is a name you control — &lt;code&gt;prod-chat&lt;/code&gt;, &lt;code&gt;vision-pipeline&lt;/code&gt;, whatever — that maps to a routing strategy and a list of backend targets. Your client sends &lt;code&gt;model="prod-chat"&lt;/code&gt; to &lt;code&gt;https://modelplane.dev/v1&lt;/code&gt;, and the gateway expands that name into a routing config and runs the request through the routing engine.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;&lt;code&gt;request.model&lt;/code&gt; is a name you control, not a provider model id.&lt;/strong&gt; You can change what that name resolves to without touching your code. That's what makes multimodal-aware routing possible — the routing config, not the client, decides which backend answers.&lt;/p&gt;

&lt;p&gt;The routing engine supports four strategy modes: &lt;code&gt;single&lt;/code&gt;, &lt;code&gt;fallback&lt;/code&gt;, &lt;code&gt;loadbalance&lt;/code&gt;, and &lt;code&gt;conditional&lt;/code&gt;. For multimodal-aware routing, you combine &lt;code&gt;fallback&lt;/code&gt; (for reliability) with target-level filtering based on request content.&lt;/p&gt;

&lt;h2&gt;
  
  
  How multimodal-aware routing works
&lt;/h2&gt;

&lt;p&gt;ModelPlane's routing engine walks a target tree for each request. The &lt;code&gt;fallback&lt;/code&gt; strategy tries targets in order and advances to the next on specified status codes — that's your high-availability backbone. What multimodal-aware routing adds is a filter at the target level: &lt;strong&gt;if the request contains an image, skip targets that can't handle images.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Concretely, your model group's routing config looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fallback"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kv_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cred:&amp;lt;userId&amp;gt;:&amp;lt;credId&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"override_params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deepseek-v4-flash"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modalities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kv_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cred:&amp;lt;userId&amp;gt;:&amp;lt;credId2&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"override_params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5.4-mini"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modalities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kv_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cred:&amp;lt;userId&amp;gt;:&amp;lt;credId3&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"override_params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5.6-sol"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modalities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway inspects the incoming request. If it's text-only, the first target (cheap, fast, text-only) handles it. If it contains an image, the router skips the first target and goes straight to a vision-capable backend. If that backend fails, it falls through to the next vision-capable one.&lt;/p&gt;

&lt;p&gt;No client-side logic. No modality checks in your app code. The routing layer handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example: DeepSeek-first, vision-capable fallback
&lt;/h2&gt;

&lt;p&gt;Let's build a real model group. The CTA for this post is "Build a DeepSeek-first, vision-capable-fallback group." Here's why that's the right shape.&lt;/p&gt;

&lt;p&gt;DeepSeek's V4 family is cost-efficient and has a 1M-token context window with a 384K max output — excellent for long-generation text workloads. But it's not a vision model. If you route an image request there, you get an error.&lt;/p&gt;

&lt;p&gt;So you build a group that's DeepSeek-first for text, with vision-capable fallbacks for image requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://modelplane.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gw-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# your ModelPlane gateway key
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Text-only request → routes to deepseek-v4-flash
&lt;/span&gt;&lt;span class="n"&gt;text_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this changelog in three bullets.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Image request → skips text-only targets, routes to a vision-capable backend
&lt;/span&gt;&lt;span class="n"&gt;image_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the error in this screenshot?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/error-screenshot.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same client. Same model group name. Different backends, chosen automatically by the router based on what the request contains.&lt;/p&gt;

&lt;p&gt;The first request hits DeepSeek V4 Flash — cheap, fast, text-only. The second request skips it and lands on a vision-capable model. If that model is rate-limited or returns a 5xx, the fallback strategy advances to the next vision-capable target. Your app never sees a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why routing beats client-side modality checks
&lt;/h2&gt;

&lt;p&gt;You could implement modality detection in your app. Check if the message contains an image, then pick a model. It's not hard — a few lines of code.&lt;/p&gt;

&lt;p&gt;But it's the wrong place for that logic, for the same reason system prompts belong at the router, not in your app: &lt;strong&gt;behavior should live with the route, not copy-pasted into every client.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Client-side modality checks have three problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They drift.&lt;/strong&gt; You have five services calling models. Each one implements its own modality check. One service forgets. Another uses a different image format. Now you have inconsistent behavior across your stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They can't react to provider changes.&lt;/strong&gt; Your text-only model gets a vision update. Your vision model gets deprecated. With client-side checks, you're updating code and redeploying. With router-side filtering, you edit the model group config and you're done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They can't enforce policy.&lt;/strong&gt; A client can always send an image to a text-only model. The router can't stop it. With multimodal-aware routing, the gateway &lt;em&gt;enforces&lt;/em&gt; the filter — an image request physically cannot land on a text-only target.&lt;/p&gt;

&lt;p&gt;The router is the single enforcement point. That's where modality awareness belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining multimodal filtering with other routing strategies
&lt;/h2&gt;

&lt;p&gt;Multimodal-aware routing isn't a replacement for the other strategies — it composes with them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback + multimodal.&lt;/strong&gt; Your vision-capable primary target fails. The router advances to the next vision-capable target. Text requests never trigger the vision fallback chain, so you're not paying vision prices for text traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loadbalance + multimodal.&lt;/strong&gt; You want to split traffic across two vision-capable models by weight — 70% to one, 30% to another — while text-only requests go to a third, cheaper tier. The modality filter narrows the candidate pool; the load-balance strategy picks among the survivors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional + multimodal.&lt;/strong&gt; You route by customer tier: free-tier users get the cheap vision model, paid users get the flagship. The modality filter runs first, then the conditional router picks among the vision-capable targets.&lt;/p&gt;

&lt;p&gt;The routing engine's target tree handles all of this. Multimodal filtering is just another constraint on which targets are eligible for a given request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for your LLM stack
&lt;/h2&gt;

&lt;p&gt;Multimodal-aware routing changes how you think about your model inventory. Instead of one model per use case, you have a pool of backends with different capabilities, and the router matches requests to the cheapest capable target.&lt;/p&gt;

&lt;p&gt;The practical wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lower cost.&lt;/strong&gt; Text requests stop paying vision premiums. If 80% of your traffic is text, that's a direct reduction in your bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer failures.&lt;/strong&gt; Image requests never hit text-only backends. No more silent image-ignoring bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One client.&lt;/strong&gt; Your app sends &lt;code&gt;model="prod-chat"&lt;/code&gt; and never thinks about modalities again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider flexibility.&lt;/strong&gt; Swap DeepSeek for another text tier, add a new vision model, rebalance weights — all in the model group config, no deploys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The provider is the wrong unit of integration. The model group is the right one. Multimodal-aware routing is what happens when you take that seriously — your routing layer knows what your requests contain, and it routes accordingly.&lt;/p&gt;

&lt;p&gt;Build a DeepSeek-first, vision-capable-fallback group and watch your text traffic stop paying vision prices.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Ready to route smarter?&lt;/strong&gt; Start free — $5 credits, no card. Create your first model group in minutes and see multimodal-aware routing in action.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series on building production-grade LLM infrastructure with ModelPlane. Stay tuned for deep dives into routing strategies, model groups, unified thinking, system-prompt injection, high availability, coding-plan routing, price-aware routing, billing, the provider catalog, teams, and the routing engine itself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>product</category>
      <category>multimodal</category>
    </item>
    <item>
      <title>Inside the ModelPlane routing engine</title>
      <dc:creator>ModelPlane</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:18:35 +0000</pubDate>
      <link>https://dev.to/modelplane/inside-the-modelplane-routing-engine-41a1</link>
      <guid>https://dev.to/modelplane/inside-the-modelplane-routing-engine-41a1</guid>
      <description>&lt;h1&gt;
  
  
  Inside the ModelPlane routing engine
&lt;/h1&gt;

&lt;p&gt;Every LLM request your app sends is a bet: that the provider you hard-coded will be up, fast, and cheap enough. That bet is why routing layers exist. But a routing layer only helps if it's fast, reliable, and transparent enough to sit on every single call you make. This post walks through exactly what happens inside the ModelPlane routing engine from the moment your request hits the gateway to the moment tokens come back — and why the design keeps accounting off the hot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The request lifecycle: auth, resolve, gate, route, account
&lt;/h2&gt;

&lt;p&gt;A single request through ModelPlane passes through five distinct stages. Understanding them matters because each one is a place where a naive gateway either slows you down, leaks credentials, or drops usage data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; — the gateway resolves your &lt;code&gt;Bearer gw-*&lt;/code&gt; key into a tenant context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config resolution&lt;/strong&gt; — it figures out &lt;em&gt;which&lt;/em&gt; routing config applies to this request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing gate&lt;/strong&gt; — a pre-request check confirms you have credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing&lt;/strong&gt; — it walks the target tree, picks a backend, and makes the upstream call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting&lt;/strong&gt; — it records usage and decrements credits asynchronously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's go through each stage with the concrete mechanisms, because the details are where the reliability lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: Authentication — the &lt;code&gt;gw-*&lt;/code&gt; key is auth only, never forwarded
&lt;/h2&gt;

&lt;p&gt;The first thing the gateway does is authenticate the request. It resolves the &lt;code&gt;Bearer gw-*&lt;/code&gt; token (or a Supabase JWT for management calls) into a &lt;code&gt;TenantContext&lt;/code&gt;. That context carries your workspace identity and the credentials needed to call upstream providers.&lt;/p&gt;

&lt;p&gt;The critical security property here: the &lt;code&gt;Authorization&lt;/code&gt; header containing your &lt;code&gt;gw-*&lt;/code&gt; key is stripped from the request before it's ever forwarded upstream. Your gateway key is tenant authentication &lt;em&gt;only&lt;/em&gt;. It is never the credential an upstream provider sees. That separation is what makes BYOK safe — your provider keys are encrypted per-tenant and only hydrated at routing time, never exposed to your clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: Config resolution — &lt;code&gt;request.model&lt;/code&gt; is a name you control
&lt;/h2&gt;

&lt;p&gt;Once authenticated, the gateway needs to decide &lt;em&gt;how&lt;/em&gt; to route. This is where the core abstraction kicks in: &lt;code&gt;request.model&lt;/code&gt; is not a provider model ID. It's a &lt;strong&gt;model group&lt;/strong&gt; — a name you control that maps to a set of targets and a routing strategy.&lt;/p&gt;

&lt;p&gt;The resolution order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explicit per-request config&lt;/strong&gt; (&lt;code&gt;x-portkey-config&lt;/code&gt; header, legacy) — overrides everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;request.model&lt;/code&gt; as a model group&lt;/strong&gt; — looked up in KV cache first, then Supabase. Credentials for the group's backends are decrypted from their &lt;code&gt;kv_ref&lt;/code&gt;s at this point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct provider call&lt;/strong&gt; (&lt;code&gt;x-portkey-provider&lt;/code&gt; header) — a single-provider bypass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most users, path #2 is the one that matters. You define a model group called &lt;code&gt;prod-chat&lt;/code&gt; in the dashboard, point it at three backends with a fallback strategy, and then in your code you just send &lt;code&gt;model="prod-chat"&lt;/code&gt;. The gateway resolves that name to the full routing config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://modelplane.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gw-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# your ModelPlane gateway key
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# "prod-chat" is a model group, not a provider model ID.
# The gateway resolves it to targets + strategy server-side.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain routing engines.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stage 3: The billing gate — a snapshot, not a lock
&lt;/h2&gt;

&lt;p&gt;Before routing, the &lt;code&gt;preRequestValidator&lt;/code&gt; checks your available credits. If you're out, you get a &lt;code&gt;402&lt;/code&gt;. If you're on the &lt;code&gt;unlimited&lt;/code&gt; plan, the gate is skipped entirely.&lt;/p&gt;

&lt;p&gt;This is a deliberate design choice: a pre-request balance &lt;em&gt;snapshot&lt;/em&gt; gate plus post-request async deduction. It's fast — no distributed transaction on the hot path — but it means there's a theoretical window where concurrent requests could overspend. The team has documented this as a known gap (PRD/002 covers reliability fixes like an outbox pattern and atomic balance updates). For the vast majority of usage, the snapshot gate is the right tradeoff: it catches the "I'm out of credits" case instantly without adding a database round-trip to every inference call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: The routing engine — walking the target tree
&lt;/h2&gt;

&lt;p&gt;This is the heart of the system. The routing engine's &lt;code&gt;tryTargetsRecursively&lt;/code&gt; walks the target tree according to the strategy mode you configured on the model group. There are four modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;single&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One target, no alternatives&lt;/td&gt;
&lt;td&gt;Dev/test, or when you want zero ambiguity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fallback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Try in order; advance on specified status codes&lt;/td&gt;
&lt;td&gt;High availability — the minimum viable LLM stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loadbalance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pick by weight (&lt;code&gt;selectProviderByWeight&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A/B tests, cost-weighted splits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conditional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Evaluate a query DSL against request metadata to pick a named target&lt;/td&gt;
&lt;td&gt;Customer-tier routing, region-based routing, feature flags&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For each target, &lt;code&gt;tryPost&lt;/code&gt; builds the upstream call, runs any configured hooks, checks the cache, and calls the retry handler. If a target fails with a status code that the strategy says to retry on, the engine advances to the next target in the tree.&lt;/p&gt;

&lt;p&gt;The fallback mode is where the reliability value lives. A production-grade setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fallback"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anthropic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-3-5-sonnet"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deepseek"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deepseek-v3"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When OpenAI returns a 5xx or times out, the engine automatically tries Anthropic, then DeepSeek. Your app sees one endpoint and one model name; the gateway handles the chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: Accounting — off the hot path, but not off the record
&lt;/h2&gt;

&lt;p&gt;After the upstream response is returned, &lt;code&gt;recordUsage&lt;/code&gt; runs asynchronously. It writes to &lt;code&gt;usage_logs&lt;/code&gt; and decrements credits, using a reference ID to keep the operation idempotent. This is what keeps the inference hot path fast — the gateway doesn't block your response on a database write.&lt;/p&gt;

&lt;p&gt;The honest caveat: because accounting is fire-and-forget, there's a small risk of dropped usage records under extreme load. This is a documented tradeoff, not a hidden bug. The team's roadmap includes moving to a more durable outbox pattern. For now, the async model buys you latency at the cost of a tiny accounting tail-risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the provider abstraction matters
&lt;/h2&gt;

&lt;p&gt;The routing engine doesn't care which provider it's calling. The provider system abstracts every upstream behind a uniform, OpenAI-compatible interface. Requests are transformed from the OpenAI parameter shape into each provider's native format, and responses are normalized back.&lt;/p&gt;

&lt;p&gt;This is what makes the &lt;a href="https://modelplane.dev/providers/" rel="noopener noreferrer"&gt;provider catalog&lt;/a&gt; so powerful. You can route to OpenAI, Anthropic, Google Gemini, DeepSeek, MiniMax, Zhipu, OpenRouter, or Novita AI — all through the same &lt;code&gt;base_url&lt;/code&gt; and the same client library. The gateway handles the protocol differences, the parameter mapping, and the response normalization.&lt;/p&gt;

&lt;p&gt;The abstraction also extends to reasoning models. A single &lt;code&gt;thinking&lt;/code&gt; parameter on the request is encoded per provider — effort scalars for some, thinking objects for others, passthrough for the rest. Your code doesn't branch on provider; the gateway's &lt;code&gt;ReasoningMapping&lt;/code&gt; handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-tenancy without slowing down inference
&lt;/h2&gt;

&lt;p&gt;The routing engine runs in a multi-tenant environment, which means isolation has to be baked in, not bolted on. The design keeps it off the hot path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credentials&lt;/strong&gt; are encrypted per-tenant (AES-GCM) and stored in Workers KV. They're decrypted only when a model group's config is hydrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing config&lt;/strong&gt; is cached in KV, so the gateway doesn't hit the database on every request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenancy&lt;/strong&gt; is workspace-scoped — all resources (API keys, backends, model groups, usage) belong to a workspace, and the billing account owns the plan and credits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: tenant isolation and encrypted credential handling don't add a round-trip to your inference call. The security model is a property of the infrastructure, not a per-request cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The target tree is the unit of resilience
&lt;/h2&gt;

&lt;p&gt;Here's the mental model that makes all of this click: &lt;strong&gt;your model group is a target tree, and the routing strategy is how the tree is traversed.&lt;/strong&gt; A single model ID in your code is a leaf. A fallback chain is a branch. A weighted load-balance is a branch with probabilities. A conditional router is a branch with a decision function.&lt;/p&gt;

&lt;p&gt;Once you think in target trees, the engineering choices become obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fallback isn't a feature&lt;/strong&gt; — for anything production-bound, it's the minimum viable LLM stack. Provider outages are when-not-if.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load-balancing by weight&lt;/strong&gt; lets you express cost policy in the routing config, not in application code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional routing&lt;/strong&gt; moves business logic (customer tier, region) into the gateway, where it's auditable and changeable without a deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hot path stays hot
&lt;/h2&gt;

&lt;p&gt;The whole design philosophy can be summarized in one sentence: &lt;strong&gt;the inference hot path does only what it must, and everything else happens around it.&lt;/strong&gt; Auth is a fast token lookup. Config resolution is a KV cache hit. The billing gate is a snapshot check. Routing is an in-memory tree walk. Accounting is async.&lt;/p&gt;

&lt;p&gt;That's why ModelPlane can sit on every call your app makes without becoming the bottleneck. The gateway isn't doing anything clever per request — it's doing the &lt;em&gt;right&lt;/em&gt; things per request and deferring everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build your first target tree
&lt;/h2&gt;

&lt;p&gt;The fastest way to understand the routing engine is to build a model group with a fallback chain. Create a group called &lt;code&gt;prod-chat&lt;/code&gt;, add two or three backends, set the strategy to &lt;code&gt;fallback&lt;/code&gt;, and point your OpenAI client at &lt;code&gt;https://modelplane.dev/v1&lt;/code&gt;. Then kill one provider and watch the gateway route around it.&lt;/p&gt;

&lt;p&gt;Start free — $5 credits, no card. Your first model group is 60 seconds away.&lt;/p&gt;

</description>
      <category>engineering</category>
    </item>
    <item>
      <title>ModelPlane for teams: orgs, workspaces &amp; shared keys</title>
      <dc:creator>ModelPlane</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:18:34 +0000</pubDate>
      <link>https://dev.to/modelplane/modelplane-for-teams-orgs-workspaces-shared-keys-4oab</link>
      <guid>https://dev.to/modelplane/modelplane-for-teams-orgs-workspaces-shared-keys-4oab</guid>
      <description>&lt;h1&gt;
  
  
  ModelPlane for teams: orgs, workspaces &amp;amp; shared keys
&lt;/h1&gt;

&lt;p&gt;The moment a second engineer joins your project, the "just use my API key" approach to LLM infrastructure breaks. Someone commits a key to a repo, another person rotates it, and suddenly production is down because the key that was working yesterday is gone. You need multi-tenancy, but you don't need the complexity of building it yourself.&lt;/p&gt;

&lt;p&gt;ModelPlane treats team access as a first-class routing problem, not an afterthought. The gateway separates identity, resources, and billing into distinct entities, gives you scoped keys that can't leak upstream, and lets an organization share credits across every workspace. This is the difference between a gateway that works for one developer and a gateway that works for a team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-tenant mess you're actually in
&lt;/h2&gt;

&lt;p&gt;Most teams start with a single shared key. It's pragmatic until it isn't. You can't tell which teammate burned through the budget, you can't revoke access for one person without breaking everyone, and you definitely can't enforce different routing policies for different services.&lt;/p&gt;

&lt;p&gt;Hard-coding a provider key in your backend is worse. That key is a direct line to your provider account, and it gets forwarded with every request. If it leaks, someone else can spend your money. If you need to rotate it, you're redeploying services.&lt;/p&gt;

&lt;p&gt;The abstraction that fixes this is the same one that fixes provider lock-in: the model group. Your application sends requests to a model group like &lt;code&gt;prod-chat&lt;/code&gt;, and the gateway decides which backend answers. Team access works the same way — you manage who can call that group, not which provider key they hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three entities, one clean boundary
&lt;/h2&gt;

&lt;p&gt;ModelPlane's multi-tenancy model separates three concerns that most systems conflate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Login identity, audit actor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workspace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resource owner — API keys, backends, model groups, usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Billing Account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Plan, credits, transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A user is who you are. A workspace is what you own. A billing account is how you pay. When these are the same thing, you get the single-developer experience. When they're separate, you get teams.&lt;/p&gt;

&lt;p&gt;The workspace is the unit of isolation. Every resource — every API key, backend, model group, and usage log — belongs to a workspace. This means you can have a &lt;code&gt;staging&lt;/code&gt; workspace and a &lt;code&gt;production&lt;/code&gt; workspace with completely different routing configurations, and the keys for one can't touch the other's resources.&lt;/p&gt;

&lt;p&gt;Organizations take this further. An organization requires a paid plan and can have multiple workspaces. Crucially, an organization's workspaces &lt;strong&gt;share the organization's credits&lt;/strong&gt;. Your team gets one billing surface, not a dozen individual accounts to reconcile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key scopes: private vs. shared
&lt;/h2&gt;

&lt;p&gt;The gateway API key is the credential your code actually uses. It's a &lt;code&gt;gw-*&lt;/code&gt; bearer token that authenticates the request to ModelPlane. Two properties make it fundamentally safer than a raw provider key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's never forwarded upstream.&lt;/strong&gt; The gateway strips the &lt;code&gt;Authorization&lt;/code&gt; header carrying your &lt;code&gt;gw-*&lt;/code&gt; key before making the upstream call. Your provider credentials are resolved server-side from encrypted storage. A leaked gateway key can't be used to call OpenAI directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only the hash is stored.&lt;/strong&gt; The plaintext key is returned exactly once at creation. If you lose it, you create a new one. There's no database of recoverable secrets to steal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keys come in two scopes, mirroring how your team actually works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private keys&lt;/strong&gt; — personal and member-scoped. Every user, including org members, can create these. They're yours, and they're tied to your identity for audit purposes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared keys&lt;/strong&gt; — available to organization owners and admins. These are workspace-scoped credentials that multiple services can use without being tied to one person's account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the key insight: shared keys are for machines, private keys are for humans. A CI pipeline gets a shared key scoped to the workspace it deploys to. A developer debugging locally uses their private key, and their usage shows up in audit logs under their identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up an organization
&lt;/h2&gt;

&lt;p&gt;The portal keeps tenant context visible before every resource action — you always know which org and workspace you're operating in. The flow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an organization (requires a paid plan).&lt;/li&gt;
&lt;li&gt;Invite members with &lt;code&gt;owner&lt;/code&gt;, &lt;code&gt;admin&lt;/code&gt;, or &lt;code&gt;member&lt;/code&gt; roles.&lt;/li&gt;
&lt;li&gt;Create workspaces for different environments or product lines.&lt;/li&gt;
&lt;li&gt;Add backends and model groups per workspace.&lt;/li&gt;
&lt;li&gt;Issue shared keys for services, private keys for people.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A free user gets one auto-created default workspace. Paid users can have multiple personal workspaces. Organizations are where the real team structure lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing that respects team boundaries
&lt;/h2&gt;

&lt;p&gt;The point of all this structure is that routing policies become team-manageable. Consider a typical setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;prod-chat&lt;/code&gt; model group in the production workspace with a fallback chain across three providers.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;staging-chat&lt;/code&gt; group in the staging workspace pointing at cheaper models.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;data-extraction&lt;/code&gt; group with a system prompt injected for structured output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each group has its own backends, its own routing strategy, and its own usage accounting. The team that owns the data pipeline can change its routing without touching the chat application's configuration. The billing account sees one consolidated view of credits consumed across all workspaces.&lt;/p&gt;

&lt;p&gt;This is what "LLM gateway for teams" actually means: not just a shared endpoint, but shared infrastructure with per-team control planes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code: one client, scoped by key
&lt;/h2&gt;

&lt;p&gt;Here's how a service uses a shared key to talk to a model group. The code is identical to calling OpenAI directly — only the &lt;code&gt;base_url&lt;/code&gt; and model name change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://modelplane.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gw-your-shared-key-here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# workspace-scoped, never forwarded upstream
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# a model group, not a provider model id
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize the incident report.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same code works for a developer using their private key. The gateway resolves the model group &lt;code&gt;prod-chat&lt;/code&gt; to the appropriate backend, applies the routing strategy, and accounts the usage to the workspace that owns the key.&lt;/p&gt;

&lt;p&gt;If the production workspace's primary provider has an outage, the fallback chain kicks in — the application doesn't need to know, and the developer doesn't need to change anything. The model group abstracts away both the provider and the team's access policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Billing that doesn't require a spreadsheet
&lt;/h2&gt;

&lt;p&gt;The shared-credits model is the operational win. When an organization's workspaces share credits, you eliminate the monthly ritual of collecting individual usage reports and trying to figure out who owes what.&lt;/p&gt;

&lt;p&gt;The gateway records usage per API key, per model group, and per model. You can see at a glance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which workspace consumed the most tokens this month.&lt;/li&gt;
&lt;li&gt;Which model group is driving cost.&lt;/li&gt;
&lt;li&gt;Whether the &lt;code&gt;staging&lt;/code&gt; group is accidentally handling production traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This visibility turns billing from an argument into a report. And because the gateway handles the routing, you can also make cost a routing criterion — prefer cheaper models for internal tools, escalate to premium models only for customer-facing features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limitations
&lt;/h2&gt;

&lt;p&gt;Multi-tenancy is a design goal, and the rollout is a process. The full org/workspace model is a v1 target, and some edge cases — like per-workspace concurrency limits surfacing billing atomicity gaps — are still being hardened. If you're evaluating ModelPlane for a large enterprise, it's worth confirming the specific org features you need are fully live.&lt;/p&gt;

&lt;p&gt;For most teams, though, the core value is immediate: separate workspaces, scoped keys, shared billing, and routing policies that don't require a shared credential or a shared fate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop sharing keys, start sharing infrastructure
&lt;/h2&gt;

&lt;p&gt;The single most important step for any team building on LLMs is to stop treating provider keys as team infrastructure. They're not — they're a liability. ModelPlane replaces them with a routing layer that gives you the isolation of separate accounts and the simplicity of a shared one.&lt;/p&gt;

&lt;p&gt;Create an organization, set up your workspaces, and issue scoped keys. Your future self, debugging a production incident at 2 AM, will thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CTA: Create an organization.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series on building production-grade LLM infrastructure. Here's what else we've covered and what's coming:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One endpoint, every model&lt;/strong&gt; — why we built ModelPlane&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing strategies, explained&lt;/strong&gt; — fallback, load-balance, conditional&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model groups&lt;/strong&gt; — the one abstraction that decouples your app from providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bring your own keys, safely&lt;/strong&gt; — how BYOK encryption works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High availability for LLM apps&lt;/strong&gt; — a fallback playbook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop paying twice&lt;/strong&gt; — route your coding-plan quota into production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price-aware routing&lt;/strong&gt; — cut your LLM bill without changing models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credits, usage &amp;amp; billing, explained&lt;/strong&gt; — one transparent bill&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1600+ models, one API&lt;/strong&gt; — the provider catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One gateway, two regions&lt;/strong&gt; — routing to global and China models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ModelPlane for teams&lt;/strong&gt; — orgs, workspaces &amp;amp; shared keys (you are here)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meet the assistant&lt;/strong&gt; — an AI helper in your gateway dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inside the routing engine&lt;/strong&gt; — how a request flows from auth to upstream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One thinking parameter, every model&lt;/strong&gt; — unified reasoning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The system prompt belongs at the router&lt;/strong&gt; — not in your app&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>teams</category>
      <category>product</category>
    </item>
    <item>
      <title>Bring Your Own Keys, Safely: The BYOK Security Model Behind ModelPlane</title>
      <dc:creator>ModelPlane</dc:creator>
      <pubDate>Fri, 07 Aug 2026 03:43:30 +0000</pubDate>
      <link>https://dev.to/modelplane/bring-your-own-keys-safely-the-byok-security-model-behind-modelplane-1hf7</link>
      <guid>https://dev.to/modelplane/bring-your-own-keys-safely-the-byok-security-model-behind-modelplane-1hf7</guid>
      <description>&lt;h1&gt;
  
  
  Bring Your Own Keys, Safely: The BYOK Security Model Behind ModelPlane
&lt;/h1&gt;

&lt;p&gt;If you're building on top of LLMs, you've already made the security decision that matters most: you're not training your own models. You're renting intelligence from someone else's API. The question is how you manage the credentials that unlock that intelligence.&lt;/p&gt;

&lt;p&gt;Most teams hardcode a provider key into a service, or worse, share one key across an entire organization. When that key leaks—and it will—you're not just out a few dollars. You're exposed to prompt injection attacks, data exfiltration, and a billing nightmare that takes weeks to untangle.&lt;/p&gt;

&lt;p&gt;The solution isn't to stop using LLMs. It's to route your traffic through a gateway that treats credentials as first-class, tenant-isolated secrets. That's the BYOK (Bring Your Own Key) model at the heart of ModelPlane. It's not just about convenience—it's about building a security boundary between your application and the providers you depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wrong Unit of Integration
&lt;/h2&gt;

&lt;p&gt;Before we talk about keys, let's talk about what you're actually integrating with. Most teams think in terms of providers: "We use OpenAI," or "We're on Anthropic now." That's the wrong mental model.&lt;/p&gt;

&lt;p&gt;The provider is the wrong unit of integration. Your application shouldn't care whether a request is answered by &lt;code&gt;gpt-4o&lt;/code&gt;, &lt;code&gt;claude-3-5-sonnet&lt;/code&gt;, or &lt;code&gt;deepseek-v3&lt;/code&gt;. It should care about the &lt;em&gt;capability&lt;/em&gt;: a fast chat model, a reasoning model, a cheap batch model. That capability is what we call a &lt;strong&gt;model group&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A model group is a name you control—like &lt;code&gt;prod-chat&lt;/code&gt;—that maps to one or more provider backends with a routing strategy. When your app sends a request to &lt;code&gt;model="prod-chat"&lt;/code&gt;, ModelPlane decides which backend answers, based on fallback rules, load-balancing weights, or conditional logic.&lt;/p&gt;

&lt;p&gt;This abstraction changes the security conversation. Instead of managing N provider keys across M services, you manage one gateway key per environment, and the gateway manages the provider keys for you. The keys become infrastructure, not application logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Keys, Two Jobs
&lt;/h2&gt;

&lt;p&gt;The first thing to understand about ModelPlane's security model is that there are two entirely different types of credentials in play. Confusing them is the root of most LLM gateway security failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gateway API Key (&lt;code&gt;gw-*&lt;/code&gt;)&lt;/strong&gt;: This is what your application sends to ModelPlane. It starts with &lt;code&gt;gw-&lt;/code&gt; and is a bearer token that authenticates your tenant. It's how we know which workspace is making the request, which model groups it can access, and which billing account to charge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend Credential&lt;/strong&gt;: This is the actual provider API key—your OpenAI key, your Anthropic key, your DeepSeek key. You upload these to ModelPlane via the Backends page or API, and we store them encrypted.&lt;/p&gt;

&lt;p&gt;The critical rule: &lt;strong&gt;the gateway key is for authentication only. It is never forwarded upstream.&lt;/strong&gt; When your request hits our edge, the &lt;code&gt;Authorization&lt;/code&gt; header carrying &lt;code&gt;gw-*&lt;/code&gt; is stripped before the request is routed to any provider. Forwarding it would be a bug—and it's a bug we've designed against at the middleware level.&lt;/p&gt;

&lt;p&gt;This separation means a leaked gateway key gives an attacker access to &lt;em&gt;your&lt;/em&gt; ModelPlane usage, not to your underlying provider accounts. They could burn your credits, but they can't exfiltrate your OpenAI or Anthropic keys. That's a meaningful reduction in blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Backend Credentials Are Stored
&lt;/h2&gt;

&lt;p&gt;When you upload a provider key to ModelPlane, it doesn't go into a database table where a SQL injection or a rogue admin could read it. It goes into Cloudflare Workers KV, encrypted.&lt;/p&gt;

&lt;p&gt;The encryption model is worth understanding because it's the difference between "we encrypt your data" and "we encrypt your data in a way that actually protects you."&lt;/p&gt;

&lt;p&gt;Each credential is stored under a key that includes your user ID: &lt;code&gt;cred:{userId}:{credId}&lt;/code&gt;. The payload is encrypted with AES-GCM, using a root key from a Worker secret. But here's the important part: that root key isn't used directly. It's fed through HKDF with your user ID as the salt, deriving a per-user encryption key.&lt;/p&gt;

&lt;p&gt;This means two things. First, your credentials are encrypted at rest with a key that's unique to you. Second, when the gateway needs to use a credential, it decrypts &lt;em&gt;only&lt;/em&gt; the keys belonging to the current tenant. Cross-tenant access—even by accident—is rejected at the hydration layer.&lt;/p&gt;

&lt;p&gt;We also never cache decrypted credentials. The routing cache may hold model-group metadata, but the API keys themselves are only decrypted in memory for the duration of a request, then discarded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Your Threat Model
&lt;/h2&gt;

&lt;p&gt;Let's be concrete about what this architecture protects against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 1: A gateway key leaks.&lt;/strong&gt; An attacker gets a &lt;code&gt;gw-*&lt;/code&gt; token from a compromised service. They can make requests through your model groups, spending your credits. But they cannot extract your provider keys, and they cannot access your other tenants' data. You revoke the key, and the attack surface closes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 2: A database backup leaks.&lt;/strong&gt; Supabase, Workers KV, S3—any of these could theoretically be compromised. But the provider keys in KV are AES-GCM encrypted with per-user keys. The gateway key hashes in Supabase are SHA-256, which means the plaintext is unrecoverable. A backup leak is a nuisance, not a catastrophe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 3: An insider threat.&lt;/strong&gt; A ModelPlane employee with database access still can't read your provider keys. The encryption keys are in Worker secrets, not in the database. This is the BYOK promise: you're not trusting us with your keys, you're trusting us with &lt;em&gt;encrypted&lt;/em&gt; keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Setup
&lt;/h2&gt;

&lt;p&gt;Here's what this looks like in practice. You sign up for ModelPlane, add your first backend, and point your OpenAI-compatible client at our endpoint.&lt;/p&gt;

&lt;p&gt;First, add a backend credential. You can do this through the portal or via the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://modelplane.dev/api/backends &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$GATEWAY_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "provider": "openai",
    "apiKey": "sk-your-openai-key",
    "options": { "models": ["gpt-4o", "gpt-4o-mini"] }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you a &lt;code&gt;backendId&lt;/code&gt;. You reference that ID in a model group, which defines the routing strategy. Then your application only ever sees the gateway key.&lt;/p&gt;

&lt;p&gt;Here's a complete Python example using the standard &lt;code&gt;openai&lt;/code&gt; client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://modelplane.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gw-your-gateway-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# never a provider key
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# "prod-chat" is a model group, not a provider model ID
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain BYOK security in one paragraph.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's missing: there's no provider key in this code. There's no &lt;code&gt;sk-&lt;/code&gt; or &lt;code&gt;anthropic-&lt;/code&gt; token. If this file leaks to GitHub, the attacker gets a gateway key that can be revoked in seconds, not a provider key that gives them direct access to your OpenAI account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gateway Key Lifecycle
&lt;/h2&gt;

&lt;p&gt;Gateway keys are designed to be ephemeral and revocable. When you create one via &lt;code&gt;POST /api/keys&lt;/code&gt;, the plaintext is returned exactly once. After that, only the SHA-256 hash is stored in Supabase. There's no "forgot my key" recovery—you create a new one and rotate.&lt;/p&gt;

&lt;p&gt;This is a feature, not a bug. It means a key that's been exposed in logs, in a leaked &lt;code&gt;.env&lt;/code&gt; file, or in a commit history is worthless to anyone who finds it later. You can't look up the plaintext, and neither can we.&lt;/p&gt;

&lt;p&gt;ModelPlane also distinguishes between &lt;strong&gt;Private&lt;/strong&gt; and &lt;strong&gt;Shared&lt;/strong&gt; keys. Private keys belong to an individual member and are scoped to their permissions. Shared keys are created by org owners or admins and can be used across a workspace. This lets you give a CI/CD pipeline its own key, then revoke it without affecting your developers' keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Teams
&lt;/h2&gt;

&lt;p&gt;If you're a solo developer, the security model matters because it's one less thing to worry about. If you're an engineering lead at a company with 50 engineers, it's a compliance requirement.&lt;/p&gt;

&lt;p&gt;The multi-tenant design means each workspace gets its own isolated credential store. Your team's keys are encrypted with your workspace's derived keys. Another team on the same ModelPlane instance—even another workspace in your own organization—cannot access them.&lt;/p&gt;

&lt;p&gt;This is the difference between a gateway that's bolted onto your stack and one that's designed for multi-tenancy from the ground up. The tenancy model (User/Workspace/Billing Account) is baked into every resource: API keys, backends, model groups, usage logs. When you audit who has access to what, the answer is clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Tradeoffs
&lt;/h2&gt;

&lt;p&gt;No security model is perfect, and we're not going to pretend otherwise.&lt;/p&gt;

&lt;p&gt;First, BYOK means you're responsible for your provider keys. If you upload a key to ModelPlane and then leak it elsewhere, that's on you. We encrypt it at rest, but we can't protect you from your own &lt;code&gt;.env&lt;/code&gt; file hygiene.&lt;/p&gt;

&lt;p&gt;Second, there's a trust boundary you're accepting: you're trusting ModelPlane to handle your keys correctly. We've designed the system so that a compromise of our infrastructure doesn't expose your plaintext keys, but you're still relying on our encryption implementation being correct.&lt;/p&gt;

&lt;p&gt;Third, the latency/accuracy tradeoff in billing means usage accounting is eventually consistent. That's a billing concern, not a security one, but it's worth knowing that the balance gate is a pre-request snapshot, and deductions happen asynchronously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The provider key is the crown jewel of your LLM infrastructure. It's the credential that can read your prompts, generate your responses, and spend your money. Treating it like a regular API key—hardcoded in services, shared across teams, stored in plaintext—is a risk that will eventually materialize.&lt;/p&gt;

&lt;p&gt;ModelPlane's BYOK model gives you a clean separation: gateway keys for your applications, encrypted backend credentials for your providers, and a routing layer that never confuses the two. The gateway key is auth; the backend key is access. Keeping those separate is the foundation of a secure LLM stack.&lt;/p&gt;

&lt;p&gt;Start free with $5 in credits—no card required. Add your first backend, create a model group, and see how the security model holds up under real traffic. Your provider keys will thank you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of our series on building production-grade LLM infrastructure. Here's what we've covered and what's coming:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One endpoint, every model&lt;/strong&gt; — why we built ModelPlane and the core abstractions of model routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing strategies, explained&lt;/strong&gt; — fallback, load-balance, and conditional routing patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model groups&lt;/strong&gt; — the one abstraction that decouples your app from providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bring your own keys, safely&lt;/strong&gt; — the BYOK security model (you are here)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High availability for LLM apps&lt;/strong&gt; — a fallback playbook for provider outages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop paying twice&lt;/strong&gt; — route your coding-plan quota into production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price-aware routing&lt;/strong&gt; — cut your LLM bill without changing models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credits, usage &amp;amp; billing, explained&lt;/strong&gt; — one transparent bill across all providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1600+ models, one API&lt;/strong&gt; — the ModelPlane provider catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One gateway, two regions&lt;/strong&gt; — routing to global and China models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ModelPlane for teams&lt;/strong&gt; — orgs, workspaces &amp;amp; shared keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meet the assistant&lt;/strong&gt; — an AI helper that lives in your gateway dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inside the ModelPlane routing engine&lt;/strong&gt; — how a single request flows from auth to upstream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One thinking parameter, every model&lt;/strong&gt; — unified reasoning across providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The system prompt belongs at the router&lt;/strong&gt; — per-model-group system-prompt injection&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Credits, usage &amp; billing, explained</title>
      <dc:creator>ModelPlane</dc:creator>
      <pubDate>Fri, 07 Aug 2026 03:43:29 +0000</pubDate>
      <link>https://dev.to/modelplane/credits-usage-billing-explained-1266</link>
      <guid>https://dev.to/modelplane/credits-usage-billing-explained-1266</guid>
      <description>&lt;h1&gt;
  
  
  Credits, usage &amp;amp; billing, explained
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;The problem with LLM usage tracking isn't that it's hard to measure tokens — it's that every provider gives you a different dashboard, and none of them tell you what you actually spent through your application.&lt;/strong&gt; When you route through a gateway, usage becomes a property of your architecture, not a per-provider afterthought. ModelPlane gives you one surface for usage across all your model groups and providers, so you can answer "what did this feature cost?" without stitching together five vendor consoles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-dashboard problem
&lt;/h2&gt;

&lt;p&gt;If you've built an AI feature in production, you know the ritual. You have a script, a spreadsheet, or a half-remembered URL for each provider's usage page. OpenAI shows you one number. Anthropic shows you another. DeepSeek shows you a third. And none of them map to the requests your application actually made, because your application doesn't call providers directly — it calls a model group.&lt;/p&gt;

&lt;p&gt;The mismatch is the core issue. Provider dashboards are organized by &lt;em&gt;provider account&lt;/em&gt;. Your costs are organized by &lt;em&gt;feature&lt;/em&gt;, &lt;em&gt;customer&lt;/em&gt;, or &lt;em&gt;environment&lt;/em&gt;. When you hard-code &lt;code&gt;model="gpt-4o"&lt;/code&gt; in your code, you've coupled your application's cost structure to a single vendor's accounting. You can't see which of your features is burning credits, which customer tier is expensive to serve, or whether your fallback routing is quietly sending traffic to a premium model you didn't intend.&lt;/p&gt;

&lt;p&gt;This is why the abstraction matters. When &lt;code&gt;request.model&lt;/code&gt; is a name you control — a model group like &lt;code&gt;prod-chat&lt;/code&gt; — usage tracking becomes something you can actually act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  One surface for usage across model groups
&lt;/h2&gt;

&lt;p&gt;ModelPlane's billing model is built around a single concept: &lt;strong&gt;credits&lt;/strong&gt;. One credit is equivalent to one US dollar. Every request through the gateway is accounted for in credits, regardless of which provider answered it. The cost formula is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost = (inputTokens × inputUnitPrice + outputTokens × outputUnitPrice) / 1_000_000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pricing for each model lives in the provider catalog, so the gateway can compute the cost of every request without you configuring per-model rates. This means your usage logs are not just token counts — they're actual spend, normalized across every provider.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;usage_logs&lt;/code&gt; table is the heart of this. Each entry records the request's dimensions: which API key made the call, which model group it went through, and which underlying model actually answered. That last point is critical. With fallback routing, the model that answers isn't always the one you asked for. Your logs show you the ground truth.&lt;/p&gt;

&lt;p&gt;This gives you three views of your spend that provider dashboards can't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;By model group.&lt;/strong&gt; What does &lt;code&gt;prod-chat&lt;/code&gt; cost this month? What about &lt;code&gt;customer-support&lt;/code&gt; or &lt;code&gt;internal-coding&lt;/code&gt;? Each group is a line item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By API key.&lt;/strong&gt; Which application or service is consuming the most? Private keys per developer, shared keys per environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By model.&lt;/strong&gt; Even within a group, you can see the split — how much traffic went to the fast tier versus the premium fallback.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The balance gate: what happens before a request runs
&lt;/h2&gt;

&lt;p&gt;ModelPlane's billing is designed to be low-latency and simple. Before a request runs, the gateway checks your credit balance. If you're out of credits, you get a &lt;code&gt;402 insufficient_credits&lt;/code&gt; response — fast, clear, and before any upstream call is made. This is the pre-request balance gate.&lt;/p&gt;

&lt;p&gt;This design is a deliberate tradeoff. The gateway doesn't do a strong-consistency reservation of funds before every request. That would add latency to every single call. Instead, it takes a snapshot of your balance, gates the request, and then accounts for the cost &lt;em&gt;after&lt;/em&gt; the request completes, asynchronously.&lt;/p&gt;

&lt;p&gt;The result is that billing is fast enough to sit on the hot path of every inference call. The tradeoff is that the balance you see can be slightly stale — up to 300 seconds, in some cases, because the balance may be served from a cache. For almost all use cases, this is the right trade. You don't want your LLM gateway to be the slow part of your stack, and you don't want it to reject requests because of a race condition in accounting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens after: async accounting
&lt;/h2&gt;

&lt;p&gt;Once the request completes, the gateway calculates the cost and deducts it from your balance. This is a fire-and-forget operation — it doesn't block the response to your application. The response you get is the model's output; the accounting happens in the background.&lt;/p&gt;

&lt;p&gt;This design is simple and fast, but it's worth being honest about the edge cases. The async deduction has no retry mechanism in its current implementation. If the background task fails, or the Worker running it is evicted, the usage row and the deduction could be silently lost. This is a known reliability gap, documented in the project's internal PRD. For most teams, this is acceptable — the cost of a few lost usage rows is far less than the cost of adding strong consistency to every request. But if you're building billing infrastructure on top of usage logs, you should know the data is eventually consistent, not exactly-once.&lt;/p&gt;

&lt;p&gt;The system also handles the accounting atomically at the database level. The &lt;code&gt;consume_credits&lt;/code&gt; RPC is atomic and allows a brief negative balance, which prevents a burst of concurrent requests from being rejected due to a race between the balance check and the deduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plans and pricing: what you actually pay
&lt;/h2&gt;

&lt;p&gt;ModelPlane's pricing is designed to be transparent. There are three tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt;: You get $5 in credits on signup. No card required. This is enough to build and test a real feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic&lt;/strong&gt;: $10 per month, which includes $15 in credits. You can top up with additional credits as needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited&lt;/strong&gt;: A special plan for high-volume internal use cases, where the credit gate is bypassed entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of the plan, there's a flat gateway fee of approximately $0.02 per 1 million tokens. This is the cost of routing — the abstraction, the fallback, the unified billing. It's a rounding error compared to the cost of the tokens themselves, but it's what pays for the gateway to exist.&lt;/p&gt;

&lt;p&gt;The key number to understand is the &lt;strong&gt;included credit&lt;/strong&gt;. On the Basic plan, you pay $10 and get $15 in credits. That means the effective cost of the gateway is negative — you're getting more credit than you pay for, and the gateway fee comes out of the included credit. The math works because the gateway fee is so small relative to model costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing it in action
&lt;/h2&gt;

&lt;p&gt;Here's what it looks like to point your OpenAI client at ModelPlane and see usage flow into one dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://modelplane.dev/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gw-your-gateway-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# "prod-chat" is a model group, not a provider model id.
# It routes across your configured backends with your chosen strategy.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this week&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s support tickets.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your application code doesn't know or care which provider answered. It doesn't know whether the request went to a coding-plan quota or a pay-per-token endpoint. It just knows it called &lt;code&gt;prod-chat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After this request runs, you can query the usage API to see exactly what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer gw-your-gateway-key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://modelplane.dev/api/usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response shows you the request's token count, the cost in credits, the model group, the API key, and the actual model that served it. This is the data you need to answer the questions that matter: "What does this feature cost per user?" "Is my fallback routing sending too much traffic to the expensive model?" "Which customer tier is burning through credits?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tradeoff: latency vs. accuracy
&lt;/h2&gt;

&lt;p&gt;Every billing system makes a tradeoff between accuracy and latency. ModelPlane's choice is explicit: &lt;strong&gt;low latency wins&lt;/strong&gt;. The pre-request balance check is a snapshot, not a reservation. The post-request deduction is asynchronous, not synchronous. This means your requests are never slowed down by billing logic, and your gateway can handle high concurrency without becoming a bottleneck.&lt;/p&gt;

&lt;p&gt;The cost of this choice is that your balance and usage data are eventually consistent. You might see a request in your logs a few seconds after it completes. Your balance might be slightly stale for a few minutes. And in rare failure cases, a usage row might be lost entirely.&lt;/p&gt;

&lt;p&gt;For most teams, this is the right trade. If you're building a customer-facing billing system on top of your LLM usage, you should build your own reconciliation layer. But if you want to see what your AI features cost, and you want to catch a runaway model group before it burns through your credits, this is more than sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats provider dashboards
&lt;/h2&gt;

&lt;p&gt;The multi-dashboard problem isn't just inconvenient — it's a blind spot. When you can't see usage by model group, you can't see which features are profitable. When you can't see usage by API key, you can't see which developer or service is consuming the most. When you can't see the actual model that answered, you can't tell if your fallback strategy is working as intended.&lt;/p&gt;

&lt;p&gt;ModelPlane gives you the dimensions that matter for running an AI product. The abstraction of the model group — the same abstraction that decouples your code from providers — also decouples your accounting from providers. Your costs are organized the same way your architecture is organized.&lt;/p&gt;

&lt;p&gt;This is the point. The provider is the wrong unit of integration for your code, and it's the wrong unit of integration for your billing. ModelPlane makes the model group the unit of both. One endpoint, one dashboard, one bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;LLM usage tracking doesn't have to be a nightmare of spreadsheets and vendor consoles. When you route through a gateway, usage becomes a first-class property of your application. ModelPlane gives you one surface to see it all: credits, spend, and usage across every model group and provider.&lt;/p&gt;

&lt;p&gt;The system is fast because it's simple. The balance gate is a snapshot, the deduction is async, and the accounting is eventually consistent. That's the right trade for a gateway that sits on every inference call.&lt;/p&gt;

&lt;p&gt;If you're tired of stitching together provider dashboards, point your OpenAI client at ModelPlane and see your usage in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See your usage in one dashboard — &lt;a href="https://modelplane.dev" rel="noopener noreferrer"&gt;start free with $5 in credits, no card required&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series on building production-grade LLM applications with ModelPlane.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ModelPlane series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/one-endpoint-every-model/" rel="noopener noreferrer"&gt;One endpoint, every model: why we built ModelPlane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/routing-strategies-explained/" rel="noopener noreferrer"&gt;Routing strategies, explained: fallback, load-balance, conditional&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/model-groups-explained/" rel="noopener noreferrer"&gt;Model groups: the one abstraction that decouples your app from providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/byok-credential-security/" rel="noopener noreferrer"&gt;Bring your own keys, safely&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/llm-high-availability-playbook/" rel="noopener noreferrer"&gt;High availability for LLM apps: a fallback playbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/coding-plan-routing/" rel="noopener noreferrer"&gt;Stop paying twice: route your coding-plan quota into production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/price-aware-routing/" rel="noopener noreferrer"&gt;Price-aware routing: cut your LLM bill without changing models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credits, usage &amp;amp; billing, explained&lt;/strong&gt; ← you are here&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/provider-catalog/" rel="noopener noreferrer"&gt;1600+ models, one API: the ModelPlane provider catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/global-and-china-routing/" rel="noopener noreferrer"&gt;One gateway, two regions: routing to global and China models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/orgs-workspaces/" rel="noopener noreferrer"&gt;ModelPlane for teams: orgs, workspaces &amp;amp; shared keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/in-product-assistant/" rel="noopener noreferrer"&gt;Meet the assistant: an AI helper that lives in your gateway dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/inside-the-routing-engine/" rel="noopener noreferrer"&gt;Inside the ModelPlane routing engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/unified-thinking-parameter/" rel="noopener noreferrer"&gt;One thinking parameter, every model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelplane.dev/blog/system-prompt-injection/" rel="noopener noreferrer"&gt;The system prompt belongs at the router, not in your app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>billing</category>
      <category>product</category>
    </item>
  </channel>
</rss>
