# Local Model Service Protocol (LMSP) 1.0, Draft > Made for Agent publishes open, machine-readable protocols that let AI agents, models and tools discover and talk to each other. It starts with LMSP, the Local Model Service Protocol. Source: https://madeforagent.com/llms-full.txt Updated: 2026-11-08 --- # Local Model Service Protocol (LMSP) > LMSP is an open protocol for discovering and calling language models that run on your own device. It defines service discovery, a model catalog, streaming, persistent tasks and OpenAI compatibility. Source: https://madeforagent.com/lmsp/ Language: en Updated: 2026-11-08 ## What LMSP does {#what} LMSP (Local Model Service Protocol) is an open protocol for software that runs language models on a user's own device or local network, and for the clients that use that software. A resident local service describes itself at a well-known address and lists its models. It answers generation requests with JSON or server-sent events, and runs long generations as persisted tasks that can be cancelled and followed again after a disconnect. The same service also speaks the OpenAI API, so the clients that exist today keep working. LMSP does not replace the OpenAI API, Ollama or MCP. It combines their strengths in a protocol layer built for resident local services. ## At a glance {#glance} | Topic | Summary | | --- | --- | | Default address | `http://127.0.0.1:17832` | | Service description | `GET /.well-known/lmsp.json` | | Core interface | `/lmsp/v1`, JSON over HTTP, streaming with server-sent events | | Compatibility interfaces | `/v1` for OpenAI clients, optional `/api` for Ollama clients | | Discovery | Manual address, probing of local addresses, optional mDNS and DNS-SD (`_lmsp._tcp.local.`) | | Authentication | Bearer tokens obtained through local pairing | | Persistence | Embedded SQLite, with tasks that survive a restart | | Contract | OpenAPI 3.2 | ## Try it {#try} If an LMSP service is running on your machine, this is the whole flow. Pair first to get a token, as described in [section 10.2](https://madeforagent.com/lmsp/1.0/security.md#pairing), and keep it in `LMSP_TOKEN`. ```sh # 1. Discover the service curl http://127.0.0.1:17832/.well-known/lmsp.json # 2. List the local models curl http://127.0.0.1:17832/lmsp/v1/models \ -H "Authorization: Bearer $LMSP_TOKEN" # 3. Generate a response curl http://127.0.0.1:17832/lmsp/v1/responses \ -H "Authorization: Bearer $LMSP_TOKEN" \ -H "Content-Type: application/json" \ -d '{"model":"qwen3:14b-q4_k_m","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Hello"}]}]}' ``` ## How LMSP relates to other protocols {#related} | Protocol | What it covers | Relation to LMSP | | --- | --- | --- | | OpenAI API | Hosted models. The Responses API is its newest interface. | LMSP follows the Responses structure for its core interface, and a service offers an OpenAI-compatible interface next to it. | | Ollama | A local runtime that already serves OpenAI-compatible endpoints. | It can sit behind an LMSP service as an adapter, and LMSP defines optional Ollama-compatible endpoints. | | MCP | Connects agents to tools and context. | LMSP connects agents to models. Both prefer stateless discovery over mandatory sessions. | | A2A | Collaboration between agents. | LMSP borrows service cards and explicit task states for its service description and its tasks. | ## Resources {#resources} - The [specification, version 1.0 (draft)](https://madeforagent.com/lmsp/1.0.md). - The OpenAPI contract, as [JSON](https://madeforagent.com/lmsp/1.0/openapi.json) or [YAML](https://madeforagent.com/lmsp/1.0/openapi.yaml). - [llms.txt](https://madeforagent.com/llms.txt) and [llms-full.txt](https://madeforagent.com/llms-full.txt), for language models. - Every page is available as Markdown. Replace the trailing slash of its address with `.md`, for example [`/lmsp/1.0/tasks.md`](https://madeforagent.com/lmsp/1.0/tasks.md). ## Frequently asked questions ### What is LMSP? LMSP, the Local Model Service Protocol, is an open protocol for language-model services that run on a user's own device or local network. It defines how clients discover a service, list its models, request responses, stream output and run long tasks. ### Does LMSP replace the OpenAI API, Ollama or MCP? No. LMSP builds on them. Its core interface follows the OpenAI Responses structure, a service can offer OpenAI-compatible and Ollama-compatible endpoints, and the protocol borrows stateless discovery from MCP and task states from A2A. ### Can existing OpenAI clients use an LMSP service? Usually yes. They need a different base URL and the LMSP token as their API key. They do not find the service automatically, because discovery needs a client that supports LMSP. ### How does a client find a local LMSP service? It tries three things in order. First an address entered by hand, then the conventional local address, where it requests /.well-known/lmsp.json, and finally optional mDNS and DNS-SD browsing for _lmsp._tcp.local. on the local network. ### Is a model service on localhost safe? Not automatically, because web pages can try to reach local ports. LMSP services bind to loopback by default, require bearer tokens obtained by local pairing, check the Origin and Host headers, and require authentication in LAN mode. ### What is the status of the specification? Version 1.0 is a first public draft and can still change incompatibly. The English text is normative, and translations are provided for convenience. --- # Overview > LMSP 1.0 draft overview. What the Local Model Service Protocol is, the designs it builds on, its layered architecture, how to read the specification, and the terms used throughout. Source: https://madeforagent.com/lmsp/1.0/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 1.1 Abstract {#abstract} The Local Model Service Protocol (LMSP) defines how clients find and use language-model services that run on a user's own device or local network. It specifies service discovery, a model catalog, synchronous and streaming generation, and asynchronous tasks that survive restarts. It also defines how one service offers an OpenAI-compatible interface next to the LMSP interface, so existing clients keep working. LMSP does not replace the OpenAI API, Ollama or MCP. It combines their strengths into a protocol layer built for a resident local service. ## 1.2 Status of this document {#status} This is the first public draft of LMSP 1.0, published on 2026-11-08. "LMSP" is a working name. The text is open for feedback and may change incompatibly until 1.0 is final. The English text is normative. Translations are provided for convenience; where they differ, the English text prevails. ## 1.3 Design basis {#design-basis} LMSP reuses proven designs instead of inventing new ones: - **OpenAI Responses API.** Structured output items, semantic server-sent events, and background execution with query and cancel for long-running work. This is the reference for LMSP's core conversation interface. See [streaming responses](https://developers.openai.com/api/docs/guides/streaming-responses) and [background mode](https://developers.openai.com/api/docs/guides/background). - **Ollama.** It serves OpenAI-compatible `/v1/models`, `/v1/chat/completions` and `/v1/responses` endpoints, which shows that one service plus an OpenAI adaptation layer sharply lowers the cost of adoption. See [Ollama's OpenAI compatibility](https://docs.ollama.com/api/openai-compatibility). - **Model Context Protocol (MCP).** Its latest design moves from mandatory session initialization to stateless discovery: every request carries its own version information, and clients learn capabilities through a discovery interface. That suits local services, where restarts, client crashes and dropped connections must not corrupt protocol state. See [MCP](https://modelcontextprotocol.io/). - **Agent2Agent (A2A).** Service cards, explicit task states, task subscription and cancellation inform LMSP's service description and its asynchronous task model. See [A2A](https://a2a-protocol.org/). ## 1.4 Architecture {#architecture} LMSP is organized in three layers, on top of the adapters that talk to the model runtimes: ```text +---------------------------------------------------------------+ | Discovery mDNS / DNS-SD + well-known service document | +---------------------------------------------------------------+ | Core model catalog, responses, SSE, async tasks | +---------------------------------------------------------------+ | Compatibility OpenAI API, optional Ollama API | +---------------------------------------------------------------+ | Adapters llama.cpp, Ollama, vLLM, custom runtimes | +---------------------------------------------------------------+ ``` - **Discovery.** mDNS/DNS-SD and a well-known service description document ([page 3](https://madeforagent.com/lmsp/1.0/discovery.md)). - **Core.** The model catalog, synchronous responses, server-sent events and asynchronous tasks ([pages 5 to 7](https://madeforagent.com/lmsp/1.0/models.md)). - **Compatibility.** An OpenAI-compatible API and, optionally, an Ollama-compatible API ([page 8](https://madeforagent.com/lmsp/1.0/compatibility.md)). - **Adapters.** Bridges to llama.cpp, Ollama, vLLM or a custom runtime. The guiding principle is **one model execution core, two interface families**. A service offers the LMSP interface and the OpenAI interface over the same core, so there is a single place where models are loaded, requests are executed and tasks are tracked. ## 1.5 Conformance language {#conformance} The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY** and **OPTIONAL** in this document are to be interpreted as described in [BCP 14](https://www.rfc-editor.org/info/bcp14) ([RFC 2119](https://www.rfc-editor.org/rfc/rfc2119), [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174)) when, and only when, they appear in all capitals. Each page is marked normative or informative. Normative pages define requirements; informative pages give guidance. Examples are informative. JSON is UTF-8, and field names use `snake_case`. ## 1.6 Terminology {#terminology} | Term | Meaning | | --- | --- | | Service | A resident process that implements LMSP and runs models through adapters. | | Client | Software that discovers and calls a service: an agent, an application or a tool. | | Adapter | The component that connects the service to one model runtime, such as llama.cpp, Ollama or vLLM. | | Model | A locally available model, identified by a stable `id`. | | Response | The result of one generation request: output items, usage and a status. | | Task | A persisted, asynchronous execution of a response request, with an explicit state. | | Event | One server-sent event with a sequence number, emitted while a response is generated. | --- # Transport and deployment > Default listen addresses, transport, streaming format and URL layout of an LMSP service, and how it runs as an operating-system service separate from the model processes. Source: https://madeforagent.com/lmsp/1.0/transport/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 2.1 Default deployment {#deployment} A service **MUST** listen on the loopback addresses `127.0.0.1` and `::1` by default. The conventional port is `17832`, which makes the default base address `http://127.0.0.1:17832`. | Item | Value | | --- | --- | | Listen addresses | `127.0.0.1` and `::1` | | Transport | HTTP/1.1 or HTTP/2 carrying JSON | | Streaming | Server-sent events, `Content-Type: text/event-stream` | | Core interface prefix | `/lmsp/v1` | | OpenAI-compatible prefix | `/v1` | | Service description | `GET /.well-known/lmsp.json` | | Machine-readable contract | `GET /lmsp/v1/openapi.json` | | Optional local channel | Unix domain socket or Windows named pipe | Requests and responses use JSON in UTF-8 (`Content-Type: application/json`), except streams (`text/event-stream`) and errors (`application/problem+json`). Timestamps are integers: seconds since the Unix epoch. Identifiers are opaque strings; a type prefix such as `resp_` or `task_` is **RECOMMENDED**. The interface contract is an OpenAPI 3.2 document. OpenAPI 3.2 can describe server-sent events, JSON Lines and JSON Sequences, so the streaming endpoints are specified in the same document as the rest. The contract for this version is published at [`/lmsp/1.0/openapi.json`](https://madeforagent.com/lmsp/1.0/openapi.json). A service **MAY** also accept connections on a Unix domain socket or a Windows named pipe with the same HTTP semantics. In 1.0, how such endpoints are advertised is left to extensions. ## 2.2 Always-on operation {#operation} A service **SHOULD** be hosted by the operating system: launchd on macOS, systemd on Linux and the Windows Service manager on Windows. The service process and the model processes **SHOULD** be separate. A model crash **SHOULD** affect only the tasks that use that model. It **SHOULD NOT** make service discovery, the model catalog or queries about earlier tasks unavailable. --- # Service discovery > How LMSP clients find a local service through manual configuration, loopback probing and optional mDNS/DNS-SD, and how to read the well-known service description document. Source: https://madeforagent.com/lmsp/1.0/discovery/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 3.1 Discovery methods {#methods} A client discovers a service by trying these methods in order: 1. **Manual configuration.** The user enters the service address directly. 2. **Probing conventional local addresses.** The client requests `/.well-known/lmsp.json` from candidate addresses. The conventional candidates are `http://127.0.0.1:17832` and `http://[::1]:17832`. 3. **LAN discovery.** The client browses for `_lmsp._tcp.local.` with mDNS and DNS-SD. A method succeeds when it yields a valid service description ([section 3.3](https://madeforagent.com/lmsp/1.0/discovery.md#service-description)). An address the user entered manually always takes precedence over discovered ones. ## 3.2 LAN advertisement {#mdns} DNS-SD uses PTR records to find service instances, SRV records to get the host and port, and TXT records to carry a small amount of extra information. Combined with mDNS it provides zero-configuration discovery on a local network ([RFC 6763](https://www.rfc-editor.org/rfc/rfc6763), [RFC 6762](https://www.rfc-editor.org/rfc/rfc6762)). An example advertisement: ```text Service type: _lmsp._tcp.local. Instance name: Workstation LMSP._lmsp._tcp.local. TXT record: v=1 id=svc_01J8M4Y8K6M5E6K6Z7Z0Y8W3M2 path=/ auth=pairing ``` | TXT key | Meaning | | --- | --- | | `v` | Version of the advertisement format. `1` for LMSP 1.0. | | `id` | The service id, equal to `service.id` in the service description. | | `path` | URL path of the service root. Clients request `{path}.well-known/lmsp.json`. | | `auth` | Authentication hint. `pairing` means a client must pair before using the service. | Rules: - A service **MUST NOT** put access tokens, model lists, file paths or any other sensitive information in the TXT record. The record only locates the service. Real capabilities **MUST** be read from the service description. - A service **MUST** keep mDNS advertisement disabled by default. It is enabled only when the user explicitly turns on LAN discovery. A service used only on the local machine does not advertise. - A service that advertises on the LAN has to listen beyond loopback, which is only allowed in LAN mode ([section 10.5](https://madeforagent.com/lmsp/1.0/security.md#lan)). - A client **MUST** treat the TXT record as an unverified hint, and confirm the service by reading its description. ## 3.3 Service description {#service-description} ```http GET /.well-known/lmsp.json ``` A service **MUST** serve this document as `application/json` without requiring authentication. ```json { "protocol": "lmsp", "versions": ["1.0"], "service": { "id": "svc_01J8M4Y8K6M5E6K6Z7Z0Y8W3M2", "name": "Workstation Local Models", "version": "1.0.0" }, "interfaces": [ { "kind": "lmsp", "version": "1.0", "url": "http://127.0.0.1:17832/lmsp/v1", "transport": "http+json" }, { "kind": "openai", "version": "responses+chat", "url": "http://127.0.0.1:17832/v1", "transport": "http+json" } ], "capabilities": { "streaming": true, "tasks": true, "event_resume": true, "conversations": true, "model_management": true }, "authentication": { "type": "bearer", "pairing_url": "http://127.0.0.1:17832/lmsp/v1/pair" }, "openapi": "http://127.0.0.1:17832/lmsp/v1/openapi.json" } ``` | Field | Meaning | | --- | --- | | `protocol` | Always `"lmsp"`. | | `versions` | The LMSP versions the service supports, as `major.minor` strings. | | `service` | `id` is a stable service identifier, `name` a display name, and `version` the version of the service software. | | `interfaces` | The interfaces on offer. Each has a `kind` (`lmsp`, `openai` or `ollama`), a `version`, a base `url` and a `transport`. For `openai`, `version` names the API families served, such as `responses+chat`. | | `capabilities` | Boolean feature flags, listed below. | | `authentication` | `type` is `bearer`. `pairing_url` is where clients exchange a pairing code for a token ([section 10.2](https://madeforagent.com/lmsp/1.0/security.md#pairing)). | | `openapi` | URL of the OpenAPI document that describes the LMSP interface. | Capability flags: | Flag | Meaning | | --- | --- | | `streaming` | Responses can be streamed as server-sent events. **MUST** be `true` in 1.0. | | `tasks` | Asynchronous tasks are available. **MUST** be `true` in 1.0. | | `event_resume` | Task event streams can be subscribed to and resumed (`GET /lmsp/v1/tasks/{task_id}/events`). | | `conversations` | The service stores responses, so `store` and `previous_response_id` work and `GET /lmsp/v1/responses/{response_id}` is available. | | `model_management` | Models can be loaded and unloaded explicitly. | ## 3.4 Client behavior {#client-behavior} A client: - **SHOULD** select the highest version in `versions` that it also supports. - **SHOULD** select the LMSP interface or the OpenAI interface from `interfaces`, according to what it can speak. - **MUST** use only the capabilities the description declares. A missing flag means `false`. - **MUST** ignore extension fields it does not recognize. - **SHOULD** re-read the service description periodically and when a request fails, instead of caching it forever. - **SHOULD** remember a chosen service by `service.id` rather than by address, because addresses can change. --- # Endpoints > The complete table of LMSP 1.0 endpoints with method, path, requirement level and purpose, plus the conventions for paths, authentication and optional endpoints. Source: https://madeforagent.com/lmsp/1.0/endpoints/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 4.1 Endpoint table {#table} A conforming service **MUST** implement every endpoint marked required. It **MAY** implement the optional ones, and signals them through the capability flags of its [service description](https://madeforagent.com/lmsp/1.0/discovery.md#service-description). | Method | Path | Requirement | Purpose | | --- | --- | --- | --- | | GET | `/.well-known/lmsp.json` | Required | [Service and capability discovery](https://madeforagent.com/lmsp/1.0/discovery.md#service-description) | | GET | `/lmsp/v1/openapi.json` | Required | Machine-readable interface specification | | GET | `/lmsp/v1/health` | Required | [Liveness and readiness check](https://madeforagent.com/lmsp/1.0/common-rules.md#health) | | POST | `/lmsp/v1/pair` | Required | [Exchange a pairing code for a bearer token](https://madeforagent.com/lmsp/1.0/security.md#pairing) | | GET | `/lmsp/v1/models` | Required | [List local models](https://madeforagent.com/lmsp/1.0/models.md#list) | | GET | `/lmsp/v1/models/{model_id}` | Required | [Get one model](https://madeforagent.com/lmsp/1.0/models.md#operations) | | POST | `/lmsp/v1/responses` | Required | [Synchronous or streaming generation](https://madeforagent.com/lmsp/1.0/responses.md#request) | | GET | `/lmsp/v1/responses/{response_id}` | Optional | [Retrieve a stored response](https://madeforagent.com/lmsp/1.0/responses.md#sync) | | POST | `/lmsp/v1/tasks` | Required | [Create an asynchronous generation task](https://madeforagent.com/lmsp/1.0/tasks.md#create) | | GET | `/lmsp/v1/tasks/{task_id}` | Required | [Get an asynchronous task](https://madeforagent.com/lmsp/1.0/tasks.md#create) | | POST | `/lmsp/v1/tasks/{task_id}/cancel` | Required | [Cancel an asynchronous task](https://madeforagent.com/lmsp/1.0/tasks.md#cancel) | | GET | `/lmsp/v1/tasks/{task_id}/events` | Optional | [Subscribe to or resume the event stream](https://madeforagent.com/lmsp/1.0/tasks.md#events) | | POST | `/lmsp/v1/models/{model_id}/load` | Optional | [Preload a model](https://madeforagent.com/lmsp/1.0/models.md#operations) | | POST | `/lmsp/v1/models/{model_id}/unload` | Optional | [Unload a model](https://madeforagent.com/lmsp/1.0/models.md#operations) | The OpenAI-compatible endpoints under `/v1` and the optional Ollama-compatible endpoints are listed in [page 8](https://madeforagent.com/lmsp/1.0/compatibility.md). ## 4.2 Conventions {#conventions} - **Bodies.** Request and response bodies are JSON (`application/json`), except event streams (`text/event-stream`) and errors (`application/problem+json`). - **Path parameters.** A name in braces, such as `{model_id}`, is a placeholder. Values **MUST** be percent-encoded. A model id like `qwen3:14b-q4_k_m` is sent as `qwen3%3A14b-q4_k_m`. - **Authentication.** Every endpoint requires `Authorization: Bearer `, except `/.well-known/lmsp.json`, `/lmsp/v1/openapi.json`, `/lmsp/v1/health` and `/lmsp/v1/pair`. - **Optional endpoints.** An optional endpoint that the service does not implement **MUST** answer `501 Not Implemented` with the error code `unsupported_capability`. - **Versions and errors.** Requests may carry version headers, and every error uses one format. Both are defined in [page 9](https://madeforagent.com/lmsp/1.0/common-rules.md). - **Contract.** The OpenAPI document published at [`/lmsp/1.0/openapi.json`](https://madeforagent.com/lmsp/1.0/openapi.json) is the machine-readable form of this page. The text is normative where the two differ. --- # Model catalog > How an LMSP service lists its local models, the model object and its capability flags, the model lifecycle states, stable model ids and catalog revisions. Source: https://madeforagent.com/lmsp/1.0/models/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 5.1 List models {#list} ```http GET /lmsp/v1/models?capability=chat&ready_only=false Authorization: Bearer ``` | Query parameter | Meaning | | --- | --- | | `capability` | Return only models that have the named capability, for example `chat`, `embeddings` or `tools`. | | `ready_only` | When `true`, return only models whose `status` is `ready`. Defaults to `false`. | ```json { "object": "list", "catalog_revision": "42", "data": [ { "id": "qwen3:14b-q4_k_m", "object": "model", "display_name": "Qwen3 14B Q4_K_M", "owned_by": "local", "family": "qwen3", "revision": "sha256:8f27c1a4e9b05d36f2a7c8e1b4d90a63c5e2f7b8a1d4c9e60b3f5a7d2c8e1b04", "status": "ready", "capabilities": { "chat": true, "completion": true, "embeddings": false, "tools": true, "vision": false, "structured_output": true, "streaming": true }, "input_modalities": ["text"], "output_modalities": ["text"], "context_window": 32768, "max_output_tokens": 8192, "runtime": { "backend": "llama.cpp", "device": "metal", "load_on_demand": true } } ] } ``` To spare clients from fetching the full catalog again and again, a service **SHOULD** return an `ETag` header, a `catalog_revision` field, or both: - `catalog_revision` is an opaque string that changes whenever the catalog changes. - A service that sends `ETag` **SHOULD** answer a matching `If-None-Match` with `304 Not Modified`. ## 5.2 Model object {#object} | Field | Meaning | | --- | --- | | `id` | Stable model identifier, used in every request that names a model. | | `object` | Always `"model"`. | | `display_name` | Human-readable name. The user can change it. | | `aliases` | Optional list of alternative names. They are informational only. | | `owned_by` | Where the model comes from. `local` for models installed on this device. | | `family` | Model family, such as `qwen3`. | | `revision` | Digest of the model content, such as `sha256:8f27...`. | | `status` | Lifecycle state ([section 5.3](https://madeforagent.com/lmsp/1.0/models.md#lifecycle)). | | `capabilities` | Boolean flags: `chat`, `completion`, `embeddings`, `tools`, `vision`, `structured_output` and `streaming`. | | `input_modalities`, `output_modalities` | The kinds of content the model reads and writes. `text` is the only modality defined in 1.0. | | `context_window` | Maximum number of tokens of context. | | `max_output_tokens` | Maximum number of tokens in one response. | | `runtime` | `backend` (for example `llama.cpp`), `device` (for example `metal`) and `load_on_demand`. | | `error` | Present when `status` is `error`. A problem object ([section 9.3](https://madeforagent.com/lmsp/1.0/common-rules.md#errors)). | A model `id` **MUST** be stable. The display name and aliases can be changed by the user, so they **MUST NOT** be used as the primary key. ## 5.3 Model lifecycle {#lifecycle} ```text installed --> loading --> ready --> unloading --> installed | +-----> error ``` | Status | Meaning | | --- | --- | | `installed` | The model exists but is not loaded. | | `loading` | Memory is being allocated or the runtime is initializing. | | `ready` | The model can process requests immediately. | | `unloading` | Resources are being released. | | `error` | Loading failed. The model object **SHOULD** carry a structured error. | A new load request moves a model in the `error` state back to `loading`. When a request names a model that is `installed` and its `runtime.load_on_demand` is `true`, the service loads it as part of the request. If the model cannot be used right away and is not loaded on demand, the service answers `409` with `model_not_ready`. If loading fails, the error is `model_load_failed`. ## 5.4 Model operations {#operations} `GET /lmsp/v1/models/{model_id}` returns one model object. An unknown id gives `404` with `model_not_found`. Two optional endpoints control loading explicitly. They are available when the `model_management` capability is `true`, and they require the `models:manage` scope ([section 10.3](https://madeforagent.com/lmsp/1.0/security.md#scopes)): | Endpoint | Behavior | | --- | --- | | `POST /lmsp/v1/models/{model_id}/load` | Preloads the model. Returns the model object with `202 Accepted` when loading has started, or `200` when it is already `ready`. | | `POST /lmsp/v1/models/{model_id}/unload` | Unloads the model. Returns the model object. Repeating the request is harmless. | Both accept an `Idempotency-Key` ([section 9.2](https://madeforagent.com/lmsp/1.0/common-rules.md#idempotency)). --- # Responses and streaming > The LMSP core conversation interface, with Responses-style requests and output items, synchronous and streaming generation, server-sent event types, event ids, reconnection and tool calls. Source: https://madeforagent.com/lmsp/1.0/responses/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 6.1 Request {#request} ```http POST /lmsp/v1/responses Authorization: Bearer Content-Type: application/json Idempotency-Key: 01J8M5T7Z2K4N6Q8S0V3X5B9DG LMSP-Version: 1.0 ``` ```json { "model": "qwen3:14b-q4_k_m", "input": [ { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "Describe a local model service in one sentence." } ] } ], "stream": false, "max_output_tokens": 512, "temperature": 0.7, "response_format": { "type": "text" }, "store": true } ``` The core protocol uses a Responses-style `input` and `output` structure instead of a plain list of message strings, so that a single request shape can carry: - multi-turn messages; - text, images, audio and files; - tool calls and tool results; - structured output; - reasoning summaries; - multimodal output. LMSP 1.0 defines text content only. Image, audio and file content types are reserved for later versions. | Field | Required | Meaning | | --- | --- | --- | | `model` | Yes | Id of the model to use. | | `input` | Yes | Array of input items. A `message` item has a `role` (`system`, `user` or `assistant`) and a `content` array of parts, such as `input_text`. Tool use adds `function_call` and `function_call_output` items ([section 6.5](https://madeforagent.com/lmsp/1.0/responses.md#tools)). | | `stream` | No | `true` to receive server-sent events. Defaults to `false`. | | `max_output_tokens` | No | Upper bound on the number of generated tokens. | | `temperature` | No | Sampling temperature. | | `response_format` | No | `{"type": "text"}` (the default), or `{"type": "json_schema", "json_schema": {"name": "...", "schema": {...}}}` for structured output. Needs the `structured_output` capability. | | `tools`, `tool_choice` | No | Function tools the model may call ([section 6.5](https://madeforagent.com/lmsp/1.0/responses.md#tools)). Needs the `tools` capability. | | `store` | No | Asks the service to keep the response, so it can be retrieved later and used as `previous_response_id`. Defaults to `false`. Ignored by services without the `conversations` capability. | | `previous_response_id` | No | Continues from a stored response. Needs the `conversations` capability. | | `extensions` | No | Private extensions ([section 9.1](https://madeforagent.com/lmsp/1.0/common-rules.md#versioning)). | A request that needs a capability the model does not have is rejected with `400` and `unsupported_capability`. A request that does not fit in the context window is rejected with `400` and `context_length_exceeded`. A service that can store conversations **MAY** also support `previous_response_id`. Otherwise every request **MUST** carry the full context. A service **MUST NOT** rely on the HTTP connection to hold conversation state. ## 6.2 Synchronous response {#sync} When `stream` is `false`, the service returns `200 OK` with a response object after generation has finished. ```json { "id": "resp_01J8M6Q4W2T7N5R3V9X1Z8B0DF", "object": "response", "created_at": 1794200000, "status": "completed", "model": "qwen3:14b-q4_k_m", "output": [ { "id": "msg_01J8M6Q4W5C8H2K9P3S7Y1A6ZE", "type": "message", "role": "assistant", "content": [ { "type": "output_text", "text": "A local model service is a resident interface layer that discovers, manages and calls models on the user's own device." } ] } ], "usage": { "input_tokens": 28, "output_tokens": 31, "total_tokens": 59 }, "error": null, "incomplete_details": null } ``` | Field | Meaning | | --- | --- | | `id` | Response identifier, such as `resp_...`. | | `object` | Always `"response"`. | | `created_at` | Creation time, in seconds since the Unix epoch. | | `status` | `in_progress`, `completed`, `incomplete`, `failed` or `cancelled`. A synchronous response is returned in a final status. | | `model` | Id of the model that produced the response. | | `output` | Array of output items. | | `usage` | Token counts: `input_tokens`, `output_tokens` and `total_tokens`. | | `error` | `null`, or a problem object ([section 9.3](https://madeforagent.com/lmsp/1.0/common-rules.md#errors)) when `status` is `failed`. | | `incomplete_details` | `null`, or an object with a `reason` such as `max_output_tokens` when `status` is `incomplete`. | Output item types: | Item type | Shape | | --- | --- | | `message` | `id`, `role` (`assistant`) and `content`, an array of `output_text` parts that each have a `text`. | | `function_call` | `id`, `call_id`, `name` and `arguments`, a JSON-encoded string. | | `reasoning` | `id` and `summary`, an array of `summary_text` parts that each have a `text`. | If generation fails, a synchronous request is answered with an error status and a problem document ([section 9.3](https://madeforagent.com/lmsp/1.0/common-rules.md#errors)), not with a response object. The `failed` status appears in streams, tasks and stored responses. `GET /lmsp/v1/responses/{response_id}` returns a response that was stored with `store: true`. It is optional and needs the `conversations` capability. An unknown id gives `404` with `response_not_found`. ## 6.3 Streaming {#streaming} To stream, set `stream` in the request: ```json { "stream": true } ``` The service answers `200 OK` with `Content-Type: text/event-stream` and sends semantic events. Each event has an `id`, an `event` name and a JSON `data` object: ```text id: 1 event: response.created data: {"response_id":"resp_01J8M6Q4W2T7N5R3V9X1Z8B0DF","status":"in_progress"} id: 2 event: response.output_item.added data: {"response_id":"resp_01J8M6Q4W2T7N5R3V9X1Z8B0DF","output_index":0,"item":{"id":"msg_01J8M6Q4W5C8H2K9P3S7Y1A6ZE","type":"message","role":"assistant","content":[]}} id: 3 event: response.output_text.delta data: {"response_id":"resp_01J8M6Q4W2T7N5R3V9X1Z8B0DF","item_id":"msg_01J8M6Q4W5C8H2K9P3S7Y1A6ZE","delta":"A local model "} id: 4 event: response.output_text.delta data: {"response_id":"resp_01J8M6Q4W2T7N5R3V9X1Z8B0DF","item_id":"msg_01J8M6Q4W5C8H2K9P3S7Y1A6ZE","delta":"service is a resident interface layer."} id: 5 event: response.output_item.done data: {"response_id":"resp_01J8M6Q4W2T7N5R3V9X1Z8B0DF","output_index":0,"item":{"id":"msg_01J8M6Q4W5C8H2K9P3S7Y1A6ZE","type":"message","role":"assistant","content":[{"type":"output_text","text":"A local model service is a resident interface layer."}]}} id: 6 event: response.completed data: {"response_id":"resp_01J8M6Q4W2T7N5R3V9X1Z8B0DF","status":"completed","usage":{"input_tokens":28,"output_tokens":12,"total_tokens":40}} ``` At least these events are defined: | Event | Meaning | Data fields | | --- | --- | --- | | `response.created` | The response was created. | `response_id`, `status` (`in_progress`) | | `response.output_item.added` | A new output item started. | `response_id`, `output_index`, `item` | | `response.output_text.delta` | Text was appended to a message item. | `response_id`, `item_id`, `delta` | | `response.output_item.done` | An output item is complete. | `response_id`, `output_index`, `item` | | `response.completed` | The response finished. Final event. | `response_id`, `status`, `usage` | | `response.incomplete` | Generation stopped early, for example at the token limit. Final event. | `response_id`, `status`, `incomplete_details`, `usage` | | `response.failed` | Generation failed. Final event. | `response_id`, `status`, `error` | | `response.cancelled` | The response was cancelled. Final event. | `response_id`, `status` | | `error` | A problem that is not tied to one response, after which the stream cannot continue. | `error`, a problem object | A service closes the stream after a final event or an `error` event. Services **MAY** send additional events, for example for tool-call arguments, images, audio and structured JSON deltas. Clients **MUST** ignore event types they do not know. If the client closes the connection of a streaming request, the service **SHOULD** stop generating. ## 6.4 Event ids and reconnection {#reconnection} The `id` of an event **MUST** be an integer sequence number that is strictly increasing within the response, starting at 1. After a dropped connection, a client resumes by sending the last id it received. It does this with a header: ```http Last-Event-ID: 2 ``` or with a query parameter: ```http GET /lmsp/v1/tasks/{task_id}/events?after=2 ``` Both go to the events endpoint of a task ([section 7.4](https://madeforagent.com/lmsp/1.0/tasks.md#events)). The service replays only the events whose sequence number is greater than 2. If both are present, `after` wins. If the requested events are older than the retention period, the service answers `410 Gone` with the error `event_history_expired`. The client then fetches the final result instead, with `GET /lmsp/v1/tasks/{task_id}`. A response streamed directly from `POST /lmsp/v1/responses` belongs to that HTTP connection and cannot be resumed. A client that needs resumable output **SHOULD** create a [task](https://madeforagent.com/lmsp/1.0/tasks.md) and follow its events. ## 6.5 Tool calls {#tools} LMSP 1.0 defines function tools that the client executes. The service does not run tools itself. The `tools:execute` scope is reserved for extensions that do ([section 10.3](https://madeforagent.com/lmsp/1.0/security.md#scopes)). First, the client lists the tools. Each entry has `type` set to `function`, a `name`, a `description` and `parameters`, a JSON Schema object. `tool_choice` is `auto` (the default), `none`, `required`, or `{"type": "function", "name": "get_weather"}`. ```json { "model": "qwen3:14b-q4_k_m", "input": [ { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "What is the weather in Hangzhou?" } ] } ], "tools": [ { "type": "function", "name": "get_weather", "description": "Get the current weather for a city.", "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ], "tool_choice": "auto" } ``` When the model decides to call a tool, the response contains a `function_call` item. Its `arguments` are a JSON-encoded string. In a stream, the item arrives complete in `response.output_item.done`. ```json { "id": "resp_01J8M7B3N6Q9T2V5X8Z1C4D7FH", "object": "response", "created_at": 1794200050, "status": "completed", "model": "qwen3:14b-q4_k_m", "output": [ { "id": "fc_01J8M7B3N8R1S4W7Y0A3E6G9JK", "type": "function_call", "call_id": "call_01J8M7B3N9T2V5X8Z1C4D7F0HM", "name": "get_weather", "arguments": "{\"city\":\"Hangzhou\"}" } ], "usage": { "input_tokens": 74, "output_tokens": 18, "total_tokens": 92 }, "error": null, "incomplete_details": null } ``` The client runs the tool, then sends a new request that carries the full context: the earlier input items, the `function_call` item as received, and a `function_call_output` item with the same `call_id`. Its `output` is a string, usually JSON-encoded. ```json { "type": "function_call_output", "call_id": "call_01J8M7B3N9T2V5X8Z1C4D7F0HM", "output": "{\"temperature_c\":17,\"condition\":\"light rain\"}" } ``` --- # Asynchronous tasks > How LMSP runs long generations as persisted tasks, with task creation, the task state machine, idempotent cancellation, event streams and restart policies for unfinished tasks. Source: https://madeforagent.com/lmsp/1.0/tasks/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 7.1 Create a task {#create} A client creates a task by posting the response request it wants to run, wrapped in a task envelope. An `Idempotency-Key` header is recommended ([section 9.2](https://madeforagent.com/lmsp/1.0/common-rules.md#idempotency)). ```http POST /lmsp/v1/tasks Authorization: Bearer Idempotency-Key: 01J8M7C4Y3N5P8R2T6V9X1Z4BH ``` ```json { "type": "response", "input": { "model": "qwen3:14b-q4_k_m", "input": [ { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "Write a long report." } ] } ] }, "on_restart": "fail" } ``` | Field | Required | Meaning | | --- | --- | --- | | `type` | Yes | `response`, the only task type in 1.0. | | `input` | Yes | A response request, exactly as for `POST /lmsp/v1/responses`. Its `stream` field is ignored. | | `on_restart` | No | `fail` (the default), `retry` or `resume`. See [section 7.5](https://madeforagent.com/lmsp/1.0/tasks.md#restart). | The service **MUST** persist the task before it answers. It then returns: ```http HTTP/1.1 202 Accepted ``` ```json { "id": "task_01J8M8R5X3V7B2N4Q9T1W6Y0ZC", "object": "task", "type": "response", "status": "queued", "created_at": 1794200100, "updated_at": 1794200100, "response_id": "resp_01J8M8R5X4B9D2F6H1K3N7Q0ZW", "events_url": "/lmsp/v1/tasks/task_01J8M8R5X3V7B2N4Q9T1W6Y0ZC/events", "on_restart": "fail", "response": null, "error": null } ``` | Task field | Meaning | | --- | --- | | `id` | Task identifier, such as `task_...`. | | `object` | Always `"task"`. | | `type` | `response`. | | `status` | The current state ([section 7.2](https://madeforagent.com/lmsp/1.0/tasks.md#states)). | | `created_at`, `updated_at` | Times in seconds since the Unix epoch. | | `response_id` | Id of the response the task produces. It is assigned when the task is created. | | `events_url` | Path of the event stream. Present when the `event_resume` capability is `true`. | | `on_restart` | The restart policy of the task. | | `response` | The response object once `status` is `succeeded`, otherwise `null`. | | `error` | A problem object ([section 9.3](https://madeforagent.com/lmsp/1.0/common-rules.md#errors)) when `status` is `failed`, otherwise `null`. | `GET /lmsp/v1/tasks/{task_id}` returns the task object. Clients that do not use event streams poll this endpoint. An unknown id gives `404` with `task_not_found`. A finished task looks like this: ```json { "id": "task_01J8M8R5X3V7B2N4Q9T1W6Y0ZC", "object": "task", "type": "response", "status": "succeeded", "created_at": 1794200100, "updated_at": 1794200162, "response_id": "resp_01J8M8R5X4B9D2F6H1K3N7Q0ZW", "events_url": "/lmsp/v1/tasks/task_01J8M8R5X3V7B2N4Q9T1W6Y0ZC/events", "on_restart": "fail", "response": { "id": "resp_01J8M8R5X4B9D2F6H1K3N7Q0ZW", "object": "response", "created_at": 1794200100, "status": "completed", "model": "qwen3:14b-q4_k_m", "output": [ { "id": "msg_01J8M8R6A2C5E8G1J4M7P0S3VY", "type": "message", "role": "assistant", "content": [ { "type": "output_text", "text": "Local model services make on-device inference a shared, discoverable utility..." } ] } ], "usage": { "input_tokens": 12, "output_tokens": 1480, "total_tokens": 1492 }, "error": null, "incomplete_details": null }, "error": null } ``` ## 7.2 State machine {#states} ```text queued --> loading --> running --> succeeded | | | | | +--> failed | | +--> cancelling --> cancelled | +--------------> failed +--------------------------> cancelled ``` | State | Meaning | Next states | | --- | --- | --- | | `queued` | Accepted and persisted, waiting to start. | `loading`, `cancelled`, `failed` | | `loading` | The model is being loaded. | `running`, `cancelling`, `failed` | | `running` | The model is generating. | `succeeded`, `failed`, `cancelling` | | `cancelling` | Cancellation was requested, and the backend has not stopped yet. | `cancelled` | | `succeeded` | Final. The response is complete. | none | | `failed` | Final. | none | | `cancelled` | Final. | none | The terminal states are `succeeded`, `failed` and `cancelled`. A task in a terminal state **MUST NOT** change again. A task whose response ends as `incomplete`, for example at the token limit, is `succeeded`. Every transition is atomic, so a task is never both cancelled and completed. Whichever transition is committed first wins. ## 7.3 Cancellation {#cancel} ```http POST /lmsp/v1/tasks/{task_id}/cancel ``` The service returns `200 OK` and the current task object. A `queued` task becomes `cancelled` at once. A `loading` or `running` task becomes `cancelling` until the backend has stopped, and then `cancelled`. Cancellation **MUST** be idempotent. Cancelling a task that is already `cancelling` or `cancelled` returns the current task and produces no new error. Cancelling a task that has already `succeeded` or `failed` is answered with `409` and `task_not_cancelable`. An unknown id gives `404` with `task_not_found`. ## 7.4 Event streams {#events} ```http GET /lmsp/v1/tasks/{task_id}/events Accept: text/event-stream ``` This endpoint is optional. It is available when the `event_resume` capability is `true`. - The events are the response events of [section 6.3](https://madeforagent.com/lmsp/1.0/responses.md#streaming), for the response that the task produces. Numbering and resumption follow [section 6.4](https://madeforagent.com/lmsp/1.0/responses.md#reconnection). - The stream starts with `response.created`, which is recorded when the task is accepted. It ends with one final event that matches the outcome: `response.completed`, `response.incomplete`, `response.failed` or `response.cancelled`. - A client that connects late first receives the events it missed, then live events. The service closes the stream after the final event. - Events are kept for a retention period ([section 11.2](https://madeforagent.com/lmsp/1.0/persistence.md#rules)). Older events give `410 Gone` with `event_history_expired`. ## 7.5 Restart semantics {#restart} `on_restart` says what happens to an unfinished task when the service restarts: | Value | Behavior | | --- | --- | | `fail` | The default. The service marks the unfinished task `failed`, with the error code `server_restarted`. | | `retry` | The service runs the task again from the beginning. | | `resume` | The service continues from a snapshot. This is allowed only when the backend explicitly supports resumable snapshots. Otherwise the create request is rejected with `400` and `unsupported_capability`. | A task that is `cancelling` when the service restarts completes its cancellation and becomes `cancelled`, whatever its policy. `retry` is not recommended as a default, because the task may already have run tools or produced external side effects. A client that chooses `retry` **MUST** combine it with an `Idempotency-Key` and make its tool calls idempotent. --- # OpenAI and Ollama compatibility > How an LMSP service stays compatible with existing OpenAI and Ollama clients through a thin data-conversion layer over one execution core, and where that compatibility ends. Source: https://madeforagent.com/lmsp/1.0/compatibility/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 8.1 OpenAI-compatible endpoints {#openai} A service **SHOULD** offer an OpenAI-compatible interface next to the LMSP interface, and declare it in `interfaces` with `kind` set to `openai`. To cover existing clients, a service that does so **MUST** implement at least these endpoints under the `/v1` prefix: | Endpoint | Notes | | --- | --- | | `GET /v1/models` | Lists the models of the LMSP catalog. Each `id` is the LMSP model id. | | `POST /v1/chat/completions` | Chat Completions, with and without streaming. | | `POST /v1/responses` | The OpenAI Responses shape. | | `POST /v1/embeddings` | The LMSP 1.0 core interface has no embeddings endpoint. The adapter serves this endpoint directly, for models whose `capabilities.embeddings` is `true`. | These endpoints accept the same bearer tokens as the LMSP interface. A client that speaks the OpenAI protocol sets the LMSP token as its API key. A first implementation can start with `/v1/models` and `/v1/chat/completions` ([page 12](https://madeforagent.com/lmsp/1.0/mvp.md)). ## 8.2 Ollama-compatible endpoints {#ollama} A service **SHOULD** also provide these endpoints, so that clients written for Ollama work too: | Endpoint | Notes | | --- | --- | | `GET /api/tags` | Lists the models of the LMSP catalog. | | `POST /api/chat` | Chat. | | `POST /api/generate` | Plain text generation. | A service that provides them declares an interface with `kind` set to `ollama`. ## 8.3 One execution core {#core} The compatibility layer **MUST** only convert data. It **MUST NOT** create a second set of model management logic. A request on a compatibility interface is mapped to an LMSP response, runs on the same core and adapter as a native request, and uses the same model catalog and authentication. ```text OpenAI Chat Completions | request mapping v LMSP Response | backend adaptation v llama.cpp / Ollama / vLLM / other runtimes ``` On the `/v1` and `/api` endpoints, a service **SHOULD** report errors in the format that the corresponding API defines, so unmodified clients can parse them. The LMSP interface always uses `application/problem+json`. ## 8.4 Boundaries {#boundaries} Compatibility has limits, and they are worth stating plainly: - An existing OpenAI-compatible client usually needs only a changed `base_url`. It does not gain LMSP discovery automatically. - Only clients that support LMSP can discover a service automatically, read its detailed capabilities, resume event streams and manage asynchronous tasks. - No server-side protocol can make a client work if it cannot be configured and accepts only official cloud addresses. --- # Common rules > Rules shared by every LMSP endpoint, covering version headers and extensions, idempotency keys, the problem+json error format with its error codes, and the health check. Source: https://madeforagent.com/lmsp/1.0/common-rules/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 9.1 Versions and extensions {#versioning} Each LMSP request **MAY** carry these headers: ```http LMSP-Version: 1.0 LMSP-Client: example-client/1.2.0 ``` `LMSP-Version` is the protocol version the client targets. `LMSP-Client` identifies the client software for diagnostics, as `name/version`. A service **MUST NOT** change protocol behavior based on it. Rules: - The `v1` in the path denotes the major version. - A request that names an unknown major version is answered with `426 Upgrade Required` and the error code `unsupported_protocol_version`. - New optional fields **MUST NOT** break 1.x compatibility. - Services and clients **MUST** ignore fields they do not know. - Private extensions go in an `extensions` object and are named in reverse-domain notation: ```json { "extensions": { "com.example.cache": { "enabled": true } } } ``` ## 9.2 Idempotency {#idempotency} Requests that create something, such as creating a response, creating a task or loading a model, accept an idempotency key: ```http Idempotency-Key: ``` The service stores the key together with a digest of the request body and the result: - The same key with the same body returns the original result. - The same key with a different body is answered with `409 Conflict` and `idempotency_conflict`. A key is scoped to the client and the endpoint. It **SHOULD** be a unique value of at most 255 characters, such as a ULID or a UUID. A service **SHOULD** keep keys for at least 24 hours. A service **MAY** ignore the key on streaming requests. A client that needs safe retries uses a [task](https://madeforagent.com/lmsp/1.0/tasks.md). ## 9.3 Errors {#errors} Every error on the LMSP interface uses `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)): ```json { "type": "urn:lmsp:error:model-not-found", "title": "Model not found", "status": 404, "code": "model_not_found", "detail": "No local model with id 'qwen3:14b-q4_k_m'", "request_id": "req_01J8M9S6Y4W8C3P5R1V7Z2A0DH", "retryable": false } ``` | Field | Required | Meaning | | --- | --- | --- | | `type` | Yes | URI that identifies the kind of error. LMSP uses `urn:lmsp:error:` followed by the code with hyphens instead of underscores. | | `title` | Yes | Short, human-readable summary. | | `status` | Yes | The HTTP status code. | | `code` | Yes | Machine-readable error code, from the list below. | | `detail` | No | Human-readable explanation of this occurrence. It **MUST NOT** contain secrets, prompts or generated content. | | `request_id` | No | Identifier of the request, for correlation with service logs. | | `retryable` | Yes | Whether repeating the request later can succeed. | Common error codes: | Code | HTTP status | Retryable | Meaning | | --- | --- | --- | --- | | `invalid_request` | 400 | No | The request is malformed or has invalid parameters. | | `unauthorized` | 401 | No | The token is missing, invalid, expired or revoked, or a pairing code is wrong. | | `forbidden` | 403 | No | The token lacks the required scope, or the `Origin` or `Host` check failed. | | `model_not_found` | 404 | No | No local model has this id. | | `model_not_ready` | 409 | Yes | The model is not loaded and is not loaded on demand. | | `model_load_failed` | 500 | Yes | Loading the model failed. | | `context_length_exceeded` | 400 | No | The input does not fit in the context window of the model. | | `unsupported_capability` | 400 or 501 | No | The model or the service does not support what the request needs. An unimplemented optional endpoint answers 501. | | `unsupported_protocol_version` | 426 | No | The service does not support the requested major version. | | `response_not_found` | 404 | No | No stored response has this id. | | `task_not_found` | 404 | No | No task has this id. | | `task_not_cancelable` | 409 | No | The task has already succeeded or failed. | | `idempotency_conflict` | 409 | No | The idempotency key was already used with a different request body. | | `event_history_expired` | 410 | No | The requested events are older than the retention period. | | `rate_limited` | 429 | Yes | Too many requests. | | `resource_exhausted` | 503 | Yes | The device is out of memory or another resource. | | `backend_unavailable` | 503 | Yes | The model runtime cannot be reached. | | `server_restarted` | 503 | Yes | The service restarted while the task was unfinished. | | `internal_error` | 500 | Varies | An unexpected failure inside the service. | The `retryable` field is authoritative. The column above shows the usual value. A service **MAY** define further codes. A client **MUST** handle a code it does not know by its HTTP status and its `retryable` field. ## 9.4 Health check {#health} ```http GET /lmsp/v1/health ``` ```json { "status": "ready", "service_id": "svc_01J8M4Y8K6M5E6K6Z7Z0Y8W3M2", "uptime_seconds": 86420, "loaded_models": 1, "active_tasks": 2, "storage": "ok" } ``` | Field | Meaning | | --- | --- | | `status` | `starting`, `ready`, `degraded` or `stopping`. | | `service_id` | The service id from the service description. | | `uptime_seconds` | Seconds since the service started. | | `loaded_models` | Number of models in the `ready` state. | | `active_tasks` | Number of tasks that are `queued`, `loading`, `running` or `cancelling`. | | `storage` | `ok` or `error`. | The status is `200 OK` when `status` is `ready` or `degraded`, and `503 Service Unavailable` otherwise. The health endpoint returns only operational status. It **MUST NOT** return model paths, prompts, access tokens or other sensitive information. --- # Local security > Security requirements for LMSP services, with loopback binding, pairing and bearer tokens, scopes, Origin and Host checks against malicious web pages, LAN mode and logging rules. Source: https://madeforagent.com/lmsp/1.0/security/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 10.1 Baseline {#baseline} Listening only on localhost is not absolute security: a malicious web page can still try to reach local ports. LMSP therefore combines several measures. - A service **MUST** bind to the loopback addresses by default. It **MUST NOT** listen on `0.0.0.0`, or on any other non-loopback address, unless LAN mode is enabled ([section 10.5](https://madeforagent.com/lmsp/1.0/security.md#lan)). - On first run, a service **SHOULD** generate a high-entropy random service secret, and use it, rather than any built-in default, to issue and verify tokens. - Every request to the core interface **MUST** carry a valid bearer token, except for the bootstrap endpoints listed in [section 4.2](https://madeforagent.com/lmsp/1.0/endpoints.md#conventions). Coming from loopback is never enough on its own. ## 10.2 Pairing and tokens {#pairing} A client gets its own bearer token with a local pairing code. The user asks the service to show a pairing code, in whatever way the service offers, and the client submits it: ```http POST /lmsp/v1/pair Content-Type: application/json ``` ```json { "client_name": "example-client", "pairing_code": "K7QD-4M2X", "scopes": ["inference"] } ``` | Request field | Meaning | | --- | --- | | `client_name` | Display name of the client, shown to the user. | | `pairing_code` | The code the user obtained from the service. | | `scopes` | Optional list of requested scopes. Defaults to `["inference"]`. | The service answers with a token. The `token_type` is always `Bearer`: ```json { "client_id": "cli_01J8MAT3W6X9B2D5F8H1K4N7QZ", "access_token": "lmsp_example_3f9a1c52e7d84b60", "token_type": "Bearer", "scopes": ["inference"] } ``` The client then sends `Authorization: Bearer ` with every request. - A pairing code **MUST** be single-use and **SHOULD** expire within 5 minutes. It **SHOULD** carry at least 40 bits of entropy. A service **MUST** rate-limit failed attempts. A wrong or expired code is answered with `401` and `unauthorized`. - Tokens are independent per client, so the user can revoke one client without affecting the others. A service **SHOULD** let the user list and revoke paired clients. - A client **SHOULD** keep its token in the operating system keychain or credential manager. A service **SHOULD** store only a hash of each token. - A missing, invalid or revoked token is answered with `401`, `unauthorized` and a `WWW-Authenticate: Bearer` header. A valid token that lacks the scope an endpoint needs is answered with `403` and `forbidden`. ## 10.3 Scopes {#scopes} Permissions are split into separate scopes. Downloading models, deleting models and executing tools never ride on the inference scope. | Scope | Allows | | --- | --- | | `inference` | Reading the model catalog, creating responses and tasks, and reading and cancelling the client's own tasks. | | `models:manage` | Loading and unloading models. | | `models:install` | Reserved. Downloading and deleting models. | | `tools:execute` | Reserved. Executing tools on the service side. | Tasks and stored responses belong to the client that created them. A service **SHOULD** answer a request from another client as if they did not exist (`task_not_found` or `response_not_found`). ## 10.4 Browser and network protections {#browser} - A service **MUST** reject cross-origin browser requests by default, and allow only `Origin` values that are explicitly configured. A request with any other `Origin` is answered with `403` and `forbidden`. - When an origin is allowed, the service answers its CORS preflight requests and sends `Access-Control-Allow-Origin` with that exact origin, never with `*`. - A service **MUST** validate the `Host` header, to prevent DNS rebinding. It accepts only host names it serves: the loopback names and addresses, plus any names configured for LAN mode. Any other value is answered with `403` and `forbidden`. ## 10.5 LAN mode {#lan} LAN mode means listening beyond the loopback addresses. - A service **MUST NOT** enable LAN mode unless the user explicitly turns it on. - In LAN mode a service **MUST** require authentication, and **SHOULD** also enable TLS. A client **SHOULD NOT** send a token over plain HTTP to a non-loopback address unless the user explicitly allows it. - mDNS advertisement is allowed only in LAN mode, when the user has also turned on LAN discovery ([section 3.2](https://madeforagent.com/lmsp/1.0/discovery.md#mdns)). ## 10.6 Logging and privacy {#logging} - Model paths, logs, prompts and generated content **MUST NOT** appear in mDNS advertisements. - By default, a service **SHOULD** log metadata only (times, endpoints, status codes, ids, token counts and durations). It **SHOULD NOT** log full prompts or generated content. - Health responses and error details **MUST NOT** contain model paths, prompts, access tokens or other sensitive information. --- # Persistence > How an LMSP service stores its state, with an embedded SQLite database, the tables to keep, the transactional rules for tasks and events, retention and recovery at startup. Source: https://madeforagent.com/lmsp/1.0/persistence/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 11.1 Storage {#storage} A service **SHOULD** keep its state in an embedded [SQLite](https://www.sqlite.org/) database. A suggested set of tables: | Table | Content | | --- | --- | | `service_config` | Service identity and configuration. | | `clients` | Paired clients, token hashes and scopes. | | `models` | Model metadata and content digests. | | `responses` | Stored responses. | | `tasks` | Asynchronous tasks. | | `task_events` | The event log of each task, with sequence numbers. | | `idempotency_keys` | Idempotency keys, request digests and results. | Model files **MUST** stay outside the database. The database holds only metadata and content digests. Table names and layouts are an implementation choice. What clients can observe, and what has to interoperate, is the behavior in the next two sections. ## 11.2 Rules {#rules} Whatever storage a service uses, it: - **MUST** persist a task before it returns `202 Accepted`. - **MUST** update event sequence numbers and task status in the same transaction. - **MUST** make the cancel transition atomic, to avoid the race of a task that is cancelled and then completed. - **SHOULD** keep finished tasks and events for a configurable retention period. - **MUST NOT** delete unfinished tasks during log cleanup. - **MUST**, at startup, scan the leftover `queued`, `loading`, `running` and `cancelling` tasks, and apply the restart policy of each ([section 7.5](https://madeforagent.com/lmsp/1.0/tasks.md#restart)). ## 11.3 Recovery at startup {#recovery} The table shows the state a leftover task is moved to, by its `on_restart` policy: | State found at startup | `fail` | `retry` | `resume` | | --- | --- | --- | --- | | `queued` | `failed` | `queued` | `queued` | | `loading` | `failed` | `queued` | `queued` | | `running` | `failed` | `queued` | `running`, continuing from the snapshot | | `cancelling` | `cancelled` | `cancelled` | `cancelled` | A task moved to `failed` gets the error code `server_restarted` and a `response.failed` event. A task moved to `cancelled` gets a `response.cancelled` event. The new state and its event are written in one transaction ([section 11.2](https://madeforagent.com/lmsp/1.0/persistence.md#rules)). --- # Minimum viable scope > A suggested scope for a first LMSP implementation, with the ten pieces that form a usable closed loop and the parts of the specification that can wait for later releases. Source: https://madeforagent.com/lmsp/1.0/mvp/ Language: en Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 12.1 First version {#scope} The first version does not have to implement every multimodal feature, model download or real-time voice. It forms a usable closed loop once it provides: 1. Service discovery through `/.well-known/lmsp.json`. 2. Optional LAN advertisement of `_lmsp._tcp.local.`. 3. `GET /lmsp/v1/models`. 4. `POST /lmsp/v1/responses`, both synchronous and as an SSE stream. 5. Asynchronous tasks: create, query and cancel, with SQLite persistence. 6. Resumption of dropped streams with `Last-Event-ID` or `after`. 7. Bearer tokens, local pairing, and `Origin` and `Host` protection. 8. The OpenAI-compatible `/v1/models` and `/v1/chat/completions` endpoints. 9. An OpenAPI 3.2 contract file, and cross-language conformance tests. 10. At least one real backend, and a check that tasks recover correctly after a service restart. The point of this design: LMSP provides the full set of capabilities, the OpenAI interface provides ready-made compatibility, mDNS and the service description provide automatic discovery, and the SQLite task model provides local persistence. ## 12.2 What can wait {#later} Anything the list above does not mention can follow in later releases. This includes the optional endpoints (stored responses, and loading and unloading models), the OpenAI `/v1/responses` and `/v1/embeddings` endpoints, the Ollama-compatible API, and every content type beyond text. This scope is a path to a usable first release, not a conformance profile. A service that claims LMSP 1.0 still implements every required endpoint in [page 4](https://madeforagent.com/lmsp/1.0/endpoints.md) and the required behavior described in the other pages.