Most teams wire up an LLM gateway before they have a reason to. Deciding when is a single LLM provider API enough for your application is a function of coupling, risk tolerance, and operational overhead. If you are shipping a feature that calls one model from one vendor and your traffic is predictable, the direct integration is simpler and cheaper to run.
1. Inventory your model dependencies
Open your codebase and list every LLM call. Search for chat.completions.create, messages.create, or equivalent SDK methods. If the list is one model from one provider (e.g., gpt-4o-mini from OpenAI or claude-3-haiku from Anthropic), you have a clear answer.
# direct_openai.py
from openai import OpenAI
client = OpenAI(api_key="sk-...")
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Summarize this ticket"}],
temperature=0.2,
)
print(resp.choices[0].message.content)
The moment you add a second provider for redundancy or a specialized model (e.g., a local Mistral for PII-heavy text), the direct-only approach starts costing you abstraction work. Embeddings often count as a separate dependency even from the same vendor; include them in the inventory.
2. Measure request volume and shape
A single provider API is enough when your peak QPS fits comfortably inside the vendor’s published rate limits with headroom. For a backend job processing 10k documents per day at 2 QPS, you will never trip a 3,500 RPM limit.
Plot your traffic. If it is batch or background, you can serialize requests with a simple queue:
import time
from openai import OpenAI
client = OpenAI()
def summarize_batch(texts):
for t in texts:
# blocking call, fine at low volume
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": t}]
)
yield r.choices[0].message.content
time.sleep(0.1) # stay under rate limit
Burst traffic from a retry storm is the common pitfall. Set client timeouts and a single retry with exponential backoff. If you expect sporadic spikes above vendor limits, a single provider is not enough unless you build a buffering layer yourself.
3. Define your failure tolerance
Ask the on-call engineer: if the provider returns 503 for ten minutes, does the product break? For an internal summarization tool, yes it is annoying but not revenue-losing. For a customer-facing chatbot, maybe not, but support tickets will pile up.
If the answer is “we can degrade gracefully,” a single provider is enough. Implement a circuit breaker:
import requests
from datetime import datetime, timedelta
class ProviderHealth:
def __init__(self):
self.last_fail = None
def mark_fail(self):
self.last_fail = datetime.now()
def is_healthy(self):
if not self.last_fail:
return True
return datetime.now() - self.last_fail > timedelta(minutes=10)
health = ProviderHealth()
When the circuit is open, return a cached response or a static message. That is a tractable amount of code. Partial degradation (e.g., higher latency but no outage) is also acceptable for many internal apps.
4. Evaluate billing and metering needs
Single-provider dashboards give per-token cost breakdown. If your finance team only needs a monthly bill, the native metering is sufficient.
For multi-team allocation, you may need tags. OpenAI supports a user field; Anthropic has metadata. Use them:
client.chat.completions.create(
model="gpt-4o-mini",
messages=[...],
user="team-billing-id-123"
)
If you later require aggregated cross-provider per-token usage metering, that is a signal to reconsider architecture. Note that provider dashboards rarely expose programmatic cost attribution per sub-team without extra plumbing.
5. Check data residency and compliance
Some teams are bound by contractual clauses that name a specific vendor. In that case, when is a single LLM provider API enough is answered by the legal doc, not the tech. Confirm the provider meets SOC 2, GDPR, or regional storage needs, then move on.
If you later expand to a second region, the single-provider assumption may still hold if that vendor has multi-region endpoints. Verify before coding.
6. Build a thin client wrapper
Even with one provider, do not scatter API calls across handlers. Wrap it:
class LLMClient:
def __init__(self, api_key):
from openai import OpenAI
self._client = OpenAI(api_key=api_key)
self._client.timeout = 15 # seconds
def complete(self, prompt, **kw):
return self._client.chat.completions.create(
model=kw.get("model", "gpt-4o-mini"),
messages=[{"role": "user", "content": prompt}],
temperature=kw.get("temperature", 0.2),
)
This gives you one place to inject logging, timeouts, and model swaps. The wrapper is ~30 lines and avoids a rewrite later. Add a stream method if you need token streaming; keep the signature consistent.
Streaming consideration
If your UI shows tokens as they arrive, the direct provider’s SSE stream is easy:
stream = client._client.chat.completions.create(
model="gpt-4o-mini",
messages=[...],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")
The wrapper should expose this without masking the underlying iterator.
7. Add minimal observability
You do not need a distributed tracing system. A structured log line per call with latency and token count is enough:
import logging, time
logger = logging.getLogger("llm")
def logged_complete(client, prompt):
start = time.monotonic()
resp = client.complete(prompt)
elapsed = time.monotonic() - start
logger.info("llm_call", extra={
"latency_ms": int(elapsed*1000),
"prompt_tokens": resp.usage.prompt_tokens,
"completion_tokens": resp.usage.completion_tokens,
})
return resp
Alert on error rate > 5% over 5 minutes. That is a cron job, not a platform. Capture provider-specific headers (like x-ratelimit-remaining) to anticipate throttling.
8. Know your exit criteria
The direct integration stops being enough when any of these become true:
- You need a second model from another provider for cost or quality.
- Rate limits threaten uptime during growth spikes.
- You must route based on user tier or content type.
- Compliance requires dynamic provider selection.
At that point, a gateway pays for itself. For example, n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models with automatic fallback when a provider is degraded, and it honors client routing directives. You swap the base URL and keep your wrapper.
# before
client = OpenAI(api_key="sk-...")
# after
client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="n4n-...")
The code change is one line; the operational gain is multi-provider redundancy without rewriting call sites.
Common pitfalls
- Hardcoding the model string in 40 files. Always reference a constant.
- No timeout: the default openai client waits 600s. Set
timeout=15. - Blind retries: retrying a 400 wastes money. Retry only 429/5xx with backoff.
- Ignoring cache-control: providers support prompt caching; pass
extra_body={"cache_control": ...}where supported to cut cost. - Missing idempotency: if a request times out, you may double-send. Use provider idempotency keys if available.
Tradeoffs summary
Direct single-provider API:
- Pros: zero middleware, lowest latency, simplest auth, native support.
- Cons: no redundancy, vendor lock-in, manual multi-model work.
Gateway:
- Pros: fallback, model diversity, unified metering.
- Cons: extra hop, another credential, potential cost markup.
When is a single LLM provider API enough? For early-stage products, internal tools, and predictable batch jobs, it is the right default. Graduate only when the triggers above appear.