← All articles

LLM API SDK Integration Patterns: OpenAI SDK, Native SDKs, and Direct HTTP — A Cross-Provider Decision Guide

Every LLM API project starts with a choice: use the OpenAI Python SDK with a custom base_url, use the provider's native SDK, or call the HTTP API directly. We break down when each pattern works, when it breaks, and how routing layers like TheRouter benefit from OpenAI SDK standardization.

· TheRouter

Every LLM API integration starts with the same fork in the road. You have an API key, a model name, and a task. Now you need to pick how your code talks to the provider. The three options — the OpenAI Python/TypeScript SDK pointed at a custom base_url, the provider's own native SDK, or raw HTTP calls — look interchangeable on day one. They diverge fast once you need streaming, tool calling, structured output, or multi-provider fallback.

We built TheRouter around OpenAI SDK compatibility because it is the closest thing to a universal adapter in the LLM API ecosystem. But we have also hit every edge case where compatibility breaks. This guide documents what we learned: which pattern to pick, what each one costs you, and where the gaps hide.

OpenAI-compatible means a provider exposes a chat-completions endpoint whose request and response shape matches the OpenAI API contract closely enough that an unmodified OpenAI SDK call works against it after swapping three values: API key, base URL, and model name. The minimum surface in practice is POST /v1/chat/completions with messages, model, and an OpenAI-shaped streaming response.

The Three Integration Patterns

Before comparing providers, here is what each pattern actually means in code.

Pattern 1: OpenAI SDK with Custom base_url

You install the openai package and change two lines — api_key and base_url. The rest of your code stays identical regardless of which provider you hit.

from openai import OpenAI

# DeepSeek
client = OpenAI(
    api_key="sk-deepseek-...",
    base_url="https://api.deepseek.com",
)

# DashScope (Qwen)
client = OpenAI(
    api_key="sk-dashscope-...",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

# SiliconFlow
client = OpenAI(
    api_key="sk-siliconflow-...",
    base_url="https://api.siliconflow.cn/v1",
)

response = client.chat.completions.create(
    model="deepseek-v4-flash",  # swap model name per provider
    messages=[{"role": "user", "content": "Hello"}],
)

What you get: One dependency, one interface, portable code across every provider that supports the /v1/chat/completions contract.

What you lose: Any feature the provider ships outside the OpenAI-compatible surface. DashScope's async task API, Anthropic's extended thinking blocks, Kimi's file-upload-via-chat — none of these exist in the OpenAI SDK's type system.

Pattern 2: Provider-Native SDK

Each major provider ships its own SDK. Anthropic has anthropic, Google has google-genai, DashScope has dashscope, Volcengine has volcengine-ark.

import anthropic

client = anthropic.Anthropic(api_key="sk-ant-...")

response = client.messages.create(
    model="claude-sonnet-5-20260514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello"}],
)

What you get: Full access to every feature the provider offers. Typed parameters for provider-specific fields. Better error types. Documentation that matches your actual endpoint.

What you lose: Portability. Switching from Anthropic to DeepSeek means rewriting every call site. Your streaming handler, your tool-calling parser, your retry logic — all provider-specific.

Pattern 3: Direct HTTP

Skip the SDK entirely. Build your own request, parse your own response.

import httpx

response = httpx.post(
    "https://api.deepseek.com/chat/completions",
    headers={"Authorization": "Bearer sk-..."},
    json={
        "model": "deepseek-v4-flash",
        "messages": [{"role": "user", "content": "Hello"}],
    },
)

data = response.json()
print(data["choices"][0]["message"]["content"])

What you get: Zero dependencies. Full control over HTTP behavior — connection pooling, proxy configuration, custom headers, retry timing. Works in any language without waiting for an SDK release.

What you lose: Type safety. Streaming SSE parsing. Automatic retry on transient errors. You own every byte of the integration code.

Which Providers Support OpenAI SDK Compatibility?

Not all "OpenAI-compatible" endpoints are equally compatible. Here is the current state across major providers, verified against their documentation as of August 2026.

ProviderBase URLChat CompletionsStreamingTool CallingStructured OutputVisionFile Upload
DeepSeekhttps://api.deepseek.comFullFullFullFullFullVia messages
DashScopehttps://dashscope.aliyuncs.com/compatible-mode/v1FullFullFullFullFullVia messages
SiliconFlowhttps://api.siliconflow.cn/v1FullFullFullPartialModel-dependentVia messages
Kimi/Moonshothttps://api.moonshot.ai/v1FullFullFullPartialFull (K2+)Via messages
xAI (Grok)https://api.x.ai/v1FullFullFullFullFullVia messages
AnthropicN/A (different API shape)Via adapter onlyDifferent formatDifferent schemaOwn formatFull (native)Own format
Google (Gemini)Via AI Studio onlyVia AI StudioDifferent formatDifferent schemaOwn formatFull (native)Own format

Sources: DeepSeek API docs (retrieved 2026-08-12), DashScope OpenAI compatibility docs (retrieved 2026-08-12), SiliconFlow quickstart (retrieved 2026-08-12), Kimi API docs (retrieved 2026-08-12), xAI API docs (retrieved 2026-08-12).

The split is clear. Chinese providers (DeepSeek, DashScope, SiliconFlow, Kimi) and xAI adopted OpenAI's API shape as their primary interface. Anthropic and Google built their own API contracts and offer compatibility only through adapter layers or limited endpoints.

When OpenAI SDK Compatibility Breaks

"Compatible" does not mean "identical." Here are the real-world gaps we have hit.

Streaming Delta Format Differences

Most providers match OpenAI's SSE format for chat.completions.chunk, but edge cases diverge:

  • Thinking/reasoning tokens. DeepSeek and DashScope both support thinking mode, but they surface reasoning content in choices[0].delta.reasoning_content — a field the OpenAI SDK does not define. You can still read it from the raw response, but typed SDK access requires casting or extension.
  • Usage in stream. OpenAI added stream_options: {"include_usage": true} to return token counts in the final streamed chunk. DashScope supports it; some SiliconFlow models omit usage from streamed responses entirely.
  • Stop reason granularity. Kimi sometimes returns stop reasons that do not map to OpenAI's enum ("length", "stop", "tool_calls", "content_filter"). The OpenAI SDK silently accepts unknown values, but downstream code that switches on finish_reason may miss cases.

Tool Calling Schema Variations

Tool calling is where compatibility gets tested hardest.

  • Parallel tool calls. OpenAI defaults to parallel_tool_calls: true. DeepSeek follows this. DashScope defaults to sequential (one tool call per response) unless you explicitly pass the parallel parameter.
  • Tool choice enforcement. tool_choice: "required" forces the model to call a tool. DeepSeek and DashScope support it. SiliconFlow support depends on the underlying model — some hosted models ignore tool_choice entirely.
  • Function name constraints. OpenAI allows function names with hyphens and dots. Some providers reject names containing dots or limit name length differently. This breaks when you define tools programmatically from existing function signatures.

Structured Output Support

OpenAI's response_format: { type: "json_schema", json_schema: {...} } is the gold standard for guaranteed-schema responses. Provider support varies:

  • DeepSeek: Full support — passes json_schema through to the model and enforces it.
  • DashScope: Full support via the OpenAI-compatible endpoint for Qwen models that support it.
  • SiliconFlow: Varies by hosted model. Open-source models may support response_format: { type: "json_object" } but not full json_schema enforcement.
  • Kimi: Supports json_object mode. Full json_schema support depends on model version.

When to Use the Provider's Native SDK Instead

The OpenAI SDK compatibility layer covers the common case. The native SDK is worth the portability cost when you need features that sit outside the OpenAI API shape.

Anthropic: Extended Thinking and Structured Output

Anthropic's Messages API has a different shape from OpenAI's Chat Completions. The native anthropic SDK gives you:

  • Extended thinking blocks — thinking content blocks with a budget_tokens parameter. No OpenAI-compatible equivalent exists.
  • Native structured output — Anthropic's structured output uses tool definitions differently from OpenAI's json_schema approach.
  • Prompt caching — anthropic-beta: prompt-caching-2024-07-31 header control, with native SDK support for cache breakpoints.
  • Streaming event types — message_start, content_block_start, content_block_delta — a fundamentally different streaming model from OpenAI's chat.completion.chunk.

If your application needs extended thinking or uses Anthropic-specific features heavily, the native SDK is the right call.

DashScope: Async Tasks and Non-Chat Endpoints

DashScope's native dashscope SDK exposes:

  • Async task submission — dashscope.Generation.call(result_format='message', ...) with async polling for long-running requests.
  • Non-chat endpoints — embeddings, reranking, image generation, and audio — not all available through the OpenAI-compatible surface.
  • Qwen-specific parameters — enable_search for built-in web search, incremental_output for streaming behavior control.

The OpenAI-compatible endpoint covers chat, but if you need the full DashScope platform, the native SDK is more complete.

Google: Multimodal and Grounding

Google's google-genai SDK gives you:

  • Native multimodal input — PDF, video, and audio as first-class input types, not just images.
  • Grounding with Google Search — built-in web grounding with attribution.
  • Code execution — sandbox code execution as a tool type.
  • Context caching — explicit cache creation and reuse.

Google AI Studio does offer an OpenAI-compatible endpoint for basic chat, but the rich multimodal and grounding features require the native SDK.

When Direct HTTP Makes Sense

Direct HTTP is not the default choice, but it is the right one in specific scenarios.

You are building in a language without a mature SDK. Rust, Go, and C++ have community OpenAI SDK wrappers, but they lag behind the official Python and TypeScript SDKs. Direct HTTP gives you day-one access to new features.

You need non-standard HTTP behavior. Custom proxy chains, mutual TLS, request signing, or latency-sensitive connection pooling — the SDK's httpx (Python) or fetch (TypeScript) client may not expose the controls you need.

You are routing requests yourself. If you already have an HTTP middleware layer (like TheRouter), the SDK adds a layer of abstraction you do not need. Your router receives raw HTTP, makes routing decisions, and forwards raw HTTP. No SDK needed on either side.

You need to hit a beta endpoint. Providers ship new endpoints before SDKs add support. Direct HTTP lets you call them immediately.

Decision Matrix: Picking Your Integration Pattern

Your situationPick thisWhy
Single provider, standard chat/completionOpenAI SDK (native or with base_url)Simplest setup, good typing, handles retries
Multi-provider fallback or routingOpenAI SDK with configurable base_urlSwap providers by changing two config values
Need provider-specific features (thinking, grounding, async tasks)Native SDK for that providerFeatures outside the OpenAI-compatible surface require native access
Building a routing layer or proxyDirect HTTPNo SDK overhead, full control over request/response lifecycle
Language without a mature SDKDirect HTTPCommunity SDK wrappers lag; HTTP is universal
Prototyping across multiple providersOpenAI SDKFastest way to test the same prompt across providers
Production pipeline with strict schema controlOpenAI SDK + provider-specific fallbackUse OpenAI SDK for the common path, native SDK for edge cases

Code Examples: Same Task, Three Patterns

Here is the same task — a streaming chat completion with tool calling — implemented three ways against DeepSeek's API.

OpenAI SDK

from openai import OpenAI

client = OpenAI(
    api_key="sk-...",
    base_url="https://api.deepseek.com",
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"},
                },
                "required": ["location"],
            },
        },
    }
]

stream = client.chat.completions.create(
    model="deepseek-v4-flash",
    messages=[{"role": "user", "content": "What is the weather in Tokyo?"}],
    tools=tools,
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.tool_calls:
        for tc in delta.tool_calls:
            print(f"Tool call: {tc.function.name}({tc.function.arguments})")
    elif delta.content:
        print(delta.content, end="")

Direct HTTP

import httpx
import json

url = "https://api.deepseek.com/chat/completions"
headers = {
    "Authorization": "Bearer sk-...",
    "Content-Type": "application/json",
}
payload = {
    "model": "deepseek-v4-flash",
    "messages": [{"role": "user", "content": "What is the weather in Tokyo?"}],
    "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get the current weather",
                "parameters": {
                    "type": "object",
                    "properties": {"location": {"type": "string"}},
                    "required": ["location"],
                },
            },
        }
    ],
    "stream": True,
}

with httpx.stream("POST", url, headers=headers, json=payload) as resp:
    for line in resp.iter_lines():
        if line.startswith("data: ") and line != "data: [DONE]":
            chunk = json.loads(line[6:])
            delta = chunk["choices"][0]["delta"]
            if "tool_calls" in delta:
                for tc in delta["tool_calls"]:
                    fn = tc.get("function", {})
                    print(f"Tool call: {fn.get('name', '')}({fn.get('arguments', '')})")
            elif "content" in delta and delta["content"]:
                print(delta["content"], end="")

TypeScript (Vercel AI SDK)

import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";

const result = streamText({
  // Point at DeepSeek via custom base URL
  model: openai("deepseek-v4-flash", {
    baseURL: "https://api.deepseek.com",
    apiKey: "sk-...",
  }),
  messages: [{ role: "user", content: "What is the weather in Tokyo?" }],
  tools: {
    getWeather: tool({
      description: "Get the current weather",
      parameters: z.object({ location: z.string() }),
    }),
  },
});

for await (const part of result.fullStream) {
  if (part.type === "tool-call") {
    console.log(`Tool call: ${part.toolName}(${JSON.stringify(part.args)})`);
  } else if (part.type === "text-delta") {
    process.stdout.write(part.textDelta);
  }
}

How Routing Layers Benefit from OpenAI SDK Standardization

The reason so many providers adopted the OpenAI API shape is not brand loyalty — it is network effects. Tools, frameworks, and routing layers that speak the OpenAI protocol can plug into any compatible provider without per-provider adapters.

For TheRouter specifically, OpenAI SDK standardization means we can route requests across configured providers without rewriting the request body. A /v1/chat/completions request arrives, our routing logic picks a provider based on the configured rules, and the request forwards with minimal transformation. The response comes back in the same shape regardless of which provider served it.

This only works because the core contract — request schema, response schema, streaming format — is shared. When a provider diverges (Anthropic's Messages API, Google's Gemini API), the routing layer needs a translation layer for each divergent provider. That translation layer is where bugs hide.

Production Checklist: Choosing Your Integration Pattern

Before you commit to a pattern, run through this checklist.

  1. List every provider you need today and may need in 6 months. If the answer is "only OpenAI" or "only Anthropic," use their native SDK. If you need two or more OpenAI-compatible providers, the OpenAI SDK with configurable base_url pays for itself immediately.

  2. List every feature you need beyond basic chat. Thinking mode, structured output, vision, file upload, embeddings, async tasks. Check the compatibility table above. If a critical feature sits outside the OpenAI-compatible surface, plan for a native SDK fallback for that specific call.

  3. Decide how you handle provider failures. If your answer is "retry the same provider," any pattern works. If your answer is "fail over to a different provider," you need a shared interface — which means OpenAI SDK or a routing layer.

  4. Check your language ecosystem. Python and TypeScript have mature OpenAI SDKs. Java, Go, Rust, and C++ have community wrappers with varying maturity. If your SDK is immature, direct HTTP may be more reliable than a half-maintained wrapper.

  5. Test the actual compatibility. Do not trust the provider's "OpenAI-compatible" label. Send your actual tool definitions, your actual streaming handler, your actual structured output schema. The gaps show up in your specific usage, not in hello-world examples.

  6. Plan for the features you cannot reach. If you chose the OpenAI SDK for portability but need Anthropic's extended thinking for one workflow, build a thin adapter for that specific call rather than converting your entire codebase to the native SDK.

FAQ

Can I use the OpenAI SDK to call Anthropic's API?

Not directly. Anthropic's Messages API has a different request/response shape. Some proxy services and routing layers (including TheRouter when configured with Anthropic as a provider) translate between the OpenAI and Anthropic formats, but the native OpenAI SDK cannot hit api.anthropic.com with a base_url swap alone.

Does changing base_url affect retry and timeout behavior?

No. The OpenAI SDK's retry logic, timeout settings, and connection pooling apply regardless of which base_url you point at. The provider's server-side rate limits still apply — the SDK just retries on 429 and 5xx as configured.

What happens when a provider adds a new parameter that the OpenAI SDK does not support?

You can pass unknown parameters via extra_body in the Python SDK or body in the TypeScript SDK. The SDK will include them in the request without validation. This is how you access provider-specific features (like DashScope's enable_search) through the OpenAI SDK without waiting for an SDK update.

Is the Vercel AI SDK a fourth pattern?

It is an abstraction layer on top of pattern 1 and pattern 2. The Vercel AI SDK (ai package) provides a unified streamText/generateText interface with provider-specific adapters. It is useful for frontend-heavy TypeScript applications but adds another layer of abstraction. Under the hood, each adapter calls the provider's API — usually via the OpenAI-compatible path or the native SDK.

Should I use LiteLLM instead of managing base_url myself?

LiteLLM is a Python proxy that normalizes 100+ provider APIs into the OpenAI format. It solves the same problem as managing base_url but adds a dependency and a translation layer. If you need many providers and do not want to manage the compatibility gaps yourself, LiteLLM or a routing layer like TheRouter is a reasonable choice. If you only use 2-3 OpenAI-compatible providers, managing base_url directly is simpler.


Sources cited in this guide: DeepSeek API documentation (retrieved 2026-08-12), DashScope OpenAI compatibility documentation (retrieved 2026-08-12), SiliconFlow quickstart guide (retrieved 2026-08-12), Kimi API documentation (retrieved 2026-08-12), xAI API documentation (retrieved 2026-08-12), Anthropic Messages API reference (retrieved 2026-08-12), OpenAI Python SDK repository (retrieved 2026-08-12).

Help & contact