OpenAI Agents SDK over our /v1/responses endpoint
The Agents SDK talks to OpenAI by default. Two client settings redirect it through our gateway onto the models in our catalog.
4 min · · Updated · агенты · development
The OpenAI Agents SDK (the openai-agents package for Python, and its JS counterpart) is built on top of the Responses API — the same POST /v1/responses our gateway exposes. An "agent" inside the library is really just a loop of client.responses.create() calls with tool calls and handoffs layered on top. Swap the underlying client for a custom base_url and key, and the whole loop routes through the gateway without touching any agent logic.
Minimal Python setup
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_your_key",
)
set_default_openai_client(client)
agent = Agent(
name="Support Triage",
instructions="Answer concisely, escalate complex cases.",
model="gpt-5.6-sol",
)
result = Runner.run_sync(agent, "A customer says their March invoice never arrived")
print(result.final_output)Two things matter here: base_url ends in /v1 (that's the OpenAI-compatible path), and set_default_openai_client runs before any agent is created — call it after, and part of the SDK's internal clients may already be cached against the default https://api.openai.com.
Model selection is per-agent
The SDK doesn't validate model names ahead of time — the string just goes straight into the request body. That means any model in our catalog works:
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="...")Different agents in the same pipeline can freely use different models — a handoff between them doesn't need a new client, because there's exactly one client for the whole app.
Tools and function calling
/v1/responses accepts tools in the request body — the SDK builds them automatically from decorated Python functions:
from agents import function_tool
@function_tool
def get_invoice_status(invoice_id: str) -> str:
"""Returns the status of an invoice by its ID."""
return db.lookup(invoice_id)
agent = Agent(
name="Billing",
model="gpt-5.6-sol",
tools=[get_invoice_status],
instructions="Use get_invoice_status to check invoices.",
)The function-calling format in the Responses API is compatible with what our text models generate through the gateway regardless of vendor — GPT, GLM, and Claude alike — because /v1/responses is a single entry point independent of which provider actually serves the model.
Streaming and handoffs
result = Runner.run_streamed(agent, "Draft a report on the last three invoices")
async for event in result.stream_events():
if event.type == "raw_response_event":
print(event.data, end="", flush=True)Streaming behaves exactly like it does against official OpenAI, because the response protocol doesn't change — only the destination address does. Handoffs between agents (handoffs=[billing_agent, support_agent]) need no extra work either: the SDK decides who takes over the conversation based on agent instructions, not on who hosts the model.
Idempotency across multi-step chains
Agent pipelines often retry a step after a network timeout — and without protection that duplicates the charge. If you wrap calls in your own retry logic, add an Idempotency-Key through extra_headers on the specific .create() call — unique per logical request and identical across its retries. Don't put it in the client's default_headers: there's one client for the whole app, and the very next request with a different body gets 422 idempotency_conflict.
import uuid
from openai import APITimeoutError
idem_key = str(uuid.uuid4()) # one per logical request; the same on retry
for attempt in range(3):
try:
resp = await client.responses.create(
model="gpt-5.6-sol",
input="Draft a report on the last three invoices",
extra_headers={"Idempotency-Key": idem_key},
)
break
except APITimeoutError:
continueA repeat request with the same key within 24 hours returns the same response without charging again — useful when a step times out client-side after the model has already produced an answer.
Tracing and balance headers
The Agents SDK's built-in tracing tries to send traces to platform.openai.com by default — a separate service unrelated to the gateway, and it won't work without an official OpenAI key. Disable it explicitly if you don't need it:
from agents import set_tracing_disabled
set_tracing_disabled(True)Otherwise your logs fill with constant 401s from the tracer — unrelated to whether /v1/responses itself is working, and easy to mistake for a gateway problem.
For cost debugging, watch the response headers X-Zukko-Cost-Micros (the cost of that call in micro-dollars) and X-Fetch-Balance-Cents (balance remaining after the charge — despite the name, also in micro-dollars, 1,000,000 = $1) — they arrive on every non-streaming responses.create() call, including intermediate steps inside a single Runner.run(). Streamed responses (Runner.run_streamed, stream: true) don't carry them — check spend there in the dashboard, GET /v1/usage or GET /v1/balance.
Common mistakes
Every call returns 404 — the base_url is missing /v1, or has an extra one (the Agents SDK doesn't append the path itself, so it must be exactly https://api.zukko.pro/v1).
The model answers but the handoff never fires — that's not a gateway issue, it's SDK logic: a handoff depends on how the model phrases its tool call, not on which provider serves that model.
402 mid-chain across several agents — balance is checked on every individual /v1/responses call, so an agent can complete three of five steps and fail on the fourth. Check the remaining balance ahead of time with GET /v1/balance using the same key.
The model list and current limits are available without a key at GET /v1/models/info.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Claude Code YOLO: Permission Modes and Working Without PromptsClaude Code has no mode literally called YOLO, but it has several permission modes and flexible rules. How to stop clicking Yes without losing files.
- Claude Code: a checklist of best practices for daily workHabits that make working with Claude Code predictable: a short CLAUDE.md, a plan before edits, small tasks, checking results and keeping spend under control.
- An MCP server in Java: a minimal example with one toolWe write a minimal MCP server in Java with one tool, register it in Claude Code, and learn to find the cause when the agent doesn't see it.