Quickstart
Make your first governed model call, then see what a plain base_url can’t
give you — a typed refusal, a budget and a span — in a few
minutes, on your laptop. You start against DDK’s local gateway simulator,
so you need no Anypoint account and no credentials. Switching to your real
Omni Gateway later is an environment change, not a code change.
What you can use
| Capability | Status | Learn more |
|---|---|---|
| Governed model access for 8 frameworks | Live | Frameworks |
| Typed refusals | Live | Typed refusals |
| Budget & pacing | Live | Budget & pacing |
| OpenTelemetry spans & cost attribution | Live | Telemetry & cost |
Local simulator & simulate() | Live | Local simulator |
| Conformance testing with pytest | Live | Testing & conformance |
CLI (init, doctor, mock, test) & decorators | Live | CLI & decorators |
| Governed tool access (MCP) | Roadmap | Tool access |
A2A agents (serve, expose, dev) | Roadmap | A2A agents |
| On-behalf-of identity | Roadmap | Identity |
| Human-in-the-loop | Roadmap | Human-in-the-loop |
| Scan & publish to the registry | Roadmap | Scan & publish |
| Policy handshake | Roadmap | Policy handshake |
Your first governed call
Install
pip install "donkey-kit[llm,local,otel]"llm adds the OpenAI client, local the gateway simulator, otel the
OpenTelemetry SDK. Quote the extras — zsh treats unquoted brackets as a glob.
Start the local gateway
In one terminal:
donkey mockThe simulator listens on 127.0.0.1:8080 and replays real gateway responses,
including every refusal shape. It enforces no policy and ignores
credentials, and every response carries x-donkey-simulator: true.
Point DDK at it
In a second terminal:
export DONKEY_LLM_PROXY_URL="http://127.0.0.1:8080"
export DONKEY_LLM_PROXY_CLIENT_ID="local" # placeholder — the simulator ignores auth
export DONKEY_LLM_PROXY_CLIENT_SECRET="local"Make the call
from donkey_kit import Donkey
with Donkey.from_env() as donkey:
client = donkey.llm.client(sync=True) # a real openai.OpenAI, governed
reply = client.responses.create(
model="gpt-5.1",
input="Tell me a one-sentence bedtime story about a unicorn.",
)
print(reply.output_text)
print("budget remaining:", donkey.budget.remaining, "tokens")A sleepy unicorn named Luma painted soft silver stars across the night sky with her glowing horn, then curled up on a moonbeam so all the children below could fall asleep beneath her gentle, sparkling light.
budget remaining: 99500 tokensdonkey.llm.client() returns the OpenAI SDK’s own client, routed through
DDK — so credentials, correlation and attribution headers are injected, and
donkey.budget is updated from the gateway’s response. The simulator replays
a response captured from a real gateway, so you get this story whatever you
ask; against your Omni Gateway the model answers your actual prompt.
Prefer one command? Run python -m examples.quickstart.main from the
python/ directory of the
SDK repository :
it boots the simulator for you and runs the steps above end to end.
Catch a typed refusal Live
Ask the simulator for a specific rejection by using the model id
donkey-sim/<shape>, then bridge the raw response into DDK’s taxonomy with
classify():
import openai
from donkey_kit import PIIDetected
from donkey_kit.core.errors import classify
try:
client.responses.create(
model="donkey-sim/pii-detected",
input="Email the report to jane.doe@example.com.",
)
except openai.APIStatusError as exc:
governed = classify(exc.response)
if isinstance(governed, PIIDetected):
print("blocked, entities:", governed.entities)blocked, entities: ['Email']A PII block is not an auth error, and a policy 429 must never be retried —
typed refusals let the agent react correctly. The framework adapters
(donkey.langgraph, …) classify for you. See Typed refusals.
Pace against the budget Live
The gateway’s token window is a first-class object. Keep a reserve in hand and wait for the window to reset instead of failing:
from donkey_kit import BudgetReserveReached
async with Donkey.from_env() as donkey:
while True:
try:
async with donkey.budget.pace(reserve=0.05):
await enrich(batch)
break
except BudgetReserveReached:
await donkey.budget.wait_for_reset()See Budget & pacing.
See a span Live
With otel installed, every governed call emits an OpenTelemetry GenAI span.
Print them to the console:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider) # set before the first callPut these lines at the top of hello.py and run it again. Alongside the
story, the console prints the span:
{
"name": "donkey.llm.chat",
"context": {
"trace_id": "0x604e2f805df7f12cfdc402f83a66c8f4",
"span_id": "0x212735c846ec8f18",
"trace_state": "[]"
},
"kind": "SpanKind.INTERNAL",
"parent_id": null,
"status": {
"status_code": "UNSET"
},
"attributes": {
"gen_ai.request.model": "gpt-5.1",
"gen_ai.system": "openai",
"gen_ai.response.model": "gpt-5.1",
"donkey.routing.type": "ModelBased",
"donkey.routing.fallback": false,
"gen_ai.usage.input_tokens": 17,
"gen_ai.usage.output_tokens": 51,
"donkey.usage.cached_tokens": 0,
"donkey.usage.cache_write_tokens": 0,
"donkey.usage.reasoning_tokens": 0,
"donkey.policy.decision": "allow",
"donkey.budget.remaining": 99000,
"donkey.correlation_id": "e2fec0b686694055846da9446fa24c96"
},
...
}Each donkey.llm.chat span carries gen_ai.usage.* token counts,
donkey.policy.decision, donkey.budget.remaining and the correlation ID.
Set OTEL_EXPORTER_OTLP_ENDPOINT and DDK exports over OTLP with no code at
all. See Telemetry & cost.
Attribute cost to a run Live
Group one logical task under a single correlation ID and cost tags, so the gateway record, your logs and your spans all join up:
async with donkey.run(id=ticket.id, team="support", project="triage"):
await triage_agent.ainvoke(ticket)Or fold it into one line with a decorator:
@donkey.governed(team="support")
async def handle_ticket(ticket): ...See CLI & decorators.
Test the refusal branch Live
Inject a real gateway refusal in-process — no server, no network:
from donkey_kit import PIIDetected
with donkey.simulate(PIIDetected):
await agent.ainvoke(...) # the next call fails as a real PIIDetectedThen grade your own agent against every refusal shape with the pytest conformance suite. See Testing & conformance.
Connect to your Omni Gateway
The same code runs against your real gateway — stop the simulator and set
three values. The proxy authenticates on a client_id / client_secret
header pair (consumer auth), not a bearer token.
export DONKEY_LLM_PROXY_URL="https://<ingress-gw>/<instance>/" # note: no /v1
export DONKEY_LLM_PROXY_CLIENT_ID="<consumer client id>"
export DONKEY_LLM_PROXY_CLIENT_SECRET="<consumer client secret>"Where the values come from:
- Create the model proxy
on Omni Gateway with Format=OpenAI. Its consumer endpoint is your
DONKEY_LLM_PROXY_URL. - Request access
to the proxy in Exchange. That registers a client application and issues the
client_id/client_secretpair.
Then check the setup:
donkey doctor[ok] config env (3 fields)
[ok] gateway reachable, responded
[ok] credentials client_id accepted
[ok] model accepted by the proxy
[i] budget 99,000 / 100,000 remaining, resets in 59s, observed 0s agodonkey doctor tells a wrong URL from wrong credentials from a model that is
not on the proxy’s allow-list, instead of one opaque failure.
Next steps
- Pick your framework — get a native LangGraph, ADK, Strands, LlamaIndex, CrewAI, OpenAI Agents SDK, Anthropic SDK or Agent Framework object in three lines.
- Examples — runnable demos for every capability on this page.
- Scenarios — support triage, a nightly batch and an internal copilot, built end to end.