n4nAI

A single Python client for GPT-5, Claude Opus 4.8, and Gemini 3

Hands-on tutorial: build one Python client to call GPT-5, Claude Opus 4.8, and Gemini 3 via an OpenAI-compatible gateway, with streaming and fallback.

n4n Team3 min read627 words

Audio narration

Coming soon — every post will get a voice note here.

Building a python client multiple llm models that spans GPT-5, Claude Opus 4.8, and Gemini 3 used to mean three SDKs, three auth schemes, and three sets of response types. A single OpenAI-compatible gateway collapses that into one HTTP interface: you point the standard openai package at a different base URL and switch the model string. This tutorial builds that client from scratch and runs it against real model identifiers.

Prerequisites

  • Python 3.10 or newer
  • openai Python package (v1.40+)
  • An API key from an OpenAI-compatible inference gateway. Set it as LLM_API_KEY in your environment.
  • Familiarity with asyncio is helpful but not required; we start synchronous.
python -m venv .venv
source .venv/bin/activate
pip install openai python-dotenv
export LLM_API_KEY="sk-..."

If you prefer a .env file, load it with python-dotenv before importing the client.

Point one client at the gateway

The openai library accepts a base_url argument. Services such as n4n.ai provide a single OpenAI-compatible endpoint that fronts 240+ models and handles fallback when a provider is degraded, which is the simplest way to run a python client multiple llm models without vendor lock. The code below configures the client once.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.n4n.ai/v1",  # replace with your gateway's URL
    api_key=os.environ["LLM_API_KEY"],
    timeout=30.0,
)

Everything downstream uses this single client object. No conditional imports, no vendor-specific exception handling.

Verify model availability

Before sending traffic, confirm the gateway exposes the models you intend to use. The models.list() call returns identifiers exactly as they should be passed to chat.completions.create.

response = client.models.list()
ids = [m.id for m in response.data]
print(f"Total models: {len(ids)}")
for model_id in ["gpt-5", "claude-opus-4-8", "gemini-3"]:
    print(model_id, "available" if model_id in ids else "missing")

Expected output (truncated):

Total models: 242
gpt-5 available
claude-opus-4-8 available
gemini-3 available

If any identifier is missing, check the gateway’s model catalog; naming follows each provider’s published slug.

First call: GPT-5

The request shape is identical to OpenAI’s own API. Only the model field changes.

resp = client.chat.completions.create(
    model="gpt-5",
    messages=[
        {"role": "system", "content": "You are a concise senior engineer."},
        {"role": "user", "content": "What is head-of-line blocking in HTTP/2?"},
    ],
    temperature=0.2,
    max_tokens=200,
)
print(resp.choices[0].message.content)
print("Usage:", resp.usage.model_dump())

Expected output:

HTTP/2 multiplexes streams over one TCP connection. Head-of-line blocking occurs when a single stalled stream (e.g., a slow packet) delays all other streams behind it at the TCP layer, undermining concurrency.
Usage: {'prompt_tokens': 24, 'completion_tokens': 41, 'total_tokens': 65}

The usage object is populated per token by the gateway, so you get per-token metering without extra calls.

Same client, different model: Claude Opus 4.8

Switch the model string. No other code changes.

resp = client.chat.completions.create(
    model="claude-opus-4-8",
    messages=[{"role": "user", "content": "Summarize that in one line."}],
    max_tokens=80,
)
print(resp.choices[0].message.content)

Expected output:

HTTP/2 head-of-line blocking is a TCP-level delay where one stalled stream holds up all others sharing the connection.

This python client multiple llm models approach means you can A/B test providers by changing one variable, not rewriting modules.

Gemini 3 with streaming

Gemini models often benefit from streaming to surface latency. The openai client supports stream=True uniformly.

stream = client.chat.completions.create(
    model="gemini-3",
    messages=[{"role": "user", "content": "List three caching strategies for LLM gateways."}],
    stream=True,
    stream_options={"include_usage": True},
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
print()
if chunk.usage:
    print("Stream usage:", chunk.usage.model_dump())

Expected output (streamed, then usage):

1. Prompt prefix caching at the provider.
2. Gateway-level response caching keyed by hashed request.
3. Semantic cache using embeddings for near-duplicate hits.
Stream usage: {'prompt_tokens': 15, 'completion_tokens': 32, 'total_tokens': 47}

Building a small router

Hardcoding model strings is fine for a script, but a real service wants a thin wrapper. Below is a minimal MultiLLM that picks a model from a config and falls back on exception.

from openai import OpenAI, APIError

class MultiLLM:
    def __init__(self, client: OpenAI, default="gpt-5"):
        self.client = client
        self.default = default

    def complete(self, prompt: str, model: str | None = None, **kw):
        model = model or self.default
        try:
            resp = self.client.chat.completions.create(
                model=model,
                messages=[{"role": "user", "content": prompt}],
                **kw,
            )
            return resp.choices[0].message.content
        except APIError as e:
            # gateway may already have tried automatic fallback;
            # if not, escalate or switch model here
            raise RuntimeError(f"Model {model} failed: {e}") from e

llm = MultiLLM(client)
print(llm.complete("Ping", model="claude-opus-4-8"))

If your gateway supports client routing directives, you can pass them via extra_headers. For example, to force a specific provider region:

resp = client.chat.completions.create(
    model="gemini-3",
    messages=[{"role": "user", "content": "Hi"}],
    extra_headers={"x-routing": "provider:google;region:us-central"},
)

Gateways that honor such hints forward them upstream and respect provider cache-control headers, which keeps prefix-cache hits intact across calls.

Batch across all three models

A common task: run the same prompt through every model and compare. The python client multiple llm models pattern makes this a loop.

prompt = "Define eventual consistency."
for model in ["gpt-5", "claude-opus-4-8", "gemini-3"]:
    try:
        out = llm.complete(prompt, model=model, max_tokens=60)
        print(f"[{model}] {out}\n")
    except RuntimeError as e:
        print(f"[{model}] error: {e}\n")

Expected output (abridged):

[gpt-5] Eventual consistency guarantees that, absent new updates, all replicas converge to the same value over time.

[claude-opus-4-8] It is a consistency model where replicated data becomes consistent asynchronously after a write.

[gemini-3] Eventual consistency: a storage system property where reads may lag but all copies sync eventually.

Handling rate limits and degradation

Even with a gateway that performs automatic fallback when a provider is rate-limited, your client should back off on 429 or 5xx. The openai library raises RateLimitError and APIConnectionError. Catch them, sleep, retry with a different model if needed.

import time
from openai import RateLimitError, APIConnectionError

def safe_complete(llm, prompt, models, max_retries=2):
    for attempt in range(max_retries):
        for m in models:
            try:
                return llm.complete(prompt, model=m, max_tokens=50)
            except RateLimitError:
                time.sleep(2 ** attempt)
            except APIConnectionError:
                time.sleep(1)
    return None

Because the gateway already collapses 240+ models behind one endpoint, this retry logic stays simple—no per-vendor SDK quirks.

Meter and log usage

Per-token usage metering is returned on every response. Aggregate it in your service:

from collections import defaultdict

totals = defaultdict(int)
for model in ["gpt-5", "claude-opus-4-8", "gemini-3"]:
    r = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": "Count this."}],
        max_tokens=10,
    )
    totals[model] += r.usage.total_tokens

print(dict(totals))

Expected output:

{'gpt-5': 12, 'claude-opus-4-8': 11, 'gemini-3': 10}

Feed this to your billing or observability pipeline. The gateway forwards exact provider counts, so you are not estimating.

Wrapping up the pattern

You now have a single OpenAI client instance that talks to GPT-5, Claude Opus 4.8, and Gemini 3 with no conditional code paths. The python client multiple llm models strategy reduces dependency surface, simplifies testing, and lets you shift traffic between providers by changing a string. For production, add structured logging, a circuit breaker, and a config-driven model map—but the core integration is exactly the code above.

If you need to support a new model next quarter, you add one slug to your routing table. No new package, no new auth, no new response parser.

Tagspythongpt-5claude-opus-4-8gemini-3

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All integrating gpt-5, claude opus 4.8, gemini 3, llama 4 & more via one api posts →