tutorials

Model Gateway basics

Overview

Large language models (LLMs) get the "large" part of their name from their scale, including billions of parameters (the numerical values learned during training). Those values have to live somewhere! For example, a model with 70 billion parameters stored at 16 bits each needs about 140 GB of memory just for its parameters, before we account for the working memory needed to generate a response. That's far more than a typical laptop can muster.1

There's also the work of computing each response. GPUs are useful here because they can perform many of the model's calculations in parallel. Shared servers with this hardware let us use demanding models without each person needing their own machine capable of running them. Smaller models and quantized models (which store weights using fewer bits) can make local use practical too.

Now imagine wanting to experiment with several models. We need somewhere to store their weights, enough memory for whichever ones we keep loaded, and the computing power to run them. We don't have to keep them all loaded at once, but switching between them can mean more downloads, setup, and waiting. Sharing model servers can spare each person from repeating all of that work.

Suppose we want to use several models, each running on a different machine. Without a shared entry point, our code may need a different server address and access setup for each one. Moving a model to another machine could mean changing the clients that use it. How can we keep those details out of each client's code?

A model gateway sits between clients and model servers. Clients send requests to the gateway, which checks access and routes each request to the selected model. We can use one interface without having to track where each model runs. That doesn't make the models interchangeable, but it simplifies how we reach them.

A client connects through one gateway to model A replicas, model B, and additional model servers
Choose a model, not a server. A model gateway provides one interface to model servers on different machines.

You don't need to specify which model host you're targeting. Just tell the gateway which model you want to "talk" with by using its model ID. The gateway handles routing your request to the server hosting that model.

Outcomes

After this tutorial, you should be able to…

  • distinguish a model, a gateway, and the client or harness around a model
  • find your course project and use your own API key without exposing it
  • list available models and make a small OpenAI-compatible request
  • count a planned chat input and check space for its requested output
  • recognize what document access, conversation history, and tools require
  • inspect completion status and ask for help with access or request limits

A shared API interface

How does our code "talk" with a model? An API (application programming interface) gives one program a way to communicate with another. Instead of typing into a chat window, we can send messages from Python and work with the response in our code.

OpenAI's API has become a widely adopted reference for this kind of interface. Other services (ex. Hugging Face's Text Generation Inference) offer OpenAI-compatible APIs, allowing us to use familiar request formats and client libraries with models served elsewhere. Think of it as a shared convention, rather than a promise that every service supports every feature in exactly the same way.

The model is not the whole application

A chat application often involves all sorts of things like assembling prompts, extracting files, managing history, and executing tools. All of that affects what information reaches the model. It helps to distinguish three pieces:

Component What it does
Model Generates a response from the input it receives.
Gateway Authorizes requests and routes them to a permitted model.
Client or harness Constructs messages and may execute tools and return their results.

An LLM ("the model") doesn't magically do all of those things. They involve an interplay between the model's outputs (including proposed decisions or actions) and the rest of the application. The application controls which actions actually happen and what information gets sent back to the model.

If we want the model to work with a document or an earlier conversation, something has to supply that information. For example, putting a title, URL, or local filename in a prompt doesn't make our simple Python client open the document and send its contents.

Tools

What would change if we wanted the model to use a tool†? We could describe an available function in a request and let the model propose calling it. Our client decides whether to execute the requested operation and, if so, sends the result back to the model:

graph TD
  C["Your client"] -->|messages| G["Gateway: access and routing"]
  G -->|input| M["Model"]
  M -->|answer or tool call| G
  G -->|response| C
  C -->|execute allowed call| T["Your tools"]
  T -->|result for next request| C

The gateway connects the client to the model, but it does not execute our local tools. The application decides which tools to offer and which proposed calls to carry out. Asking for an operation isn't the same as having executed it.

We'll explore that loop separately. We can start with ordinary messages, without adding tool execution to the mix.

Our course gateway

With those distinctions in mind, let's connect to the gateway we'll be using in the course. It organizes access into projects. Your project determines which models you can use, and your own project key lets your Python client make requests. The models run elsewhere, so you don't need to download them or run them on your laptop.

Before you start

Use your configured Python environment and your own course account. You'll find the gateway address, assigned project, and model identifier in Class Maestro (CM). We'll use those values below, rather than hard-code a particular model into the tutorial.

Find your project and create a key

Follow the gateway link in CM and sign in with your course account. Open your assigned project. Don't see it? Ask for help. Borrowing someone else's key or choosing an unrelated project won't fix your course access.

The student Projects page lists an enabled ling-582 project with the member role
Open the project you have access to. Project names and available models may differ from this example.

Open the project's Keys tab and create your own key. Signing in through your browser gets you into the gateway's interface. The key is what lets your code make API requests.

The project Keys tab has a Create key control and displays its logging policy
Create your own key in the project's Keys tab. No key values are shown here.

Set up the Python client

We'll use the openai Python package to handle the requests. If it isn't already installed, install it in your existing course environment:

python -m pip install openai

Our client needs three things: where to send requests, which model to use, and our key. We'll keep these in environment variables, which our Python code can read without putting the key into the script itself.

Replace the two placeholders below with the API URL ending in /v1 and the assigned model ID from CM. Python's getpass asks for your key without showing it as you type or putting it into shell history. This example works in Bash and Bourne-style shells:

export GATEWAY_BASE_URL='PASTE_THE_ASSIGNED_API_URL_ENDING_IN_/v1'
export GATEWAY_MODEL='PASTE_THE_ASSIGNED_MODEL_ID'

GATEWAY_API_KEY=$(python -c 'import getpass; print(getpass.getpass("Gateway key: "))')
export GATEWAY_API_KEY

Run Python from that same terminal so it inherits these variables. If you're using a notebook, there's one more detail to keep in mind:

Make a small request

Before sending a long document, let's check that we can make a small request. We'll connect to the gateway, check that our assigned model is available, and ask a short question:

import os
from openai import OpenAI

client = OpenAI(
    base_url=os.environ["GATEWAY_BASE_URL"],
    api_key=os.environ["GATEWAY_API_KEY"],
    timeout=120,
    max_retries=0,
)

# Check access to the assigned model before sending a request.
model = os.environ["GATEWAY_MODEL"]
available = {item.id for item in client.models.list().data}
if model not in available:
    raise RuntimeError("The assigned model is not available to this key.")

response = client.chat.completions.create(
    model=model,
    messages=[{"role": "user", "content": "Explain a held-out evaluation in two sentences."}],
    max_tokens=256,
    stream=False,
)
choice = response.choices[0]
print("Finish reason:", choice.finish_reason)
print(choice.message.content)

Notice how the earlier distinctions show up in the code. base_url tells the client where to send the request, api_key authorizes it, and model says which model we want to talk with. Changing the model ID doesn't require us to find that model's server address.

A model card displays its logical model ID, source, revision, and expandable capability information
Read the card for your assigned model. Access through the same API does not make models interchangeable.

The gateway takes care of access checks and routing, but we still have choices to make about the input and output. Use the model assigned in CM, even if your key permits other models. Read its card to see its limits and the available evidence about what it supports. When documenting a run, record the model ID and any available version information.

What did we actually send? The messages list contains our conversation. Each message has a role and content. Here, there's just one user message. The model's answer comes back as an assistant message. Calling it an assistant doesn't make its answer authoritative, of course!

We use the SDK's Chat Completions interface because our gateway supports that interface. OpenAI's own service and our gateway need not support every API feature or model setting in the same way.

There are no tools in this request, and no earlier conversation is included. If we make another request, the client doesn't automatically add this exchange to it. We have to supply any history we want the model to receive.

What about max_tokens=256? That's a cap on the generated output, not the size of the input we can send. Check the printed finish reason too. A value of length tells us the response reached a length limit, so we should check whether it was cut short rather than assume we received a complete answer.

If you omit max_tokens, the gateway currently defaults to 2,048 output tokens. Set it explicitly when comparing runs so the intended budget is clear. The model may stop before reaching that cap.

Count tokens before a longer request

A model's context window must accommodate both the input and generated output. Our messages, conversation history, and chat formatting take up space before the model starts answering. max_tokens caps the output, but it doesn't enlarge the context window.

Let's imagine a model has a 4,096-token context window. If our messages use 3,000 tokens and we ask for up to 1,500 output tokens, the planned total is 4,500 tokens. That exceeds 4,096. We either need a smaller output budget or a different, explicitly chosen input. These numbers illustrate the calculation. Check the limits for your assigned model.

The gateway also adds token_limits to each chat model in /v1/models. Inspect the assigned model's entry to find the gateway output ceiling, default budget, and context window evidence:

assigned = next(item for item in client.models.list().data if item.id == model)
print(assigned.model_dump()["token_limits"])

These additional fields extend the OpenAI-compatible response with gateway-specific token limits. OpenAI's own API does not provide these fields.

A context value of null means unknown. Its status can also mark a stale observation or configured fallback. The gateway's output ceiling is separate from the model's total context, and may be higher than the model can actually use for a given request.

Despite its name, reserve_output_tokens doesn't reserve anything on the server. It tells the tokenizer how much planned output to include in this calculation:

remaining_tokens = context_limit - count - reserve_output_tokens

Here, count is the number of input tokens. fits is true when the result is zero or greater. In the example above, the result is -404 and fits is false. The counting call generates no answer and doesn't change a later generation request.

Count and generate with the same output limit

Tokenization must be enabled for your project and supported by the assigned model. If the Tokenizer view is missing or access is denied, ask your instructor. It isn't necessary to create another key: these requests use the same project bearer key as generation.

The example below uses the environment variables and client from the earlier example. We use httpx, an HTTP library installed with the OpenAI client, to call the gateway's own utility endpoints. Those endpoints start with /api/v1, whereas the OpenAI-compatible generation endpoint starts with /v1.

import httpx

# GATEWAY_BASE_URL ends in /v1; these utilities use /api/v1 instead.
gateway_url = os.environ["GATEWAY_BASE_URL"].rstrip("/").removesuffix("/v1")
headers = {"Authorization": "Bearer " + os.environ["GATEWAY_API_KEY"]}

messages = [
    {"role": "user", "content": "Explain a held-out evaluation in two sentences."}
]
output_budget = 1024

with httpx.Client(base_url=gateway_url, headers=headers, timeout=120) as gateway:
    discovery = gateway.get("/api/v1/tokenizer/models")
    discovery.raise_for_status()
    supported = any(
        item["id"] == model and item["capabilities"]["tokenize"]
        for item in discovery.json()["data"]
    )
    if not supported:
        raise RuntimeError("Ask your instructor about tokenization for this model.")

    counted = gateway.post(
        "/api/v1/tokenize",
        json={
            "model": model,
            "messages": messages,
            "reserve_output_tokens": output_budget,
        },
    )
    counted.raise_for_status()
    budget = counted.json()

print("Input tokens:", budget["count"])
print("Total context limit:", budget["context_limit"])
print("Gateway output ceiling:", budget["gateway_output_cap"])
print("Space left after planned input and output:", budget["remaining_tokens"])
print("Fits:", budget["fits"])
if not budget["fits"]:
    raise RuntimeError("Revise the input or output budget before generating.")

# This is a separate generation request. Repeat the output budget explicitly.
response = client.chat.completions.create(
    model=model,
    messages=messages,
    max_tokens=output_budget,
    stream=False,
)
print(response.choices[0].message.content)

Use the same model and full message list for both calls. Include any conversation history and, when using tools, the same tool definitions. Counting just a paper's text would miss the instructions, questions, and chat formatting around it.

This utility currently supports text/chat counting through supported vLLM models. It doesn't account for image inputs, response schemas, or every model-specific reasoning setting. fits: true is a token limit check, not a guarantee that generation will succeed. Tokenization also has its own input and returned-token limits. Ask for help if it can't count your intended input.

When a request fails

If the small request doesn't work, start with the basics:

  • Is this the right project?
  • Does the model ID match the one you were given?
  • Is your key still active?

A typo or access problem is easier to isolate with a short prompt than with an entire paper.

For a context overflow, the gateway returns HTTP 400 with code: "context_length_exceeded". For example, requesting 4,097 output tokens from a model with a 4,096-token context window produces:

{
  "error": {
    "message": "This model's context limit is 4096 tokens, shared by input and output. Requested output: 4097 tokens. Reduce the messages or max_tokens.",
    "type": "invalid_request_error",
    "param": "max_tokens",
    "code": "context_length_exceeded",
    "details": {
      "requested_output_tokens": 4097,
      "context_limit": 4096
    }
  }
}

An oversized input gets the same error code, with param: "messages". When the backend reports only a lower bound, fields such as prompt_tokens_at_least make that distinction explicit. Counts appear when the backend supplies them.

Exceeding the gateway's separate output cap returns HTTP 400 with code: "token_limit" and the requested and allowed output counts.†

For a longer request, check the input and output limits. If you cut information to fit within the limit, you also change what information is available to the model to generate its response. You may need to get creative with how you break down the task to meet context constraints.

The client does not automatically retry token limit or byte size errors. Change the request first. We set max_retries=0 in this example so temporary failures also return immediately, making each attempt easier to inspect.

Still stuck? Keep the error and request ID, if available, and ask for help. Leave the key out of anything you share. Once you get an answer, check its substantive claims against the source. Getting a response tells us the request worked, not that the answer is right.

Footnotes

  1. This is a weight-only estimate: 70 billion values times 2 bytes gives 140 billion bytes (140 GB in decimal units). Actual memory needs depend on precision, model structure, and the request. Hugging Face's model memory estimator distinguishes storing model weights from the additional memory needed for inference. ↩