Skip to content

HTTP API

Endpoints

Chat and embeddings

POST /v1/chat/completions

OpenAI-compatible chat completions endpoint.

Stored Chat Completions lifecycle

These routes proxy the native OpenAI-compatible stored Chat Completions lifecycle:

Method and path Operation
GET /v1/chat/completions list stored completions
GET /v1/chat/completions/{completion_id} retrieve one stored completion
POST /v1/chat/completions/{completion_id} update its metadata
DELETE /v1/chat/completions/{completion_id} delete it
GET /v1/chat/completions/{completion_id}/messages list its stored messages

The provider must be OpenAI-compatible and explicitly enable capabilities.chat_completions_lifecycle. A successful Chat-to-Chat creation with store: true binds the returned upstream ID to the creating provider account; lifecycle calls do not retry or fall back to another target. If more than one provider enables the capability, a list request must select one with X-LunarGate-Provider. After a binding expires or the process restarts, an ID-specific operation also needs an explicit provider.

For store: true Chat-to-Chat creation with lifecycle enabled, LunarGate binds only the original upstream identity. A non-stream response requires an exact, non-empty id and exact object: chat.completion. Every stream chunk requires exact object: chat.completion.chunk and one unchanged ID. A malformed, padded, missing, synthetic, or changing identity fails closed and is never bound. store: false retains ordinary compatibility normalization.

POST /v1/responses

OpenAI-compatible Responses API endpoint (regular JSON and SSE streaming).

POST /v1/embeddings

OpenAI-compatible embeddings endpoint.

Responses

GET /v1/responses

OpenAI-compatible WebSocket mode for Responses API.

GET /v1/responses/{response_id}

Retrieves a stored response. A local snapshot rejects include, include_obfuscation, and starting_after; native query parameters are proxied to the bound owner. A successful native resource must return exact object: response and the requested ID.

DELETE /v1/responses/{response_id}

Deletes a stored response. Local deletion returns 204 No Content with an empty body; deleting the same local ID again returns 404. Native deletion is proxied to the bound owner.

POST /v1/responses/{response_id}/cancel

Cancels an in-progress native response when the owning provider declares cancellation support. A local snapshot is already terminal, so local cancellation returns 400 unsupported_feature. A successful native resource must return exact object: response and the requested ID.

GET /v1/responses/{response_id}/input_items

Lists the input items retained for a stored response. Native query parameters are proxied to the owner. Locally, after is an exact opaque cursor, limit must be 1–100 and defaults to 20, order is asc or desc and defaults to desc, and include is rejected.

POST /v1/responses/compact

Compacts a Responses input through one deterministically selected native provider. Select it with X-LunarGate-Provider or a canonical provider/model when more than one configured provider enables compaction. If exactly one provider enables the capability, LunarGate selects it automatically. Because this operation is stateful, LunarGate does not retry it or send it through fallback targets. A successful result must be one JSON object no larger than 16 MiB with exact object: response.compaction and a non-empty exact ID.

POST /v1/responses/input_tokens

Counts input tokens through one deterministically selected native provider. Selection follows the same rules as compaction: one capable provider is selected automatically, while multiple capable providers require X-LunarGate-Provider or a canonical provider/model. The operation does not retry or use fallback targets.

Generated Responses wire contract

Responses synthesized from Chat Completions expose the same stable resource fields over regular HTTP, SSE, and WebSocket. Supported request controls are reflected in the resource. Omitted controls use store: true, parallel_tool_calls: true, temperature: 1, top_p: 1, tool_choice: auto, empty tools and metadata, text format text, and truncation disabled. Nullable fields are emitted as null, and completed resources include completed_at.

Generated text is carried in output[].content[]. LunarGate does not emit a top-level output_text field in generated wire responses; output_text is an SDK convenience accessor derived from output content.

When translated usage is available, input_tokens_details always contains cached_tokens and cache_write_tokens, while output_tokens_details always contains reasoning_tokens, including zero values. Chat reasoning-token counts are preserved. If usage is unavailable, usage is null. A valid native terminal payload remains upstream-defined and is not supplemented with invented usage.

Conversations

POST /v1/conversations

Creates a conversation, optionally with initial items and metadata.

GET /v1/conversations/{conversation_id}

Retrieves a conversation.

POST /v1/conversations/{conversation_id}

Updates conversation metadata.

DELETE /v1/conversations/{conversation_id}

Deletes a conversation.

POST /v1/conversations/{conversation_id}/items

Adds one or more items to a conversation.

GET /v1/conversations/{conversation_id}/items

Lists conversation items. Locally managed conversations support after, limit, and order pagination.

GET /v1/conversations/{conversation_id}/items/{item_id}

Retrieves one conversation item.

DELETE /v1/conversations/{conversation_id}/items/{item_id}

Deletes one conversation item.

Responses and Conversations objects remain bound to the provider or local store that created them. Follow-up operations do not use another route, retry, or fallback target. Native lifecycle and Conversations calls require explicit provider capabilities; unsupported native operations fail instead of being approximated by a lossy translation.

An explicit provider or canonical provider/model selects native Conversations; one capable provider is selected automatically. With no capable provider, LunarGate uses the local store. Multiple capable providers without an explicit selection return ambiguous_provider.

Local item batches are atomic and limited to 20 items. Known message, function_call, function_call_output, and reasoning shapes are validated: messages require a supported role and content, function items require a correlation call_id, and reasoning requires an ID and summary array. item_reference is unsupported locally. Additive fields and unknown future item types remain opaque and round-trip unchanged. Native requests remain pass-through, while successful native responses are bounded to 16 MiB and validated for their minimal object, ID, and deleted-resource contract; an invalid 2xx response returns 502 without changing the binding.

Opaque resource identifiers

Resource identifiers are opaque and must be reused exactly as returned. Gateway-interpreted response_id, completion_id, conversation_id, item_id, previous_response_id, conversation IDs, tool correlation IDs, and local after cursors must be non-empty and contain no surrounding whitespace. Internal whitespace is legal and significant and must be URL-encoded in paths. LunarGate never trims an identifier into an alias. Invalid client values return 400 invalid_request_error with the affected parameter and code: invalid_value. Native resource objects repeat the exact requested ID wherever the operation defines that contract.

Stateful ownership conflicts and recovery

Native object ownership is scoped to the configured provider account, including its provider type, endpoint, organization where applicable, and credentials where applicable. Response bindings also retain the selected route, model, upstream protocol, and whether the object has a local snapshot. LunarGate retains the first owner it observes for a Chat Completion, Response, or Conversation ID.

If a different provider account later returns the same object ID, LunarGate replaces the owner with a conflict tombstone. An implicit lifecycle or continuation request for that ID then returns an OpenAI-style 400 invalid_request_error with code: provider_binding_conflict. It does not guess an owner, route the operation elsewhere, retry it, use a fallback, or call either conflicting upstream.

If the client knows which native provider owns the object, it can explicitly set X-LunarGate-Provider for a targeted recovery or cleanup call. This does not make later implicit calls unambiguous; the conflict tombstone remains fail-closed until it expires. A successful native delete removes an ordinary matching owner binding, but never erases a conflict tombstone created concurrently.

If an account-sensitive provider setting changes after an object was bound, implicit follow-up calls return 400 invalid_request_error with code: provider_binding_stale. If a local Response snapshot loses its matching owner binding through expiry, eviction, or conflict, LunarGate discards the orphaned snapshot. Retrieval, input-item access, cancellation, deletion, and previous_response_id continuation never expose, mutate, or reroute that orphaned state.

Gateway error contract

Gateway-generated errors use the OpenAI-style error envelope. Routing and client-lifecycle failures have stable status and type/code semantics:

Condition HTTP status error.type error.param error.code
no configured route matches 502 routing_error omitted omitted
explicitly requested provider is unavailable in the first matching route 400 invalid_request_error provider provider_not_found
explicitly requested model is unavailable in the first matching route 400 invalid_request_error model model_not_found
a selected target cannot preserve a requested feature 400 invalid_request_error affected field unsupported_feature
client context is cancelled or its deadline expires 499 client_cancelled omitted omitted

Requested provider and model failures happen before an upstream call. The first matching route is authoritative, so LunarGate does not search a later route to avoid either error. A client termination also stops retry and fallback and is not counted as a provider or circuit-breaker failure. If the client has already disconnected, it may of course be unable to receive the 499 response body.

Models and health

GET /v1/models

Returns the discovered and configured models visible through the gateway.

GET /v1/models/{model}

Returns details for a single discovered/configured model.

GET /health

Container and process health endpoint.

GET /ready

Readiness endpoint for orchestration and probes.

GET /metrics

Prometheus metrics scrape endpoint.

Stateful API retention

Translated Responses and locally managed Conversations use bounded, in-memory state:

State Retention and limits
stored Responses 30-minute TTL, at most 1,000 entries, 64 MiB shared byte budget
local Conversations 30-minute TTL, at most 1,000 conversations, 1,000 items per conversation, 64 MiB shared byte budget
native object owner bindings 30-minute TTL, at most 1,000 IDs per object type, 1 MiB shared byte budget per object type

The oldest eligible entries are evicted when an entry or byte limit is reached. State is process-local: it is lost on restart and is not shared between gateway replicas. Use a native provider contract when durable, cross-replica lifecycle state is required.

For Responses, store defaults to true. Setting store: false prevents the Response and its owner binding from being retained for later retrieval or previous_response_id continuation. It also bypasses the exact-match response cache.

An explicitly associated Conversation is independent of Response storage:

  • for a locally managed Conversation, LunarGate appends the request input and completed output to that Conversation even when store: false
  • for a native Conversation, LunarGate forwards the Conversation association and the explicit store: false unchanged; the native provider owns the Conversation mutation, and LunarGate does not copy it into local Conversation state

In both cases, store: false controls storage of the Response object, not the explicitly associated Conversation.

For a locally retained translated Response, previous_response_id continuation replays every output item from the previous response before the new input. The retained history is not reduced to only assistant messages and function calls: reasoning, computer/program/tool items, and additive fields on those items remain part of the continuation payload.

Before that retained history is sent through a translated, non-native Responses target, LunarGate verifies that every translatable message, function call, and function-call output has either no lifecycle status or status: completed. An incomplete or unknown item status cannot be represented faithfully by Chat Completions, Anthropic Messages, or Ollama Chat, so the continuation returns status 400 with type: invalid_request_error, the precise item status path, and code: unsupported_feature without contacting an upstream. A native Responses target continues to receive the original item and status unchanged.

Native object ownership is also remembered only in bounded process memory so follow-up calls can return to the creating provider. Keep a stateful client on the same gateway process, or use one replica, until the native object lifecycle is complete.

Request and streaming limits

Boundary Limit
one HTTP JSON request body 10 MiB; an oversized body returns 413
one Responses WebSocket client message 10 MiB
one upstream SSE event or NDJSON record 4 MiB
native Responses SSE preflight at most 1,024 complete records and one 4 MiB record budget before the first useful event
one translated Responses stream's accumulated state 16 MiB and at most 128 tool calls
Responses WebSocket continuation cache one prior response state and 16 MiB per connection

These bounds are independent of the stored-state TTL and entry limits above. If a stream violates a record or accumulated-state limit after delivery has begun, the gateway follows the failure-terminal contract below instead of retaining a falsely completed response.

Example chat completion request

curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.2",
    "messages": [
      {"role": "user", "content": "Hello from LunarGate"}
    ]
  }'

Example embeddings request

curl http://localhost:8080/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "ollama/nomic-embed-text-v2-moe",
    "input": [
      "LunarGate can proxy embeddings requests.",
      "Embeddings are useful for semantic search."
    ]
  }'

Custom request headers

Header Description
X-LunarGate-Provider Force a specific provider
X-LunarGate-Model Override the model
X-LunarGate-Route Force a named route
X-LunarGate-SessionID Session correlation identifier used in request metadata/logs
X-LunarGate-No-Cache Bypass cache when set to true
X-LunarGate-No-Retry Disable retries when set to true
X-LunarGate-No-Fallback Disable fallback targets when set to true

Safe upstream control headers

LunarGate forwards only a small provider-specific allowlist from the client request:

Upstream type Forwarded client headers
OpenAI-compatible Idempotency-Key, OpenAI-Beta
Anthropic Anthropic-Beta
Ollama none

Configured credentials and account headers always win. LunarGate does not copy client Authorization, X-Api-Key, OpenAI-Organization, cookies, or arbitrary extension headers to an upstream. Native lifecycle proxy calls additionally preserve Accept. A WebSocket handshake's Idempotency-Key is not reused for its later response.create messages, because one key must not identify multiple creates.

Response headers

Header Description
X-LunarGate-Request-ID Unique request identifier
X-LunarGate-Provider Provider that served the request
X-LunarGate-Model Model used for the request
X-LunarGate-Route Route that matched
X-LunarGate-Cache-Status HIT or MISS
X-LunarGate-Latency-Ms End-to-end latency in milliseconds
X-LunarGate-Overhead-Duration-Ms Gateway overhead timing header

Streaming

The gateway supports SSE streaming on chat completions and Responses endpoints.

Startup and terminal behavior

The gateway does not commit a successful SSE response until it has received one complete, usable provider record. A normal upstream HTTP error still follows the gateway's status and error-envelope preservation rules.

  • If the upstream transport or stream ends, sends malformed data, or exceeds a record limit before that point, LunarGate returns a regular 502 JSON error rather than a partial SSE response.
  • If a Chat Completions upstream stream fails after delivery has begun, LunarGate emits exactly one canonical Chat error chunk followed by exactly one data: [DONE] terminator.
  • If a Responses upstream stream fails after delivery has begun, LunarGate emits exactly one response.failed terminal. It does not turn a broken stream into response.completed or response.incomplete.
  • For a native Responses stream, the first valid upstream terminal (response.completed, response.incomplete, response.failed, or cancelled/canceled) is authoritative. A read error, truncated stream, or end-of-stream without a terminal produces a synthetic response.failed; that synthetic failure is not retained as native lifecycle state.

Native Responses SSE validation

LunarGate preserves accepted native frames byte-for-byte unless it must inject a gateway-managed local Conversation association, but native passthrough is not an unvalidated pipe:

  • the upstream must return exact HTTP status 200 for an SSE stream; a 202 or an SSE body returned with an error/redirect status is rejected before any provider event is exposed
  • every non-empty SSE data record must be one strict JSON object; malformed, non-object, oversized, or unterminated records fail closed
  • leading comments and empty records are retained only within the bounded preflight budget shown above
  • a terminal must contain a Response object, and any supplied SSE event name, JSON type, and nested Response status must describe the same terminal state
  • the first valid exact top-level response_id or nested response.id locks the stream identity; identifiers with surrounding whitespace are invalid, response.created and every terminal require an exact non-empty nested response.id, and every later explicit ID must match the locked value
  • every non-empty event requires an exact non-negative integer sequence_number, strictly greater than the previously accepted value; a native series may start above zero
  • the first valid terminal is the only terminal forwarded; LunarGate closes the upstream body immediately and does not expose later deltas or duplicate terminals

An invalid native event before streaming starts returns the regular JSON 502 error. The same violation after an accepted event has been delivered produces one synthetic response.failed using the locked Response identity observed earlier in the stream, or a generated identity if no event had established one. Client cancellation and downstream write failure are exceptions: LunarGate stops immediately and does not try to write another terminal to a closed or failed client connection.

Every gateway-generated Responses event has a sequence_number. A synthetic series starts at 0 and increments by exactly one. LunarGate does not synthesize event_id. If native streaming fails after accepted events, the generated response.failed event uses the next sequence number. WebSocket warmup uses 0 for response.created and 1 for response.completed; a standalone WebSocket error starts at 0.

For translated Responses streams, LunarGate requests usage from the upstream adapters and includes any observed usage in the terminal response according to the generated wire contract above. Chat Completions stream usage is emitted only when the client requests stream_options.include_usage: true.

Example streaming request:

curl -N http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.2",
    "stream": true,
    "messages": [
      {"role": "user", "content": "Write a short haiku about LunarGate."}
    ]
}'

Responses SSE request:

curl -N http://localhost:8080/v1/responses \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.2",
    "stream": true,
    "input": "Write a short haiku about LunarGate."
  }'

Responses WebSocket mode

Use GET /v1/responses with a WebSocket client and send response.create frames.

  • Gateway converts each response.create frame into a Responses request and always streams events back.
  • Events are sent as JSON WebSocket messages (response.created, response.output_text.delta, response.completed, and error events).
  • Request or upstream HTTP failures that occur before a usable response stream are returned as an error event. A native stream that starts but ends without a valid terminal produces response.failed; the terminal and usage rules above otherwise apply unchanged.
  • Native response-ID locking and terminal validation use the same rules as HTTP SSE. An invalid native event is never cached as WebSocket continuation state.
  • previous_response_id is validated against response IDs created earlier on the same WebSocket connection.
  • One request is processed at a time per connection.
  • The connection keeps the provider/routing/model-selection generation captured at its handshake. A hot reload affects newly established connections, not later messages on an existing connection.
  • If x-lunargate-sessionid is missing on the WebSocket handshake, gateway generates one automatically (wsresp_<uuid>).
  • The same session ID is injected into each upstream request created from response.create frames, so collector/request logs can correlate multiple upstream requests from one WS session.

Example with wscat:

wscat -c ws://localhost:8080/v1/responses

Then send:

{"type":"response.create","model":"openai/gpt-5.2","input":"Say hello from LunarGate"}

Compatibility notes

LunarGate normalizes some client payload variants before routing to upstream providers. That helps preserve OpenAI compatibility even when upstream or intermediate clients serialize text content differently.

Chat/Responses translation preserves significant leading and trailing whitespace, newlines, and code indentation in text, reasoning, and refusal fields. Whitespace is used only to detect semantic emptiness, never as the emitted value. Refusals map to protocol-native refusal fields rather than ordinary assistant text.

Responses incomplete reasons max_output_tokens and max_messages map to Chat finish_reason: length; content_filter maps to content_filter. Unknown incomplete reasons fail closed instead of being reported as successful or silently coerced.

For embeddings specifically:

  • the public endpoint is POST /v1/embeddings
  • accepted request fields are model, input, encoding_format, dimensions, and user
  • input follows the selected upstream contract; Ollama accepts only one string or an array of strings
  • encoding_format may be omitted or set to float; base64 requires an OpenAI-compatible provider with capabilities.embeddings_base64: true
  • Ollama rejects dimensions and user because its embeddings API has no faithful equivalents
  • native OpenAI-compatible responses retain additive envelope fields and exact numeric embedding values
  • a common routing pattern is to match /v1/embeddings separately from /v1/chat/completions
  • fallback candidates that cannot preserve the requested embeddings contract are skipped
  • local Ollama is a good smoke-test target for embeddings before building retrieval or RAG flows