Skip to content
Version 1.0.0

The gateway relay

The HTTP interface the publisher commits to: addressing, transformations, streaming, refusals and transport limits.

This page describes what a third-party program can write against the gateway. It is the contractual part of its interface: what appears here does not change without the change being announced.

Anything that belongs to running a deployment (environment variables, roles, tables) lives on three other pages, marked “internal”.

Three surfaces make it up: the relay, through which you call a model, the offline documentation, which the gateway serves when the operator has installed it, and the question service through which an agent queries it.

ALL /:endpoint/*

The route through which a third-party program calls a model. The first path segment names an endpoint declared by the operator; everything that follows (path and query string) is concatenated after the provider’s base address. All HTTP methods go through it. It is a catch-all: it takes any first segment that the routes mounted before it have not reserved.

The gateway concatenates: the provider’s base address, as the operator declared it, followed by whatever remains of the path after the first segment, query string included.

appel https://passerelle.interne/sonnet/v1/messages?beta=1
endpoint sonnet
relayé <adresse de base du fournisseur>/v1/messages?beta=1

Three consequences that addressing imposes:

  • the first segment is removed by its length, so only that one. A path that repeats the endpoint name further along keeps it;
  • the path leaves as the caller wrote it, without being reformatted;
  • / and // are not gateway addresses. They carry no first segment, the route does not recognize them, and the response is the one from the underlying HTTP framework: text, not the JSON envelope described below.

Where the caller presents its proof of identity

Section titled “Where the caller presents its proof of identity”

Two headers, and no others:

Authorization: Bearer <jeton>
x-api-key: <clé>

Both are removed from the request before it leaves for the provider: the proof presented to the gateway does not travel beyond it.

These headers are removed:

  • host
  • accept-encoding
  • authorization
  • x-api-key
  • content-length

In place of the removed proof, the gateway sets the provider’s key, in the header the operator declared for it.

The body is relayed as is, with one exception: if it parses as JSON, its model field is overwritten by the model of the target endpoint. A body that does not parse as JSON leaves unchanged.

The gateway validates neither the body’s schema, nor its content type, nor its size.

These headers are removed:

  • content-encoding
  • transfer-encoding

The provider’s status, other headers and body are relayed as is.

The response is relayed as it arrives as soon as either of two conditions is met:

  • the caller requested "stream": true in a JSON body;
  • the provider responds with a text/event-stream content type.

Either one is enough: a provider that omits the content type must not reduce the exchange to a buffered response.

When streaming, these headers are removed as well:

  • content-length

They all carry the same envelope, { "error": "<phrase>" }, except the 404 for an unknown endpoint, which carries a second field.

A refusal issued by the provider is not in this table: it is relayed as is, with its own body. Those of the offline documentation have their own table, below.

CodeWhenBody
401No usable proof of identity accompanies the request: neither Authorization: Bearer nor x-api-key, or one of the two is empty.{ "error": "Missing credentials. Set apiKey in your Lemniscate config." }
401A proof is presented and refused. The gateway does not say why: the reason goes to the decision log, because telling the caller what did not pass helps them guess what would have.{ "error": "Unauthorized." }
403The subject appears on the instance’s revocation list, and the revocation has not been lifted. The reason and the author of the revocation stay in the log.{ "error": "Your access to this instance has been revoked. Contact the operator of this deployment." }
403The call names, through the x-lemniscate-session header, an agent session that administration has revoked, or a sub-agent of such a session. Only that session is refused: the same person, under another session or naming none, is served. The response carries the time of the revocation and the x-lemniscate-session-standing: revoked header; the author and reason stay in the log.{ "error": "This agent session was revoked by your organization's administration.", "code": "agent-session-revoked", "revokedAt": "<instant ISO>" }
404The first path segment names no declared endpoint. This is the only refusal whose body carries a second field: available lists the existing endpoint names.{ "error": "Unknown endpoint: <nom>", "available": ["<nom>", "…"] }
403The endpoint exists but the operator has disabled it.{ "error": "Endpoint \"<nom>\" is disabled." }
403The deployment’s license does not cover this request. Three reasons lead here, and the returned body says which: the number of licensed seats is reached for the current month, the requested capability is not included in the license, or the license has expired and its grace period is over. The check is placed after endpoint resolution (its name is needed to judge the capability) and before authorization: a deployment without a license right has no business consulting the role policy. This refusal exists only on the on-premise profile: in serverless, commercial limits go through billing.{ "error": "The licensed seat limit for this deployment has been reached for the current month. Contact the operator to increase the seat count." } — ou { "error": "The capability \"<nom>\" is not included in this deployment's license." } — ou { "error": "The deployment license has expired and the grace period has elapsed. Contact the operator to renew the license." }
403Identity is established, and no role held by the subject grants the right to call this endpoint. This is also everyone’s state on a fresh instance: with no role assigned, the set of permissions is empty.{ "error": "Your identity is recognised, but no role you hold carries the right to call this endpoint." }
403No plan is assigned to the account. Distinct from the cap being reached: no payment is charged to a plan that does not exist, and the following month changes nothing. Not applicable in a customer deployment, where there is nothing to resell.{ "error": "No plan assigned. Ask your administrator to assign one." }
402The plan’s monthly cap is reached. Not applicable in a customer deployment.{ "error": "Plan limit reached" }
503The gateway could not read what it needs in order to decide: the subject revocation list, the agent session list, or the roles and the policy in force. It refuses rather than assume: a policy installed to restrict, silently replaced by the broadest one, would be an opening disguised as fault tolerance.{ "error": "Authorization decision could not be made, so the request was refused." }
503Approval was granted, and the decision log could not record it. An approval the log did not take is not an approval.{ "error": "Authorization decision could not be recorded, so the request was refused." }
500The endpoint names an environment variable for the provider’s key, and that variable is missing from the gateway’s environment. This is a configuration failure on the operator’s side, not a rights issue.{ "error": "Missing env variable: <nom de la variable>" }
502The gateway could not reach the provider: unreachable corporate proxy, missing internal certificate authority, rejected client certificate. 502 and not 500, because the failure is downstream.{ "error": "<le message de la couche de sortie réseau, qui nomme la variable à poser>" }
503The billing database did not respond when checking the account’s plan and cap. The gateway refuses rather than serve a call it knows it cannot count, and it refuses before calling the provider, so nothing is paid for. This refusal exists only on the serverless profile: on-premise there is nothing to bill.{ "error": "Usage cannot be metered right now, so the request was refused rather than served unbilled. Nothing was sent upstream." }
503The provider responded, and the billing record for that response could not be written. The response is withheld rather than served for free: the call to the provider is already paid for. Concerns only non-streamed responses: on a stream, the caller has already received everything when the write fails.{ "error": "This answer could not be metered, so it was not served. Nothing was billed for it either." }

What the caller must plan for, and above all what nothing protects them against:

LimitState
Request duration capIt exists. 1800 seconds by default, adjustable by the operator.
Rate limitNone.
Body size limitNone.
CORS preflight requestNot handled.
Correlation identifier returnedNone. The gateway reads x-request-id for its log, it does not return it.

These absences are contractual information just as much as an error code: whoever builds on this gateway must know that nothing protects them from a call that is too large or too frequent.

The gateway serves the product documentation when the operator has installed it. That is what makes it readable on a closed network, where the public site is unreachable by design.

Two addresses, and a single reserved path segment:

RouteWhat it does
ALL /vThe root of the segment the documentation reserves. It redirects to the installed version, which gives an entry address that does not change when the deployment is updated. Read-only and without authentication, like everything that lives under this segment.
ALL /v/*The pages, stylesheets, search index and agent files of the documentation archive the operator has unpacked (D-33). An address carrying the installed version number serves the file; an address carrying none redirects to the installed version; another number is refused. Read-only, without authentication, and never a directory listing.

Three properties to know before writing against these addresses:

  • No authentication is required, and a presented proof has no effect. The same pages are published without an account on the public site, and a browser cannot present the proof headers described above.
  • The first segment is reserved. A relay endpoint carrying that name would be masked by these two routes, which are mounted before the relay.
  • The address without a version number is stable. An address that carries no number redirects to the installed version: it stays valid after a deployment update, which makes it the value to configure rather than the address carrying the number.

What the gateway refuses under these addresses:

CodeWhenBody
501No documentation archive is declared on this deployment. The segment stays reserved all the same: a segment that only appeared once the archive was unpacked would let an endpoint carrying that name work, until the day the operator installs it.{ "error": "This gateway serves no documentation: no directory is configured (LEMNISCATE_DOCS_DIR). Ask the operator of this deployment to unpack the documentation archive shipped with the server release and point the gateway at it." }
405A method other than read is used under the documentation segment.{ "error": "Documentation is read-only: only GET and HEAD are served under this path." }
404The address targets a version number other than the installed one. It is not redirected to the one that is: returning a version other than the one requested would be an invisible lie, since the pages look alike.{ "error": "This gateway serves documentation version <numéro installé>, at <base>. Version <numéro demandé> is not installed here." }
400The path tries to escape the archive: a segment that goes up, an encoded slash, an invalid encoding.{ "error": "Not a documentation path." }
404No file matches the address under the installed version, or the target directory has no index page: a directory never returns a listing of its contents.{ "error": "No such documentation page." }

Nothing is served until the operator has set LEMNISCATE_DOCS_DIR. This setting is described on the environment variables page.

The archive’s two machine files (llms.txt and llms-full.txt) are read directly, without a version number: /v/llms.txt and /v/llms-full.txt. They return the entire corpus.

For a targeted question, two more addresses, under the same reserved segment:

RouteWhat it does
ALL /v/_searchA question, and the passages of the installed documentation that cover it, with each page’s address on this gateway. No language model is called: the service returns text it has, never text it fabricates, and when nothing covers the question it returns an empty list and says so. Read-only and without authentication, like everything that lives under this segment.
ALL /v/_mcpThe protocol through which an agent queries the installed documentation: JSON-RPC 2.0, stateless, without an event stream, a single search tool. It only translates the search route below, and returns the same data. No language model is called. POST only; a read receives a refusal, as this server opens no event channel.

No language model is called, and that is the property to know before writing against these addresses. The service returns passages of the documentation, never a drafted answer. Three consequences:

  • what it returns is published text, verifiable by following the address attached to each passage;
  • it costs nothing per question, and nothing leaves the machine hosting it;
  • when nothing covers the question, it returns an empty list and says so. The answered field is then false and a note field carries the sentence. An empty list is a fact about the documentation, not a service failure.
GET /v/_search?q=comment+plafonner+la+depense+d+un+client&limit=3
{ "version": "1.0.0", "base": "/v/1.0.0/",
"query": "comment plafonner la depense d un client",
"terms": ["plafonner", "depense", "client"],
"answered": true,
"results": [ { "page": "Plafonner la dépense d'un client", "section": null,
"url": "/v/1.0.0/guides/plafonner-la-depense/",
"excerpt": "…", "score": 11.4, "coverage": 1 } ] }

The MCP server exposes the same service under the protocol that agent clients know how to speak. It is stateless: each message is independent, no session is opened, and a read is refused since it has no events to push. It implements initialize, ping, tools/list and tools/call, and offers a single tool, search_documentation, whose structured data is exactly the response above.

What these two addresses refuse:

CodeWhenBody
501No documentation archive is declared on this deployment, so there is nothing to query. The path stays mounted all the same, for the same reason as the rest of the segment: an address that only appeared once the archive was unpacked would hide the reservation until that day.{ "error": "This gateway serves no documentation, so it cannot answer questions about it (LEMNISCATE_DOCS_DIR is not set). Ask the operator of this deployment to unpack the documentation archive shipped with the server release and point the gateway at it." }
501The unpacked archive does not carry llms-full.txt, the machine corpus the index is drawn from. The pages keep being served: only the search is unavailable. The gateway does not refuse to start for all that: shutting it down entirely, and with it the relay to the models, over a convenience file would be out of proportion.{ "error": "The documentation installed here carries no llms-full.txt, which is the machine-readable corpus this search reads. Pages are still served; only search is unavailable. That file sits at the root of every documentation archive, so the most likely cause is a partly unpacked archive." }
405A method other than read is used on the search.{ "error": "Documentation search is read-only: only GET and HEAD are served at this path." }
400No question accompanies the call, or it is empty. The refusal names the parameter and gives a complete example, rather than returning an empty list that a caller would read as “the documentation does not know”.{ "error": "Ask a question with the \"q\" query parameter, for example /v/_search?q=how+do+I+disable+telemetry. Add \"limit\" to ask for fewer or more passages." }
405A read is attempted on the MCP server. The protocol opens its event channel with a read; this server has nothing to push and refuses, rather than open a silent channel that a client would wait on indefinitely.{ "error": "This MCP endpoint answers POST only. It is stateless and opens no event stream. To read the same answers without MCP, use /v/_search?q=…" }
400The request body does not parse as JSON. A protocol error on a well-formed message, by contrast, is returned in JSON-RPC with a 200 status: the transport did not fail.{ "error": "This MCP endpoint expects a single JSON-RPC 2.0 message as the request body." }

There is no OpenAPI description, and that is deliberate

Section titled “There is no OpenAPI description, and that is deliberate”

The relay does not define an interface of its own: it concatenates the caller’s address after the provider’s. An OpenAPI file could therefore only describe the catch-all route above (from which no useful client is generated) or else copy the provider’s interface, which is not ours and would go stale here with every change on their side.

Above all, it would announce a request shape the gateway does not check: it validates neither schema, nor content type, nor size. What you really need to know to write against it is the concatenation rule and the refusal table, and that is what this page gives.