OpenAI Agents SDK через наш /v1/responses

Agents SDK по умолчанию ходит в OpenAI. Меняем два параметра клиента — и агенты работают через наш шлюз на моделях из каталога.

4 мин · · Обновлено · агенты · разработка

OpenAI Agents SDK (пакет openai-agents для Python и его JS-аналог) построен поверх Responses API — того самого POST /v1/responses, который есть и у нас. Агент внутри библиотеки — это просто цикл вызовов client.responses.create() с добавлением tool calls и хендоффов между агентами. Если подменить клиента на кастомный base_url и ключ, весь этот цикл поедет через шлюз без переписывания логики агентов.

Минимальная настройка на Python

bash
from agents import Agent, Runner, set_default_openai_client
from openai import AsyncOpenAI

client = AsyncOpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key="fetch_ваш_ключ",
)
set_default_openai_client(client)

agent = Agent(
    name="Support Triage",
    instructions="Отвечай кратко, по-русски, эскалируй сложные случаи.",
    model="gpt-5.6-sol",
)

result = Runner.run_sync(agent, "Клиент пишет, что не приходит счёт за март")
print(result.final_output)

Два момента здесь решают всё: base_url заканчивается на /v1 (это OpenAI-совместимый путь), и set_default_openai_client подставляется до создания агента — если вызвать после, часть внутренних клиентов SDK успеет закэшироваться на дефолтный https://api.openai.com.

Модель можно назначать per-agent

SDK не проверяет имя модели заранее — оно летит в тело запроса как строка. Значит любая модель из нашего каталога подходит:

bash
triage_agent = Agent(name="Triage", model="glm-5.3", instructions="...")
writer_agent = Agent(name="Writer", model="claude-sonnet-5", instructions="...")
vision_agent = Agent(name="Vision", model="claude-opus-5-5", instructions="...")

Разные агенты в одном пайплайне спокойно используют разные модели — хендофф между ними не требует пересоздания клиента, потому что клиент один на всё приложение.

Tools и function calling

/v1/responses принимает tools в теле — SDK формирует их сам из декорированных Python-функций:

bash
from agents import function_tool

@function_tool
def get_invoice_status(invoice_id: str) -> str:
    """Возвращает статус счёта по его ID."""
    return db.lookup(invoice_id)

agent = Agent(
    name="Billing",
    model="gpt-5.6-sol",
    tools=[get_invoice_status],
    instructions="Используй get_invoice_status для проверки счетов.",
)

Формат вызова функций в Responses API совместим с тем, что генерируют текстовые модели через шлюз — не только GPT-линейка, но и GLM, и Claude, поскольку у нас /v1/responses — это единая точка входа независимо от провайдера модели.

Streaming и хендоффы

bash
result = Runner.run_streamed(agent, "Составь отчёт по трём последним счетам")
async for event in result.stream_events():
    if event.type == "raw_response_event":
        print(event.data, end="", flush=True)

Стриминг работает так же, как с официальным OpenAI, потому что протокол ответа не меняется — меняется только адрес, куда летит запрос. Хендоффы между агентами (handoffs=[billing_agent, support_agent]) тоже не требуют доработок: SDK сам решает, кому передать разговор, основываясь на инструкциях агента, а не на том, кто хостит модель.

Guardrails и контекст между агентами

SDK поддерживает input_guardrails и output_guardrails — функции-проверки, которые запускаются до или после основного вызова модели и могут прервать выполнение раньше, чем запрос вообще уйдёт на /v1/responses:

bash
from agents import input_guardrail, GuardrailFunctionOutput

@input_guardrail
async def block_pii(ctx, agent, input_text):
    contains_card = "4111 1111" in input_text
    return GuardrailFunctionOutput(
        output_info={"blocked": contains_card},
        tripwire_triggered=contains_card,
    )

agent = Agent(
    name="Billing",
    model="claude-sonnet-5",
    input_guardrails=[block_pii],
)

Это полезно не только для безопасности, но и для экономии: guardrail, сработавший на входе, останавливает цепочку до того, как модель что-либо посчитает, а значит и до списания. Контекст между шагами одного Runner.run() SDK хранит сам, передавать историю сообщений вручную не нужно — она автоматически попадает в следующий вызов /v1/responses внутри той же сессии.

Идемпотентность в многошаговых цепочках

Агентные пайплайны часто ретраят шаг при таймауте сети — и без защиты это дублирует списание. Если оборачиваете вызовы своей обвязкой поверх retry-логики, добавьте Idempotency-Key через extra_headers в конкретном вызове .create() — уникальный для каждого логического запроса и одинаковый при его повторах. Не кладите его в default_headers клиента: клиент один на всё приложение, и уже второй запрос с другим телом получит 422 idempotency_conflict.

bash
import uuid
from openai import APITimeoutError

idem_key = str(uuid.uuid4())  # один на логический запрос; при повторе — тот же
for attempt in range(3):
    try:
        resp = await client.responses.create(
            model="gpt-5.6-sol",
            input="Составь отчёт по трём последним счетам",
            extra_headers={"Idempotency-Key": idem_key},
        )
        break
    except APITimeoutError:
        continue

Повтор с тем же ключом в течение 24 часов вернёт тот же ответ без повторного списания — полезно, если шаг агента упал по таймауту уже после того, как модель отработала.

Трассировка и балансовые заголовки

Встроенный трейсинг Agents SDK по умолчанию пытается отправить трассы на platform.openai.com — это отдельный, не связанный со шлюзом сервис, и он не заработает без официального OpenAI-ключа. Если он не нужен, отключите его явно:

bash
from agents import set_tracing_disabled
set_tracing_disabled(True)

Иначе в логах будут постоянные 401 от трейсинга — они не имеют отношения к работе самих агентов через /v1/responses, и это легко перепутать с реальной проблемой шлюза.

Для отладки стоимости смотрите заголовки ответа X-Zukko-Cost-Micros (цена конкретного вызова в микродолларах) и X-Fetch-Balance-Cents (остаток после списания — несмотря на название, тоже в микродолларах, 1 000 000 = $1) — они приходят на каждый нестриминговый вызов responses.create(), включая промежуточные шаги внутри одного Runner.run(). У потоковых ответов (Runner.run_streamed, stream: true) этих заголовков нет — расход там смотрите в кабинете, GET /v1/usage или GET /v1/balance.

Частые ошибки

404 на каждом вызове — забыт /v1 в base_url, или наоборот, добавлен лишний (Agents SDK сам не подставляет путь, значит указывать нужно ровно https://api.zukko.pro/v1).

Модель отвечает, но хендофф не срабатывает — это не проблема шлюза, а логики самого SDK: хендофф зависит от того, как модель формулирует tool call, а не от того, какой провайдер модель обслуживает.

402 посреди цепочки из нескольких агентов — баланс проверяется на каждом отдельном вызове /v1/responses, значит агент может успеть отработать три шага из пяти и упасть на четвёртом. Проверить остаток заранее: GET /v1/balance с тем же ключом.

Список моделей и их актуальные лимиты — без ключа, на GET /v1/models/info.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.

OpenAI Agents SDK через /v1/responses — настройка · Zukko