Skip to main content

When One API Knows Every Provider

What happens when one API knows every provider? The real challenge is keeping a simple interface while making room for the differences behind it—without losing clarity.

Code Cracking
15m read
#APIDesign#ProviderIntegration#SoftwareArchitecture
When One API Knows Every Provider - Featured blog post image
Mahmoud Zalt

1:1 Mentor

Are you a software engineer moving into AI?

Let's have a call. I'll help you modernize your skills and learn the tools, systems, and architecture behind reliable AI products. One session or ongoing.

Vibe Coding
with Confidence

The Vibecoder's Handbook, from idea to production

4.8

Everything you need to know about shipping software with AI, from the App idea to production.

What it covers

  • 0IntroductionWhat this book is & how to read it
  • 1Set UpGet your tools and a running app ready
  • 2PlanStructure your idea into a clear specification
  • 3ArchitectLay out a modular codebase for your AI
Start Reading Free

We’re examining how LiteLLM routes one OpenAI-shaped API across many model providers. LiteLLM gives callers common operations such as completion, embedding, transcription, and speech, while translating them for services including OpenAI, Azure, Anthropic, Bedrock, and Vertex AI. The critical file is litellm/main.py, where completion and its dispatch machinery turn a normalized request into a provider-specific call. I’m Mahmoud Zalt, an AI solutions architect. The question is not whether a universal façade should hide provider differences—it should—but how to prevent its central dispatcher from becoming the place where every difference accumulates. We’ll trace the existing boundary, show why CompletionDispatchContext is the right migration seam, and connect routing design to concurrency and operations.

A Stable Façade Needs Internal Boundaries

LiteLLM’s public contract is valuable precisely because callers do not have to learn each provider’s message rules, credentials, headers, endpoints, streaming behavior, or error shapes. A request enters completion, receives shared validation, passes through get_llm_provider, and eventually reaches a provider adapter. The result returns as a shared response contract such as ModelResponse or CustomStreamWrapper.

Caller
  │
  ▼
completion() / acompletion()
  │  validate messages, tools, options
  ▼
get_llm_provider()
  │  resolve provider, credentials, endpoint
  ▼
CompletionDispatchContext
  │
  ├── _complete_azure()
  ├── _complete_anthropic()
  ├── _complete_bedrock()
  └── _complete_()
          │
          ▼
ModelResponse / CustomStreamWrapper
A common request narrows into a provider strategy, then returns through a common response contract.

The early validation establishes the correct boundary: every provider starts with trustworthy messages, tools, and optional parameters. The problem begins when the same orchestration flow also owns compatibility bridges, pricing, logging, timeout policy, credential resolution, and a growing sequence of provider decisions.

The report estimates completion at roughly 500 source lines, with cyclomatic complexity 70 and cognitive complexity 100. Cyclomatic complexity counts independent control-flow paths; cognitive complexity approximates how hard that flow is to follow. These figures matter because route order can encode behavior: an exact provider match may interact with model-name checks, deprecated paths, or OpenAI-compatible fallbacks.

Use the Context Object to Evolve Routing

LiteLLM already has the most important seam for that change: CompletionDispatchContext. This context object carries resolved request state—model, messages, credentials, headers, timeout, logger, client, provider configuration, and optional parameters—as a named package rather than a fragile list of arguments.

ResponsibilityBoundaryReason
Validate common request shapescompletionEvery adapter receives a reliable baseline.
Carry resolved request stateCompletionDispatchContextAdapters share an explicit contract.
Translate provider protocols_complete_ and handlersProvider differences remain local.
Select an adapterDispatcher or registryRouting can change without rewriting transport code.
Normalize responses and errorsShared response and exception layersCallers retain one outward API.

The safe next step is not a wholesale rewrite. It is to move only exact provider mappings into a dispatch registry, while preserving exceptional routes as named, visible logic. Azure AI, Bedrock subroutes, model-dependent compatibility checks, and custom providers should not be forced into a dictionary merely to make the code look uniform.

# Proposed incremental refactor
_COMPLETION_DISPATCHERS: Final = {
    "azure": _complete_azure,
    "azure_text": _complete_azure_text,
    "deepseek": _complete_deepseek,
    "mistral": _complete_mistral,
}

dispatcher = _COMPLETION_DISPATCHERS.get(custom_llm_provider)
if dispatcher is not None:
    response = dispatcher(_dispatch_ctx)
elif custom_llm_provider == "azure_ai":
    response = _complete_azure_ai(_dispatch_ctx)
Exact mappings become routing data; exceptional predicates remain explicit during migration.

This preserves route ordering while enabling table-driven route-resolution tests. The same discipline applies to repeated HTTP-handler wiring: centralize only calls that are genuinely identical. Adapters with special header merging, credential precedence, logging, or client-lifetime behavior should remain specialized, because those differences can be security-sensitive.

Before moving any route, protect the seam with contract tests for dispatch selection, response and exception mapping, streaming lifetime, and concurrent credential isolation. The context object makes that practical: it is a stable input contract for both adapters and tests.

Treat Async Bridging as a Capacity Decision

Cleaner routing improves change safety, but it does not remove runtime pressure. LiteLLM’s async wrapper copies Python contextvars before running synchronous orchestration in the default executor. Context variables carry request-scoped values such as trace identity, so copying them keeps that identity when work moves to a worker thread.

This is a pragmatic compatibility bridge: async callers can remain responsive while legacy synchronous work executes elsewhere. But async syntax does not create unlimited concurrency. If requests block in the default executor, queueing grows even when the event loop itself appears healthy.

The report describes the scaling progression clearly. At 10× traffic, connection reuse and bounded concurrency matter most. At 100×, executor saturation, retry amplification, logging volume, and global mutable SDK configuration become likely bottlenecks. At 1000×, the design needs async-native transports, explicit client pools, bounded queues, per-provider circuit breakers, and isolated workers or services.

Global values such as openai.organization, openai.api_type, and mutable litellm configuration make this an isolation problem as well as a throughput problem. Requests using different tenant credentials or organizations need concurrent isolation tests because process-wide configuration can bleed across request boundaries.

Operate the Gateway as a Control Plane

Once a request can queue for executor capacity, wait for a connection, retry, fall back, and call an external provider, provider latency alone no longer explains system health. The gateway needs measurements that reveal where time was spent and why a route changed.

MetricQuestion it answersUseful signal
completion_end_to_end_latency_ms by provider, model, stream, and outcomeIs delay in a provider or high-level routing?Track provider-specific p99 against the agreed product budget.
executor_queue_depth and executor_active_workersAre async requests waiting on synchronous capacity?Investigate sustained queueing beyond a typical provider request.
provider_request_total, provider_error_total, retry_total, and fallback_totalIs failure being amplified or hidden?Alert on deviations from the provider baseline.
connection_pool_wait_msAre shared sessions reusing connections?Keep wait time a small fraction of end-to-end latency.

Retries require equal care. Idempotency means repeating an operation has the same effect as doing it once. This file does not establish a universal idempotency-key contract, so blindly retrying generation or tool-capable requests can duplicate billable work or external side effects. Retry policy must distinguish safe transport recovery from workflows that may act twice.

The gateway is also a data-governance boundary. Messages, audio, metadata, headers, provider responses, and exception text may contain customer data or credentials. Structured logs should retain safe dimensions—provider, normalized model, endpoint, stream mode, timeout, attempt, and outcome—while redacting prompts, authorization headers, API keys, raw bodies, and file paths by default. Tracking redaction_failures and logging_payload_bytes turns that policy into something operators can verify.

The Practical Lesson

The primary lesson is that a universal AI API remains sustainable only when its provider variation is represented as explicit boundaries rather than accumulated in one dispatcher. LiteLLM has already established the key boundary with CompletionDispatchContext; the next step is to use it to transfer simple routing knowledge into a registry while keeping genuine exceptions visible and tested.

  1. Separate mappings from exceptions. Put exact provider-to-adapter routes in a registry. Keep model-dependent compatibility rules and unusual provider behavior in small, named resolvers.
  2. Test seams before extracting them. Add table-driven dispatch tests, streaming and exception-contract tests, async context-propagation tests, and concurrent credential-isolation tests before relocating behavior.
  3. Scale the waiting points, not just the providers. Watch executor queues, connection-pool waits, retries, fallbacks, payload size, and redaction failures alongside provider latency.

The façade should stay simple for callers. Its implementation does not need one function that knows every provider forever; it needs a disciplined migration from implicit control flow to explicit, observable, request-scoped contracts.

Full Source Code

Direct source from the upstream repository. Preview it inline or open it on GitHub.

heads/main/litellm/main.py

BerriAI/litellm • refs

Read Code on GitHub

Thanks for reading! I hope this was useful. If you have questions or thoughts, feel free to reach out.

Content Creation Process: This article was generated via a semi-automated workflow using AI tools. I prepared the strategic framework, including specific prompts and data sources. From there, the automation system conducted the research, analysis, and writing. The content passed through automated verification steps before being finalized and published without manual intervention.

Mahmoud Zalt

About the Author

I’m Zalt, a technologist with 16+ years of experience, passionate about designing and building AI systems that move us closer to a world where machines handle everything and humans reclaim wonder.

Let's connect if you're working on interesting AI projects, looking for technical advice or want to discuss anything.

Support this content

Share this article

Stay in touch

An occasional note when I build or write something new. Leave anytime.

Hire AI Employees

Hire AI Employees that work 24/7. No code.