Framework objects & model handles
No adapter returns a wrapper. donkey.langgraph.chat_model(...) hands back a
real langchain_openai.ChatOpenAI, so everything your framework can do with a
model still works and nothing new appears in your stack traces. LangGraph is
the one deep, conformance-gated adapter; the other seven are supported at
connection_kwargs() — the SDK gives you the base URL, headers and client
configuration, and you pass them to the framework’s own constructor. The
companion example covers model handles: resolve() gives a local capability
handle, and list_models(live=True) raises a ConfigError explaining that the
proxy has no catalog endpoint rather than guessing one.
| Example | Shows | Needs |
|---|---|---|
| Narrative demo 07 | resolve() capability handles, list_models(live=True) raising ConfigError, and config validation listing every missing field at once | Nothing |
| Narrative demo 08 | One factory call per framework and what came back, then connection_kwargs() for the shallow adapters | Nothing — objects are constructed, no network calls |
Run it
make demo N=07
make demo N=08════════════════════════════════════════════════════════════════════════════════════════
Demo 07 — model handles and honest gaps
What the SDK does when the platform has no endpoint for what you asked.
════════════════════════════════════════════════════════════════════════════════════════
Run context
───────────
target offline — no gateway, no simulator, no credentials
output masking on
[1] resolve() — a local capability handle for a known model id
handle = donkey.llm.resolve("gpt-4o")
handle.capabilities
gpt-4o ModelCapabilities(function_calling=True, vision=True, json_output=True, is_heuristic=True)
gpt-4o-mini ModelCapabilities(function_calling=True, vision=True, json_output=True, is_heuristic=True)
o3 ModelCapabilities(function_calling=True, vision=False, json_output=False, is_heuristic=True)
claude-3-5-sonnet ModelCapabilities(function_calling=True, vision=False, json_output=False, is_heuristic=True)
something-unknown-9 ModelCapabilities(function_calling=True, vision=False, json_output=False, is_heuristic=True)
These are heuristics derived from the model id, and the SDK says so rather than
implying it asked the gateway. They are useful for routing decisions in your own code;
they are not a governed catalog.
[2] list_models(live=True) — the honest failure
await donkey.llm.list_models(live=True)
raised ConfigError
The governed LLM proxy exposes no /models endpoint (GET /models → 404, verified
docs/verified-apis.md §2): it only routes requests carrying `model` in the body. Live
model listing is not available from the proxy. Use resolve(model_id) or source the
catalog from Exchange/provider config.
PASS It names the verified absence and points at the alternative, instead of guessing a /models path that would 404 in your sandbox.
[3] The same discipline applied to configuration
The most common reason someone abandons an SDK in the first five minutes is the one-
missing-variable-per-run loop: fix a variable, re-run, discover the next one. So
validation reports everything at once.
DonkeyConfig(llm_proxy_url="https://…").validated(need="llm")
Configuration for 'llm' is incomplete. Missing:
- llm_proxy_client_id (env DONKEY_LLM_PROXY_CLIENT_ID)
- llm_proxy_client_secret (env DONKEY_LLM_PROXY_CLIENT_SECRET)
Set them via kwargs, environment variables, or .donkey-kit.toml.
Two missing fields, one error, each naming the environment variable that sets it. And
note the LLM proxy credential is validated separately from the Anypoint control-plane
one — a developer may legitimately have proxy access and no Exchange access.
When the failure is live rather than a missing variable — wrong URL, wrong
credentials, or a model the allow-list does not include — `donkey doctor` is the CLI
that distinguishes those three. It reuses the same remediation strings the typed
errors carry (demo 02).
────────────────────────────────────────────────────────────────────────────────────────════════════════════════════════════════════════════════════════════════════════════════
Demo 08 — native framework objects
One deep adapter, seven at connection_kwargs(), and no wrappers anywhere.
════════════════════════════════════════════════════════════════════════════════════════
Run context
───────────
target offline — no gateway, no simulator, no credentials
output masking on
[1] One call per framework, and what came back
langgraph deep — the conformance-gated adapter
donkey.langgraph.chat_model(…)
PASS returned langchain_openai.chat_models.base.ChatOpenAI
adk connection_kwargs()
donkey.adk.model(…)
not installed: pip install "donkey-kit[adk]"
strands connection_kwargs()
donkey.strands.model(…)
not installed: pip install "donkey-kit[strands]"
agent_framework connection_kwargs()
donkey.agent_framework.chat_client(…)
not installed: pip install "donkey-kit[agent_framework]"
openai_agents no connection_kwargs() — builds its own client
donkey.openai_agents.model(…)
not installed: pip install "donkey-kit[openai-agents]"
anthropic connection_kwargs()
donkey.anthropic.client()
not installed: pip install "donkey-kit[anthropic]"
crewai connection_kwargs()
donkey.crewai.llm(…)
not installed: pip install "donkey-kit[crewai]"
llamaindex connection_kwargs()
donkey.llamaindex.llm(…)
not installed: pip install "donkey-kit[llamaindex]"
[2] connection_kwargs() — the surface that actually carries the roster
kwargs = donkey.strands.connection_kwargs()
SomeFrameworkModel(model="gpt-4o", **kwargs)
langgraph.connection_kwargs()
base_url https://demo-gateway.example.invalid/openai-sdk/
api_key client-id-enforced
default_headers.client_id <redacted> (36 chars)
default_headers.client_secret <redacted> (40 chars)
http_async_client <donkey_kit.core.transport.DonkeyAsyncClient object at 0x10aac2a50>
max_retries 0
use_responses_api True
Same base URL, same verified client_id / client_secret pair, handed to the framework's
own constructor. Bringing a framework up to the deep bar is demand-driven and happens
one at a time, so this is not a stepping stone that everything is queued behind — it
is the supported surface.
LangGraph is the only adapter held to the conformance bar, and it sets
use_responses_api=True so ChatOpenAI calls the live-verified /responses route rather
than the unverified /chat/completions default.
What is and is not verified here
────────────────────────────────
The proxy contract these objects are configured against is live-verified: the base URL
shape, the credential header pair, the rejection shapes. The exact framework class
names and constructor kwargs are not — they are checked against installed packages by
a nightly matrix rather than asserted from documentation.
Where a class name cannot be confirmed, the adapter raises 'blocked on verification'
rather than guessing. A guessed class name that fails on a developer's first import
costs more than the missing adapter.
────────────────────────────────────────────────────────────────────────────────────────Narrative demo 08 uses obviously-fake config, so it needs no credentials.
Frameworks that are not installed are reported with their exact pip install
line.
Key code
The roster narrative demo 08 walks — attribute on Donkey, factory method, and
depth:
ROSTER = [
("langgraph", "chat_model", True, "deep — the conformance-gated adapter"),
("adk", "model", True, "connection_kwargs()"),
("strands", "model", True, "connection_kwargs()"),
("agent_framework", "chat_client", True, "connection_kwargs()"),
("openai_agents", "model", True, "no connection_kwargs() — builds its own client"),
("anthropic", "client", False, "connection_kwargs()"),
("crewai", "llm", True, "connection_kwargs()"),
("llamaindex", "llm", True, "connection_kwargs()"),
]For the shallow adapters, connection_kwargs() is the whole supported surface:
kwargs = donkey.strands.connection_kwargs()
SomeFrameworkModel(model="gpt-4o", **kwargs)Model handles and the missing catalog (narrative demo 07):
handle = donkey.llm.resolve("gpt-4o")
handle.capabilities
await donkey.llm.list_models(live=True) # raises ConfigErrorAnd config validation reports every missing field in one error, each naming the environment variable that sets it:
DonkeyConfig(llm_proxy_url="https://…").validated(need="llm")donkey.openai_agents is the OpenAI Agents SDK adapter; donkey.openai() is
the raw OpenAI client factory. The LangGraph adapter sets
use_responses_api=True, so ChatOpenAI calls the /responses route. Where
an adapter cannot confirm a framework’s class name or constructor, it raises
“blocked on verification” rather than guessing.
resolve() capabilities are heuristics derived from the model id, not a
governed catalog. The gateway returns 404 for GET /models because
model-based routing only routes requests that already carry model in the
body. When a live call fails — wrong URL, wrong credentials, or a model the
allow-list does not include — donkey doctor tells those apart.
Learn more: Model access · LangGraph · CLI & decorators
Source: narrative demo 07 · narrative demo 08