OpenAI API Compatibility¶
LangGraph OpenAI Serve is an OpenAI-client compatibility layer, not a separate LangGraph-specific HTTP API. Public Responses, Chat Completions, and model behavior remain reachable through the configured OpenAI-compatible base URL.
The same contract lets LGOS run behind OpenAI-compatible intermediaries without a project-specific inference adapter. Generic gateway requirements are in the proxy guide; concrete Chainlit, Open WebUI, Files, LiteLLM, and Bifrost implementations belong to the Demo Stack.
Contract¶
- Registered graph names are exposed as OpenAI
modelvalues. - Core graph behavior must fit OpenAI-compatible request fields, Responses items, Chat objects, tool calls, streaming events, metadata, or error envelopes.
- Do not require custom payloads, headers, routes, or SSE event shapes for core behavior unless the OpenAI client path still works.
- Treat direct HTTP calls such as
curlas diagnostics; validate compatibility through OpenAI client behavior.
The implemented endpoints are listed in Reference.
Model Feature Discovery¶
The OpenAI Model object
has no metadata field. LGOS keeps its standard fields unchanged and places
feature discovery in a namespaced extension on model-list and
model-retrieval responses. Runtime settings remain detail-only. Tools are
advertised by name only; clients still select them per request:
{
"id": "simple-graph",
"object": "model",
"created": 1720000000,
"owned_by": "langgraph-openai-serve",
"lgos": {
"description": "Streams responses with configurable history and audience.",
"features": [],
"server_tools": [],
"client_tools": [],
"client_settings": {
"json_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"use_history": {
"type": "boolean",
"default": false
},
"audience": {
"type": "string",
"enum": ["general", "beginner", "expert"],
"default": "general"
}
},
"additionalProperties": false
},
"defaults": {
"use_history": false,
"audience": "general"
}
}
}
}
The standard OpenAI Model object has no description field. The required
GraphConfig.description is therefore exposed as
lgos.description on both list entries and detailed model
responses. It is API-owned presentation text; clients decide how to render it.
GraphConfig.features is the single declaration for optional runtime behavior
and client capabilities. Model listing and retrieval serialize it for
discovery.
GraphConfig.client_settings is an explicit, allowlisted public Pydantic model;
LGOS never publishes a graph's internal LangGraph context schema automatically.
New extension fields and feature values are additive; clients ignore unknown
fields and feature values.
| Feature | Enabled behavior |
|---|---|
file_inputs |
The graph accepts native file parts and resolves their opaque file_id values. |
background |
The graph can run through the configured background backend and be polled through the standard Response lifecycle. |
interrupts |
The server supports the checkpointed interrupt/resume flow. |
mcp_tools |
Clients may attach and execute tools from their configured MCP gateway. The gateway owns discovery and authorization. |
GraphConfig.server_tools and GraphConfig.client_tools are serialized as
the sorted lgos.server_tools and lgos.client_tools name lists, so clients
know which tools a graph accepts without hard-coding graph names. A
server tool runs inside the graph, and the client selects it
per request. A client tool is a client-owned function the graph can call, such
as display_file; a client offers it only when it
implements that name. The lists carry no descriptions or schemas: the graph
owns its server tools, and the client owns its functions.
GET /v1/models remains lightweight. Every entry contains the standard id,
object, created, and owned_by fields plus a small
lgos object with description, features, server_tools, and
client_tools. Client-settings schemas remain detail-only.
Every successful LGOS GET /v1/models/{model} response includes the complete
lgos extension, even when its feature and tool lists are empty and it
has no client settings. A client of this API reads descriptions from the list
and retrieves the selected model details through the same OpenAI client.
This keeps large schemas out of list responses and keeps internal or
secret-bearing runtime context out of discovery.
OpenAI treats added response properties as backward-compatible.
Direct JavaScript clients can read the property normally, and the
OpenAI Python SDK exposes it through model_extra.
An intermediary may rebuild a retrieved model from the standard fields and drop
extensions. For one LGOS deployment, a client can use one OpenAI base URL for
model listing, model retrieval, Responses, and Chat Completions. A federating
gateway may expose a normalized catalog for provider and model
routing, but that catalog is not necessarily a source of LGOS descriptions or
capabilities. Standard Responses requests do not depend on the extension. A UI
that offers graph-specific settings or capability controls must obtain the
selected graph's full metadata through a route that preserves it. The demo's
LiteLLM clients read native /model/info, using model_name for routing and
model_info.lgos for the extension. An LGOS-owned sync
copies the detail into that native field; the UIs never contact LGOS directly.
Bifrost clients decode the equivalent JSON string from
additional_attributes.lgos on native /v1/models. Its
catalog sync writes base pricing
rows and publishes complete LGOS details through Bifrost's native management
API. The /openai/v1/models conversion still drops those attributes.
Concrete gateway configurations and native Responses requirements are documented under
OpenAI-Compatible Proxies.
Limited functionality signal
A missing description in model listing or missing or invalid
lgos metadata on model retrieval means the configured
endpoint is not preserving the optional LGOS discovery contract. A UI may
continue plain Responses text, but it must visibly label the model or chat
as Limited functionality and must not assume runtime settings, file
inputs, client tools, interrupts, or background execution are available. A
normalized routing catalog cannot remove this requirement.
Runtime Settings¶
The request keeps each concern in its standard OpenAI location:
| Concern | OpenAI request location |
|---|---|
| System instructions | Responses instructions or an input system/developer message; a system message in Chat |
| Small graph-specific values | One metadata.lgos_settings string containing a JSON object |
| Graph selection | model |
| Background create retry identity | Optional Idempotency-Key HTTP header |
| Conversation correlation | Optional metadata.conversation_id string |
Only small graph-specific values belong to ClientSettings. A graph may expose
controlled semantic choices such as intended audience, but not arbitrary system
instruction text. Client-authored system instructions remain ordinary graph-input
messages.
OpenAI metadata permits at most 16 string pairs, with keys up to 64 characters
and values up to 512 characters. Public settings consume one pair; a
conversation correlation value consumes another. Clients use json.dumps() or
JSON.stringify() to encode the complete settings string and omit values equal
to the advertised defaults. The
advertised JSON Schema describes the available settings; LGOS remains the
validation authority. The descriptor's separate defaults object is the
authoritative validated baseline; JSON Schema default keywords are annotations
and may precede Pydantic field normalization. Native OpenAI fields keep their
standard semantics. Graphs that need identity, authorization,
database clients, secrets, or other server-owned per-request context combine
client_settings with context_factory(request, settings).
LGOS reserves metadata keys beginning with lgos_; applications should use
their own names outside that prefix. The independent keys remain separate
rather than sharing one JSON envelope so each value retains OpenAI's full
512-character allowance. Arbitrary non-LGOS metadata continues through the
protocol-neutral graph request unchanged.
metadata.conversation_id is an optional, client-owned correlation value. It is
a documented metadata convention, not a server-managed conversation resource.
A client uses the same stable value for every Responses or Chat Completions request in
one conversation. With Langfuse enabled, LGOS sets it as the Langfuse session
of every observation; each request remains a separate trace, while Langfuse
groups those traces in one
session. It does
not select checkpoint state or cause LGOS to retain conversation history; clients
still supply the input needed by each ordinary request. Omit the field when no
conversation exists; LGOS does not generate a fallback ID. Application graphs
may explicitly use it to scope their own stored data, but it is not authorization.
Clients targeting Langfuse should use an ASCII value of at most 200 characters;
Langfuse drops a longer session. The value is distinct from the end-user
safety_identifier and user fields, the interrupt run ID, and per-request
trace or request identifiers.
Per-Request Resolution¶
Every graph request starts from the registered defaults. Values supplied in
metadata.lgos_settings replace matching top-level defaults, and LGOS
validates the complete result. The merge is shallow: a supplied nested object
replaces that whole default value rather than recursively merging its keys.
Client settings are not persisted between requests. The paused Response ID and
interrupt call IDs identify durable state, but they do not restore runtime context.
Clients must resend non-default settings on every request that needs them,
including interrupt-resume requests. A later request that omits
lgos_settings uses registered defaults again.
When the required extension is missing or unsupported, the client omits runtime settings and shows the limited-functionality warning described above. See Configure LangGraph Runtime Settings for the complete author and client flow. Adapter support is summarized under demo client capability matrix.
Message And Schema Adaptation¶
Incoming OpenAI messages are converted to LangChain messages. The protocol
decoder also produces the small, protocol-neutral GraphRequest received by
GraphConfig adapters, keeping custom LangGraph schemas behind the public API
boundary. See
LangGraph Integration and
Custom Graphs.
Responses input_file.file_id content and native Chat Completions file parts
normalize to the same LangChain file content. LGOS does not expose Files routes
or own file storage. A client uploads through an external OpenAI-compatible
Files API and sends the returned file_id to a graph; the graph still owns file
interpretation. See Accept And Display Files.
Supported Responses Subset¶
POST /v1/responses implements ordinary stateless text, files, client functions,
Responses-compatible server-executed custom tools, OpenAI-shaped web search,
streaming, and an opt-in polling-only background path. It intentionally does
not claim every field in the upstream OpenAI API.
| Request field or item | LGOS behavior |
|---|---|
model, input, instructions |
Supported. String input and ordered user, system, developer, and replayed assistant messages become LangChain messages. New instructions are rejected on interrupt resumes because a paused invocation cannot consume them. |
input_text |
Supported. |
input_file.file_id |
Supported and normalized to the existing graph file block. |
function tools, named function tool_choice, parallel_tool_calls |
Supported for client-owned functions. A function whose name is registered as a server tool is rejected so ownership stays unambiguous. |
name-only custom tools and named custom tool_choice |
Select a custom tool registered in GraphConfig.server_tools. The graph owns its description, string input contract, and implementation; arbitrary client-defined custom tools and formats are outside this subset. |
async on function/custom tool declarations and replayed calls |
Omitted, null, and false values are accepted for SDK round trips. async: true is rejected because LGOS does not implement the pending-call lifecycle for async tool calling; registered server tools execute and return their call and output within one Response. |
tools=[{"type":"web_search"}] |
Selects a registered graph-owned web search. Use tool_choice="required" with only this tool to force search. Named built-in choices, optional filters, and location fields are outside the current subset. |
web_search_call |
Returned after the graph executes search and may be replayed as history. Final text carries standard URL citations. |
custom_tool_call and string-valued custom_tool_call_output |
Returned together after LGOS executes a registered custom tool and accepted as history. |
function_call and string-valued function_call_output |
Supported for client-function continuation and accepted as history, including answered lgos_interrupt calls. Interrupt resumes accept only function_call_output items with previous_response_id. |
metadata, safety_identifier, user |
Supported and passed through the protocol-neutral graph request boundary. safety_identifier names the end user in at most 128 characters, OpenAI's documented limit, and replaces OpenAI's deprecated user, which remains its alias; when both are sent, safety_identifier wins and a Response echoes each as sent. They are not authentication. |
stream |
Supported with typed Responses SSE events for foreground work. background=true requires stream=false. |
store |
Omitted, null, and false mean false. Foreground store=true is rejected. Background requests accept either value; the background engine keeps the result either way. |
text.format.type="text" |
Supported. |
previous_response_id |
Supported for interruptible graphs to answer pending interrupts, in the foreground or with background=true. Rejected for non-interruptible graphs. |
background |
Omitted, null, and false select foreground execution. True is supported only for a model declaring GraphFeature.BACKGROUND and a server configured with a BackgroundBackend. |
conversation |
Rejected because LGOS has no Responses conversation store. |
include, reasoning, generation controls, service tier, stream options, reusable prompts, prompt-cache fields, truncation |
Rejected rather than accepted without semantics. |
Unknown request fields also fail validation. Exact errors use the standard OpenAI envelope and identify the unsupported parameter where it is known.
Stateless Item Continuation¶
For foreground work, LGOS generates an opaque Response ID for correlation but does not persist it. Foreground IDs are not retrievable, and there are no delete, compact, or input-item routes. Clients therefore keep an input ledger and resend the items needed by the next turn instead of using a server-side Conversation. The retrieve and cancel routes described below address only background Response IDs.
When continuing a response, append every item from response.output unchanged.
Execute returned client function calls, then append their function_call_output
items. Registered custom calls already have matching outputs. Replaying complete
SDK items preserves custom and function exchanges, message and call IDs,
citations, refusals, and assistant phase in the LangChain content consumed by
model adapters. Replayed
web_search_call items also retain their ID, action, and status. Completed and
incomplete assistant message items can both be replayed. The current
SDK may serialize optional function-call caller and namespace fields and
the stream helper's output-text parsed field as null; LGOS accepts those null
values but rejects non-null program, namespace, or parsed-output semantics.
This state model follows OpenAI's documented manual item replay while
keeping storage in the client.
Each replayed function or custom call requires one matching output of the same type. Missing, duplicate, mismatched, or unmatched results fail validation before graph execution, including when the client requests streaming.
Answered interrupts replay the same way: a later request carries the
lgos_interrupt calls, then the outputs that answered them, so the graph sees
what was asked and answered. Only an input that ends with interrupt calls or
outputs is a resume.
An interrupt continuation uses a narrower stateful path. The client sends the
paused Response ID as previous_response_id and sends only matching
function_call_output items. LGOS uses those opaque IDs to locate and validate
the paused checkpoint; it does not reconstruct ordinary conversation history.
OpenAI's previous-response chaining defines the client exchange, but does not execute or persist a LangGraph graph. LGOS remains responsible for checkpoint storage, complete-batch validation, and coordination across workers. The checkpointer owns paused execution state; the coordinator prevents overlapping runs. There is no separate interrupt-response store or Chat Completions resume codec.
LangGraph checkpoint and Store persistence are separate. A checkpointer keeps only paused workflow execution; a graph Store keeps explicit application data. Neither makes a Response ID retrievable or lets LGOS reconstruct a conversation.
Polling-Only Background Lifecycle¶
For an opted-in graph, background=true submits the validated request to the
configured BackgroundBackend and returns the queued Response. The caller
keeps the opaque Response ID and uses:
GET /v1/responses/{response_id}for a current JSON snapshot; andPOST /v1/responses/{response_id}/cancelfor idempotent cancellation.
The public states are queued, in_progress, completed, incomplete,
failed, and cancelled. LGOS configures no retries, so a run that raises
fails its Response; create a new Response to try again. Retrieval has no
streaming or cursor mode, and LGOS does not replay
background events. Unknown, unauthorized, and wrong-scope IDs share the same
non-revealing not-found behavior.
Background create also accepts an
IETF Idempotency-Key
header, which OpenAI does not define, so retrying SDKs and gateways cannot
start a second run; see
idempotent creation.
The engine's run record is not a Conversation. previous_response_id answers
a run paused at an interrupt in either mode. A background answer is a new
queued Response that the worker validates once it holds the run's checkpoint
lease, so a stale or racing answer fails its own Response.
See Run Responses In The Background for graph constraints and backend configuration.
Responses Output¶
| Graph result | Responses representation |
|---|---|
| Final assistant text | Completed message item with phase="final_answer" and output_text content |
| Model refusal | Message refusal content and native response.refusal.delta / done events |
| Truncated or filtered final model output | status="incomplete", incomplete_details, and a terminal response.incomplete event |
| Visible streaming status | Separate completed message item with phase="commentary" |
| Client tool or interrupt | One function_call item per call |
| Tool result on the next request | Matching function_call_output item |
| LGOS server custom tool | custom_tool_call and matching custom_tool_call_output in the same response |
| LGOS server web search | web_search_call followed by a cited assistant message |
| URL citation | url_citation annotation on output_text |
| Provider-reported usage | usage on the completed or incomplete Response |
Completed function arguments are JSON strings. Truncated arguments remain raw strings on incomplete function-call items; clients must not execute them. The graph runner does not emit incremental arguments, so the Responses stream sends one argument delta before the corresponding done event. IDs and output indices remain stable throughout the typed event lifecycle.
On failure, the terminal response.failed object retains any partial answer
already streamed, with unfinished output items marked incomplete.
LGOS reads refusals from LangChain content blocks or the OpenAI Chat provider's
additional_kwargs.refusal. It reads incomplete Responses metadata and Chat
finish_reason values length and content_filter from the final assistant
message. Graph output adapters must retain that metadata when returning model
output. Refusals remain distinct from ordinary text on the wire; the maintained
UIs display their explanation. An incomplete response is not eligible for
automatic client-tool execution.
Consumers must handle response.incomplete and response.failed directly to
retain their terminal status and reason instead of assuming every stream ends
with response.completed. See the official
streaming event reference.
Streaming¶
Streaming responses use OpenAI-compatible Server-Sent Events. See LangGraph Integration for internal event handling and Request Cancellation for request-scoped disconnect cancellation, proxy behavior, and cooperative limits.
While a graph produces no output, such as during a long tool call, LGOS sends an
SSE comment (: ping) every 15 seconds, as FastAPI's native SSE responses do, so
idle proxy timeouts do not close the stream; SSE clients, including the OpenAI
SDKs, ignore comments. Streams also send Cache-Control: no-cache and
X-Accel-Buffering: no, which stops Nginx from buffering them.
LGOS aggregates usage reported by LangChain model calls across the graph run.
Completed and incomplete Responses include it in usage, and a Responses stream
carries it on its terminal Response object. Chat streams add the standard final
empty-choices usage chunk only when the request sets
stream_options={"include_usage": true}. When underlying providers report no
usage, LGOS omits it rather than estimating tokens.
A model call that streams reports usage only when its request asks for it, and
LangChain's ChatOpenAI asks automatically only for OpenAI's default URL. Set
stream_usage=True
on models that stream through another base URL, such as a gateway. This includes
private calls tagged nostream, which still stream from the provider.
Assistant Text Parity¶
The final rendered AIMessage.text is the canonical assistant text.
Non-streaming returns it directly. Ordinary streaming emits each non-empty
AIMessageChunk.text, then compares their concatenation with the final text.
If no text streamed, LGOS emits the final text as a fallback; a mismatch instead
produces the protocol's failure sequence. Requests selecting server tools also
consume completed LangGraph updates for tool activity, without disabling message
chunks. This check covers one graph run, not two
independent LLM executions. Transient status events are excluded.
When multiple streamed model calls contribute text, the graph's
output_to_message adapter must render their messages in the same order.
Streaming Status¶
Graph nodes publish status with status_event(). Any other LangGraph custom
stream data stays private. Streaming Responses exposes statuses; Chat
Completions ignores all custom stream data.
Responses Commentary¶
A streaming Responses request needs no metadata opt-in. LGOS maps every
non-empty status description to its own completed assistant message with
phase="commentary" and maps the durable answer to a message with
phase="final_answer".
Commentary is transient and streaming-only. Non-streaming execution calls the
graph once for its durable result and does not collect status history. The
OpenAI Python SDK's Response.output_text convenience property concatenates
text across both phases, so UIs must select final_answer messages for
the transcript and render commentary separately. The maintained Chainlit and
Open WebUI adapters do this. Other clients may ignore phase or show all text
as one answer; that is a client presentation limitation, not a reason to add a
custom server event.
Chat Completions vs Responses Boundary¶
Complex workflow features—such as streaming status commentary, selecting
server tools, checkpointed persistence, and human-in-the-loop
interrupts—are available through the native Responses API (/v1/responses).
The Chat Completions API (/v1/chat/completions) provides strict, standard OpenAI
compatibility for assistant text and client tool calling. It streams plain text
delta.content chunks, ignores custom streaming events, and never selects
server tools. Only interrupt-enabled models fail fast with HTTP 400.
Chat message content is limited to strings, explicit text parts, and native
file parts containing only file.file_id. Image and audio parts, inline file
data or filenames, and prompt-cache fields are outside the supported subset and
fail request validation.
| Graph result | Responses | Chat Completions |
|---|---|---|
| Assistant text | final_answer message |
delta.content |
| Interrupt requiring input | function_call item |
Unsupported (HTTP 400) |
| LGOS server tool | Native call and result items | Not selectable |
| Citation | output_text.annotations |
message/final-delta annotations |
| Passive status | commentary message when streaming |
Ignored |
| Other custom stream data | Ignored | Ignored |
| Midstream failure | error then response.failed |
OpenAI error object |
Status is deliberately not a tool call. In OpenAI function calling, a client-owned function call asks the client to execute work and return a result. A passive status describes backend work already in progress.
Citation Ownership¶
OpenAI url_citation annotations are the canonical citation contract. Their
URL, title, and text span associate a source with the answer. end_index is
the cited span's inclusive last-character index. LGOS validates each span
against its containing text block and checks cited_text when present.
Graphs attach LangChain citation annotations to their final AIMessage.
Responses returns them on output_text.annotations and emits
response.output_text.annotation.added during streaming. Chat Completions
returns them as message.annotations and as an extension on the final delta.
LGOS does not define a UI-specific source schema or reconstruct citations from
custom events.
Portable resource presentation belongs in the assistant text, not in the
annotation object. Graphs may return ordinary Markdown links and images in
message.content, including visible inline citation markers. Annotations do not
require clients to synthesize marker text. When a graph also emits structured
attribution, its url_citation remains limited to its standard URL, title, and
text span. Audio and video resources should use ordinary Markdown links rather
than UI-specific players. RAG graphs must preserve only resource URLs supplied
by their retrieved context and must not invent or rewrite them.
Structured citations remain available to OpenAI clients that need
machine-readable provenance. The citation-events demo showcases that optional
contract.
Only the Chat streaming field is a compatibility extension because the published Chat delta schema does not declare annotations. Responses annotations and their typed streaming event are standard fields.
See the official OpenAI citation contract.
Errors¶
OpenAI-compatible routes return errors in the OpenAI envelope:
{
"error": {
"message": "The model 'missing' does not exist.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}
An unknown model returns HTTP 404 on every route, as OpenAI does. Code that
rejects a request raises InvalidRequestError with its param, code, and
status; graph adapters such as context_factory may raise it too. Shared
handlers translate FastAPI validation and HTTP errors into the same envelope.
A GraphError or any other unexpected failure returns HTTP 500 with
type: "server_error" and the message Internal server error; the details are
logged, not returned. A LangChain ContextOverflowError, which provider
integrations such as langchain-openai raise when a model call exceeds the
context window, returns HTTP 400 with code: "context_length_exceeded" instead,
so OpenAI clients do not retry it. Streams and background Responses report it as
server_error, like any other graph failure.
Invalid runtime settings return HTTP 400 with
param: "metadata.lgos_settings". A proxy-stripped model
extension does not make plain text generation invalid, but clients surface it
as limited functionality rather than silently presenting a fully capable
model.
Malformed interrupt inputs and duplicate tool results return HTTP 400. A
validly shaped exchange that is missing results from the durable pending set,
does not match it, or is stale or already completed,
returns HTTP 409 with code: "interrupt_state_conflict". A request that cannot
acquire its interrupt-run lease returns HTTP 409 with code: "run_busy".
Tool Calls And Interrupts¶
Tool definitions are accepted for OpenAI compatibility. Graphs can read their
normalized function definitions and choices from GraphRequest in
request_to_input, or they can load and execute server-owned tools independently.
MCP discovery is not part of LGOS: the PostgreSQL demo's native UI client
discovers gateway tools, supplies their standard function definitions, and
executes returned calls.
Responses accepts flat function tool definitions, named or automatic tool
choice, returned function_call items, and matching string-valued
function_call_output items. Chat supports the modern nested tools,
tool_choice, assistant tool_calls, and tool messages with matching
tool_call_id values. The deprecated Chat functions, singular
function_call, and function message role are rejected rather than silently
ignored.
Chat request fields outside the supported schema are rejected by
normal request validation, including generation controls such as temperature,
max_tokens, and n. Graphs own their model configuration; LGOS cannot apply
those fields to arbitrary workflows. Deprecated function fields have no separate parser
or migration path. An assistant message's function_call: null is accepted so
clients can replay SDK message objects unchanged.
Interrupt graphs require a client application that can collect and submit tool
results. Interrupts and checkpoint resumes are supported exclusively via
the Responses API (/v1/responses). Requesting an interrupt-enabled model via
Chat Completions returns HTTP 400 Bad Request.
Server Tools¶
GraphConfig.server_tools is the allowlist of tool names a graph executes.
Model listing and retrieval advertise it as lgos.server_tools, so a client
offers one selector per name: web_search with its built-in type and any other
name with the custom type. The client selects them per request:
A registered custom selector contains only type and name; the graph owns its
description, string input contract, binding, and implementation. Client
overrides are rejected. An unregistered custom name is rejected, while a
function remains client-owned unless its name collides with a registered server
tool.
Selecting web_search also requires that name in the graph's allowlist.
OpenAI's ordinary custom-tool workflow returns a call for caller code to execute, then accepts the matching output in another model request. LGOS deliberately changes that ownership and timing for registered names: the graph executes the tool and returns the call, output, and answer in one Response. This is an OpenAI Responses-compatible server-execution policy, not a claim that OpenAI hosts the registered custom tool.
GraphRequest.server_tools contains only the selected registered names, while
GraphRequest.tools contains normalized client functions. tool_choice
preserves none, auto, required, or a named function or custom choice. The
graph uses those values to bind its own LangChain tools.
This uses OpenAI Responses tool and item representations, not an LGOS tool envelope. Execution ownership is the intentional semantic difference:
- A registered server
customtool has free-form string input. LGOS executes it and returns itscustom_tool_calland matchingcustom_tool_call_outputin the same response. - A client
functionretains JSON object arguments and client-side execution. web_searchuses OpenAI's built-in declaration andweb_search_calloutput.
OpenAI's custom tools guide
defines free-form tool inputs and its standard caller-executed loop. The graph
instead owns execution for registered custom tools: the model emits a call, its
ToolNode runs it, and the result returns to the model before LGOS completes the
outer Response. The client replays the completed pair and executes only returned
function calls.
The web_search declaration does not prescribe a backend. The public graph tool
is always an ordinary LangChain web_search tool. The demo can implement that
tool with a compatible SearXNG or Degoog endpoint or with a private upstream
OpenAI Responses call. Either way, the root graph exposes the same tool call and
ToolMessage, and the answer node attaches citations for exact links retained
from the tool's source metadata. Provider-specific call/result blocks stay
inside the tool.
With only web_search selected, tool_choice="required" forces search. Named
built-in choices are outside LGOS's supported tool_choice subset.
Requests selecting server tools consume completed root-graph LangGraph
updates. Selected calls and matching ToolMessage results become typed
Responses items. Nested updates are private. Custom inputs produce native
response.custom_tool_call_input.delta and done events;
completed searches emit
response.web_search_call.completed. Backend result payloads stay private.
Streaming requests also subscribe to LangGraph messages. Answer tokens are
emitted immediately, and citations are attached before the message completes.
Graphs tag private tool-selection model calls with LangGraph's nostream tag so
their text never enters the public answer. User-facing
progress uses the existing status_event() contract.
Status events remain progress-only; they do not carry tool call IDs or results.
Completed tool inputs still use one input delta; LGOS does not parse partial
tool-call arguments.
The graph must emit each public call ID once; repeated IDs are rejected. Replayed
input is graph input rather than a new update, so it is not emitted again. A
selected server call must have a result before successful completion. Streaming
failures retain partial activity on
response.failed; unselected internal tools and private graph data stay private.
The server-tool demo uses LangChain
@custom_tool, @tool, and an explicit StateGraph. A private selection node
binds the selected tools once; a native ToolNode containing only those tools
executes them, including parallel calls. A separate answer node streams the
final text. Chat Completions can still invoke the graph but
cannot select server tools.
Files And display_file¶
Portable generated files use the standard Files API plus a client-owned
function. The client offers strict display_file arguments; the graph uploads
the bytes and returns their file_id; the trusted client backend downloads and
persists the file through its native UI; and the client appends a small matching
function_call_output. Neither file bytes nor a protected bearer URL are placed
in the transcript. There is no LGOS artifact field or custom chart event. A
graph that calls the function lists display_file in GraphConfig.client_tools,
so clients offer it only to graphs that advertise it. See
Accept And Display Files.
Interrupt Operation Identity¶
An initial interrupt request does not require metadata. LGOS generates a UUID
run ID for every new interrupt-enabled run and embeds it in the paused Response
ID. Clients cannot choose the run ID, so retrying a lost initial request starts
a new run. To retry safely, create the run in the background with an
Idempotency-Key,
which returns the original run instead of starting another; the graph must also
declare GraphFeature.BACKGROUND. Background runs choose their run ID at
submission.
The run UUID is not a UI chat ID. LGOS derives a fixed-length internal
checkpointer key from a server-trusted scope, the registered model, and the
run ID, so two models or authenticated tenant scopes do not share state.
Configure the server scope from trusted request or authentication state, never
caller-controlled metadata or the end-user safety_identifier and user
fields. The default shared scope
is appropriate only for a single-tenant or shared-trust deployment.
Conversation history remains client-owned; the checkpoint contains only the
isolated workflow state needed while this operation is paused.
The authenticated scope must remain stable between the initial request and all resumes. A request resolved into another scope cannot address the pending checkpoint, even if it presents the same Response and call IDs.
Interrupt Tool Envelope¶
Every pending LangGraph interrupt becomes an OpenAI function tool call named
lgos_interrupt. Its arguments string contains the JSON payload directly:
{
"question": "How should the refund be handled?",
"choices": ["approve", "reject"],
"allow_other": true
}
Response and call IDs are opaque. The Response ID locates the paused operation; each call ID carries one native LangGraph interrupt ID. Clients must persist and return both values unchanged.
Resuming an Interrupt¶
Each pause finishes the current Response with status: "completed" and
function_call output items, following the native
Responses function-calling flow.
The workflow waits in its LangGraph checkpoint. Submitting the function outputs
creates a new Response with its own ID and a previous_response_id link to the
paused Response. The client needs no checkpoint fields or payload envelope.
Add background=true to continue the paused run in the background worker; see
Run Responses In The Background.
Clients can resume using standard OpenAI previous_response_id:
{
"model": "interruptible",
"previous_response_id": "resp_lg_f654e9041bd84fd6a8bf53a49ca25699_0123456789abcdef0123456789abcdef",
"input": [
{
"type": "function_call_output",
"call_id": "call_lg_5f2b3c9d8e7a41f0b6c2d4e8a9f01b3c",
"output": "Verify the delivery address first."
}
],
"store": false
}
Each function_call_output.output is a string, following the standard
OpenAI function-output pattern.
LGOS passes it unchanged to LangGraph as the return value of interrupt().
Graphs that need structured answers own their JSON parsing and schema validation.
Parallel interrupts are one atomic interrupt batch: the resume request must answer all of them. A client must not select one call, mix ordinary function calls into that request, duplicate a result, or synthesize a call ID. Streaming clients persist the terminal Response ID and completed function-call items instead of reconstructing them from argument deltas.
Each node invocation may call interrupt() once. To collect another answer,
finish the node and route to another node invocation, including by looping through
an edge. Parallel nodes may each interrupt once. LGOS rejects a node that reaches
a second interrupt() after it resumes, matching LangGraph's recommended
human-input validation pattern.
The UI owns persistence of the paused Response ID and exact calls. It must store them before soliciting input so a reconnect can reproduce the same resume request. Persisting only rendered prompt text or only the user's response is insufficient.
Durable Validation And Recovery¶
For an interrupt-enabled run, LGOS uses LangGraph exit durability and holds a run-scoped coordinator lease while it reads state, validates a resume, and executes the graph. Same-key contention is rejected instead of queued. Exit durability stores state when the invocation pauses or finishes without retaining every intermediate superstep. LGOS drains the invocation before it exposes interrupt tool calls. It compares the submitted native interrupt IDs with the complete pending set in the durable checkpoint before passing answers to LangGraph. The displayed interrupt payload is not part of the resume input. Concurrent work for another operation remains independent; a second request for the same operation receives HTTP 409.
LGOS preserves checkpoint state only after it produces an interrupt batch for
the client. It deletes the isolated thread after terminal completion and
best-effort after failure or cancellation before a batch. Cleanup that fails, or
does not finish within 10 seconds, can leave an unreachable thread until its
expiry deletes it; it never replaces the original execution error. If the
terminal HTTP response is lost, replaying the old resume returns a safe HTTP 409
and does not re-execute the completed operation. This is conflict detection,
not durable storage of the terminal response. For result replay, resume in the
background with an
Idempotency-Key,
so a retry returns the original Response; otherwise add a result store at the
application boundary.
An interrupted node restarts from its beginning when resumed. Any side effect
before interrupt() can therefore run again; make it idempotent or move it
after the interrupt. This is a LangGraph execution rule, documented in the
official interrupt guidance.
Moving work after interrupt() avoids replaying it when the node restarts, but
it does not make an external side effect exactly once: a process can still fail
after the effect succeeds and before its task result is durably recorded. Put
external effects in durable tasks and give the downstream operation an
idempotency key when duplicates are unacceptable; LangGraph's
idempotency guidance
describes that remaining crash window. The coordinator prevents overlapping
run execution, not crash-time exactly-once delivery.
Pending runs abandoned by users remain checkpoint data until an expiry policy
deletes them. Schedule
delete_expired_interrupt_runs with a TTL
longer than the maximum response window; do not treat ordinary database backups
or retention as an active-run cleanup policy. See
LangGraph's persistence documentation
for the underlying checkpoint model.
Known Differences From OpenAI¶
modelselects a registered LangGraph graph, not an OpenAI-hosted model.- Responses implements the explicit subset above. It has no general foreground
response storage, Conversations, or general previous-response chaining.
Background Responses exist only for opted-in polling-only runs, and the
background engine keeps them; there are no event replay, delete, input-item,
or compact routes.
Arbitrary client-defined custom tools and formats, built-in tools beyond the
documented
web_searchsubset, structured output, and unconsumed generation controls are rejected.previous_response_idis reserved for interrupt continuation, the only way to resume one: unlike an ordinary function call, a pause cannot continue from a replayed transcript. - Registered custom tools use standard Responses declarations and items but run inside LGOS; OpenAI's ordinary custom-tool flow delegates execution to caller code between model requests.
- Chat Completions remains a direct compatibility surface, while maintained demo UIs use Responses for every graph.
- The package exposes model listing/retrieval and health, but no Files storage; deploy a separate OpenAI-compatible Files service when graphs use file IDs.
- Authentication is not enforced by default.
- Token usage is present only when underlying LangChain model calls report it. LGOS aggregates reported usage across the graph run and never estimates missing counts.