Open Workspace

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.

Your server holds the key (Node)
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);
Your page calls your server, never the API key
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.

Install
pip install dyadic
Quick start
from 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)
Streaming
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
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)
Async and images
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())
Runs: your agent, Dyadic's tools, your functions
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)
Plans: Propose, Approve, Carry Out and Follow
# 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"))
Connected apps: saving into a person's Workspace
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"])
Responses
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)
Embeddings, audio and moderation
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)
Files and batches
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)
Black Box: record your agent, investigate, verify
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 offline
The Anthropic API and OpenRouter's, on the same key
message = 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 Dyadic
Errors
from dyadic import DyadicError

try:
    client.ask("Hello", model="iris")
except DyadicError as error:
    print(error.status, error.code, error.message)