← Все статьи

Паттерны интеграции LLM API SDK: OpenAI SDK, нативные SDK и прямой HTTP — кросс-провайдерное руководство

Каждый проект с LLM API начинается с выбора: использовать OpenAI Python SDK с кастомным base_url, нативный SDK провайдера или работать с HTTP напрямую. Разбираем, когда какой паттерн работает, когда ломается, и как routing-слои вроде TheRouter выигрывают от стандартизации OpenAI SDK.

· TheRouter

Любая интеграция с LLM API начинается с одного и того же выбора. У вас есть API key, название модели и задача. Дальше нужно решить, как код будет общаться с провайдером. Три варианта — OpenAI Python/TypeScript SDK с кастомным base_url, нативный SDK провайдера или прямые HTTP-запросы — в первый день выглядят взаимозаменяемо. Различия появляются, когда вам нужен streaming, tool calling, structured output или fallback между провайдерами.

Мы строили TheRouter на совместимости с OpenAI SDK, потому что это ближайший аналог универсального адаптера в экосистеме LLM API. Но мы также наступили на каждый edge case, где совместимость ломается. Это руководство фиксирует наш опыт: какой паттерн выбирать, чем каждый из них обходится, и где прячутся разрывы в совместимости.

OpenAI-совместимость означает, что провайдер предоставляет endpoint chat-completions, чей контракт запроса и ответа достаточно близок к API OpenAI, чтобы немодифицированный вызов OpenAI SDK работал после замены трёх значений: API key, base URL, название модели. Минимальная поверхность на практике —POST /v1/chat/completions с messages, model и потоковым ответом в форме OpenAI.

Три паттерна интеграции

Прежде чем сравнивать провайдеров, разберёмся, что каждый паттерн означает на уровне кода.

Паттерн 1: OpenAI SDK с кастомным base_url

Устанавливаете пакет openai, меняете две строки — api_key и base_url. Остальной код одинаковый независимо от того, какой провайдер на другом конце.

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",  # меняем название модели под провайдера
    messages=[{"role": "user", "content": "Hello"}],
)

Что получаете: одна зависимость, один интерфейс, переносимый код между всеми провайдерами, поддерживающими контракт /v1/chat/completions.

Что теряете: всё, что провайдер предлагает за пределами OpenAI-совместимой поверхности. Асинхронный API задач DashScope, extended thinking blocks Anthropic, загрузка файлов через чат у Kimi — ничего из этого нет в типовой системе OpenAI SDK.

Паттерн 2: Нативный SDK провайдера

Каждый крупный провайдер поставляет собственный SDK. У Anthropic — anthropic, у Google — google-genai, у DashScope — dashscope, у Volcengine — 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"}],
)

Что получаете: полный доступ ко всем функциям провайдера. Типизированные параметры для провайдер-специфичных полей. Более точные типы ошибок. Документация, которая совпадает с вашим реальным endpoint.

Что теряете: переносимость. Переход с Anthropic на DeepSeek означает переписывание каждого вызова. Ваш streaming handler, парсер tool calling, логика ретраев — всё привязано к конкретному провайдеру.

Паттерн 3: Прямой HTTP

Без SDK. Формируете запрос сами, парсите ответ сами.

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"])

Что получаете: ноль зависимостей. Полный контроль над HTTP-поведением — connection pooling, proxy, кастомные заголовки, тайминг ретраев. Работает на любом языке без ожидания релиза SDK.

Что теряете: типобезопасность. Парсинг SSE для streaming. Автоматические ретраи при transient-ошибках. Каждый байт интеграционного кода — ваша ответственность.

Какие провайдеры поддерживают совместимость с OpenAI SDK?

Не все «OpenAI-совместимые» endpoint одинаково совместимы. Вот текущее состояние по крупным провайдерам, проверенное по официальной документации на август 2026 года.

ПровайдерBase URLChat CompletionsStreamingTool CallingStructured OutputVisionЗагрузка файлов
DeepSeekhttps://api.deepseek.comПолнаяПолнаяПолнаяПолнаяПолнаяЧерез messages
DashScopehttps://dashscope.aliyuncs.com/compatible-mode/v1ПолнаяПолнаяПолнаяПолнаяПолнаяЧерез messages
SiliconFlowhttps://api.siliconflow.cn/v1ПолнаяПолнаяПолнаяЧастичнаяЗависит от моделиЧерез messages
Kimi/Moonshothttps://api.moonshot.ai/v1ПолнаяПолнаяПолнаяЧастичнаяПолная (K2+)Через messages
xAI (Grok)https://api.x.ai/v1ПолнаяПолнаяПолнаяПолнаяПолнаяЧерез messages
AnthropicН/Д (другая структура API)Через адаптерДругой форматДругая schemaСвой форматПолная (нативно)Свой формат
Google (Gemini)Только AI StudioТолько AI StudioДругой форматДругая schemaСвой форматПолная (нативно)Свой формат

Источники: документация DeepSeek API (данные на 2026-08-12), документация DashScope по совместимости с OpenAI (данные на 2026-08-12), руководство SiliconFlow quickstart (данные на 2026-08-12), документация Kimi API (данные на 2026-08-12), документация xAI API (данные на 2026-08-12).

Картина ясна. Китайские провайдеры (DeepSeek, DashScope, SiliconFlow, Kimi) и xAI приняли форму API OpenAI как свой основной интерфейс. Anthropic и Google построили собственные API-контракты и предлагают совместимость только через адаптерные слои или ограниченные endpoint.

Где совместимость OpenAI SDK ломается

«Совместимый» не значит «идентичный». Вот реальные расхождения, с которыми мы столкнулись.

Различия формата streaming delta

Большинство провайдеров совпадают с SSE-форматом OpenAI для chat.completions.chunk, но edge cases расходятся.

Thinking/reasoning токены. DeepSeek и DashScope поддерживают thinking mode, но выводят reasoning-контент в choices[0].delta.reasoning_content — поле, которое OpenAI SDK не определяет. Прочитать его из сырого ответа можно, но типизированный доступ через SDK требует приведения типов.

Usage в stream. OpenAI добавил stream_options: {"include_usage": true} для возврата подсчёта токенов в последнем streaming chunk. DashScope поддерживает. Некоторые модели SiliconFlow в streaming-ответах usage вообще не возвращают.

Гранулярность stop reason. Kimi иногда возвращает stop reason, которых нет в enum OpenAI ("length", "stop", "tool_calls", "content_filter"). OpenAI SDK молча принимает неизвестные значения, но код, который делает switch по finish_reason, может пропустить ветку.

Различия schema в tool calling

Tool calling — это место, где совместимость испытывается сильнее всего.

Параллельные tool calls. OpenAI по умолчанию ставит parallel_tool_calls: true. DeepSeek следует этому. DashScope по умолчанию выполняет последовательно (один tool call на ответ), если не передать параметр параллельности явно.

Принудительный tool choice. tool_choice: "required" заставляет модель вызвать инструмент. DeepSeek и DashScope поддерживают. У SiliconFlow поддержка зависит от конкретной модели — некоторые hosted-модели игнорируют tool_choice полностью.

Ограничения на имена функций. OpenAI разрешает дефисы и точки в именах функций. Часть провайдеров отклоняет имена с точками или ограничивает длину по-своему. Это ломается, когда tool definitions генерируются из существующих сигнатур функций.

Поддержка structured output

response_format: { type: "json_schema", json_schema: {...} } от OpenAI — эталон для гарантированного соответствия ответов заданной schema. Поддержка у провайдеров разная.

DeepSeek поддерживает полностью — передаёт json_schema модели и принуждает соблюдать. DashScope полностью поддерживает через OpenAI-совместимый endpoint для моделей Qwen, которые это умеют. SiliconFlow зависит от hosted-модели — open-source модели могут поддерживать response_format: { type: "json_object" }, но не полное принуждение json_schema. Kimi поддерживает режим json_object, полная поддержка json_schema зависит от версии модели.

Когда использовать нативный SDK провайдера

Слой совместимости OpenAI SDK покрывает типовой случай. Нативный SDK оправдывает потерю переносимости, когда нужные функции выходят за рамки формы API OpenAI.

Anthropic: Extended Thinking и structured output

Messages API от Anthropic имеет другую форму, чем Chat Completions от OpenAI. Нативный SDK anthropic даёт:

  • Extended thinking blocks — контент-блоки thinking с параметром budget_tokens. В OpenAI-совместимом слое эквивалента нет.
  • Нативный structured output — structured output Anthropic использует tool definitions иначе, чем подход json_schema у OpenAI.
  • Prompt caching — управление через заголовок anthropic-beta: prompt-caching-2024-07-31, с нативной поддержкой cache breakpoints в SDK.
  • Типы streaming-событий — message_start, content_block_start, content_block_delta — принципиально другая модель streaming по сравнению с chat.completion.chunk OpenAI.

DashScope: асинхронные задачи и не-chat endpoint

Нативный SDK dashscope открывает:

  • Отправку асинхронных задач — dashscope.Generation.call(result_format='message', ...) с polling для долгих запросов.
  • Не-chat endpoint — embeddings, reranking, генерация изображений, аудио — не всё доступно через OpenAI-совместимую поверхность.
  • Qwen-специфичные параметры — enable_search для встроенного веб-поиска, incremental_output для управления streaming-поведением.

Google: мультимодальность и grounding

SDK google-genai даёт:

  • Нативный мультимодальный ввод — PDF, видео и аудио как входные типы первого класса, а не только изображения.
  • Grounding с Google Search — встроенное веб-дополнение с атрибуцией.
  • Исполнение кода — sandbox code execution как тип инструмента.
  • Кэширование контекста — явное создание и повторное использование кэша.

Когда прямой HTTP имеет смысл

Прямой HTTP — не дефолтный выбор, но в ряде сценариев он оптимален.

Язык без зрелого SDK. У Rust, Go и C++ есть community-обёртки для OpenAI SDK, но они отстают от официальных Python и TypeScript SDK. Прямой HTTP даёт доступ к новым функциям с первого дня.

Нестандартное HTTP-поведение. Кастомные proxy-цепочки, mutual TLS, подписание запросов или latency-чувствительное управление connection pool — httpx (Python) или fetch (TypeScript) внутри SDK могут не открывать нужные вам рычаги.

Вы сами маршрутизируете запросы. Если у вас уже есть HTTP-middleware (вроде TheRouter), SDK добавляет уровень абстракции, который вам не нужен. Router принимает сырой HTTP, принимает решение о маршрутизации, перенаправляет сырой HTTP. SDK не нужен ни на одной стороне.

Beta endpoint. Провайдеры выпускают новые endpoint раньше, чем SDK получает поддержку. Прямой HTTP позволяет вызвать их немедленно.

Матрица принятия решений

Ваша ситуацияВыбирайтеПочему
Один провайдер, стандартный chat/completionOpenAI SDK (нативный или с base_url)Простейшая настройка, хорошая типизация, ретраи из коробки
Multi-provider fallback или routingOpenAI SDK с настраиваемым base_urlСмена провайдера — изменение двух строк конфига
Нужны провайдер-специфичные фичи (thinking, grounding, async tasks)Нативный SDK этого провайдераФункции за пределами OpenAI-совместимой поверхности доступны только через нативный SDK
Строите routing layer или proxyПрямой HTTPБез оверхеда SDK, полный контроль над жизненным циклом запроса/ответа
Язык без зрелого SDKПрямой HTTPCommunity-обёртки отстают; HTTP универсален
Прототипируете на нескольких провайдерахOpenAI SDKБыстрейший способ протестировать один prompt на разных провайдерах
Production pipeline с жёстким контролем schemaOpenAI SDK + провайдер-специфичный fallbackОбщий путь через OpenAI SDK, edge cases через нативный SDK

Примеры кода: одна задача, три реализации

Одна и та же задача — streaming chat completion с tool calling — реализована тремя способами через API DeepSeek.

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="")

Прямой 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({
  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);
  }
}

Как routing layers выигрывают от стандартизации OpenAI SDK

Столько провайдеров приняли форму API OpenAI не из-за лояльности к бренду, а из-за сетевого эффекта. Инструменты, фреймворки и routing layers, говорящие на протоколе OpenAI, подключаются к любому совместимому провайдеру без провайдер-специфичных адаптеров.

Для TheRouter стандартизация OpenAI SDK означает, что мы маршрутизируем запросы между настроенными провайдерами без переписывания тела запроса. Запрос /v1/chat/completions приходит, routing-логика выбирает провайдера по настроенным правилам, запрос перенаправляется с минимальными изменениями. Ответ приходит в одинаковой форме, независимо от того, какой провайдер его обслужил.

Это работает только потому, что ядро контракта — schema запроса, schema ответа, формат streaming — общее. Когда провайдер отклоняется (Messages API Anthropic, Gemini API Google), routing layer нуждается в translation layer для каждого отклоняющегося провайдера. Именно в таких translation layers прячутся баги.

Чеклист для production-выбора

Прежде чем фиксировать паттерн, пройдитесь по этому чеклисту.

  1. Перечислите всех провайдеров, которые нужны сегодня и могут понадобиться через 6 месяцев. Если ответ «только OpenAI» или «только Anthropic» — используйте их нативный SDK. Если нужны два или больше OpenAI-совместимых провайдера, OpenAI SDK с настраиваемым base_url окупается сразу.

  2. Перечислите все функции, нужные за пределами базового chat. Thinking mode, structured output, vision, загрузка файлов, embeddings, асинхронные задачи. Сверьтесь с таблицей совместимости выше. Если критическая функция за пределами OpenAI-совместимой поверхности — планируйте fallback на нативный SDK для конкретного вызова.

  3. Решите, как обрабатывать отказы провайдера. Если ваш ответ «ретраить того же провайдера» — подойдёт любой паттерн. Если «переключиться на другого» — нужен единый интерфейс, то есть OpenAI SDK или routing layer.

  4. Проверьте экосистему вашего языка. Python и TypeScript имеют зрелые OpenAI SDK. Java, Go, Rust, C++ имеют community-обёртки разной зрелости. Если ваш SDK незрелый, прямой HTTP может быть надёжнее полу-поддерживаемой обёртки.

  5. Протестируйте реальную совместимость. Не доверяйте лейблу «OpenAI-compatible» провайдера. Отправьте ваши реальные tool definitions, ваш реальный streaming handler, вашу реальную structured output schema. Разрывы совместимости проявляются в вашем конкретном использовании, не в hello-world примерах.

  6. Запланируйте обход недоступных функций. Если вы выбрали OpenAI SDK для переносимости, но одному workflow нужен extended thinking от Anthropic — напишите тонкий адаптер для этого конкретного вызова, а не конвертируйте всю кодовую базу на нативный SDK.

FAQ

Можно ли использовать OpenAI SDK для вызова API Anthropic?

Не напрямую. Messages API Anthropic имеет другую форму запроса/ответа. Некоторые proxy-сервисы и routing layers (включая TheRouter при настройке Anthropic как провайдера) транслируют между форматами OpenAI и Anthropic, но нативный OpenAI SDK не может обратиться к api.anthropic.com простой сменой base_url.

Влияет ли смена base_url на поведение ретраев и таймаутов?

Нет. Логика ретраев, настройки таймаутов и connection pooling OpenAI SDK применяются независимо от base_url. Серверные rate limits провайдера по-прежнему действуют — SDK просто ретраит при 429 и 5xx как настроено.

Что если провайдер добавил параметр, которого нет в OpenAI SDK?

Передавайте через extra_body в Python SDK или body в TypeScript SDK. SDK включит их в тело запроса без валидации. Так вы получаете доступ к провайдер-специфичным функциям (например, enable_search у DashScope) через OpenAI SDK, не дожидаясь обновления SDK.

Vercel AI SDK — это четвёртый паттерн?

Это уровень абстракции поверх паттерна 1 и паттерна 2. Vercel AI SDK (пакет ai) предоставляет единый интерфейс streamText/generateText с адаптерами под провайдеров. Полезен для frontend-ориентированных TypeScript-приложений, но добавляет ещё один уровень абстракции. Под капотом каждый адаптер вызывает API провайдера — обычно через OpenAI-совместимый путь или нативный SDK.

Стоит ли использовать LiteLLM вместо самостоятельного управления base_url?

LiteLLM — Python-прокси, нормализующий API 100+ провайдеров в формат OpenAI. Решает ту же проблему, что и управление base_url, но добавляет зависимость и translation layer. Если нужно много провайдеров и не хотите сами разбираться с разрывами совместимости, LiteLLM или routing layer вроде TheRouter — разумный выбор. Если используете 2–3 OpenAI-совместимых провайдера, управление base_url напрямую проще.


Источники, цитируемые в этом руководстве: документация DeepSeek API (данные на 2026-08-12), документация DashScope по совместимости с OpenAI (данные на 2026-08-12), руководство SiliconFlow quickstart (данные на 2026-08-12), документация Kimi API (данные на 2026-08-12), документация xAI API (данные на 2026-08-12), справочник Anthropic Messages API (данные на 2026-08-12), репозиторий OpenAI Python SDK (данные на 2026-08-12).

Помощь и контакты