{"id":1091,"date":"2026-08-24T09:30:00","date_gmt":"2026-08-23T23:30:00","guid":{"rendered":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/"},"modified":"2026-11-03T20:27:49","modified_gmt":"2026-11-03T09:27:49","slug":"llm-gateway-one-door-for-every-model","status":"publish","type":"post","link":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/","title":{"rendered":"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11)"},"content":{"rendered":"\n<p class=\"has-pale-cyan-blue-background-color has-background wp-block-paragraph\">Every model call on our lab&#8217;s private AI server goes through one gateway: one key per client, a token budget checked before the GPU sees anything, a trace for every request, and no local model allowed to fall back to the cloud. Here is how we built it, what went wrong, and how to set it up yourself.<\/p>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Where this fits \u00b7 Running Local AI in Production \u00b7 Part 2 of 11<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:17px\">You are reading Part 2 of 11 in a series where we built a private AI server in our lab and ran realistic test workloads on it for seven weeks. Each part stands on its own. The previous part covered why we built the lab on our own hardware, what ran on it and what that choice cost; this one covers one LLM gateway as the only way to reach a model: keys, budgets and tracing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">\u2190 Part 1: Local AI or Cloud AI? Why We Built a Private AI Server<\/a> &nbsp;\u00b7&nbsp; <a href=\"https:\/\/catharsis.net.au\/blog\/running-local-ai-in-production-series-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Series guide<\/a> &nbsp;\u00b7&nbsp; <a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 3: Why Does a Local AI Server Freeze? Sizing Memory for LLMs \u2192<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\">Just want it working? Follow the <a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">step-by-step setup guide<\/a>: local AI with Ollama and LiteLLM, from a bare server to a secure gateway.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See all 11 parts of the series<\/summary>\n\n<ol class=\"wp-block-list\">\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Local AI or Cloud AI? Why We Built a Private AI Server<\/a>: why we built the lab on our own hardware, what ran on it and what that choice cost<\/li>\n\n\n<li><strong>How Do You Control Access to Local AI Models? Use a Gateway<\/strong> (you are here): one LLM gateway as the only way to reach a model: keys, budgets and tracing<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Why Does a Local AI Server Freeze? Sizing Memory for LLMs<\/a>: five hard freezes and the memory budget that ended them<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Happens When a Prompt Is Too Big for a Local LLM?<\/a>: fitting every prompt to a local model&#8217;s context window<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Run Several AI Models on One GPU Without Slowdowns?<\/a>: routing requests across models on a single GPU without constant model swaps<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/gitops-for-a-gpu-ai-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Keep an AI Server Stable? GitOps for Local AI<\/a>: running the server from a Git repository: deploys, drift, pinning and safe AI agents<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/observability-for-local-ai\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Is Your Local AI Server Actually Doing? Observability Basics<\/a>: making the server observable: traces, dashboards, usage and heat<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/multi-agent-stock-analysis-local-llm\/\" target=\"_blank\" rel=\"noreferrer noopener\">Can Local AI Agents Analyse Stocks? How We Tested and Scored Them<\/a>: a multi-agent stock-analysis experiment and how we scored the agents<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/telegram-ai-agent-local-llms\/\" target=\"_blank\" rel=\"noreferrer noopener\">Why Was Our Telegram AI Agent So Slow? The Hidden Prompt Cost<\/a>: a Telegram AI agent on local models, and why its fixed prompt became the budget<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-business-workflows\/\" target=\"_blank\" rel=\"noreferrer noopener\">Can AI Automate Business Workflows Without Sending Data Out?<\/a>: document, email and knowledge-base workflows that keep data on the server<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/private-ai-lab-lessons-and-whats-next\/\" target=\"_blank\" rel=\"noreferrer noopener\">Is Private AI Worth It? Lessons From Seven Weeks in Our Lab<\/a>: the lessons from seven weeks, the honest limits and what comes next<\/li>\n\n<\/ol>\n\n<\/details>\n\n<\/div><\/div>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><em>This series documents a private AI server we built and stress-tested in our lab. Workloads and data are synthetic or generalised; the problems, measurements and fixes are real.<\/em><\/p>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">TL;DR<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>How do you control access? Put one gateway in front of every model and make it the only way in, so every call has a named key, a budget check and a trace.<\/li>\n\n\n<li>The problem: clients could reach our lab&#8217;s models three different ways, nearly everything shared one all-powerful admin key, and oversized prompts were silently cut short.<\/li>\n\n\n<li>Our approach: make LiteLLM, an open-source gateway, the only door, with one key per client, a token budget checked before the GPU sees a request, and no local model allowed to fall back to a cloud model.<\/li>\n\n\n<li>The result: a 45,183-token request is refused in milliseconds, every call is traced to a named key, and a pre-commit check refuses any configuration that could send data to the cloud.<\/li>\n\n\n<li>The lesson: a fallback belongs to the model, not the key, and a server that refuses is better than a guard that alerts.<\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Jargon buster: 15 terms used in this post, explained in plain English<\/summary>\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Slot (parallel requests)<\/strong>: One request a model server can work on at the same time. More slots serve more people at once but reserve more memory.<\/li>\n\n\n<li><strong>Ollama<\/strong>: Free, open-source software that downloads and runs AI models on your own machine and answers requests over a local API.<\/li>\n\n\n<li><strong>LiteLLM<\/strong>: Open-source software that puts one front door in front of many AI models, with per-user keys, budgets, limits and logging.<\/li>\n\n\n<li><strong>LLM gateway<\/strong>: A single service every AI request must pass through, so access, budgets and logging are controlled in one place.<\/li>\n\n\n<li><strong>API key \/ virtual key<\/strong>: A secret string that identifies the program making a request. Giving each program its own key lets you limit and track it separately.<\/li>\n\n\n<li><strong>Langfuse<\/strong>: Open-source software that records every AI request (prompt, answer, timing and cost) so you can see what happened and why.<\/li>\n\n\n<li><strong>Trace<\/strong>: A detailed record of one request as it passes through each part of a system, used to find where time or errors come from.<\/li>\n\n\n<li><strong>Telegram bot<\/strong>: An automated account on the Telegram messaging app that people chat with; here, the front end for an AI agent.<\/li>\n\n\n<li><strong>OpenRouter<\/strong>: A cloud service that offers many AI models from different providers through one API, including some free models.<\/li>\n\n\n<li><strong>Docker \/ container<\/strong>: A way of packaging an application with everything it needs so it runs the same anywhere; each packaged application runs in its own container.<\/li>\n\n\n<li><strong>Pinning<\/strong>: Locking software to an exact version so it can&#8217;t change unexpectedly during an update.<\/li>\n\n\n<li><strong>Pre-commit check<\/strong>: An automatic check that runs before a change is saved to the repository and blocks it if it breaks a rule.<\/li>\n\n\n<li><strong>Loopback<\/strong>: A network address that only the server itself can reach, so a service listening there isn&#8217;t exposed to the network.<\/li>\n\n\n<li><strong>HTTP status codes (400, 413, 429)<\/strong>: Standard codes a server returns: 400 means a bad request, 413 means it&#8217;s too large, 429 means too many requests.<\/li>\n\n\n<li><strong>PostgreSQL \/ ClickHouse \/ Redis<\/strong>: Open-source databases: PostgreSQL for general records, ClickHouse for fast analysis of large logs, Redis for fast short-term data.<\/li>\n\n<\/ul>\n\n<\/details>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">In this post<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><a href=\"#the-problem-three-ways-in-one-key-for-everything\">The problem: three ways in, one key for everything<\/a><\/li>\n\n\n<li><a href=\"#what-we-were-aiming-for\">What we were aiming for<\/a><\/li>\n\n\n<li><a href=\"#how-we-approached-it\">How we approached it<\/a><\/li>\n\n\n<li><a href=\"#what-we-built\">What we built<\/a><\/li>\n\n\n<li><a href=\"#challenges-we-hit\">Challenges we hit<\/a><\/li>\n\n\n<li><a href=\"#the-result\">The result<\/a><\/li>\n\n\n<li><a href=\"#do-it-yourself-put-litellm-in-front-of-ollama\">Do it yourself: put LiteLLM in front of Ollama<\/a><\/li>\n\n\n<li><a href=\"#key-takeaways\">Key takeaways<\/a><\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-problem-three-ways-in-one-key-for-everything\">The problem: three ways in, one key for everything<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In <a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 1<\/a> we built a private AI server in our lab, with local models served by Ollama. Its first, hand-built version had a basic control problem: a client could reach a model three ways. It could go through <strong>LiteLLM<\/strong>, the gateway meant to be the front door. It could go straight to Ollama, which listened on the lab network. Or it could use a second backend inside Open WebUI, the chat UI, which talked to Ollama directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Only the first path knew who was calling. On the second day of our reorganisation, minutes after the server came back from a hard freeze, a test laptop&#8217;s IDE plugin called Ollama directly and loaded <code>nemotron-3-nano:30b<\/code>, the model whose loading had come just before the earlier freezes. We had removed it from the gateway&#8217;s routing the day before, which did nothing for a client that never used the gateway.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keys were just as loose. Nearly everything used the gateway&#8217;s <strong>admin key<\/strong>, which can create and delete keys and call every model, and copies of it had spread into old IDE templates and a workflow. In a real business that is a privacy and reliability risk at once: you can&#8217;t tell who sent what, you can&#8217;t limit anyone, and one leaked key opens everything.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"what-we-were-aiming-for\">What we were aiming for<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>One door: every model call goes through the gateway, and nothing else on the network can reach a model.<\/li>\n\n\n<li>One key per client, with its own model list and limits, and the admin key used for administration only.<\/li>\n\n\n<li>Oversized prompts refused or trimmed before they reach the GPU, never silently truncated.<\/li>\n\n\n<li>No path, deliberate or accidental, by which local data reaches a cloud model, and a trace for every call.<\/li>\n\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"how-we-approached-it\">How we approached it<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">An <strong>LLM gateway<\/strong> is a proxy between clients and models. Every client talks to one OpenAI-compatible URL, and the gateway decides which backend answers. It is the one place where identity, policy, budgets, concurrency and tracing can all live, so a path that skips it skips all five. We chose LiteLLM because it is open source, speaks the OpenAI API that most tools already support, and has <strong>virtual keys<\/strong>, each with its own alias, model list, rate limit and optional budget.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We moved the clients first and closed the side doors after, because closing a path before moving its users breaks them. For budgets, we expected LiteLLM&#8217;s built-in settings to do the job, and planned our own check only if they didn&#8217;t. For cloud models, we chose a rule enforced before every change over trusting reviewers to spot a risky line.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"what-we-built\">What we built<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Closing the side doors<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The stock-analysis experiment and the Telegram agent got their own keys, we switched off the chat UI&#8217;s direct Ollama backend, and each knowledge base became an ordinary model behind the gateway. Then we bound every service that only other containers need to the host&#8217;s <strong>loopback<\/strong> interface, the address only the machine itself can reach. Everything except the gateway now listens on the server itself, the gateway and dashboards need authentication, and Ollama&#8217;s own access log is a second witness for anything that slips past.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">One key per client<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>Kind<\/th>\n<th>Alias pattern<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>a service on the server<\/td>\n<td>the service&#8217;s name: the chat UI, n8n, the router, each knowledge-base worker, the Telegram agent, the stock-analysis tool<\/td>\n<\/tr>\n<tr>\n<td>a lab device<\/td>\n<td>named after the device<\/td>\n<\/tr>\n<tr>\n<td>our own tests and proofs<\/td>\n<td>a dedicated test key<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Model lists are opt-in, so the riskiest model, by then in its own sandbox, is only available to keys that ask for it. Interactive keys get a lower requests-per-minute ceiling than batch tools, so one runaway loop can&#8217;t fill the queue. We removed every copy and moved each client to its own key. The admin key is used by LiteLLM and admin commands only.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">AI coding agents are treated like a new contractor: scoped, unprivileged access, no secrets in any file they can read, and every change through the repository and review. The live gateway configuration is read-only between deploys (Part 6).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A budget check at the gate<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After the freezes described in <a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 3<\/a>, each model gets a 32,768-token <strong>context window<\/strong> per request, shared by the prompt and the answer. At that limit Ollama doesn&#8217;t refuse; it <strong>truncates silently<\/strong>, and an agent loses part of its own reasoning without knowing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We built an admission check: a small pre-call hook in the gateway that counts each request against the model&#8217;s budget and refuses anything over it with an OpenAI-style <code>400 context_length_exceeded<\/code> error, naming the count, the budget and what to do. The input budgets are <strong>26,000<\/strong> tokens for the four 32k models and the <code>auto<\/code> router, leaving at least 6,768 tokens for the reply, and <strong>13,000<\/strong> for the two models with a 16k window. Later, refusing became compacting at the gateway (<a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 4<\/a>).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Queueing and tracing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each heavy model has <code>max_parallel_requests: 2<\/code>. In the LiteLLM version we ran, a request over that cap waited for a free slot; current releases reject it at once with a <code>429<\/code> instead. Setting it to 1 would only have moved the queue from Ollama into LiteLLM, so we made queueing visible instead (<a href=\"https:\/\/catharsis.net.au\/blog\/observability-for-local-ai\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 7<\/a>). Every request leaves a row in LiteLLM&#8217;s spend log and a <strong>trace<\/strong> in Langfuse, an open-source tool for observing LLM calls.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">No cloud fallback, and one narrow exception<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">LiteLLM <strong>fallbacks<\/strong> retry a failed or slow request on another model. We use them only between the two resident local models, <code>gemma4<\/code> and <code>qwen3-coder<\/code>. A pre-commit egress check refuses:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li>any fallback (ordinary, context-window or content-policy) from a local model to a cloud model;<\/li>\n\n\n<li>any <code>default_fallbacks<\/code> entry that names a cloud model, because a default applies to local models too;<\/li>\n\n\n<li>any OpenRouter deployment whose model id doesn&#8217;t end in <code>:free<\/code>;<\/li>\n\n\n<li>any fallback naming a model that doesn&#8217;t exist, because a typo would silently switch off the first rule.<\/li>\n\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The one exception covers public data only. TradingAgents, the stock-analysis experiment (<a href=\"https:\/\/catharsis.net.au\/blog\/multi-agent-stock-analysis-local-llm\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 8<\/a>), makes about 30 local analyst calls per run plus two &quot;deep&quot; calls, and those two may use a model on OpenRouter&#8217;s <strong>free tier<\/strong>, only for the lab&#8217;s public-market test data. Free tiers may log or train on prompts, so nothing else goes to them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Four controls keep that exception narrow:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>the cloud models are granted to the stock-analysis key only;<\/li>\n\n\n<li>the OpenRouter key has a $0 credit limit;<\/li>\n\n\n<li>each sets <code>allow_fallbacks: false<\/code>, so OpenRouter can&#8217;t re-route to another <em>provider<\/em> of the same model (it doesn&#8217;t stop a model fallback configured elsewhere);<\/li>\n\n\n<li>the only fallback goes from each free model to local <code>qwen3-coder<\/code>, with prompts compacted to the local budget first, and every call leaves a log line saying where it ran.<\/li>\n\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-pale-cyan-blue-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:32px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Thinking about private AI for your organisation?<\/strong> We design, build and secure on-premises AI platforms for Australian organisations, with the guardrails this series describes built in from the start.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/local-ai\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-02-mid\" target=\"_blank\" rel=\"noreferrer noopener\">Explore Private AI<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"challenges-we-hit\">Challenges we hit<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The built-in budget setting doesn&#8217;t enforce anything<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We assumed LiteLLM&#8217;s per-model <code>max_input_tokens<\/code> would stop oversized prompts. By default it is only model metadata. LiteLLM enforces it only when pre-call checks are on (<code>enable_pre_call_checks<\/code>), and then it filters deployments rather than telling the caller what went wrong. That is why we wrote our own hook. Even then, a budget in the wrong place did nothing: the hook reads <code>model_info<\/code>, and a value under <code>litellm_params<\/code> looks right but is ignored. A pre-commit check now refuses a local model without a declared budget, and the guard re-checks the running gateway.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The token counter was wrong by 322 times<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The gate had been letting through prompts it should have refused, because it never counted tool definitions. A short message carrying a dozen tool schemas counted as <strong>11 tokens but cost 3,540<\/strong>. The counter also grouped digits three to a token, while these models spend one token per digit. Of 76 samples, 59 were unsafe before the fix.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Traces that never arrived<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The Langfuse callbacks had sat under the wrong configuration section since the first build, and LiteLLM ignores them there without complaint, so no traces had ever been recorded. Moved to the right section, every trace batch crashed the Langfuse web container. Langfuse v3 needs Postgres, ClickHouse, Redis, S3-compatible blob storage and a worker, and ours had been installed without the last three. It restarted 72 times before we took the callbacks out and completed the stack.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Keys leak in quiet ways<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In LiteLLM an empty model list means every model, cloud ones included, which is how a dashboard key quietly gained access to everything. Any key with every-model access must now be listed in a file in the repository, so the commit is the approval. A load test once fell back to the admin key and raised &quot;admin key used&quot;, the one alert that must never be noise, so tests now use a dedicated key limited to local models with a $1 per 30 days tripwire. Open WebUI stores its key in its environment and its database, and the database copy wins at start-up.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A fallback can send a document to the cloud<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Picture a document-reading model with a short timeout and a cloud fallback. Whenever the single local slot stays busy past the timeout, LiteLLM re-sends the request, meaning the document itself, to the cloud provider, and the caller can&#8217;t tell. <strong>A fallback belongs to a model, not a key<\/strong>, so it applies to every caller. The lab&#8217;s vision model has no cloud fallback and no timeout; a busy slot means the call waits.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-result\">The result<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In the first test of the gate, a 45,183-token request was refused in milliseconds and never reached the GPU. After the counting fix, none of the 76 samples was unsafe. Traces now arrive in Langfuse, and a soak test ran with no restarts. From another lab machine the database and model ports were refused while the gateway answered. A scheduled guard compares gateway and Ollama logs, alerts on admin-key use, flags unapproved every-model keys and counts refusals per key. We added that last check after the Telegram agent had 20 requests refused over five days and nothing said so (Part 9).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One door has costs. The gateway is a single point of failure: a configuration change means a restart of about 10 seconds, or about 30 for a recreate. Budgets sit below the model card. With one slot per model, a colliding caller waited a median of 18 seconds over seven days of test traffic, with 90 per cent of waits under 58 seconds. Every new client needs a key and a model list, on purpose. And alerts are not enforcement: scoped access, read-only configuration and pre-commit checks are what prevent problems.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"do-it-yourself-put-litellm-in-front-of-ollama\">Do it yourself: put LiteLLM in front of Ollama<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What you need:<\/strong> the server from <a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 1<\/a> with Ollama on loopback, Python 3, Postgres (a distro package or the official container), and about two hours.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li>\n<p><strong>Prepare the database and secrets.<\/strong> Create a service account, a Postgres role and a database, then an environment file only that account can read. LiteLLM&#8217;s admin key, the master key, must start with <code>sk-<\/code>.<\/p>\n<pre><code class=\"language-bash\">sudo useradd --system --home \/opt\/litellm litellm\nsudo -u postgres createuser -P litellm &amp;&amp; sudo -u postgres createdb -O litellm litellm\nsudo install -d -m 750 -o litellm \/etc\/litellm\nprintf '%s=%s\\n%s=%s\\n' LITELLM_MASTER_KEY &quot;sk-$(openssl rand -hex 32)&quot; \\\n  DATABASE_URL 'postgresql:\/\/litellm:&lt;db-password&gt;@127.0.0.1:5432\/litellm' \\\n  | sudo install -m 600 -o litellm \/dev\/stdin \/etc\/litellm\/litellm.env\n<\/code><\/pre>\n<p>Check: <code>sudo -u postgres psql -lqt | grep litellm<\/code> lists the database, and <code>sudo ls -l \/etc\/litellm\/litellm.env<\/code> shows <code>-rw-------<\/code>.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Install a pinned proxy<\/strong> in its own virtual environment. The <code>extra_proxy<\/code> extra brings the Prisma client the proxy needs once a database is configured.<\/p>\n<pre><code class=\"language-bash\">sudo python3 -m venv \/opt\/litellm\nsudo \/opt\/litellm\/bin\/pip install 'litellm[proxy,extra_proxy]==&lt;version&gt;'\n<\/code><\/pre>\n<p>Check: <code>\/opt\/litellm\/bin\/pip show litellm prisma<\/code> prints both, with the version you pinned. Read the release notes before any upgrade, because pip doesn&#8217;t run database migrations for you.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Write <code>\/etc\/litellm\/config.yaml<\/code>.<\/strong> Use <code>ollama_chat\/<\/code> so tool calls work, and set the budget in <code>model_info<\/code>.<\/p>\n<pre><code class=\"language-yaml\">model_list:\n  - model_name: qwen3-coder\n    litellm_params:\n      model: ollama_chat\/qwen3-coder-32k\n      api_base: http:\/\/127.0.0.1:11434\n      max_parallel_requests: 2\n    model_info:\n      max_input_tokens: 26000\nrouter_settings:\n  enable_pre_call_checks: true\ngeneral_settings:\n  master_key: os.environ\/LITELLM_MASTER_KEY\n  database_url: os.environ\/DATABASE_URL\n<\/code><\/pre>\n<p>Add no <code>fallbacks<\/code> or <code>default_fallbacks<\/code> that point at a cloud model. On current releases a request over <code>max_parallel_requests<\/code> gets an immediate <code>429<\/code>, so pin the version you tested, raise the cap and let Ollama queue, or handle <code>429<\/code> in clients. For a quick start, <code>enable_pre_call_checks<\/code> filters out models too small for a request. For a clear error and a count that includes tool definitions, write the hook in <a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 4&#8217;s steps<\/a> and register it under <code>litellm_settings: callbacks<\/code>.\nCheck: the file is committed to Git.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Run the gateway as a systemd service<\/strong> under the unprivileged account, bound to loopback.<\/p>\n<pre><code class=\"language-ini\">[Service]\nUser=litellm\nEnvironmentFile=\/etc\/litellm\/litellm.env\nExecStart=\/opt\/litellm\/bin\/litellm --config \/etc\/litellm\/config.yaml --host 127.0.0.1 --port 4000\nRestart=on-failure\n<\/code><\/pre>\n<p>Save it as <code>\/etc\/systemd\/system\/litellm.service<\/code> with <code>[Install]<\/code> <code>WantedBy=multi-user.target<\/code>, then <code>sudo systemctl enable --now litellm<\/code>. Put a TLS reverse proxy in front for other machines, or firewall port 4000 to known hosts; keys are bearer tokens, so never send them over plain HTTP.\nCheck: <code>curl -s localhost:4000\/health\/liveliness<\/code> answers that it is alive.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Mint one key per client<\/strong>, on the server itself, so the master key never crosses the network. Load it first with <code>set -a; . \/etc\/litellm\/litellm.env; set +a<\/code> in a root shell.<\/p>\n<pre><code class=\"language-bash\">curl -s localhost:4000\/key\/generate -H &quot;Authorization: Bearer $LITELLM_MASTER_KEY&quot; \\\n  -H 'Content-Type: application\/json' \\\n  -d '{&quot;key_alias&quot;:&quot;chat-ui&quot;,&quot;models&quot;:[&quot;qwen3-coder&quot;],&quot;rpm_limit&quot;:30}'\n<\/code><\/pre>\n<p>Check: the JSON contains a <code>key<\/code>. Store it only in that client&#8217;s configuration.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Send a test request with the client key.<\/strong><\/p>\n<pre><code class=\"language-bash\">curl -s localhost:4000\/v1\/chat\/completions -H &quot;Authorization: Bearer $CLIENT_KEY&quot; \\\n  -H 'Content-Type: application\/json' \\\n  -d '{&quot;model&quot;:&quot;qwen3-coder&quot;,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;Say hello.&quot;}]}'\n<\/code><\/pre>\n<p>Check: you get an answer, the call appears in the spend log under <code>chat-ui<\/code>, and asking for a model not on the key&#8217;s list returns an authorisation error.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Test the budget.<\/strong> Send a prompt of around 40,000 words, generated with a short script.\nCheck: you get an error, not an answer, and Ollama&#8217;s log (<code>journalctl -u ollama<\/code>) shows no new <code>POST \/api\/chat<\/code> line for it. Then send a short message with a dozen tool definitions. With only the built-in checks it will almost certainly get through, because LiteLLM&#8217;s counter ignores tool schemas; Part 4&#8217;s hook closes that gap.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Refuse cloud fallbacks before commit.<\/strong> Add a pre-commit script that parses <code>config.yaml<\/code> and fails if any fallback leads from an <code>ollama<\/code> model to another provider.\nCheck: add a bad fallback on purpose; the commit is refused.<\/p>\n<\/li>\n\n\n<li>\n<p><strong>Confirm only the gateway is reachable.<\/strong> From another machine, run <code>curl -m 5 http:\/\/&lt;server&gt;:11434<\/code>, then request the gateway&#8217;s health path through your TLS proxy.\nCheck: the first is refused or times out and the second answers.<\/p>\n<\/li>\n\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"key-takeaways\">Key takeaways<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>One door makes control possible. Bind everything else to loopback and keep the model server&#8217;s log as a second witness.<\/li>\n\n\n<li>The admin key is for administration only, not even for your own tests.<\/li>\n\n\n<li>LiteLLM&#8217;s <code>max_input_tokens<\/code> is metadata by default. Switch on pre-call checks or enforce it yourself, and count tool definitions.<\/li>\n\n\n<li>Fallbacks belong to the model, not the key. Refuse local-to-cloud fallbacks in CI rather than hoping a review catches them.<\/li>\n\n\n<li>If you allow cloud models at all, keep them narrow: one key, public data only, no provider re-routing, a local fallback and a log line for every call.<\/li>\n\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-light-green-cyan-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:40px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h2 class=\"wp-block-heading\">Planning private AI?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Catharsis Cyber Security is a Sydney cyber security company. We design, build and secure private AI platforms like the one in this lab, and help you decide where local or cloud AI fits.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/contact-us\/?type=contact&amp;service=local-ai&amp;utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-02-contact\" target=\"_blank\" rel=\"noreferrer noopener\">Talk to us about AI<\/a><\/div>\n\n\n<div class=\"wp-block-button is-style-outline is-style-outline--1\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/secure-ai\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-02-end\" target=\"_blank\" rel=\"noreferrer noopener\">Explore Secure AI<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Keep reading the series<\/strong><\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button is-style-outline is-style-outline--2\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">\u2190 Part 1<\/a><\/div>\n\n\n<div class=\"wp-block-button is-style-outline is-style-outline--3\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/running-local-ai-in-production-series-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Series guide<\/a><\/div>\n\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Next: Part 3 \u2192<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Every model call on our lab&#8217;s private AI server goes through one gateway: one key per client, a token budget checked before the GPU sees anything, a trace for every request, and no local model allowed to fall back to the cloud. Here is how we built it, what went wrong, and how to set it up yourself.<\/p>\n","protected":false},"author":2,"featured_media":1090,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[69],"tags":[87,83,86,78,85,81,84,75,80,88],"class_list":["post-1091","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","tag-admission-control","tag-ai-lab","tag-api-keys","tag-data-sovereignty","tag-langfuse","tag-litellm","tag-llm-gateway","tag-local-llm","tag-ollama","tag-openrouter"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11) - Catharsis | Blog<\/title>\n<meta name=\"description\" content=\"How do you control access to local AI models? Put one gateway in front of them: a key per client, budgets at the gate, no cloud fallback. Here is how.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11) - Catharsis | Blog\" \/>\n<meta property=\"og:description\" content=\"How do you control access to local AI models? Put one gateway in front of them: a key per client, budgets at the gate, no cloud fallback. Here is how.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/\" \/>\n<meta property=\"og:site_name\" content=\"Catharsis | Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-23T23:30:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-11-03T09:27:49+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-02.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1280\" \/>\n\t<meta property=\"og:image:height\" content=\"720\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Sajal Jain\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Sajal Jain\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"19 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/\"},\"author\":{\"name\":\"Sajal Jain\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\"},\"headline\":\"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11)\",\"datePublished\":\"2026-08-23T23:30:00+00:00\",\"dateModified\":\"2026-11-03T09:27:49+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/\"},\"wordCount\":3428,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-02.jpg\",\"keywords\":[\"admission control\",\"AI lab\",\"API keys\",\"data sovereignty\",\"Langfuse\",\"LiteLLM\",\"LLM gateway\",\"local LLM\",\"Ollama\",\"OpenRouter\"],\"articleSection\":[\"Artificial Intelligence\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/\",\"name\":\"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11) - Catharsis | Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-02.jpg\",\"datePublished\":\"2026-08-23T23:30:00+00:00\",\"dateModified\":\"2026-11-03T09:27:49+00:00\",\"description\":\"How do you control access to local AI models? Put one gateway in front of them: a key per client, budgets at the gate, no cloud fallback. Here is how.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#primaryimage\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-02.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-02.jpg\",\"width\":1280,\"height\":720,\"caption\":\"Fibre optic patch cables in a network panel\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/llm-gateway-one-door-for-every-model\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"name\":\"Catharsis | Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\",\"name\":\"Catharsis | Blog\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"width\":215,\"height\":60,\"caption\":\"Catharsis | Blog\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\",\"name\":\"Sajal Jain\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"caption\":\"Sajal Jain\"},\"description\":\"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/author\\\/sajal-jain\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11) - Catharsis | Blog","description":"How do you control access to local AI models? Put one gateway in front of them: a key per client, budgets at the gate, no cloud fallback. Here is how.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/","og_locale":"en_US","og_type":"article","og_title":"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11) - Catharsis | Blog","og_description":"How do you control access to local AI models? Put one gateway in front of them: a key per client, budgets at the gate, no cloud fallback. Here is how.","og_url":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/","og_site_name":"Catharsis | Blog","article_published_time":"2026-08-23T23:30:00+00:00","article_modified_time":"2026-11-03T09:27:49+00:00","og_image":[{"width":1280,"height":720,"url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-02.jpg","type":"image\/jpeg"}],"author":"Sajal Jain","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Sajal Jain","Est. reading time":"19 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#article","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/"},"author":{"name":"Sajal Jain","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099"},"headline":"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11)","datePublished":"2026-08-23T23:30:00+00:00","dateModified":"2026-11-03T09:27:49+00:00","mainEntityOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/"},"wordCount":3428,"commentCount":0,"publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-02.jpg","keywords":["admission control","AI lab","API keys","data sovereignty","Langfuse","LiteLLM","LLM gateway","local LLM","Ollama","OpenRouter"],"articleSection":["Artificial Intelligence"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/","url":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/","name":"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11) - Catharsis | Blog","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#primaryimage"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-02.jpg","datePublished":"2026-08-23T23:30:00+00:00","dateModified":"2026-11-03T09:27:49+00:00","description":"How do you control access to local AI models? Put one gateway in front of them: a key per client, budgets at the gate, no cloud fallback. Here is how.","breadcrumb":{"@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#primaryimage","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-02.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-02.jpg","width":1280,"height":720,"caption":"Fibre optic patch cables in a network panel"},{"@type":"BreadcrumbList","@id":"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/catharsis.net.au\/blog\/"},{"@type":"ListItem","position":2,"name":"How Do You Control Access to Local AI Models? Use a Gateway (Part 2 of 11)"}]},{"@type":"WebSite","@id":"https:\/\/catharsis.net.au\/blog\/#website","url":"https:\/\/catharsis.net.au\/blog\/","name":"Catharsis | Blog","description":"","publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/catharsis.net.au\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/catharsis.net.au\/blog\/#organization","name":"Catharsis | Blog","url":"https:\/\/catharsis.net.au\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","width":215,"height":60,"caption":"Catharsis | Blog"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099","name":"Sajal Jain","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","caption":"Sajal Jain"},"description":"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management","url":"https:\/\/catharsis.net.au\/blog\/author\/sajal-jain\/"}]}},"_links":{"self":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1091","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/comments?post=1091"}],"version-history":[{"count":3,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1091\/revisions"}],"predecessor-version":[{"id":1148,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1091\/revisions\/1148"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media\/1090"}],"wp:attachment":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media?parent=1091"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/categories?post=1091"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/tags?post=1091"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}