Skip to content

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 model values.
  • 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 curl as 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; and
  • POST /v1/responses/{response_id}/cancel for 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:

{
  "tools": [
    {"type": "custom", "name": "package_version"},
    {"type": "web_search"}
  ]
}

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 custom tool has free-form string input. LGOS executes it and returns its custom_tool_call and matching custom_tool_call_output in the same response.
  • A client function retains JSON object arguments and client-side execution.
  • web_search uses OpenAI's built-in declaration and web_search_call output.

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

  • model selects 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_search subset, structured output, and unconsumed generation controls are rejected. previous_response_id is 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.