# Local Model Service Protocol (LMSP) 1.0, 초안 > Made for Agent는 AI 에이전트, 모델, 도구가 서로를 찾고 대화할 수 있게 하는, 기계가 읽을 수 있는 오픈 프로토콜을 공개합니다. 첫 번째 프로토콜은 로컬 모델 서비스 프로토콜(LMSP)입니다. Source: https://madeforagent.com/ko/llms-full.txt Updated: 2026-11-08 --- # 로컬 모델 서비스 프로토콜(LMSP) > LMSP는 사용자 기기에서 실행되는 언어 모델을 찾아 호출하기 위한 오픈 프로토콜입니다. 서비스 검색, 모델 카탈로그, 스트리밍, 영속 작업, OpenAI 호환을 정의합니다. Source: https://madeforagent.com/ko/lmsp/ Language: ko Updated: 2026-11-08 ## LMSP가 하는 일 {#what} LMSP(Local Model Service Protocol)는 사용자의 기기나 로컬 네트워크에서 언어 모델을 실행하는 소프트웨어와, 그 소프트웨어를 사용하는 클라이언트를 위한 오픈 프로토콜입니다. 상주하는 로컬 서비스는 잘 알려진 주소에서 스스로를 설명하고 모델을 나열합니다. 생성 요청에는 JSON 또는 서버 전송 이벤트로 응답하고, 장시간 생성은 영속화된 작업으로 실행합니다. 작업은 취소할 수 있고, 연결이 끊긴 뒤에도 다시 따라갈 수 있습니다. 같은 서비스가 OpenAI API도 지원하므로, 지금 있는 클라이언트가 그대로 동작합니다. LMSP는 OpenAI API, Ollama, MCP를 대체하지 않습니다. 각각의 강점을 상주형 로컬 서비스에 맞는 프로토콜 계층으로 묶은 것입니다. ## 한눈에 보기 {#glance} | 항목 | 요약 | | --- | --- | | 기본 주소 | `http://127.0.0.1:17832` | | 서비스 설명 | `GET /.well-known/lmsp.json` | | 핵심 인터페이스 | `/lmsp/v1`. HTTP 위의 JSON이며, 스트리밍은 서버 전송 이벤트 사용 | | 호환 인터페이스 | OpenAI 클라이언트용 `/v1`, Ollama 클라이언트용 선택적 `/api` | | 검색 | 수동 주소, 로컬 주소 탐색, 선택적 mDNS 및 DNS-SD(`_lmsp._tcp.local.`) | | 인증 | 로컬 페어링으로 얻는 베어러 토큰 | | 영속화 | 내장 SQLite. 작업은 재시작 후에도 유지됨 | | 계약 | OpenAPI 3.2 | ## 사용해 보기 {#try} 사용 중인 컴퓨터에서 LMSP 서비스가 실행 중이라면, 전체 흐름은 다음과 같습니다. 먼저 [10.2절](https://madeforagent.com/ko/lmsp/1.0/security.md#pairing)의 설명대로 페어링해 토큰을 받고, 그 토큰을 `LMSP_TOKEN`에 넣어 두세요. ```sh # 1. 서비스 찾기 curl http://127.0.0.1:17832/.well-known/lmsp.json # 2. 로컬 모델 나열 curl http://127.0.0.1:17832/lmsp/v1/models \ -H "Authorization: Bearer $LMSP_TOKEN" # 3. 응답 생성 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"}]}]}' ``` ## 다른 프로토콜과의 관계 {#related} | 프로토콜 | 다루는 범위 | LMSP와의 관계 | | --- | --- | --- | | OpenAI API | 호스팅되는 모델. Responses API가 가장 새로운 인터페이스. | LMSP는 핵심 인터페이스에서 Responses 구조를 따르고, 서비스는 그 옆에 OpenAI 호환 인터페이스를 함께 제공한다. | | Ollama | 이미 OpenAI 호환 엔드포인트를 제공하는 로컬 런타임. | LMSP 서비스 뒤에 어댑터로 둘 수 있다. LMSP는 선택적인 Ollama 호환 엔드포인트도 정의한다. | | MCP | 에이전트를 도구와 컨텍스트에 연결한다. | LMSP는 에이전트를 모델에 연결한다. 둘 다 필수 세션보다 무상태 검색을 선호한다. | | A2A | 에이전트 간 협업. | LMSP는 서비스 설명과 작업에 서비스 카드와 명시적인 작업 상태를 가져온다. | ## 리소스 {#resources} - [사양 1.0 버전(초안)](https://madeforagent.com/ko/lmsp/1.0.md). - OpenAPI 계약. [JSON](https://madeforagent.com/lmsp/1.0/openapi.json)과 [YAML](https://madeforagent.com/lmsp/1.0/openapi.yaml)로 제공됩니다. - 언어 모델을 위한 [llms.txt](https://madeforagent.com/ko/llms.txt)와 [llms-full.txt](https://madeforagent.com/ko/llms-full.txt). - 모든 페이지는 Markdown으로도 받을 수 있습니다. 페이지 주소 끝의 슬래시를 `.md`로 바꾸세요. 예: [`/ko/lmsp/1.0/tasks.md`](https://madeforagent.com/ko/lmsp/1.0/tasks.md). ## 자주 묻는 질문 ### LMSP란 무엇인가요? LMSP(Local Model Service Protocol)는 사용자의 기기나 로컬 네트워크에서 실행되는 언어 모델 서비스를 위한 오픈 프로토콜입니다. 클라이언트가 서비스를 찾고, 모델을 나열하고, 응답을 요청하고, 출력을 스트리밍하고, 장시간 작업을 실행하는 방법을 정의합니다. ### LMSP는 OpenAI API, Ollama, MCP를 대체하나요? 아니요. LMSP는 이들 위에 구축됩니다. 핵심 인터페이스는 OpenAI Responses 구조를 따르고, 서비스는 OpenAI 호환 및 Ollama 호환 엔드포인트를 함께 제공할 수 있으며, 프로토콜은 MCP의 무상태 검색과 A2A의 작업 상태를 가져옵니다. ### 기존 OpenAI 클라이언트로 LMSP 서비스를 사용할 수 있나요? 대체로 가능합니다. 기본 URL을 바꾸고 LMSP 토큰을 API 키로 쓰면 됩니다. 다만 서비스 검색에는 LMSP를 지원하는 클라이언트가 필요하므로, 서비스를 자동으로 찾지는 못합니다. ### 클라이언트는 로컬 LMSP 서비스를 어떻게 찾나요? 세 가지를 순서대로 시도합니다. 먼저 직접 입력한 주소, 그다음 관례적인 로컬 주소에서 /.well-known/lmsp.json을 요청하는 방법, 마지막으로 로컬 네트워크에서 mDNS와 DNS-SD로 _lmsp._tcp.local.을 찾는 선택적 방법입니다. ### localhost에서 실행되는 모델 서비스는 안전한가요? 자동으로 안전해지지는 않습니다. 웹 페이지가 로컬 포트에 접근을 시도할 수 있기 때문입니다. LMSP 서비스는 기본적으로 루프백에 바인딩하고, 로컬 페어링으로 얻은 베어러 토큰을 요구하고, Origin과 Host 헤더를 검사하며, LAN 모드에서는 인증을 요구합니다. ### 사양의 현재 상태는 어떤가요? 1.0 버전은 첫 공개 초안이며 앞으로 호환되지 않는 방식으로 바뀔 수 있습니다. 영어 텍스트가 규범이며, 번역본은 편의를 위해 제공됩니다. --- # 개요 > LMSP 1.0 초안의 개요. 로컬 모델 서비스 프로토콜이 무엇인지, 바탕이 된 설계, 계층형 아키텍처, 사양을 읽는 방법, 문서 전체에서 쓰는 용어를 설명합니다. Source: https://madeforagent.com/ko/lmsp/1.0/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 1.1 요약 {#abstract} 로컬 모델 서비스 프로토콜(LMSP)은 클라이언트가 사용자의 기기나 로컬 네트워크에서 실행되는 언어 모델 서비스를 찾아 사용하는 방법을 정의한다. 서비스 검색, 모델 카탈로그, 동기 생성과 스트리밍 생성, 재시작 후에도 유지되는 비동기 작업을 규정한다. 또한 하나의 서비스가 LMSP 인터페이스와 함께 OpenAI 호환 인터페이스를 제공하는 방법을 정의하여, 기존 클라이언트가 계속 동작하도록 한다. LMSP는 OpenAI API, Ollama, MCP를 대체하지 않는다. 각각의 강점을 상주형 로컬 서비스에 맞는 프로토콜 계층으로 묶은 것이다. ## 1.2 이 문서의 상태 {#status} 이 문서는 2026-11-08에 공개된 LMSP 1.0의 첫 공개 초안이다. "LMSP"는 가칭이다. 내용은 피드백을 받고 있으며, 1.0이 확정되기 전까지는 호환되지 않는 방식으로 바뀔 수 있다. 영어 텍스트가 규범이다. 번역본은 편의를 위해 제공되며, 둘이 다를 경우 영어 텍스트가 우선한다. ## 1.3 설계 기반 {#design-basis} LMSP는 새로운 설계를 발명하지 않고, 검증된 설계를 재사용한다. - **OpenAI Responses API.** 구조화된 출력 항목, 의미를 가진 서버 전송 이벤트, 장시간 작업을 위한 조회와 취소를 갖춘 백그라운드 실행. LMSP 핵심 대화 인터페이스의 기준이다. [스트리밍 응답](https://developers.openai.com/api/docs/guides/streaming-responses)과 [백그라운드 모드](https://developers.openai.com/api/docs/guides/background)를 참고한다. - **Ollama.** OpenAI 호환 `/v1/models`, `/v1/chat/completions`, `/v1/responses` 엔드포인트를 제공하며, 하나의 서비스에 OpenAI 적응 계층을 더하면 도입 비용이 크게 낮아진다는 것을 보여 준다. [Ollama의 OpenAI 호환성](https://docs.ollama.com/api/openai-compatibility)을 참고한다. - **Model Context Protocol(MCP).** 최신 설계는 필수 세션 초기화에서 무상태 검색으로 옮겨 갔다. 모든 요청이 자체 버전 정보를 담고, 클라이언트는 검색 인터페이스를 통해 기능을 알아낸다. 재시작, 클라이언트 충돌, 연결 끊김이 있어도 프로토콜 상태가 손상되어서는 안 되는 로컬 서비스에 알맞다. [MCP](https://modelcontextprotocol.io/)를 참고한다. - **Agent2Agent(A2A).** 서비스 카드, 명시적인 작업 상태, 작업 구독과 취소는 LMSP의 서비스 설명과 비동기 작업 모델에 영향을 주었다. [A2A](https://a2a-protocol.org/)를 참고한다. ## 1.4 아키텍처 {#architecture} LMSP는 모델 런타임과 통신하는 어댑터 위에 3개의 계층으로 구성된다. ```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 | +---------------------------------------------------------------+ ``` - **검색.** mDNS/DNS-SD와 well-known 서비스 설명 문서([3장](https://madeforagent.com/ko/lmsp/1.0/discovery.md)). - **핵심.** 모델 카탈로그, 동기 응답, 서버 전송 이벤트, 비동기 작업([5~7장](https://madeforagent.com/ko/lmsp/1.0/models.md)). - **호환.** OpenAI 호환 API와 선택적인 Ollama 호환 API([8장](https://madeforagent.com/ko/lmsp/1.0/compatibility.md)). - **어댑터.** llama.cpp, Ollama, vLLM 또는 사용자 정의 런타임으로의 브리지. 기본 원칙은 **하나의 모델 실행 코어, 두 개의 인터페이스 계열**이다. 서비스는 같은 코어 위에서 LMSP 인터페이스와 OpenAI 인터페이스를 제공하므로, 모델을 로드하고 요청을 실행하고 작업을 추적하는 곳이 한 군데로 정해진다. ## 1.5 적합성 표현 {#conformance} 이 문서에서 굵게 표기된 **해야 한다**(MUST), **해서는 안 된다**(MUST NOT), **필수**(REQUIRED), **것이 좋다**(SHOULD), **않는 것이 좋다**(SHOULD NOT), **권장**(RECOMMENDED), **수 있다**(MAY), **선택**(OPTIONAL)은 [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))에 설명된 대로 해석한다. 굵게 표기되지 않은 경우에는 일반적인 말로 쓰인 것이다. 각 페이지에는 규범적인지 참고용인지가 표시되어 있다. 규범적 페이지는 요구 사항을 정의하고, 참고용 페이지는 안내를 제공한다. 예시는 모두 참고용이다. JSON은 UTF-8을 쓰며, 필드 이름은 `snake_case`를 쓴다. ## 1.6 용어 {#terminology} | 용어 | 의미 | | --- | --- | | 서비스 | LMSP를 구현하고 어댑터를 통해 모델을 실행하는 상주 프로세스. | | 클라이언트 | 서비스를 찾아 호출하는 소프트웨어. 에이전트, 애플리케이션, 도구 등. | | 어댑터 | 서비스를 하나의 모델 런타임(llama.cpp, Ollama, vLLM 등)에 연결하는 구성 요소. | | 모델 | 로컬에서 사용할 수 있는 모델. 안정적인 `id`로 식별된다. | | 응답 | 하나의 생성 요청의 결과. 출력 항목, 사용량, 상태로 이루어진다. | | 작업 | 응답 요청을 영속화하여 비동기로 실행하는 것. 명시적인 상태를 가진다. | | 이벤트 | 응답이 생성되는 동안 내보내는, 순번이 붙은 서버 전송 이벤트 하나. | --- # 전송과 배포 > LMSP 서비스의 기본 수신 주소, 전송 방식, 스트리밍 형식, URL 구성과 함께, 모델 프로세스와 분리된 운영체제 서비스로 상시 실행하는 방법을 정의합니다. Source: https://madeforagent.com/ko/lmsp/1.0/transport/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 2.1 기본 배포 {#deployment} 서비스는 기본적으로 루프백 주소 `127.0.0.1`과 `::1`에서 수신 대기**해야 한다**. 관례적인 포트는 `17832`이며, 기본 베이스 주소는 `http://127.0.0.1:17832`이다. | 항목 | 값 | | --- | --- | | 수신 주소 | `127.0.0.1`과 `::1` | | 전송 | JSON을 운반하는 HTTP/1.1 또는 HTTP/2 | | 스트리밍 | 서버 전송 이벤트, `Content-Type: text/event-stream` | | 핵심 인터페이스 접두사 | `/lmsp/v1` | | OpenAI 호환 접두사 | `/v1` | | 서비스 설명 | `GET /.well-known/lmsp.json` | | 기계 판독 가능한 계약 | `GET /lmsp/v1/openapi.json` | | 선택적 로컬 채널 | Unix 도메인 소켓 또는 Windows 명명된 파이프 | 요청과 응답은 UTF-8 JSON(`Content-Type: application/json`)을 사용한다. 다만 스트림(`text/event-stream`)과 오류(`application/problem+json`)는 예외이다. 타임스탬프는 정수이며 Unix 에포크 이후의 초 단위이다. 식별자는 불투명한 문자열이며, `resp_`나 `task_` 같은 유형 접두사를 붙이기를 **권장**한다. 인터페이스 계약은 OpenAPI 3.2 문서이다. OpenAPI 3.2는 서버 전송 이벤트, JSON Lines, JSON Sequences를 기술할 수 있으므로, 스트리밍 엔드포인트도 나머지와 같은 문서에서 규정된다. 이 버전의 계약은 [`/lmsp/1.0/openapi.json`](https://madeforagent.com/lmsp/1.0/openapi.json)에서 공개된다. 서비스는 같은 HTTP 의미 체계로 Unix 도메인 소켓이나 Windows 명명된 파이프의 연결도 받아들일 **수 있다**. 1.0에서는 이러한 엔드포인트를 어떻게 알릴지를 확장에 맡긴다. ## 2.2 상시 운영 {#operation} 서비스는 운영체제가 호스팅하는 **것이 좋다**. macOS에서는 launchd, Linux에서는 systemd, Windows에서는 Windows 서비스 관리자를 사용한다. 서비스 프로세스와 모델 프로세스는 분리하는 **것이 좋다**. 모델이 충돌하더라도 그 모델을 쓰는 작업에만 영향을 주는 **것이 좋다**. 서비스 검색, 모델 카탈로그, 이전 작업에 대한 조회를 사용할 수 없게 만들지 **않는 것이 좋다**. --- # 서비스 검색 > LMSP 클라이언트가 수동 설정, 루프백 탐색, 선택적 mDNS/DNS-SD로 로컬 서비스를 찾는 방법과, well-known 서비스 설명 문서를 읽는 방법을 설명합니다. Source: https://madeforagent.com/ko/lmsp/1.0/discovery/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 3.1 검색 방법 {#methods} 클라이언트는 다음 방법을 순서대로 시도하여 서비스를 찾는다. 1. **수동 설정.** 사용자가 서비스 주소를 직접 입력한다. 2. **관례적인 로컬 주소 탐색.** 클라이언트가 후보 주소에 `/.well-known/lmsp.json`을 요청한다. 관례적인 후보는 `http://127.0.0.1:17832`와 `http://[::1]:17832`이다. 3. **LAN 검색.** 클라이언트가 mDNS와 DNS-SD로 `_lmsp._tcp.local.`을 탐색한다. 유효한 서비스 설명([3.3절](https://madeforagent.com/ko/lmsp/1.0/discovery.md#service-description))을 얻으면 그 방법은 성공이다. 사용자가 직접 입력한 주소는 항상 검색된 주소보다 우선한다. ## 3.2 LAN 광고 {#mdns} DNS-SD는 PTR 레코드로 서비스 인스턴스를 찾고, SRV 레코드로 호스트와 포트를 얻고, TXT 레코드로 소량의 추가 정보를 전달한다. mDNS와 결합하면 로컬 네트워크에서 무구성 검색을 제공한다([RFC 6763](https://www.rfc-editor.org/rfc/rfc6763), [RFC 6762](https://www.rfc-editor.org/rfc/rfc6762)). 광고 예시: ```text Service type: _lmsp._tcp.local. Instance name: Workstation LMSP._lmsp._tcp.local. TXT record: v=1 id=svc_01J8M4Y8K6M5E6K6Z7Z0Y8W3M2 path=/ auth=pairing ``` | TXT 키 | 의미 | | --- | --- | | `v` | 광고 형식의 버전. LMSP 1.0에서는 `1`. | | `id` | 서비스 id. 서비스 설명의 `service.id`와 같다. | | `path` | 서비스 루트의 URL 경로. 클라이언트는 `{path}.well-known/lmsp.json`을 요청한다. | | `auth` | 인증 힌트. `pairing`은 서비스를 쓰기 전에 클라이언트가 페어링해야 함을 뜻한다. | 규칙: - 서비스는 액세스 토큰, 모델 목록, 파일 경로 등 민감한 정보를 TXT 레코드에 넣**어서는 안 된다**. 이 레코드는 서비스의 위치만 알려 준다. 실제 기능은 서비스 설명에서 읽**어야 한다**. - 서비스는 mDNS 광고를 기본적으로 비활성화해 두**어야 한다**. 사용자가 LAN 검색을 명시적으로 켠 경우에만 활성화된다. 로컬 컴퓨터에서만 쓰는 서비스는 광고하지 않는다. - LAN에서 광고하는 서비스는 루프백 밖에서도 수신 대기해야 하는데, 이는 LAN 모드에서만 허용된다([10.5절](https://madeforagent.com/ko/lmsp/1.0/security.md#lan)). - 클라이언트는 TXT 레코드를 검증되지 않은 힌트로 취급하고, 서비스 설명을 읽어 서비스를 확인**해야 한다**. ## 3.3 서비스 설명 {#service-description} ```http GET /.well-known/lmsp.json ``` 서비스는 이 문서를 인증을 요구하지 않고 `application/json`으로 제공**해야 한다**. ```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" } ``` | 필드 | 의미 | | --- | --- | | `protocol` | 항상 `"lmsp"`. | | `versions` | 서비스가 지원하는 LMSP 버전. `major.minor` 형식의 문자열. | | `service` | `id`는 안정적인 서비스 식별자, `name`은 표시 이름, `version`은 서비스 소프트웨어의 버전. | | `interfaces` | 제공하는 인터페이스. 각각 `kind`(`lmsp`, `openai`, `ollama`), `version`, 기본 `url`, `transport`를 가진다. `openai`의 경우 `version`은 제공하는 API 계열을 나타낸다(예: `responses+chat`). | | `capabilities` | 불리언 기능 플래그. 아래에 나열한다. | | `authentication` | `type`은 `bearer`. `pairing_url`은 클라이언트가 페어링 코드를 토큰으로 교환하는 위치이다([10.2절](https://madeforagent.com/ko/lmsp/1.0/security.md#pairing)). | | `openapi` | LMSP 인터페이스를 기술하는 OpenAPI 문서의 URL. | 기능 플래그: | 플래그 | 의미 | | --- | --- | | `streaming` | 응답을 서버 전송 이벤트로 스트리밍할 수 있다. 1.0에서는 `true`여**야 한다**. | | `tasks` | 비동기 작업을 사용할 수 있다. 1.0에서는 `true`여**야 한다**. | | `event_resume` | 작업 이벤트 스트림을 구독하고 재개할 수 있다(`GET /lmsp/v1/tasks/{task_id}/events`). | | `conversations` | 서비스가 응답을 저장하므로 `store`와 `previous_response_id`가 동작하고 `GET /lmsp/v1/responses/{response_id}`를 사용할 수 있다. | | `model_management` | 모델을 명시적으로 로드하고 언로드할 수 있다. | ## 3.4 클라이언트 동작 {#client-behavior} 클라이언트는 다음과 같이 한다. - `versions` 중 자신도 지원하는 가장 높은 버전을 선택하는 **것이 좋다**. - 자신이 사용할 수 있는 방식에 따라 `interfaces`에서 LMSP 인터페이스 또는 OpenAI 인터페이스를 선택하는 **것이 좋다**. - 서비스 설명이 선언한 기능만 사용**해야 한다**. 플래그가 없으면 `false`를 뜻한다. - 인식하지 못하는 확장 필드는 무시**해야 한다**. - 서비스 설명을 영원히 캐시하지 말고, 주기적으로 그리고 요청이 실패했을 때 다시 읽는 **것이 좋다**. - 선택한 서비스는 주소가 아니라 `service.id`로 기억하는 **것이 좋다**. 주소는 바뀔 수 있기 때문이다. --- # 엔드포인트 > LMSP 1.0 엔드포인트 전체 표. 메서드, 경로, 요구 수준, 용도와 함께 경로, 인증, 선택적 엔드포인트에 관한 규칙을 정리합니다. Source: https://madeforagent.com/ko/lmsp/1.0/endpoints/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 4.1 엔드포인트 표 {#table} 적합한 서비스는 필수로 표시된 모든 엔드포인트를 구현**해야 한다**. 선택적인 엔드포인트는 구현할 **수 있으며**, 그 경우 [서비스 설명](https://madeforagent.com/ko/lmsp/1.0/discovery.md#service-description)의 기능 플래그로 알린다. | 메서드 | 경로 | 요구 수준 | 용도 | | --- | --- | --- | --- | | GET | `/.well-known/lmsp.json` | 필수 | [서비스 및 기능 검색](https://madeforagent.com/ko/lmsp/1.0/discovery.md#service-description) | | GET | `/lmsp/v1/openapi.json` | 필수 | 기계 판독 가능한 인터페이스 사양 | | GET | `/lmsp/v1/health` | 필수 | [활성 상태 및 준비 상태 확인](https://madeforagent.com/ko/lmsp/1.0/common-rules.md#health) | | POST | `/lmsp/v1/pair` | 필수 | [페어링 코드를 베어러 토큰으로 교환](https://madeforagent.com/ko/lmsp/1.0/security.md#pairing) | | GET | `/lmsp/v1/models` | 필수 | [로컬 모델 나열](https://madeforagent.com/ko/lmsp/1.0/models.md#list) | | GET | `/lmsp/v1/models/{model_id}` | 필수 | [모델 하나 가져오기](https://madeforagent.com/ko/lmsp/1.0/models.md#operations) | | POST | `/lmsp/v1/responses` | 필수 | [동기 또는 스트리밍 생성](https://madeforagent.com/ko/lmsp/1.0/responses.md#request) | | GET | `/lmsp/v1/responses/{response_id}` | 선택 | [저장된 응답 조회](https://madeforagent.com/ko/lmsp/1.0/responses.md#sync) | | POST | `/lmsp/v1/tasks` | 필수 | [비동기 생성 작업 만들기](https://madeforagent.com/ko/lmsp/1.0/tasks.md#create) | | GET | `/lmsp/v1/tasks/{task_id}` | 필수 | [비동기 작업 가져오기](https://madeforagent.com/ko/lmsp/1.0/tasks.md#create) | | POST | `/lmsp/v1/tasks/{task_id}/cancel` | 필수 | [비동기 작업 취소](https://madeforagent.com/ko/lmsp/1.0/tasks.md#cancel) | | GET | `/lmsp/v1/tasks/{task_id}/events` | 선택 | [이벤트 스트림 구독 또는 재개](https://madeforagent.com/ko/lmsp/1.0/tasks.md#events) | | POST | `/lmsp/v1/models/{model_id}/load` | 선택 | [모델 미리 로드](https://madeforagent.com/ko/lmsp/1.0/models.md#operations) | | POST | `/lmsp/v1/models/{model_id}/unload` | 선택 | [모델 언로드](https://madeforagent.com/ko/lmsp/1.0/models.md#operations) | `/v1` 아래의 OpenAI 호환 엔드포인트와 선택적인 Ollama 호환 엔드포인트는 [8장](https://madeforagent.com/ko/lmsp/1.0/compatibility.md)에 나열되어 있다. ## 4.2 규칙 {#conventions} - **본문.** 요청과 응답의 본문은 JSON(`application/json`)이다. 다만 이벤트 스트림(`text/event-stream`)과 오류(`application/problem+json`)는 예외이다. - **경로 매개변수.** `{model_id}`처럼 중괄호로 묶은 이름은 자리 표시자이다. 값은 퍼센트 인코딩**해야 한다**. `qwen3:14b-q4_k_m` 같은 모델 id는 `qwen3%3A14b-q4_k_m`으로 보낸다. - **인증.** `/.well-known/lmsp.json`, `/lmsp/v1/openapi.json`, `/lmsp/v1/health`, `/lmsp/v1/pair`를 제외한 모든 엔드포인트는 `Authorization: Bearer `을 요구한다. - **선택적 엔드포인트.** 서비스가 구현하지 않은 선택적 엔드포인트는 오류 코드 `unsupported_capability`와 함께 `501 Not Implemented`로 응답**해야 한다**. - **버전과 오류.** 요청은 버전 헤더를 실을 수 있고, 모든 오류는 하나의 형식을 쓴다. 둘 다 [9장](https://madeforagent.com/ko/lmsp/1.0/common-rules.md)에서 정의한다. - **계약.** [`/lmsp/1.0/openapi.json`](https://madeforagent.com/lmsp/1.0/openapi.json)에 공개된 OpenAPI 문서는 이 페이지의 기계 판독 가능한 형태이다. 둘이 다를 경우 본문이 규범이다. --- # 모델 카탈로그 > LMSP 서비스가 로컬 모델을 나열하는 방법, 모델 객체와 기능 플래그, 모델 수명 주기 상태, 안정적인 모델 id, 카탈로그 리비전을 설명합니다. Source: https://madeforagent.com/ko/lmsp/1.0/models/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 5.1 모델 나열 {#list} ```http GET /lmsp/v1/models?capability=chat&ready_only=false Authorization: Bearer ``` | 쿼리 매개변수 | 의미 | | --- | --- | | `capability` | 지정한 기능을 가진 모델만 반환한다. 예: `chat`, `embeddings`, `tools`. | | `ready_only` | `true`이면 `status`가 `ready`인 모델만 반환한다. 기본값은 `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 } } ] } ``` 클라이언트가 전체 카탈로그를 반복해서 가져오지 않도록, 서비스는 `ETag` 헤더, `catalog_revision` 필드, 또는 둘 다를 반환하는 **것이 좋다**. - `catalog_revision`은 카탈로그가 바뀔 때마다 바뀌는 불투명한 문자열이다. - `ETag`를 보내는 서비스는 일치하는 `If-None-Match`에 `304 Not Modified`로 응답하는 **것이 좋다**. ## 5.2 모델 객체 {#object} | 필드 | 의미 | | --- | --- | | `id` | 안정적인 모델 식별자. 모델을 지정하는 모든 요청에서 쓴다. | | `object` | 항상 `"model"`. | | `display_name` | 사람이 읽을 수 있는 이름. 사용자가 바꿀 수 있다. | | `aliases` | 다른 이름의 선택적 목록. 참고용일 뿐이다. | | `owned_by` | 모델의 출처. 이 기기에 설치된 모델은 `local`. | | `family` | 모델 계열. 예: `qwen3`. | | `revision` | 모델 내용의 다이제스트. 예: `sha256:8f27...`. | | `status` | 수명 주기 상태([5.3절](https://madeforagent.com/ko/lmsp/1.0/models.md#lifecycle)). | | `capabilities` | 불리언 플래그: `chat`, `completion`, `embeddings`, `tools`, `vision`, `structured_output`, `streaming`. | | `input_modalities`, `output_modalities` | 모델이 읽고 쓰는 콘텐츠의 종류. 1.0에서 정의된 모달리티는 `text`뿐이다. | | `context_window` | 컨텍스트의 최대 토큰 수. | | `max_output_tokens` | 응답 하나의 최대 토큰 수. | | `runtime` | `backend`(예: `llama.cpp`), `device`(예: `metal`), `load_on_demand`. | | `error` | `status`가 `error`일 때 존재한다. problem 객체([9.3절](https://madeforagent.com/ko/lmsp/1.0/common-rules.md#errors)). | 모델 `id`는 안정적이어**야 한다**. 표시 이름과 별칭은 사용자가 바꿀 수 있으므로 기본 키로 써서는 **안 된다**. ## 5.3 모델 수명 주기 {#lifecycle} ```text installed --> loading --> ready --> unloading --> installed | +-----> error ``` | 상태 | 의미 | | --- | --- | | `installed` | 모델은 있지만 로드되지 않았다. | | `loading` | 메모리를 할당하는 중이거나 런타임을 초기화하는 중이다. | | `ready` | 모델이 즉시 요청을 처리할 수 있다. | | `unloading` | 리소스를 해제하는 중이다. | | `error` | 로드에 실패했다. 모델 객체는 구조화된 오류를 담는 **것이 좋다**. | `error` 상태의 모델은 새 로드 요청이 오면 `loading`으로 돌아간다. 요청이 `installed` 상태이면서 `runtime.load_on_demand`가 `true`인 모델을 지정하면, 서비스는 요청의 일부로 그 모델을 로드한다. 모델을 바로 쓸 수 없고 요청 시 로드되지도 않으면, 서비스는 `409`와 `model_not_ready`로 응답한다. 로드에 실패하면 오류는 `model_load_failed`이다. ## 5.4 모델 작업 {#operations} `GET /lmsp/v1/models/{model_id}`는 모델 객체 하나를 반환한다. 알 수 없는 id는 `404`와 `model_not_found`를 반환한다. 두 개의 선택적 엔드포인트로 로드를 명시적으로 제어한다. 이들은 `model_management` 기능이 `true`일 때 사용할 수 있으며, `models:manage` 스코프가 필요하다([10.3절](https://madeforagent.com/ko/lmsp/1.0/security.md#scopes)). | 엔드포인트 | 동작 | | --- | --- | | `POST /lmsp/v1/models/{model_id}/load` | 모델을 미리 로드한다. 로드가 시작되면 `202 Accepted`와 함께, 이미 `ready`이면 `200`과 함께 모델 객체를 반환한다. | | `POST /lmsp/v1/models/{model_id}/unload` | 모델을 언로드한다. 모델 객체를 반환한다. 요청을 반복해도 해가 없다. | 둘 다 `Idempotency-Key`([9.2절](https://madeforagent.com/ko/lmsp/1.0/common-rules.md#idempotency))를 받는다. --- # 응답과 스트리밍 > LMSP의 핵심 대화 인터페이스. Responses 형식의 요청과 출력 항목, 동기 생성과 스트리밍 생성, 서버 전송 이벤트 유형, 이벤트 id, 재연결, 도구 호출을 정의합니다. Source: https://madeforagent.com/ko/lmsp/1.0/responses/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 6.1 요청 {#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 } ``` 핵심 프로토콜은 메시지 문자열의 단순한 목록 대신 Responses 형식의 `input`과 `output` 구조를 사용하므로, 하나의 요청 형태로 다음을 담을 수 있다. - 여러 턴의 메시지 - 텍스트, 이미지, 오디오, 파일 - 도구 호출과 도구 결과 - 구조화된 출력 - 추론 요약 - 멀티모달 출력 LMSP 1.0은 텍스트 콘텐츠만 정의한다. 이미지, 오디오, 파일 콘텐츠 유형은 이후 버전을 위해 예약되어 있다. | 필드 | 필수 | 의미 | | --- | --- | --- | | `model` | 예 | 사용할 모델의 id. | | `input` | 예 | 입력 항목의 배열. `message` 항목은 `role`(`system`, `user`, `assistant`)과 `input_text` 같은 파트로 이루어진 `content` 배열을 가진다. 도구를 사용하면 `function_call` 항목과 `function_call_output` 항목이 추가된다([6.5절](https://madeforagent.com/ko/lmsp/1.0/responses.md#tools)). | | `stream` | 아니요 | `true`이면 서버 전송 이벤트를 받는다. 기본값은 `false`. | | `max_output_tokens` | 아니요 | 생성할 토큰 수의 상한. | | `temperature` | 아니요 | 샘플링 온도. | | `response_format` | 아니요 | `{"type": "text"}`(기본값), 또는 구조화된 출력을 위한 `{"type": "json_schema", "json_schema": {"name": "...", "schema": {...}}}`. `structured_output` 기능이 필요하다. | | `tools`, `tool_choice` | 아니요 | 모델이 호출할 수 있는 함수 도구([6.5절](https://madeforagent.com/ko/lmsp/1.0/responses.md#tools)). `tools` 기능이 필요하다. | | `store` | 아니요 | 서비스에 응답을 보관하도록 요청해, 나중에 조회하거나 `previous_response_id`로 쓸 수 있게 한다. 기본값은 `false`. `conversations` 기능이 없는 서비스는 무시한다. | | `previous_response_id` | 아니요 | 저장된 응답에서 이어 간다. `conversations` 기능이 필요하다. | | `extensions` | 아니요 | 비공개 확장([9.1절](https://madeforagent.com/ko/lmsp/1.0/common-rules.md#versioning)). | 모델에 없는 기능이 필요한 요청은 `400`과 `unsupported_capability`로 거부된다. 컨텍스트 윈도우에 들어가지 않는 요청은 `400`과 `context_length_exceeded`로 거부된다. 대화를 저장할 수 있는 서비스는 `previous_response_id`도 지원할 **수 있다**. 그렇지 않으면 모든 요청이 전체 컨텍스트를 담**아야 한다**. 서비스는 대화 상태를 유지하는 데 HTTP 연결에 의존해서는 **안 된다**. ## 6.2 동기 응답 {#sync} `stream`이 `false`이면, 서비스는 생성이 끝난 뒤 응답 객체와 함께 `200 OK`를 반환한다. ```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 } ``` | 필드 | 의미 | | --- | --- | | `id` | 응답 식별자. 예: `resp_...`. | | `object` | 항상 `"response"`. | | `created_at` | 생성 시각. Unix 에포크 이후의 초 단위. | | `status` | `in_progress`, `completed`, `incomplete`, `failed`, `cancelled`. 동기 응답은 최종 상태로 반환된다. | | `model` | 응답을 생성한 모델의 id. | | `output` | 출력 항목의 배열. | | `usage` | 토큰 수: `input_tokens`, `output_tokens`, `total_tokens`. | | `error` | `null`. `status`가 `failed`이면 problem 객체([9.3절](https://madeforagent.com/ko/lmsp/1.0/common-rules.md#errors)). | | `incomplete_details` | `null`. `status`가 `incomplete`이면 `max_output_tokens` 같은 `reason`을 가진 객체. | 출력 항목 유형: | 항목 유형 | 형태 | | --- | --- | | `message` | `id`, `role`(`assistant`), 그리고 각각 `text`를 가진 `output_text` 파트의 배열인 `content`. | | `function_call` | `id`, `call_id`, `name`, 그리고 JSON으로 인코딩된 문자열인 `arguments`. | | `reasoning` | `id`와, 각각 `text`를 가진 `summary_text` 파트의 배열인 `summary`. | 생성에 실패하면 동기 요청에는 응답 객체가 아니라 오류 상태와 problem 문서([9.3절](https://madeforagent.com/ko/lmsp/1.0/common-rules.md#errors))로 응답한다. `failed` 상태는 스트림, 작업, 저장된 응답에서 나타난다. `GET /lmsp/v1/responses/{response_id}`는 `store: true`로 저장된 응답을 반환한다. 이는 선택 사항이며 `conversations` 기능이 필요하다. 알 수 없는 id는 `404`와 `response_not_found`를 반환한다. ## 6.3 스트리밍 {#streaming} 스트리밍하려면 요청에서 `stream`을 설정한다. ```json { "stream": true } ``` 서비스는 `Content-Type: text/event-stream`과 함께 `200 OK`로 응답하고 의미를 가진 이벤트를 보낸다. 각 이벤트는 `id`, `event` 이름, JSON `data` 객체를 가진다. ```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}} ``` 최소한 다음 이벤트가 정의되어 있다. | 이벤트 | 의미 | 데이터 필드 | | --- | --- | --- | | `response.created` | 응답이 만들어졌다. | `response_id`, `status`(`in_progress`) | | `response.output_item.added` | 새 출력 항목이 시작되었다. | `response_id`, `output_index`, `item` | | `response.output_text.delta` | message 항목에 텍스트가 추가되었다. | `response_id`, `item_id`, `delta` | | `response.output_item.done` | 출력 항목이 완료되었다. | `response_id`, `output_index`, `item` | | `response.completed` | 응답이 끝났다. 최종 이벤트. | `response_id`, `status`, `usage` | | `response.incomplete` | 생성이 일찍 멈췄다. 예를 들어 토큰 한도에서. 최종 이벤트. | `response_id`, `status`, `incomplete_details`, `usage` | | `response.failed` | 생성에 실패했다. 최종 이벤트. | `response_id`, `status`, `error` | | `response.cancelled` | 응답이 취소되었다. 최종 이벤트. | `response_id`, `status` | | `error` | 하나의 응답에 묶이지 않는 문제로, 이후 스트림을 계속할 수 없다. | `error`(problem 객체) | 서비스는 최종 이벤트 또는 `error` 이벤트 뒤에 스트림을 닫는다. 서비스는 예컨대 도구 호출 인수, 이미지, 오디오, 구조화된 JSON 델타를 위해 추가 이벤트를 보낼 **수 있다**. 클라이언트는 알지 못하는 이벤트 유형을 무시**해야 한다**. 클라이언트가 스트리밍 요청의 연결을 닫으면, 서비스는 생성을 중단하는 **것이 좋다**. ## 6.4 이벤트 id와 재연결 {#reconnection} 이벤트의 `id`는 응답 안에서 엄격하게 증가하는, 1에서 시작하는 정수 순번이어**야 한다**. 연결이 끊긴 뒤 클라이언트는 마지막으로 받은 id를 보내 재개한다. 헤더로 보내는 방법: ```http Last-Event-ID: 2 ``` 쿼리 매개변수로 보내는 방법: ```http GET /lmsp/v1/tasks/{task_id}/events?after=2 ``` 둘 다 작업의 이벤트 엔드포인트([7.4절](https://madeforagent.com/ko/lmsp/1.0/tasks.md#events))로 보낸다. 서비스는 순번이 2보다 큰 이벤트만 다시 보낸다. 둘 다 있으면 `after`가 우선한다. 요청한 이벤트가 보존 기간보다 오래되었으면 서비스는 `410 Gone`과 오류 `event_history_expired`로 응답한다. 그러면 클라이언트는 대신 `GET /lmsp/v1/tasks/{task_id}`로 최종 결과를 가져온다. `POST /lmsp/v1/responses`에서 직접 스트리밍된 응답은 그 HTTP 연결에 속하며 재개할 수 없다. 재개 가능한 출력이 필요한 클라이언트는 [작업](https://madeforagent.com/ko/lmsp/1.0/tasks.md)을 만들어 그 이벤트를 따라가는 **것이 좋다**. ## 6.5 도구 호출 {#tools} LMSP 1.0은 클라이언트가 실행하는 함수 도구를 정의한다. 서비스는 도구를 직접 실행하지 않는다. `tools:execute` 스코프는 도구를 실행하는 확장을 위해 예약되어 있다([10.3절](https://madeforagent.com/ko/lmsp/1.0/security.md#scopes)). 먼저 클라이언트가 도구를 나열한다. 각 항목은 `function`으로 설정된 `type`, `name`, `description`, JSON Schema 객체인 `parameters`를 가진다. `tool_choice`는 `auto`(기본값), `none`, `required`, 또는 `{"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" } ``` 모델이 도구를 호출하기로 결정하면 응답에 `function_call` 항목이 담긴다. 그 `arguments`는 JSON으로 인코딩된 문자열이다. 스트림에서는 항목이 `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 } ``` 클라이언트는 도구를 실행한 뒤 전체 컨텍스트를 담은 새 요청을 보낸다. 여기에는 이전 입력 항목, 받은 그대로의 `function_call` 항목, 같은 `call_id`를 가진 `function_call_output` 항목이 들어간다. 그 `output`은 문자열이며 보통 JSON으로 인코딩된다. ```json { "type": "function_call_output", "call_id": "call_01J8M7B3N9T2V5X8Z1C4D7F0HM", "output": "{\"temperature_c\":17,\"condition\":\"light rain\"}" } ``` --- # 비동기 작업 > LMSP가 장시간 생성을 영속화된 작업으로 실행하는 방법. 작업 생성, 작업 상태 기계, 멱등한 취소, 이벤트 스트림, 미완료 작업의 재시작 정책을 정의합니다. Source: https://madeforagent.com/ko/lmsp/1.0/tasks/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 7.1 작업 만들기 {#create} 클라이언트는 실행하려는 응답 요청을 작업 엔벨로프에 담아 게시하여 작업을 만든다. `Idempotency-Key` 헤더를 권장한다([9.2절](https://madeforagent.com/ko/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" } ``` | 필드 | 필수 | 의미 | | --- | --- | --- | | `type` | 예 | `response`. 1.0의 유일한 작업 유형. | | `input` | 예 | `POST /lmsp/v1/responses`와 똑같은 응답 요청. 그 `stream` 필드는 무시된다. | | `on_restart` | 아니요 | `fail`(기본값), `retry`, `resume`. [7.5절](https://madeforagent.com/ko/lmsp/1.0/tasks.md#restart) 참고. | 서비스는 응답하기 전에 작업을 영속화해**야 한다**. 그런 다음 다음을 반환한다. ```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 } ``` | 작업 필드 | 의미 | | --- | --- | | `id` | 작업 식별자. 예: `task_...`. | | `object` | 항상 `"task"`. | | `type` | `response`. | | `status` | 현재 상태([7.2절](https://madeforagent.com/ko/lmsp/1.0/tasks.md#states)). | | `created_at`, `updated_at` | Unix 에포크 이후의 초 단위 시각. | | `response_id` | 작업이 만드는 응답의 id. 작업이 만들어질 때 할당된다. | | `events_url` | 이벤트 스트림의 경로. `event_resume` 기능이 `true`일 때 존재한다. | | `on_restart` | 작업의 재시작 정책. | | `response` | `status`가 `succeeded`가 되면 응답 객체, 그렇지 않으면 `null`. | | `error` | `status`가 `failed`이면 problem 객체([9.3절](https://madeforagent.com/ko/lmsp/1.0/common-rules.md#errors)), 그렇지 않으면 `null`. | `GET /lmsp/v1/tasks/{task_id}`는 작업 객체를 반환한다. 이벤트 스트림을 쓰지 않는 클라이언트는 이 엔드포인트를 폴링한다. 알 수 없는 id는 `404`와 `task_not_found`를 반환한다. 끝난 작업은 다음과 같다. ```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 상태 기계 {#states} ```text queued --> loading --> running --> succeeded | | | | | +--> failed | | +--> cancelling --> cancelled | +--------------> failed +--------------------------> cancelled ``` | 상태 | 의미 | 다음 상태 | | --- | --- | --- | | `queued` | 접수되어 영속화되었으며 시작을 기다린다. | `loading`, `cancelled`, `failed` | | `loading` | 모델을 로드하는 중이다. | `running`, `cancelling`, `failed` | | `running` | 모델이 생성하는 중이다. | `succeeded`, `failed`, `cancelling` | | `cancelling` | 취소가 요청되었지만 백엔드가 아직 멈추지 않았다. | `cancelled` | | `succeeded` | 최종. 응답이 완료되었다. | 없음 | | `failed` | 최종. | 없음 | | `cancelled` | 최종. | 없음 | 종료 상태는 `succeeded`, `failed`, `cancelled`이다. 종료 상태의 작업은 다시 바뀌어서는 **안 된다**. 응답이 `incomplete`로 끝나는 작업(예: 토큰 한도)은 `succeeded`이다. 모든 전이는 원자적이므로, 작업이 취소됨과 완료됨을 동시에 겪는 일은 없다. 먼저 커밋된 전이가 이긴다. ## 7.3 취소 {#cancel} ```http POST /lmsp/v1/tasks/{task_id}/cancel ``` 서비스는 `200 OK`와 현재 작업 객체를 반환한다. `queued` 작업은 즉시 `cancelled`가 된다. `loading` 또는 `running` 작업은 백엔드가 멈출 때까지 `cancelling`이 되었다가 `cancelled`가 된다. 취소는 멱등해**야 한다**. 이미 `cancelling` 또는 `cancelled`인 작업을 취소하면 현재 작업을 반환하며 새 오류를 내지 않는다. 이미 `succeeded` 또는 `failed`인 작업을 취소하면 `409`와 `task_not_cancelable`로 응답한다. 알 수 없는 id는 `404`와 `task_not_found`를 반환한다. ## 7.4 이벤트 스트림 {#events} ```http GET /lmsp/v1/tasks/{task_id}/events Accept: text/event-stream ``` 이 엔드포인트는 선택 사항이다. `event_resume` 기능이 `true`일 때 사용할 수 있다. - 이벤트는 작업이 만드는 응답에 대한 [6.3절](https://madeforagent.com/ko/lmsp/1.0/responses.md#streaming)의 응답 이벤트이다. 번호 매김과 재개는 [6.4절](https://madeforagent.com/ko/lmsp/1.0/responses.md#reconnection)을 따른다. - 스트림은 작업이 접수될 때 기록되는 `response.created`로 시작한다. 결과에 맞는 최종 이벤트 하나로 끝난다. `response.completed`, `response.incomplete`, `response.failed`, `response.cancelled` 중 하나이다. - 늦게 연결한 클라이언트는 먼저 놓친 이벤트를 받고, 그다음에 실시간 이벤트를 받는다. 서비스는 최종 이벤트 뒤에 스트림을 닫는다. - 이벤트는 보존 기간 동안 보관된다([11.2절](https://madeforagent.com/ko/lmsp/1.0/persistence.md#rules)). 더 오래된 이벤트는 `410 Gone`과 `event_history_expired`를 반환한다. ## 7.5 재시작 의미 {#restart} `on_restart`는 서비스가 재시작될 때 미완료 작업에 어떤 일이 일어나는지를 나타낸다. | 값 | 동작 | | --- | --- | | `fail` | 기본값. 서비스는 미완료 작업을 `failed`로 표시하고, 오류 코드는 `server_restarted`이다. | | `retry` | 서비스는 작업을 처음부터 다시 실행한다. | | `resume` | 서비스는 스냅샷에서 이어서 실행한다. 백엔드가 재개 가능한 스냅샷을 명시적으로 지원할 때만 허용된다. 그렇지 않으면 생성 요청이 `400`과 `unsupported_capability`로 거부된다. | 서비스가 재시작될 때 `cancelling`이던 작업은 정책과 관계없이 취소를 마치고 `cancelled`가 된다. `retry`를 기본값으로 쓰는 것은 권장하지 않는다. 작업이 이미 도구를 실행했거나 외부 부작용을 일으켰을 수 있기 때문이다. `retry`를 선택하는 클라이언트는 `Idempotency-Key`와 함께 쓰고 도구 호출을 멱등하게 만들어**야 한다**. --- # OpenAI 및 Ollama 호환성 > LMSP 서비스가 하나의 실행 코어 위에 얇은 데이터 변환 계층을 두어 기존 OpenAI 및 Ollama 클라이언트와 호환되는 방법과, 그 호환성의 한계를 설명합니다. Source: https://madeforagent.com/ko/lmsp/1.0/compatibility/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 8.1 OpenAI 호환 엔드포인트 {#openai} 서비스는 LMSP 인터페이스와 함께 OpenAI 호환 인터페이스를 제공하고 `interfaces`에 `kind`를 `openai`로 선언하는 **것이 좋다**. 기존 클라이언트를 지원하기 위해, 그렇게 하는 서비스는 `/v1` 접두사 아래에 최소한 다음 엔드포인트를 구현**해야 한다**. | 엔드포인트 | 비고 | | --- | --- | | `GET /v1/models` | LMSP 카탈로그의 모델을 나열한다. 각 `id`는 LMSP 모델 id이다. | | `POST /v1/chat/completions` | Chat Completions. 스트리밍 여부 모두 지원. | | `POST /v1/responses` | OpenAI Responses 형태. | | `POST /v1/embeddings` | LMSP 1.0 핵심 인터페이스에는 embeddings 엔드포인트가 없다. 어댑터가 `capabilities.embeddings`가 `true`인 모델을 위해 이 엔드포인트를 직접 제공한다. | 이 엔드포인트들은 LMSP 인터페이스와 같은 베어러 토큰을 받는다. OpenAI 프로토콜을 쓰는 클라이언트는 LMSP 토큰을 API 키로 설정한다. 첫 구현은 `/v1/models`와 `/v1/chat/completions`로 시작할 수 있다([12장](https://madeforagent.com/ko/lmsp/1.0/mvp.md)). ## 8.2 Ollama 호환 엔드포인트 {#ollama} 서비스는 Ollama용으로 작성된 클라이언트도 동작하도록 다음 엔드포인트도 제공하는 **것이 좋다**. | 엔드포인트 | 비고 | | --- | --- | | `GET /api/tags` | LMSP 카탈로그의 모델을 나열한다. | | `POST /api/chat` | 채팅. | | `POST /api/generate` | 일반 텍스트 생성. | 이를 제공하는 서비스는 `kind`가 `ollama`인 인터페이스를 선언한다. ## 8.3 하나의 실행 코어 {#core} 호환 계층은 데이터 변환만 수행해**야 한다**. 모델 관리 로직을 별도로 만들어서는 **안 된다**. 호환 인터페이스의 요청은 LMSP 응답으로 매핑되어, 네이티브 요청과 같은 코어와 어댑터에서 실행되며, 같은 모델 카탈로그와 인증을 사용한다. ```text OpenAI Chat Completions | 요청 매핑 v LMSP Response | 백엔드 적응 v llama.cpp / Ollama / vLLM / 기타 런타임 ``` `/v1`과 `/api` 엔드포인트에서 서비스는 수정하지 않은 클라이언트가 해석할 수 있도록, 해당 API가 정의한 형식으로 오류를 보고하는 **것이 좋다**. LMSP 인터페이스는 항상 `application/problem+json`을 사용한다. ## 8.4 한계 {#boundaries} 호환성에는 한계가 있으며, 이를 분명히 밝혀 둘 필요가 있다. - 기존 OpenAI 호환 클라이언트는 보통 `base_url`만 바꾸면 된다. LMSP 검색이 자동으로 생기지는 않는다. - 서비스를 자동으로 찾고, 자세한 기능을 읽고, 이벤트 스트림을 재개하고, 비동기 작업을 관리할 수 있는 것은 LMSP를 지원하는 클라이언트뿐이다. - 설정할 수 없고 공식 클라우드 주소만 받아들이는 클라이언트를 동작하게 만들 수 있는 서버 측 프로토콜은 없다. --- # 공통 규칙 > 모든 LMSP 엔드포인트가 공유하는 규칙. 버전 헤더와 확장, 멱등성 키, 오류 코드를 가진 problem+json 오류 형식, 상태 점검을 정의합니다. Source: https://madeforagent.com/ko/lmsp/1.0/common-rules/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 9.1 버전과 확장 {#versioning} 각 LMSP 요청은 다음 헤더를 실을 **수 있다**. ```http LMSP-Version: 1.0 LMSP-Client: example-client/1.2.0 ``` `LMSP-Version`은 클라이언트가 대상으로 하는 프로토콜 버전이다. `LMSP-Client`는 진단을 위해 클라이언트 소프트웨어를 `name/version` 형식으로 식별한다. 서비스는 이를 근거로 프로토콜 동작을 바꾸어서는 **안 된다**. 규칙: - 경로의 `v1`은 주 버전을 뜻한다. - 알 수 없는 주 버전을 지정한 요청에는 `426 Upgrade Required`와 오류 코드 `unsupported_protocol_version`으로 응답한다. - 새로운 선택적 필드는 1.x 호환성을 깨뜨려서는 **안 된다**. - 서비스와 클라이언트는 알지 못하는 필드를 무시해**야 한다**. - 비공개 확장은 `extensions` 객체에 넣고 역도메인 표기로 이름을 짓는다. ```json { "extensions": { "com.example.cache": { "enabled": true } } } ``` ## 9.2 멱등성 {#idempotency} 응답 생성, 작업 생성, 모델 로드처럼 무언가를 만드는 요청은 멱등성 키를 받는다. ```http Idempotency-Key: ``` 서비스는 키를 요청 본문의 다이제스트 및 결과와 함께 저장한다. - 같은 키에 같은 본문이면 원래 결과를 반환한다. - 같은 키에 다른 본문이면 `409 Conflict`와 `idempotency_conflict`로 응답한다. 키의 범위는 클라이언트와 엔드포인트이다. 키는 ULID나 UUID처럼 최대 255자의 고유한 값인 **것이 좋다**. 서비스는 키를 최소 24시간 보관하는 **것이 좋다**. 서비스는 스트리밍 요청에서는 키를 무시할 **수 있다**. 안전한 재시도가 필요한 클라이언트는 [작업](https://madeforagent.com/ko/lmsp/1.0/tasks.md)을 사용한다. ## 9.3 오류 {#errors} LMSP 인터페이스의 모든 오류는 `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 } ``` | 필드 | 필수 | 의미 | | --- | --- | --- | | `type` | 예 | 오류의 종류를 식별하는 URI. LMSP는 `urn:lmsp:error:` 뒤에 밑줄을 하이픈으로 바꾼 코드를 붙여 쓴다. | | `title` | 예 | 사람이 읽을 수 있는 짧은 요약. | | `status` | 예 | HTTP 상태 코드. | | `code` | 예 | 아래 목록에 있는, 기계 판독 가능한 오류 코드. | | `detail` | 아니요 | 이번 오류에 대한 사람이 읽을 수 있는 설명. 비밀 값, 프롬프트, 생성된 콘텐츠를 담아서는 **안 된다**. | | `request_id` | 아니요 | 서비스 로그와 대조하기 위한 요청의 식별자. | | `retryable` | 예 | 나중에 요청을 반복하면 성공할 수 있는지 여부. | 일반적인 오류 코드: | 코드 | HTTP 상태 | 재시도 가능 | 의미 | | --- | --- | --- | --- | | `invalid_request` | 400 | 아니요 | 요청 형식이 잘못되었거나 매개변수가 올바르지 않다. | | `unauthorized` | 401 | 아니요 | 토큰이 없거나, 올바르지 않거나, 만료되었거나, 폐기되었다. 또는 페어링 코드가 틀렸다. | | `forbidden` | 403 | 아니요 | 토큰에 필요한 스코프가 없거나, `Origin` 또는 `Host` 검사에 실패했다. | | `model_not_found` | 404 | 아니요 | 이 id의 로컬 모델이 없다. | | `model_not_ready` | 409 | 예 | 모델이 로드되어 있지 않고 요청 시 로드되지도 않는다. | | `model_load_failed` | 500 | 예 | 모델 로드에 실패했다. | | `context_length_exceeded` | 400 | 아니요 | 입력이 모델의 컨텍스트 윈도우에 들어가지 않는다. | | `unsupported_capability` | 400 또는 501 | 아니요 | 모델 또는 서비스가 요청에 필요한 것을 지원하지 않는다. 구현되지 않은 선택적 엔드포인트는 501로 응답한다. | | `unsupported_protocol_version` | 426 | 아니요 | 서비스가 요청한 주 버전을 지원하지 않는다. | | `response_not_found` | 404 | 아니요 | 이 id의 저장된 응답이 없다. | | `task_not_found` | 404 | 아니요 | 이 id의 작업이 없다. | | `task_not_cancelable` | 409 | 아니요 | 작업이 이미 성공했거나 실패했다. | | `idempotency_conflict` | 409 | 아니요 | 그 멱등성 키가 이미 다른 요청 본문과 함께 쓰였다. | | `event_history_expired` | 410 | 아니요 | 요청한 이벤트가 보존 기간보다 오래되었다. | | `rate_limited` | 429 | 예 | 요청이 너무 많다. | | `resource_exhausted` | 503 | 예 | 기기의 메모리나 다른 리소스가 부족하다. | | `backend_unavailable` | 503 | 예 | 모델 런타임에 연결할 수 없다. | | `server_restarted` | 503 | 예 | 작업이 끝나지 않은 상태에서 서비스가 재시작되었다. | | `internal_error` | 500 | 경우에 따라 다름 | 서비스 내부의 예기치 못한 실패. | `retryable` 필드가 기준이다. 위 열은 일반적인 값을 보여 준다. 서비스는 코드를 더 정의할 **수 있다**. 클라이언트는 알지 못하는 코드를 HTTP 상태와 `retryable` 필드로 처리해**야 한다**. ## 9.4 상태 점검 {#health} ```http GET /lmsp/v1/health ``` ```json { "status": "ready", "service_id": "svc_01J8M4Y8K6M5E6K6Z7Z0Y8W3M2", "uptime_seconds": 86420, "loaded_models": 1, "active_tasks": 2, "storage": "ok" } ``` | 필드 | 의미 | | --- | --- | | `status` | `starting`, `ready`, `degraded`, `stopping`. | | `service_id` | 서비스 설명에 있는 서비스 id. | | `uptime_seconds` | 서비스가 시작된 뒤 지난 초. | | `loaded_models` | `ready` 상태인 모델의 수. | | `active_tasks` | `queued`, `loading`, `running`, `cancelling` 상태인 작업의 수. | | `storage` | `ok` 또는 `error`. | `status`가 `ready` 또는 `degraded`이면 `200 OK`, 그렇지 않으면 `503 Service Unavailable`이다. 상태 점검 엔드포인트는 운영 상태만 반환한다. 모델 경로, 프롬프트, 액세스 토큰 또는 그 밖의 민감한 정보를 반환해서는 **안 된다**. --- # 로컬 보안 > LMSP 서비스의 보안 요구 사항. 루프백 바인딩, 페어링과 베어러 토큰, 스코프, 악성 웹 페이지에 대비한 Origin 및 Host 검사, LAN 모드, 로깅 규칙을 정의합니다. Source: https://madeforagent.com/ko/lmsp/1.0/security/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 10.1 기본 원칙 {#baseline} localhost에서만 수신 대기한다고 절대적으로 안전한 것은 아니다. 악성 웹 페이지가 여전히 로컬 포트에 접근을 시도할 수 있기 때문이다. 그래서 LMSP는 여러 조치를 결합한다. - 서비스는 기본적으로 루프백 주소에 바인딩해**야 한다**. LAN 모드가 켜져 있지 않은 한([10.5절](https://madeforagent.com/ko/lmsp/1.0/security.md#lan)) `0.0.0.0`이나 그 밖의 루프백이 아닌 주소에서 수신 대기해서는 **안 된다**. - 서비스는 처음 실행할 때 엔트로피가 높은 임의의 서비스 비밀 값을 생성하고, 내장된 기본값이 아니라 그 값으로 토큰을 발급하고 검증하는 **것이 좋다**. - 핵심 인터페이스에 대한 모든 요청은 [4.2절](https://madeforagent.com/ko/lmsp/1.0/endpoints.md#conventions)에 나열된 부트스트랩 엔드포인트를 제외하고 유효한 베어러 토큰을 담**아야 한다**. 루프백에서 왔다는 것만으로는 결코 충분하지 않다. ## 10.2 페어링과 토큰 {#pairing} 클라이언트는 로컬 페어링 코드로 자신만의 베어러 토큰을 얻는다. 사용자는 서비스가 제공하는 방법으로 서비스가 페어링 코드를 보여 주게 하고, 클라이언트는 그 코드를 제출한다. ```http POST /lmsp/v1/pair Content-Type: application/json ``` ```json { "client_name": "example-client", "pairing_code": "K7QD-4M2X", "scopes": ["inference"] } ``` | 요청 필드 | 의미 | | --- | --- | | `client_name` | 클라이언트의 표시 이름. 사용자에게 보인다. | | `pairing_code` | 사용자가 서비스에서 얻은 코드. | | `scopes` | 요청하는 스코프의 선택적 목록. 기본값은 `["inference"]`. | 서비스는 토큰으로 답한다. `token_type`은 항상 `Bearer`이다. ```json { "client_id": "cli_01J8MAT3W6X9B2D5F8H1K4N7QZ", "access_token": "lmsp_example_3f9a1c52e7d84b60", "token_type": "Bearer", "scopes": ["inference"] } ``` 그 뒤 클라이언트는 모든 요청에 `Authorization: Bearer `을 보낸다. - 페어링 코드는 일회용이어**야 하며** 5분 안에 만료되는 **것이 좋다**. 최소 40비트의 엔트로피를 담는 **것이 좋다**. 서비스는 실패한 시도에 속도 제한을 걸어**야 한다**. 틀렸거나 만료된 코드에는 `401`과 `unauthorized`로 응답한다. - 토큰은 클라이언트마다 독립적이므로, 사용자는 다른 클라이언트에 영향을 주지 않고 한 클라이언트를 폐기할 수 있다. 서비스는 사용자가 페어링된 클라이언트를 나열하고 폐기할 수 있게 하는 **것이 좋다**. - 클라이언트는 토큰을 운영체제 키체인이나 자격 증명 관리자에 보관하는 **것이 좋다**. 서비스는 각 토큰의 해시만 저장하는 **것이 좋다**. - 토큰이 없거나, 올바르지 않거나, 폐기된 경우에는 `401`, `unauthorized`, `WWW-Authenticate: Bearer` 헤더로 응답한다. 토큰은 유효하지만 엔드포인트가 요구하는 스코프가 없으면 `403`과 `forbidden`으로 응답한다. ## 10.3 스코프 {#scopes} 권한은 별개의 스코프로 나뉜다. 모델 다운로드, 모델 삭제, 도구 실행이 inference 스코프에 얹혀 가는 일은 결코 없다. | 스코프 | 허용하는 것 | | --- | --- | | `inference` | 모델 카탈로그 읽기, 응답과 작업 만들기, 클라이언트 자신의 작업 읽기와 취소. | | `models:manage` | 모델 로드와 언로드. | | `models:install` | 예약됨. 모델 다운로드와 삭제. | | `tools:execute` | 예약됨. 서비스 측에서 도구 실행. | 작업과 저장된 응답은 그것을 만든 클라이언트에 속한다. 서비스는 다른 클라이언트의 요청에는 그것들이 없는 것처럼 응답하는 **것이 좋다**(`task_not_found` 또는 `response_not_found`). ## 10.4 브라우저 및 네트워크 보호 {#browser} - 서비스는 기본적으로 교차 출처 브라우저 요청을 거부하고, 명시적으로 설정된 `Origin` 값만 허용해**야 한다**. 다른 `Origin`을 가진 요청에는 `403`과 `forbidden`으로 응답한다. - 출처가 허용되면 서비스는 그 CORS 프리플라이트 요청에 응답하고, `Access-Control-Allow-Origin`에 `*`가 아니라 정확히 그 출처를 담아 보낸다. - 서비스는 DNS 리바인딩을 막기 위해 `Host` 헤더를 검증해**야 한다**. 서비스가 담당하는 호스트 이름, 즉 루프백 이름과 주소, 그리고 LAN 모드를 위해 설정된 이름만 받아들인다. 그 밖의 값에는 `403`과 `forbidden`으로 응답한다. ## 10.5 LAN 모드 {#lan} LAN 모드는 루프백 주소 밖에서도 수신 대기하는 것을 뜻한다. - 서비스는 사용자가 명시적으로 켜지 않는 한 LAN 모드를 활성화해서는 **안 된다**. - LAN 모드에서 서비스는 인증을 요구해**야 하며** TLS도 활성화하는 **것이 좋다**. 클라이언트는 사용자가 명시적으로 허용하지 않는 한 루프백이 아닌 주소로 일반 HTTP를 통해 토큰을 보내지 **않는 것이 좋다**. - mDNS 광고는 LAN 모드이면서 사용자가 LAN 검색도 켠 경우에만 허용된다([3.2절](https://madeforagent.com/ko/lmsp/1.0/discovery.md#mdns)). ## 10.6 로깅과 개인정보 {#logging} - 모델 경로, 로그, 프롬프트, 생성된 콘텐츠는 mDNS 광고에 포함되어서는 **안 된다**. - 기본적으로 서비스는 메타데이터(시각, 엔드포인트, 상태 코드, id, 토큰 수, 소요 시간)만 기록하는 **것이 좋다**. 전체 프롬프트나 생성된 콘텐츠는 기록하지 **않는 것이 좋다**. - 상태 점검 응답과 오류 상세 정보에는 모델 경로, 프롬프트, 액세스 토큰, 그 밖의 민감한 정보가 포함되어서는 **안 된다**. --- # 영속화 > LMSP 서비스가 상태를 저장하는 방법. 내장 SQLite 데이터베이스, 유지할 테이블, 작업과 이벤트에 대한 트랜잭션 규칙, 보존 기간, 시작 시 복구를 정의합니다. Source: https://madeforagent.com/ko/lmsp/1.0/persistence/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 11.1 저장소 {#storage} 서비스는 상태를 내장 [SQLite](https://www.sqlite.org/) 데이터베이스에 보관하는 **것이 좋다**. 제안하는 테이블 구성은 다음과 같다. | 테이블 | 내용 | | --- | --- | | `service_config` | 서비스 정체성과 설정. | | `clients` | 페어링된 클라이언트, 토큰 해시, 스코프. | | `models` | 모델 메타데이터와 내용 다이제스트. | | `responses` | 저장된 응답. | | `tasks` | 비동기 작업. | | `task_events` | 각 작업의 이벤트 로그. 순번 포함. | | `idempotency_keys` | 멱등성 키, 요청 다이제스트, 결과. | 모델 파일은 데이터베이스 밖에 두어**야 한다**. 데이터베이스에는 메타데이터와 내용 다이제스트만 담는다. 테이블 이름과 레이아웃은 구현이 선택한다. 클라이언트가 관찰할 수 있고 상호 운용되어야 하는 것은 다음 두 절의 동작이다. ## 11.2 규칙 {#rules} 서비스는 어떤 저장소를 쓰든 다음을 지킨다. - `202 Accepted`를 반환하기 전에 작업을 영속화해**야 한다**. - 이벤트 순번과 작업 상태를 같은 트랜잭션에서 갱신해**야 한다**. - 취소 전이를 원자적으로 만들어, 작업이 취소된 뒤 완료되는 경합을 피해**야 한다**. - 끝난 작업과 이벤트를 설정할 수 있는 보존 기간 동안 보관하는 **것이 좋다**. - 로그를 정리할 때 미완료 작업을 삭제해서는 **안 된다**. - 시작할 때 남아 있는 `queued`, `loading`, `running`, `cancelling` 작업을 훑어, 각각의 재시작 정책을 적용해**야 한다**([7.5절](https://madeforagent.com/ko/lmsp/1.0/tasks.md#restart)). ## 11.3 시작 시 복구 {#recovery} 아래 표는 남아 있던 작업이 `on_restart` 정책에 따라 옮겨 가는 상태를 보여 준다. | 시작 시 발견된 상태 | `fail` | `retry` | `resume` | | --- | --- | --- | --- | | `queued` | `failed` | `queued` | `queued` | | `loading` | `failed` | `queued` | `queued` | | `running` | `failed` | `queued` | `running`(스냅샷에서 이어서) | | `cancelling` | `cancelled` | `cancelled` | `cancelled` | `failed`로 옮겨진 작업은 오류 코드 `server_restarted`와 `response.failed` 이벤트를 받는다. `cancelled`로 옮겨진 작업은 `response.cancelled` 이벤트를 받는다. 새 상태와 그 이벤트는 하나의 트랜잭션으로 기록된다([11.2절](https://madeforagent.com/ko/lmsp/1.0/persistence.md#rules)). --- # 최소 실행 가능 범위 > 첫 LMSP 구현을 위한 권장 범위. 쓸 만한 닫힌 흐름을 이루는 열 가지 항목과, 사양 중 이후 릴리스로 미룰 수 있는 부분을 제시합니다. Source: https://madeforagent.com/ko/lmsp/1.0/mvp/ Language: ko Specification: LMSP 1.0 (draft) Updated: 2026-11-08 ## 12.1 첫 번째 버전 {#scope} 첫 번째 버전이 모든 멀티모달 기능, 모델 다운로드, 실시간 음성을 구현할 필요는 없다. 다음을 제공하면 쓸 만한 닫힌 흐름이 만들어진다. 1. `/.well-known/lmsp.json`을 통한 서비스 검색. 2. `_lmsp._tcp.local.`의 선택적 LAN 광고. 3. `GET /lmsp/v1/models`. 4. `POST /lmsp/v1/responses`. 동기 방식과 SSE 스트림 모두. 5. 비동기 작업: 생성, 조회, 취소와 SQLite 영속화. 6. `Last-Event-ID` 또는 `after`로 끊긴 스트림 재개. 7. 베어러 토큰, 로컬 페어링, `Origin` 및 `Host` 보호. 8. OpenAI 호환 `/v1/models` 및 `/v1/chat/completions` 엔드포인트. 9. OpenAPI 3.2 계약 파일과 언어 간 적합성 테스트. 10. 최소 하나의 실제 백엔드, 그리고 서비스를 재시작한 뒤 작업이 올바르게 복구되는지 확인. 이 설계의 핵심은 이렇다. LMSP가 전체 기능을 제공하고, OpenAI 인터페이스가 즉시 쓸 수 있는 호환성을 제공하며, mDNS와 서비스 설명이 자동 검색을 제공하고, SQLite 작업 모델이 로컬 영속화를 제공한다. ## 12.2 미룰 수 있는 것 {#later} 위 목록에 없는 것은 이후 릴리스에서 추가할 수 있다. 여기에는 선택적 엔드포인트(저장된 응답, 모델 로드와 언로드), OpenAI `/v1/responses` 및 `/v1/embeddings` 엔드포인트, Ollama 호환 API, 텍스트 이외의 모든 콘텐츠 유형이 포함된다. 이 범위는 쓸 만한 첫 릴리스로 가는 길이지 적합성 프로필이 아니다. LMSP 1.0을 지원한다고 밝히는 서비스는 여전히 [4장](https://madeforagent.com/ko/lmsp/1.0/endpoints.md)의 필수 엔드포인트를 모두 구현하고, 다른 페이지에 기술된 필수 동작도 구현한다.