Serve any LangGraph graph over the OpenAI Chat Completions + Responses APIs.
Project description
GraphServe
GraphServe is an open-source Python library that serves any LangGraph graph over the OpenAI Chat Completions and Responses APIs, enabling seamless integration of agent workflows into OpenAI-compatible applications.
The problem
We want LangGraph agents driven from off-the-shelf open-source chat UIs like Open WebUI. Three schema mismatches sit in between:
- LangGraph speaks its own streaming protocol, not OpenAI's.
graph.stream()emits typed chunks chosen bystream_mode(values/updates/messages/custom/debug) —(message_chunk, metadata)pairs tagged by node name, not OpenAI SSE deltas. (docs) - The open-source chat-UI ecosystem standardized on the OpenAI API. Not just Open WebUI — LibreChat, LobeChat, AnythingLLM, and HuggingFace Chat UI all integrate arbitrary backends the same way: give them a base URL that serves
GET /v1/modelsandPOST /v1/chat/completions, or the UI can't connect. (Open WebUI docs) - Backends emit "thinking"/reasoning in incompatible response shapes. The same reasoning output arrives differently depending on the model behind LangChain: Claude puts a
reasoningdict (summary/encrypted_content) inadditional_kwargs, vLLM/LiteLLM put a flatreasoning_contentstring, and others emit content blocks withtype: "thinking". None of these match the OpenAI reasoning schema that clients expect. (OpenRouter, vLLM)
GraphServe sits between the compiled graph and the client: it presents each graph as an OpenAI model, translates LangGraph's token stream into OpenAI SSE, and normalizes the different backend reasoning shapes into OpenAI's reasoning schema.
Detailed, provider-specific evidence and citations for each problem live in docs/problems/.
Installation
Into a uv project (needs an existing pyproject.toml):
uv add graphserve
Or into a plain environment / with pip:
uv pip install graphserve # or: pip install graphserve
Quickstart
from fastapi import FastAPI
from langgraph.graph import StateGraph, START, END
from graphserve import GraphRegistry, create_openai_router
# 1. Build your LangGraph graph
graph = StateGraph(...)
# ... add nodes and edges ...
compiled = graph.compile()
# 2. Register it under a model name
registry = GraphRegistry()
registry.register("my-agent", compiled)
# Optional: only stream tokens from specific graph nodes
registry.register("my-agent-2", compiled, streamable_node_names=["responder"])
# 3. Mount the OpenAI-compatible router on your FastAPI app
app = FastAPI()
app.include_router(create_openai_router(registry), prefix="/v1")
Your app now exposes:
| Route | Description |
|---|---|
GET /v1/models |
List registered graphs |
POST /v1/responses |
Create a response (streaming or non-streaming) |
GET /v1/responses/{id} |
Retrieve a previous response |
DELETE /v1/responses/{id} |
Delete a response |
POST /v1/chat/completions |
Chat Completions API |
Public API
| Export | Description |
|---|---|
GraphRegistry |
Registry mapping model names to compiled graphs |
create_openai_router |
Builds a FastAPI APIRouter with all OpenAI-compatible routes |
GraphRegistry.register
registry.register(
model_name, # str — the OpenAI "model" name clients request
graph, # an already-compiled LangGraph graph
*,
streamable_node_names=None, # list[str] | None — if set, only stream tokens
# emitted by these node names; None streams all
)
GraphServe never compiles graphs for you — build and .compile() in your app, then
pass the compiled graph in. Registering the same model_name twice raises ValueError.
Scope: a pure OpenAI↔LangGraph converter
create_openai_router(registry) takes only the registry — there are no knobs. GraphServe
converts between the OpenAI wire format and LangGraph; everything cross-cutting stays your
job, applied with standard tools:
- Auth — apply it where you mount the router:
app.include_router(create_openai_router(registry), prefix="/v1", dependencies=[Depends(verify_api_key)])
- Callbacks / tracing — attach to your graph when you build it.
Runtime context passed to your graph
Different graphs need different per-request information — a caller's identity, a tenant ID,
a turn counter, feature flags, custom instructions. GraphServe does not invent a bespoke
field per use case. Instead it maps the standard OpenAI request fields onto a single LangGraph
runtime context envelope, and gives you one open-ended channel (metadata) for everything else:
| OpenAI request field | Becomes | Notes |
|---|---|---|
user |
context["user_id"] |
The standard OpenAI end-user identifier. |
instructions (Responses API) |
context["metadata"]["custom_instructions"] |
Folded into metadata so graphs read it in one place. |
metadata (arbitrary key/values) |
context["metadata"] |
Passed through verbatim — your extension point. |
If the request carries none of these, context is None and the graph runs with no runtime
context. The context dict is handed to LangGraph via ainvoke(..., context=...) /
astream(..., context=...), so your nodes and middleware read it as normal runtime context.
The metadata field is the mechanism for any new use case. Anything a client puts there
arrives unchanged at context["metadata"] — no GraphServe change required. For example, a
client sends:
{
"model": "my-agent",
"input": "…",
"user": "user-42",
"metadata": {
"identity_number": "VN001",
"turn_number": 2,
"tenant": "clinic-7"
}
}
and a node reads it off the runtime context:
from langgraph.runtime import get_runtime
def my_node(state):
ctx = get_runtime().context or {}
user_id = ctx.get("user_id") # "user-42"
meta = ctx.get("metadata", {})
identity = meta.get("identity_number") # "VN001"
turn = meta.get("turn_number") # 2
...
Graphs read what they need and ignore the rest, so adding a new field is a client-side change only — it never requires touching GraphServe.
Stateful responses and previous_response_id
All state is managed through the registered graph's LangGraph checkpointer, keyed by thread_id.
If a graph is compiled without a checkpointer, GraphServe automatically injects an InMemorySaver
(with a warning). Response IDs encode the model (resp_<model>.<hex>) so GET/DELETE routes can
resolve the owning graph without any external metadata store.
To use a persistent checkpointer:
from langgraph.checkpoint.memory import MemorySaver
compiled = graph.compile(checkpointer=MemorySaver())
Streaming
Pass "stream": true in the request body to receive Server-Sent Events following the OpenAI Responses API event schema (response.created, response.output_item.added, response.output_text.delta, response.completed, etc.).
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file graphserve-0.2.2.tar.gz.
File metadata
- Download URL: graphserve-0.2.2.tar.gz
- Upload date:
- Size: 125.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2037eb128e09aabcaa5c6ec0a9af6a2ebad9ac10bae748fe6287d0dbad98f592
|
|
| MD5 |
1e53ea0af542b1cc07528c34906e200e
|
|
| BLAKE2b-256 |
146d6c70691b99b7a9f1fdf924e0ab4f056ca08390f7c143af322f09bfe48fa7
|
Provenance
The following attestation bundles were made for graphserve-0.2.2.tar.gz:
Publisher:
ci.yml on AndrewNgo-ini/graphserve
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graphserve-0.2.2.tar.gz -
Subject digest:
2037eb128e09aabcaa5c6ec0a9af6a2ebad9ac10bae748fe6287d0dbad98f592 - Sigstore transparency entry: 2084463577
- Sigstore integration time:
-
Permalink:
AndrewNgo-ini/graphserve@ee72f94b3ba7880b88fba0f39166e9d9594248ab -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/AndrewNgo-ini
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@ee72f94b3ba7880b88fba0f39166e9d9594248ab -
Trigger Event:
push
-
Statement type:
File details
Details for the file graphserve-0.2.2-py3-none-any.whl.
File metadata
- Download URL: graphserve-0.2.2-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71fc6d91fbc9a7536d4482805bcda5151042c1ece388b08c636bf2bf0fc52025
|
|
| MD5 |
2f0e8b51b852c3a9db1c4010476176f7
|
|
| BLAKE2b-256 |
731bc4d4db49a949b0a0a15eb2944726ed0960714256ee4f8ce9d1d14ed83340
|
Provenance
The following attestation bundles were made for graphserve-0.2.2-py3-none-any.whl:
Publisher:
ci.yml on AndrewNgo-ini/graphserve
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graphserve-0.2.2-py3-none-any.whl -
Subject digest:
71fc6d91fbc9a7536d4482805bcda5151042c1ece388b08c636bf2bf0fc52025 - Sigstore transparency entry: 2084463589
- Sigstore integration time:
-
Permalink:
AndrewNgo-ini/graphserve@ee72f94b3ba7880b88fba0f39166e9d9594248ab -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/AndrewNgo-ini
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@ee72f94b3ba7880b88fba0f39166e9d9594248ab -
Trigger Event:
push
-
Statement type: