Skip to Content
ExamplesGoverned client

Governed client

You do not need the SDK to reach the gateway — a stock OpenAI client with a base_url and two headers gets there. These examples show that first, then show what the stock client leaves you holding. Both clients hit the same gateway and get the same PII refusal back; the stock one gives you an openai.APIStatusError and a JSON body to parse, the governed one gives you a PIIDetected with the flagged entities and a remediation string. And because every request leaves through one client, that same call also updated the budget window, stamped a correlation id and opened a GenAI span, with no wiring from you.

ExampleShowsNeeds
Narrative demo 01donkey.openai() and sync=True, the injected headers, one call and what it updated, stock vs governed on the same PII refusal, @donkey.governed / @donkey.toolNothing (simulator)
OpenAI script 01Stock OpenAI with no gateway — the baselineOPENAI_API_KEY
OpenAI script 02donkey.openai() plus last_call on a successful callProxy credentials
OpenAI script 09@donkey.governed and @donkey.tool on their ownNothing

Run it

make demo N=01 # offline, against the local simulator make demo N=01 ARGS="--target live" # against your gateway
Expected output (offline)
════════════════════════════════════════════════════════════════════════════════════════ Demo 01 — the governed client Reaching the gateway is easy. Everything that hangs off the client is the product. ════════════════════════════════════════════════════════════════════════════════════════ Run context ─────────── target mock proxy base_url http://127.0.0.1:8080/ output masking on credentials fake — the simulator enforces no auth [1] donkey.openai() returns a real OpenAI client, already governed donkey = Donkey.from_env() client = donkey.openai() # -> openai.AsyncOpenAI blocking = donkey.openai(sync=True) # -> openai.OpenAI, same governance type openai.AsyncOpenAI sync=True openai.OpenAI base_url http://127.0.0.1:8080/ headers the SDK injects: Accept application/json Content-Type application/json User-Agent AsyncOpenAI/Python 3.19.2 client_id <redacted> (41 chars) client_secret <redacted> (45 chars) OpenAI-Organization <redacted> OpenAI-Project <redacted> Note the base URL has no /v1 — the ingress is https://<host>/<instance>/ and the OpenAI SDK appends /responses itself. Auth is the client_id / client_secret header pair, not a bearer token. sync=True is the same client without asyncio — useful for a straight-line script. [2] One call. Nothing new to learn — it is the OpenAI SDK. response = await client.responses.create( model='gpt-4o', input='Say hello in exactly three words.', ) WARN The simulator replays a captured success response, so the reply below answers the prompt that was recorded, not the one just sent. Run with --target live for a real completion. reply 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. input tokens 17 output tokens 51 That call also did these things nobody asked for, because every request leaves through one client: budget.remaining 98000 budget.limit 100000 budget.fraction_used 2.0% budget.observed_at 2026-09-24 09:27:22.710265+00:00 last_call.status observed last_call.served_model gpt-5.1 last_call.total_tokens 68 • a correlation id went out on the request • a gen_ai.* span opened and closed around it (demo 06) donkey.last_call is the success-path counterpart to a typed refusal: who served this, what they actually routed to, and what the call cost. Demo 10 walks the whole record — routing, fallback, cached/reasoning tokens, and the opt-in ModelSubstituted error. [3] The same refusal, through both clients The simulator serves a real captured PII rejection when the model id is the sentinel below. This is the byte-identical body a live gateway sent. # A: stock OpenAI client, no SDK — just base_url + headers raw = openai.AsyncOpenAI(base_url=..., api_key=..., default_headers=...) await raw.responses.create(model='donkey-sim/pii-detected', input='My email is a@b.com') A: raised openai.PermissionDeniedError A: status 403 A: you get a JSON body to parse, and a status code to guess from # B: the same request, through the governed client try: await client.responses.create(model=..., input="...") except openai.APIStatusError as exc: raise classify(exc.response) from exc B: raised PIIDetected B: policy pii-detection B: entities ['Email'] B: remediation The PII-detection policy blocked this request because the prompt (or completion) contained personally identifiable information. Remove or redact the flagged values, or relax the policy's entity list / action in API Manager. PASS A 403 that is a policy refusal, not an auth failure — and it says so. classify() is the bridge, because the raw client raises openai.* errors and the SDK does not silently re-map them. Demo 02 walks the full taxonomy. [4] @donkey.governed and @donkey.tool — the one-line on-ramps @donkey.governed(team="support") async def handle_ticket(ticket): ... # every model call inside shares one run id There is deliberately no id= on the decorator: a fixed id pinned across every call would collapse unrelated tickets into one correlation. When you need to pin a business id, use donkey.run(id=...) directly (demo 06). run id inside 7512d15e0ea94fe4b6f321aae903a781 cost tags team=support project=triage run id inside 491f95bfc2cc4d88b7b0ae1f200de6b5 cost tags team=support project=triage PASS each invocation opened a fresh run run id after 235d9e98804b480092a2738f58fd7997 PASS restored to the enclosing context — nested run() rebinds, then restores @donkey.tool def lookup_sku(sku: str) -> str: """Return stock for a product SKU.""" ... registered lookup_sku signature (sku: 'str') -> 'str' docstring Return stock for a product SKU. PASS same function object — the decorator records it, it does not wrap it PASS ValueError — an undescribed tool is rejected at decoration time The same marker is what a Phase 2 scanner and an A2A agent-card generator will both read. Neither consumer is built yet; this is the annotation they will look for, not a wrapper around the tool. The point ───────── The wrapper is not sold as a way to reach the gateway. It is the one place every request enters and every response leaves — which is why budget, last_call, typed refusals, correlation ids, spans and simulation can all attach without the developer wiring each one. @donkey.governed is that attachment as a function decorator; @donkey.tool is the marker a scanner can find without executing it. ────────────────────────────────────────────────────────────────────────────────────────
python "demos/human-made/openai/01 - basic-responses-no-gw.py" # needs OPENAI_API_KEY python "demos/human-made/openai/02 - basic-responses-gw.py" # needs proxy credentials python "demos/human-made/openai/09 - governed-and-tool.py" # no gateway

Against the simulator the reply in act 2 is a captured response, so it answers the recorded prompt rather than the one just sent. The demo prints a warning saying so. The PII act against --target live needs the PII detection policy applied with Email among its entities and its action set to Reject — the default action, Log, does not block.

Key code

The governed client is a real OpenAI client, already pointed at the proxy (OpenAI script 02):

async with Donkey.from_env() as donkey: client = donkey.openai() # THIS is returning the native openai response = await client.responses.create( model="gpt-4o", input="Say hello in exactly three words.", ) print(response.output_text) last = donkey.last_call print("last_call.status ", last.status.value) print("last_call.served_model", last.served_model)

The same refusal through the governed client, typed with classify() (narrative demo 01, act 3):

client = donkey.openai() try: await client.responses.create(model=pii_model, input=PII_PROMPT) except openai.APIStatusError as exc: governed = classify(exc.response) say.field("B: raised", type(governed).__name__) say.field("B: policy", getattr(governed, "policy", "—")) say.field("B: entities", getattr(governed, "entities", []), raw=True) say.field("B: remediation", getattr(governed, "remediation", "—"))

The one-line on-ramps (OpenAI script 09). @donkey.governed opens a fresh run per invocation; @donkey.tool records the function without wrapping it, and rejects a tool with no docstring at decoration time:

@donkey.governed(team="support", project="triage") def handle_ticket(ticket: str) -> str: print("run id inside", current_correlation_id()) print("cost tags ", current_cost_tags()) return ticket @donkey.tool def lookup_sku(sku: str) -> str: """Return stock for a product SKU.""" return "42" print("same function object ", lookup_sku is registered_tools()[-1].func)

Note the base URL has no /v1, and auth is a client_id / client_secret header pair rather than a bearer token. There is deliberately no id= on @donkey.governed: a fixed id would collapse unrelated calls into one correlation. Use donkey.run(id=...) when you need to pin a business id (see Telemetry).

Learn more: Introduction · Model access · CLI & decorators

Source: narrative demo 01  · script 01  · script 02  · script 09 

Last updated on