LangGraph
LangGraph (and LangChain more broadly) gets a governed ChatOpenAI pointed at
your Agent Fabric LLM proxy. LangGraph is the deep adapter: every proxy
header and the SDK’s shared async transport reach the native client, and the
adapter runs the full conformance suite in CI.
What you get
- A native
langchain_openai.ChatOpenAI— nothing LangGraph-specific wraps it. - Per-run correlation IDs that reach every graph node.
- Typed gateway refusals (
PIIDetected,TokenBudgetExceeded, …) inside nodes. - A conformance suite you can run against your own graph.
Install
pip install "donkey-kit[langgraph]"Quickstart
Python
from donkey_kit.integrations.langgraph import chat_model
llm = chat_model("gpt-4o")llm is a real langchain_openai.ChatOpenAI instance. Drop it straight into
your graph nodes or chains.
Three ways to construct
1. Off a shared Donkey instance (reuses one HTTP client and lifecycle
across every adapter you use in a run):
from donkey_kit import Donkey
async with Donkey.from_env() as donkey:
llm = donkey.langgraph.chat_model("gpt-4o")The adapter is also callable: donkey.langgraph("gpt-4o") is the same as
donkey.langgraph.chat_model("gpt-4o").
2. Module-level factory (shortest — uses a cached, env-configured default
Donkey):
from donkey_kit.integrations.langgraph import chat_model
llm = chat_model("gpt-4o")3. Governed kwargs, native constructor (you call ChatOpenAI yourself):
from donkey_kit import Donkey
from langchain_openai import ChatOpenAI
async with Donkey.from_env() as donkey:
llm = ChatOpenAI(model="gpt-4o", **donkey.langgraph.connection_kwargs())Manual equivalent
The factories make this native constructor call for you:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o",
base_url=..., # from DONKEY_LLM_PROXY_URL, no /v1 suffix
api_key=...,
default_headers=..., # client_id / client_secret header pair, not bearer
http_async_client=..., # the SDK's shared httpx async client
max_retries=0, # the SDK retries in its own transport layer
use_responses_api=True, # the proxy's data plane is /responses
)connection_kwargs() returns exactly these keys, so you can drop the factory
and construct ChatOpenAI by hand at any time. Pass use_responses_api=False
to chat_model(...) only if your deployment exposes chat-completions instead
of the Responses API (/responses).
Graph-level features
Correlation IDs reach every node
Bind a run ID once with donkey.run(id=…) and every node sees it via
current_correlation_id(), with nothing threaded through graph state.
LangGraph runs nodes on asyncio tasks that copy the current context, so the
ID propagates on its own:
from donkey_kit.core.telemetry import current_correlation_id
async def prepare(state):
logger.info("handling", extra={"correlation_id": current_correlation_id()})
return {}
async with donkey.run(id=ticket.id):
await graph.ainvoke({"messages": [("user", ticket.text)]})Typed refusals inside a node
On a proxy refusal, LangChain raises its own wrapped
OpenAIPermissionDeniedError, not the SDK’s typed exception. Wrap the model
call in typed_refusals() and a gateway refusal comes back through the
error taxonomy instead:
from donkey_kit.integrations.langgraph import typed_refusals
async def call_model(state):
with donkey.langgraph.typed_refusals(): # or: with typed_refusals():
reply = await model.ainvoke(state["messages"])
return {"messages": [reply]}A PII block now propagates out of graph.ainvoke(...) as PIIDetected, a
budget block as TokenBudgetExceeded, and so on — each carrying the
correlation and call IDs the client sent. Transport-level errors with no HTTP
response (connection failures, timeouts) pass through unchanged.
interrupt() composes with typed refusals
A human-in-the-loop interrupt() and a typed refusal don’t interfere: the
graph pauses cleanly at the interrupt, and on resume a refusal in a downstream
model node still surfaces as its typed exception.
Run the conformance suite against your own graph
The suite that tests this adapter is also a pytest plugin you can point at your own agent:
pytest --donkey-conformance --agent=my_app:buildbuild returns an object with an awaitable run(text). The suite checks that
it doesn’t retry a budget refusal, surfaces PIIDetected typed, carries the
correlation ID into its logs, and tolerates a response with no budget headers.
The examples/langgraph
factory has exactly this shape. See Testing.
Notes
base_url,api_key,default_headers, and a customhttp_async_clientare all forwarded, so proxy auth headers and the SDK’s transport (retries, correlation IDs) reach every request.max_retries=0is intentional: retries live in the SDK’s transport layer, so the SDK and the OpenAI client don’t both retry.- The proxy is OpenAI-compatible but not the full OpenAI API: the base URL has
no
/v1prefix, there is no/modelsendpoint, and auth is aclient_id/client_secretheader pair rather than a bearer token.
See the error taxonomy for how proxy rejections surface as typed exceptions.