Developers / SDKs
SDKs.
Official Python and TypeScript clients for the Dyadic API, or the OpenAI and Anthropic SDKs you already use.
Keep Your Key on a Server
A Dyadic API key is a long-lived secret that spends your credits. Never put it in a web page, a browser extension, a mobile app or anything else you ship to people: anyone can read it there and use it. Call the API from your own server, and let your front end call your server, behind your own sign-in and limits. If a key was exposed, revoke it in the Workspace.
import { createServer } from "node:http";
import { Dyadic } from "dyadic";
const client = new Dyadic({ apiKey: process.env.DYADIC_API_KEY });
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/api/ask") return res.writeHead(404).end();
// Check your own user's session and limits here: this route spends your credits.
let body = "";
for await (const chunk of req) body += chunk;
const { question } = JSON.parse(body || "{}");
const reply = await client.chat.completions.create({
model: "iris",
messages: [{ role: "user", content: String(question ?? "").slice(0, 4000) }],
});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ answer: reply.choices[0].message.content }));
}).listen(3000);const response = await fetch("/api/ask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question }),
});
const { answer } = await response.json();Python
The dyadic package on PyPI: sync and async clients for every endpoint (chat, Responses, embeddings, audio, moderations, images, files, batches), streaming and typed errors. Python 3.9 or newer; its one dependency is httpx.
pip install dyadicfrom dyadic import Dyadic
client = Dyadic() # reads DYADIC_API_KEY
print(client.ask("Give me three names for a podcast about markets.", model="iris"))
reply = client.chat.completions.create(
model="iris",
messages=[{"role": "user", "content": "Summarize today's tech news in three bullets."}],
temperature=0.4,
)
print(reply.choices[0].message.content, reply.dyadic.credits)for chunk in client.chat.completions.create(model="iris", messages=[{"role": "user", "content": "Tell me a story"}], stream=True):
print(chunk.text, end="", flush=True)tools = [{"type": "function", "function": {"name": "get_weather", "parameters": {
"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}]
reply = client.chat.completions.create(model="iris", messages=[{"role": "user", "content": "Weather in Oslo?"}], tools=tools)
for call in reply.choices[0].message.tool_calls or []:
print(call.function.name, call.function.arguments)import asyncio
from dyadic import AsyncDyadic
async def main():
async with AsyncDyadic() as client:
async for chunk in await client.chat.completions.create(model="iris", messages=[{"role": "user", "content": "Hi"}], stream=True):
print(chunk.text, end="")
image = await client.images.generate("A neon city at dusk, isometric")
image.save("city.png")
asyncio.run(main())def lookup_order(order_id):
"""Look up an order by id."""
return {"order_id": order_id, "status": "shipped"}
lookup_order.schema = {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}
run = client.runs.run(
agent={"id": "helpdesk", "name": "Helpdesk", "instructions": "You answer customers of a small shop."},
messages=[{"role": "user", "content": "Where is order 1042?"}],
builtin_tools=["data.web_search", "data.web_fetch"],
functions={"lookup_order": lookup_order},
end_user="customer-17",
max_cost_usd=0.05,
)
print(run.output_text, run.cost_usd)# The key's Plans switch. A plan waits for approval; the server enforces it.
proposal = client.runs.create(
agent="iris",
messages=[{"role": "user", "content": "Move our blog from WordPress to a static site."}],
plan_mode=True, # investigate and propose: nothing that changes anything runs
conversation_id="ticket-88",
)
plan = client.plans.retrieve(proposal.plans[0].id)
for step in plan.steps:
print(step.key, step.title, step.status)
plan = client.plans.approve(plan.id, revision=plan.revision) # the revision your user reviewed
for event in client.runs.create(agent="iris", messages=[{"role": "user", "content": "Go ahead."}],
plan_id=plan.id, conversation_id="ticket-88", stream=True):
if event.type == "plan.updated":
print(event.plan.progress.percent, "% done")
# Anywhere else: follow it live (it resumes after the last event it saw, and ends once the plan is finished)
for event in client.plans.stream(plan.id):
print(event.event, event.data.get("step"), event.data.get("status"))from dyadic import DyadicError
status = client.connections.discord(discord_id)
if not status.authorized:
print("Allow this app:", status.authorize_url)
try:
saved = client.workspace.saves.create(discord_id=discord_id, kind="message", text="Worth keeping")
print(saved.url)
except DyadicError as error:
if error.code == "authorization_required":
print("Allow this app:", error.body["authorize_url"])reply = client.responses.create(model="iris", input="Name three moons of Jupiter.")
print(reply.text)
follow = client.responses.create(model="iris", input="Which is largest?", previous_response_id=reply.id)
print(follow.text)vectors = client.embeddings.create(model="text-embedding-3-small", input=["first text", "second text"])
client.audio.speech.create(model="tts-1", voice="alloy", input="Hello from Dyadic.").save("hello.mp3")
print(client.audio.transcriptions.create(file="hello.mp3", model="whisper-1").text)
print(client.moderations.create(input="I love baking bread.").results[0].flagged)batch_file = client.files.create(file="requests.jsonl", purpose="batch")
batch = client.batches.create(input_file_id=batch_file.id, endpoint="/v1/chat/completions")
batch = client.batches.retrieve(batch.id) # poll until status is completed
print(client.files.content(batch.output_file_id).text)from dyadic.blackbox import verify_bundle
recorder = client.blackbox.recorder()
with recorder.run("Refund order 1042", agent="support-bot") as run:
with run.tool_call("refund", kind="external_api", effect=True) as call:
call.result({"refunded": True})
recorder.flush()
for event in client.blackbox.runs.stream(run.id): # follows a run live, reconnecting as needed
print(event.event, event.data.get("type"))
for finding in client.blackbox.findings(run_id=run.id).findings: # hypotheses, with their evidence
client.blackbox.findings.update(finding.id, status="confirmed", note="reviewed")
print(verify_bundle(client.blackbox.runs.export(run.id))["ok"]) # evidence anyone can check offlinemessage = client.anthropic.messages.create(model="iris", max_tokens=1024, system="Be brief.",
messages=[{"role": "user", "content": "Hello"}])
print(message.text, message.stop_reason, message.usage.output_tokens) # Anthropic's answer; .text joins its text blocks
stream = client.anthropic.messages.create(model="iris", max_tokens=1024, stream=True,
messages=[{"role": "user", "content": "Tell me a story"}])
for event in stream: # Anthropic's events: message_start, content_block_delta, ...
if event.type == "content_block_delta" and event.delta.type == "text_delta":
print(event.delta.text, end="")
batch = client.anthropic.messages.batches.create(requests=[{"custom_id": "a", "params": {
"model": "iris", "max_tokens": 256, "messages": [{"role": "user", "content": "Hi"}]}}])
results = client.anthropic.messages.batches.results(batch.id) # once processing_status is "ended"
# OpenRouter's models through the AI gateway (Dyadic turns it on for an app's key on request):
reply = client.openrouter.chat.completions.create(model="anthropic/claude-haiku-5.5",
messages=[{"role": "user", "content": "Hello"}])
print(client.openrouter.key().data.limit_remaining) # this key's limits, answered by Dyadicfrom dyadic import DyadicError
try:
client.ask("Hello", model="iris")
except DyadicError as error:
print(error.status, error.code, error.message)TypeScript and JavaScript
The dyadic package on npm: no dependencies, typed requests and responses, async-iterable streams. For your server: Node 18+, Deno and Bun. A page in a browser calls your server, never the API with your key.
npm install dyadicimport { Dyadic } from "dyadic";
const client = new Dyadic({ apiKey: process.env.DYADIC_API_KEY });
console.log(await client.ask("Give me three names for a podcast about markets.", { model: "iris" }));
const reply = await client.chat.completions.create({
model: "iris",
messages: [{ role: "user", content: "Summarize today's tech news in three bullets." }],
});
console.log(reply.choices[0].message.content, reply.dyadic?.credits);const stream = await client.chat.completions.create({ model: "iris", messages: [{ role: "user", content: "Tell me a story" }], stream: true });
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? "");const run = await client.runs.run({
agent: { id: "helpdesk", name: "Helpdesk", instructions: "You answer customers of a small shop." },
messages: [{ role: "user", content: "Where is order 1042?" }],
builtin_tools: ["data.web_search", "data.web_fetch"],
functions: {
lookup_order: {
description: "Look up an order by id.",
parameters: { type: "object", properties: { order_id: { type: "string" } }, required: ["order_id"] },
handler: async ({ order_id }) => ({ order_id, status: "shipped" }),
},
},
end_user: "customer-17",
max_cost_usd: 0.05,
});
console.log(run.output_text, run.cost_usd);// The key's Plans switch. A plan waits for approval; the server enforces it.
const proposal = await client.runs.create({
agent: "iris",
messages: [{ role: "user", content: "Move our blog from WordPress to a static site." }],
planMode: true, // investigate and propose: nothing that changes anything runs
conversation_id: "ticket-88",
});
let plan = await client.plans.retrieve(proposal.plans![0].id);
plan = await client.plans.approve(plan.id, { revision: plan.revision }); // the revision your user reviewed
const run = client.runs.create({ agent: "iris", messages: [{ role: "user", content: "Go ahead." }], planId: plan.id, conversation_id: "ticket-88" });
for await (const event of client.plans.stream(plan.id)) { // resumes after the last event; ends once the plan is finished
console.log(event.event, event.data.step ?? "", event.data.status ?? "");
if (event.event === "plan.interrupted") break; // the run ended with steps left: another run with planId resumes it
}
await run;const status = await client.connections.discord(discordId);
if (!status.authorized) console.log("Allow this app:", status.authorize_url);
const saved = await client.workspace.saves.create({ discord_id: discordId, kind: "message", text: "Worth keeping" });
console.log(saved.url);const reply = await client.responses.create({ model: "iris", input: "Name three moons of Jupiter." });
console.log(reply.output_text);
const stream = await client.responses.create({ model: "iris", input: "Which is largest?", previous_response_id: reply.id, stream: true });
for await (const event of stream) if (event.type === "response.output_text.delta") process.stdout.write(event.delta ?? "");import { openAsBlob } from "node:fs";
const vectors = await client.embeddings.create({ model: "text-embedding-3-small", input: "Hello from Dyadic" });
const audio = await client.audio.speech.create({ model: "tts-1", voice: "alloy", input: "Hello from Dyadic." });
const text = await client.audio.transcriptions.create({ file: await openAsBlob("clip.mp3"), model: "whisper-1" });
const file = await client.files.create({ file: { name: "requests.jsonl", data: await openAsBlob("requests.jsonl") }, purpose: "batch" });
const batch = await client.batches.create({ input_file_id: file.id, endpoint: "/v1/chat/completions" });const message = await client.anthropic.messages.create({ model: "iris", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }] });
console.log(message.content, message.stop_reason, message.usage.output_tokens); // Anthropic's answer
const stream = await client.anthropic.messages.create({ model: "iris", max_tokens: 1024, stream: true, messages: [{ role: "user", content: "Tell me a story" }] });
for await (const event of stream) if (event.type === "content_block_delta" && event.delta?.type === "text_delta") process.stdout.write(event.delta.text);
console.log(stream.usage); // input and output tokens
const batch = await client.anthropic.messages.batches.create({ requests: [{ custom_id: "a", params: { model: "iris", max_tokens: 256, messages: [{ role: "user", content: "Hi" }] } }] });
const results = await client.anthropic.messages.batches.results(batch.id); // once processing_status is "ended"
// OpenRouter's models through the AI gateway (Dyadic turns it on for an app's key on request):
const reply = await client.openrouter.chat.completions.create({ model: "anthropic/claude-haiku-5.5", messages: [{ role: "user", content: "Hello" }] });
const { data } = await client.openrouter.key(); // this key's limits, answered by Dyadicimport { DyadicError } from "dyadic";
try {
await client.ask("Hello");
} catch (error) {
if (error instanceof DyadicError) console.log(error.status, error.code, error.message);
}import { verifyWebhook } from "dyadic";
for await (const event of client.blackbox.runs.stream(runId)) console.log(event.event, event.data);
const { findings } = await client.blackbox.findings({ run_id: runId });
await client.blackbox.findings.update(findings[0].id, { status: "dismissed", note: "expected" });
// In your webhook handler (Standard Webhooks, signed with the sink's whsec_ secret):
const message = await verifyWebhook(rawBody, request.headers, process.env.BLACKBOX_WEBHOOK_SECRET!);OpenAI SDKs
The official OpenAI SDKs work with the endpoints and fields in the compatibility table: set the base URL and use your Dyadic key. Tested with the Python SDK 3.26.0; a field the API can't honor comes back as a 400 unsupported_parameter error naming it.
from openai import OpenAI
client = OpenAI(base_url="https://dyadic.app/api/openai/v1", api_key="dya-...")
print(client.chat.completions.create(model="iris", messages=[{"role": "user", "content": "Hello"}]).choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://dyadic.app/api/openai/v1", apiKey: process.env.DYADIC_API_KEY });
const reply = await client.chat.completions.create({ model: "iris", messages: [{ role: "user", content: "Hello" }] });OpenRouter's models through the gateway: the same SDKs, with the base URL https://dyadic.app/api/openrouter/v1 and OpenRouter's model ids.
Anthropic SDKs
The official Anthropic SDKs work with Dyadic's Anthropic-compatible API: set the base URL and use your Dyadic key. Tested with the Python SDK 1.15.0: messages, streaming, tools, thinking, server tools, files and message batches. Claude Code works the same way with ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY.
import anthropic
client = anthropic.Anthropic(base_url="https://dyadic.app/api/anthropic", api_key="dya-...")
with client.messages.stream(model="iris", max_tokens=1024, tools=[{"type": "web_search_20250305", "name": "web_search"}],
messages=[{"role": "user", "content": "What changed in the news today?"}]) as stream:
for text in stream.text_stream:
print(text, end="")import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://dyadic.app/api/anthropic", apiKey: process.env.DYADIC_API_KEY });
const message = await client.messages.create({ model: "iris", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }] });export ANTHROPIC_BASE_URL=https://dyadic.app/api/anthropic
export ANTHROPIC_API_KEY=dya-...
claude