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: falseunchanged; 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
502JSON 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.failedterminal. It does not turn a broken stream intoresponse.completedorresponse.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 syntheticresponse.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
200for an SSE stream; a202or an SSE body returned with an error/redirect status is rejected before any provider event is exposed - every non-empty SSE
datarecord 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 Responsestatusmust describe the same terminal state - the first valid exact top-level
response_idor nestedresponse.idlocks the stream identity; identifiers with surrounding whitespace are invalid,response.createdand every terminal require an exact non-empty nestedresponse.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.createframe 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
errorevent. A native stream that starts but ends without a valid terminal producesresponse.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_idis 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-sessionidis missing on the WebSocket handshake, gateway generates one automatically (wsresp_<uuid>). - The same session ID is injected into each upstream request created from
response.createframes, so collector/request logs can correlate multiple upstream requests from one WS session.
Example with wscat:
Then send:
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, anduser inputfollows the selected upstream contract; Ollama accepts only one string or an array of stringsencoding_formatmay be omitted or set tofloat;base64requires an OpenAI-compatible provider withcapabilities.embeddings_base64: true- Ollama rejects
dimensionsanduserbecause 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/embeddingsseparately 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