From 63ac3e66d8c00a00197ff9b134d34e889cd317d0 Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 9 Jan 2026 16:25:40 +0900 Subject: [PATCH 1/4] docs/protocol+proto: Move to LCP v0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define the v0.3 call → quote → pay → stream → complete flow. - Update the wire spec (docs/protocol) to v0.3 message types and TLVs. - Redesign the LCPD gRPC API around CallSpec/Quote/Complete. - Regenerate Go protobuf/gRPC stubs (go-lcpd/gen and proto-go). --- docs/protocol/protocol-ja.md | 80 +- docs/protocol/protocol.md | 797 +++++------ go-lcpd/gen/go/lcpd/v1/lcpd.cobra.pb.go | 64 +- go-lcpd/gen/go/lcpd/v1/lcpd.pb.go | 1662 ++++++++++------------- go-lcpd/gen/go/lcpd/v1/lcpd_grpc.pb.go | 44 +- go-lcpd/proto/lcpd/v1/lcpd.proto | 261 ++-- proto-go/lcpd/v1/lcpd.pb.go | 1662 ++++++++++------------- proto-go/lcpd/v1/lcpd_grpc.pb.go | 44 +- 8 files changed, 1993 insertions(+), 2621 deletions(-) diff --git a/docs/protocol/protocol-ja.md b/docs/protocol/protocol-ja.md index bae78b9..0df9ec5 100644 --- a/docs/protocol/protocol-ja.md +++ b/docs/protocol/protocol-ja.md @@ -1,44 +1,40 @@ -# LCP v0.1 仕様(日本語概要) +# LCP v0.3 仕様(日本語概要) -このページは LCP v0.1 の日本語概要です。 +このページは LCP v0.3 の日本語概要です。 正規(normative)な仕様は英語版の `docs/protocol/protocol.md` を参照してください(このリポジトリでは英語版が SSOT です)。 ## 概要 LCP(Lightning Compute Protocol)は、Lightning の既存トランスポート(BOLT #1 カスタムメッセージ)上で動作するアプリケーション層プロトコルです。 -Quote → Pay → Result の流れで、計算ジョブに対する支払いと結果受け渡しを行います。 +LCP v0.3 は **manifest → call → quote → pay → stream → complete** の流れで、メソッド呼び出し(`method`)の実行と支払い・結果配送を行います。 - BOLT(Lightning 仕様)を変更しません -- 直接の Lightning ピア接続(Requester と Provider の peering)が必要です -- 支払い(BOLT11 invoice)とジョブ条件(Terms)を `terms_hash` でバインドし、invoice swapping を防ぎます - -## ゴール(抜粋) - -LCP v0.1 は以下を定義します: - -- ジョブ交渉のための最小 P2P メッセージセット -- 支払いとジョブ条件のバインディング(invoice swapping 対策) -- 結果を 1 メッセージで返す仕組み -- アプリケーション層の idempotency / リプレイ耐性 -- Provider が受理するタスクテンプレート(`task_kind` + `params`)を接続スコープで宣言する方法 +- Requester と Provider の **直接の Lightning ピア接続**が必要です +- 支払い(BOLT11 invoice)とコール条件(Terms)を `terms_hash` でバインドし、invoice swapping を防ぎます ## 基本フロー(概念) -1. Requester → Provider: `lcp_quote_request` -2. Provider → Requester: `lcp_quote_response`(`price_msat` / `terms_hash` / BOLT11 invoice 等) -3. Requester が invoice を支払う(`description_hash == terms_hash` 等を検証) -4. Provider → Requester: `lcp_result` +1. 双方が `lcp_manifest` を交換(必須) +2. Requester → Provider: `lcp_call`(`method` + optional `params`) +3. Requester → Provider: request stream(`lcp_stream_begin/chunk/end`, `stream_kind=request`) +4. Provider → Requester: `lcp_quote`(`price_msat` / `terms_hash` / BOLT11 invoice) +5. Requester が invoice を支払う(`description_hash == terms_hash` 等を検証) +6. Provider → Requester: response stream(`stream_kind=response`) +7. Provider → Requester: `lcp_complete`(終端、必要に応じて response の hash/len 等を含む) -## メッセージ種別(v0.1) +## メッセージ種別(v0.3) -LCP v0.1 は BOLT #1 の odd type のカスタムメッセージを使います(unknown odd は ignore / unknown even は fatal のルールに準拠)。 +LCP v0.3 は BOLT #1 の odd type のカスタムメッセージを使います(unknown odd は ignore / unknown even は fatal のルールに準拠)。 -- 42081: `lcp_manifest` -- 42083: `lcp_quote_request` -- 42085: `lcp_quote_response` -- 42087: `lcp_result` -- 42095: `lcp_cancel` -- 42097: `lcp_error` +- 42101: `lcp_manifest` +- 42103: `lcp_call` +- 42105: `lcp_quote` +- 42107: `lcp_complete` +- 42109: `lcp_stream_begin` +- 42111: `lcp_stream_chunk` +- 42113: `lcp_stream_end` +- 42115: `lcp_cancel` +- 42117: `lcp_error` ## TLV と拡張性(要点) @@ -48,20 +44,30 @@ LCP v0.1 は BOLT #1 の odd type のカスタムメッセージを使います - 同一 type の重複はしません - unknown TLV は無視します(forward compatibility) -## LCP v0.2 の `task_kind`(概要) +また、全メッセージに `protocol_version`(v0.3 は `3`)が含まれます。`lcp_manifest` 以外の call-scope メッセージは `call_id` / `msg_id` / `expiry` を持ち、受信側はリプレイ防止のために (`call_id`,`msg_id`) で de-duplicate します。 + +## ストリーム(request/response) + +大きな payload は `lcp_stream_*` で分割転送します。 + +- `stream_kind=1`: request +- `stream_kind=2`: response + +チャンクは `seq` が 0 から 1 ずつ増える必要があります。`lcp_stream_end` では decoded bytes の `total_len` と `sha256` を検証します(`sha256` は decoded bytes の SHA256)。 + +`content_encoding` は少なくとも `identity` をサポートする必要があります。 -LCP v0.2 では以下の `task_kind` を定義し、OpenAI 互換 API の HTTP request/response body bytes(JSON / SSE)を LCP の input/result stream の decoded bytes としてそのまま運びます(中身は解釈しません)。 +## `terms_hash` と invoice binding(要点) -* `task_kind="openai.chat_completions.v1"`(`POST /v1/chat/completions`) -* `task_kind="openai.responses.v1"`(`POST /v1/responses`) +Provider は BOLT11 invoice の `description_hash` を `terms_hash` と一致させることで、請求書とコール条件をバインドします。 +Requester は支払い前に少なくとも以下を検証します: -- input stream: `content_type="application/json; charset=utf-8"`, `content_encoding="identity"` -- `params` は TLV stream で、少なくとも `model` を含みます -- result stream: - - `stream` omitted/false(non-streaming): `content_type="application/json; charset=utf-8"`, `content_encoding="identity"` - - `stream:true`(streaming): `content_type="text/event-stream; charset=utf-8"`, `content_encoding="identity"`(SSE bytes をそのまま搬送) +- `description_hash == terms_hash` +- invoice の payee pubkey が Provider の peer pubkey と一致 +- invoice の金額が `price_msat` と一致(amount-less invoice は不可) +- invoice の expiry が `quote_expiry` を超えない -詳細・正規(normative)な仕様は英語版 `docs/protocol/protocol.md` の §5.2.1 を参照してください。 +`terms_hash` 自体は、仕様で定義された `terms_tlvs` の canonical TLV stream を `SHA256` したものです(詳細は英語版仕様)。 ## 参考 diff --git a/docs/protocol/protocol.md b/docs/protocol/protocol.md index 32b05bd..94fa60b 100644 --- a/docs/protocol/protocol.md +++ b/docs/protocol/protocol.md @@ -1,723 +1,664 @@ -# LCP v0.2 Specification (Draft) +# LCP v0.3 Specification Draft -## LCP v0.2 Overview +## Streaming Method Call Protocol over Lightning Custom Messages -LCP is an application-layer protocol carried over Lightning’s existing transport. -It enables a **quote → pay → stream** flow using existing Lightning node capabilities (plugins / external processes). -LCP does not modify the BOLTs. +--- -This document defines **LCP v0.2**. +## 1. Overview, goals, and non-goals -Key changes (v0.2): +### 1.1 Overview -* **NOT backward compatible** with v0.1. -* Adds **chunked streaming** for large payloads (input and result). -* Payment is **single upfront payment per job** (one invoice per job). -* `lcp_manifest` exchange is **mandatory** before any job-scope messages. +LCP is an application-layer protocol carried over Lightning’s existing transport (BOLT #1 custom messages). +It enables a **manifest → call → quote → pay → stream → complete** flow using existing Lightning node capabilities (plugins / external processes). +LCP does not modify the BOLTs. -LCP v0.2 uses BOLT #1 custom messages over a direct peer connection. +This document defines **LCP v0.3**. ---- +Key properties (v0.3): -## 1. Goals +* **Not backward compatible** with earlier drafts. +* Uses a **method-call model** (`method` string identifies the procedure). +* **Streaming-only payloads**: request and response bodies are always transferred via streams (chunked messages). +* Provider capability advertisement via mandatory **`lcp_manifest`** before any call messages. -LCP v0.2 defines: +### 1.2 Goals -* A minimal P2P message set to negotiate a compute job -* Binding between payment (BOLT11 invoice) and job terms to prevent invoice swapping -* Streaming delivery of large **input** and **result** payloads using chunked messages -* Application-layer idempotency and replay resistance -* Provider capability advertisement at connection scope (`lcp_manifest`) +LCP v0.3 defines: -Non-goals: +* A minimal P2P message set to negotiate and execute a **method call** +* Binding between payment (BOLT11 invoice) and call terms to prevent invoice swapping +* Streaming transfer of large **request** and **response** payloads using chunked messages +* Connection-scope capability advertisement (supported methods + limits) via `lcp_manifest` +* Application-layer idempotency and replay resistance for call-scope messages and stream chunks + +### 1.3 Non-goals + +LCP v0.3 does **not** define: * Forwarding via onion messages / blinded paths * Atomic swap guarantees beyond invoice binding +* Standard semantics for any particular method (method semantics are referenced by method documentation) * Multi-party orchestration +* A new non-Lightning transport --- -## 2. Conventions and terminology - -The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as described in RFC 2119. +## 2. Transport -* Requester: the node requesting a job -* Provider: the node executing a job and delivering the result -* Job: a compute task identified by `job_id` (32 bytes) -* Stream: a large payload transferred via `lcp_stream_*` messages -* Connection scope: information tied to a connection (not tied to a job) -* Job scope: information tied to a specific job +### 2.1 Direct peer requirement -All additional fields are encoded only as TLV streams. +All LCP v0.3 messages are sent as BOLT #1 custom messages. -### 2.1 Protocol versioning +* The Requester and Provider MUST have an active Lightning peer connection. +* LCP MUST NOT define or require a new non-Lightning transport. -LCP versions are written as `vMAJOR.MINOR` (for example, v0.2). +How the Requester discovers the Provider’s network address is out of scope. -The wire field `protocol_version` is a `u16` derived from the (major, minor) pair: +### 2.2 Custom message type range -* `protocol_version = major*100 + minor` +LCP v0.3 uses the following message type numbers (all ≥ 32768). -Notes: +BOLT #1 compatibility (odd/even parity rules): -* This specification defines LCP v0.2, therefore `protocol_version = 2`. -* Patch-level revisions to this document (v0.2.x) do not change `protocol_version`. +* All LCP message types MUST be odd. + (Unknown odd messages are ignored; unknown even messages are fatal.) ---- +Assignments: -## 3. Transport +* 42101: `lcp_manifest` +* 42103: `lcp_call` +* 42105: `lcp_quote` +* 42107: `lcp_complete` +* 42109: `lcp_stream_begin` +* 42111: `lcp_stream_chunk` +* 42113: `lcp_stream_end` +* 42115: `lcp_cancel` +* 42117: `lcp_error` -### 3.1 Direct peer requirement +Receivers MUST ignore unknown TLVs. +Receivers MUST apply BOLT #1 odd/even parity rules to unknown custom message types. -All LCP v0.2 messages are sent as BOLT #1 custom messages. +--- -* The Requester and Provider MUST have an active Lightning peer connection. -* LCP MUST NOT define or require a new non-Lightning transport. +## 3. Envelope and encoding -Operational note (non-normative): +### 3.1 Terminology and requirements language -* How the Requester learns the Provider's network address is out of scope. - This spec assumes a known pubkey (and a reachable address). +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as described in RFC 2119. -### 3.2 Custom message type range +* Requester: the node initiating a call +* Provider: the node executing a call and delivering the response +* Call: a method invocation identified by `call_id` (32 bytes) +* Stream: a payload transferred via `lcp_stream_*` messages +* Connection scope: information tied to a connection (not tied to a call) +* Call scope: information tied to a specific call -LCP v0.2 uses the following message type numbers (all ≥ 32768). +### 3.2 Protocol versioning -BOLT #1 compatibility (odd/even parity rules): +LCP versions are written as `vMAJOR.MINOR` (for example, v0.3). -* All LCP message types MUST be odd. - (Unknown odd messages are ignored; unknown even messages are fatal.) +The wire field `protocol_version` is a `u16` derived from the (major, minor) pair: -Assignments: +* `protocol_version = major*100 + minor` -* 42081: `lcp_manifest` -* 42083: `lcp_quote_request` -* 42085: `lcp_quote_response` -* 42087: `lcp_result` (terminal job completion) -* 42089: `lcp_stream_begin` -* 42091: `lcp_stream_chunk` -* 42093: `lcp_stream_end` -* 42095: `lcp_cancel` -* 42097: `lcp_error` +This specification defines LCP v0.3, therefore: -Receivers MUST ignore unknown TLVs. -Receivers MUST apply BOLT #1 odd/even parity rules to unknown custom message types. +* `protocol_version = 3` ---- +Patch-level revisions (v0.3.x) do not change `protocol_version`. -## 4. TLV (envelope) +### 3.3 TLV stream encoding -Each LCP message payload is a TLV stream called `tlvs`. Unknown TLVs MUST be ignored. +Each LCP message payload is a TLV stream called `tlvs`. -TLV stream encoding (MUST): +Normative rules (MUST): * Each TLV record is encoded as `bigsize(type)`, `bigsize(length)`, then `value` (same as BOLT TLV streams). * Records MUST be sorted in ascending `type`. * Duplicate records with the same `type` MUST NOT be included. +* Unknown TLVs MUST be ignored. -### 4.1 type: 1 (`protocol_version`) +### 3.4 Common TLVs + +#### type: 1 (`protocol_version`) * data: [`u16`:`protocol_version`] -Meaning: +Rules: * `protocol_version` MUST be included in every LCP message. -* In this specification (LCP v0.2), `protocol_version` is 2. +* Implementations that do not support the version SHOULD ignore the message and MAY disconnect. -### 4.2 Job-scope envelope TLVs +### 3.5 Call-scope envelope TLVs -The following TLVs are common to all job-scope LCP messages (everything except `lcp_manifest`) (MUST). +The following TLVs are common to all call-scope messages (everything except `lcp_manifest`) (MUST): -The sender MUST include `job_id`, `msg_id`, and `expiry`. - -* type: 2 (`job_id`): [`32*byte`:`job_id`] +* type: 2 (`call_id`): [`32*byte`:`call_id`] * type: 3 (`msg_id`): [`32*byte`:`msg_id`] * type: 4 (`expiry`): [`tu64`:`expiry`] Meaning: -* `job_id` MUST be unpredictable (generated by the Requester). -* `msg_id` MUST be unique within the replay window, per sender, per `job_id`, - except for `lcp_stream_chunk` where `msg_id` is deterministic (§5.5.4). +* `call_id` MUST be unpredictable (generated by the Requester). +* `msg_id` MUST be unique within the replay window, per sender, per `call_id`, + except for `lcp_stream_chunk` where `msg_id` is deterministic (§6.4). * `expiry` is Unix epoch seconds. The receiver MUST ignore messages where `expiry < now`. Replay / idempotency retention: -To avoid unbounded memory usage, receivers MUST bound how long they retain replay / pending state. - Define: -* `MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS`: an implementation-defined constant (RECOMMENDED: 600). +* `MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS`: implementation-defined (RECOMMENDED: 600). * `effective_expiry = min(expiry, now + MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS)` Rules (MUST): -* The receiver MUST de-duplicate by (`job_id`, `msg_id`) at least until `effective_expiry`. -* If a duplicate (`job_id`, `msg_id`) is received before `effective_expiry`, it MUST be ignored. +* The receiver MUST de-duplicate by (`call_id`, `msg_id`) at least until `effective_expiry`. +* If a duplicate (`call_id`, `msg_id`) is received before `effective_expiry`, it MUST be ignored. * If `expiry > now + MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS`, the receiver MUST either: - * reject/ignore the message, or - * process it but retain replay/pending state only until `effective_expiry` (clamp). - -### 4.3 Encoding of `byte` TLVs -The LCP wire format is a TLV stream. Therefore, application-defined data is carried in the value of `byte` TLVs. + * reject/ignore the message, or + * process it but retain state only until `effective_expiry` (clamp). -Normative rules: +### 3.6 Encoding of byte/string TLVs -* Any `byte` TLV explicitly specified as a UTF-8 string in this spec MUST be valid UTF-8. -* All other `byte` TLVs MUST be treated as application-defined opaque bytes. - If the receiver cannot interpret them, it MAY ignore them. +* Any TLV explicitly specified as a UTF-8 string in this spec MUST be valid UTF-8. +* All other `byte` TLVs MUST be treated as opaque bytes. -### 4.4 List encodings (inside TLV values) +### 3.7 List encodings inside TLV values To represent multiple elements without repeating TLV types in the outer stream, this spec defines list encodings inside TLV values. -#### 4.4.1 `bigsize_list` - -* data: `bigsize(count)` + `count` repetitions of `bigsize(value_i)` - -#### 4.4.2 `string_list` +#### 3.7.1 `string_list` * data: `bigsize(count)` + `count` repetitions of (`bigsize(len_i)` + `len_i` bytes) * Each element MUST be a UTF-8 string. -#### 4.4.3 `bytes_list` +#### 3.7.2 `bytes_list` * data: `bigsize(count)` + `count` repetitions of (`bigsize(len_i)` + `len_i` bytes) -* Each element MUST be treated as application-defined opaque bytes. +* Each element MUST be treated as opaque bytes. --- -## 5. Messages +## 4. Manifest -## 5.1 type: 42081 (`lcp_manifest`) +### 4.1 `lcp_manifest` -`lcp_manifest` is a **mandatory** connection-scope message in which each peer declares limits and (optionally) supported task templates. +Type: 42101 (`lcp_manifest`) -* data: [`tlv_stream`:`tlvs`] +`lcp_manifest` is a **mandatory** connection-scope message in which each peer declares limits and (optionally) supported methods. Constraints: -* `lcp_manifest` is connection-scope and MUST NOT include job-scope envelope TLVs (`job_id`, `msg_id`, `expiry`). +* `lcp_manifest` is connection-scope and MUST NOT include call-scope envelope TLVs (`call_id`, `msg_id`, `expiry`). * Each side MUST send exactly one `lcp_manifest` per connection. -* No job-scope messages MUST be sent until **both** manifests have been received. +* No call-scope messages MUST be sent until **both** manifests have been received. + +If a call-scope message is received before manifest exchange completes, the receiver MUST ignore it and MAY respond with `lcp_error(code=manifest_required)` (if `call_id` is present). -Message-specific TLVs: +### 4.2 Manifest TLVs -### type: 11 (`max_payload_bytes`) +#### type: 11 (`max_payload_bytes`) * data: [`tu32`:`max_payload_bytes`] Meaning: -* Declares the maximum payload size (the BOLT custom message payload portion) that the sender can receive. -* The peer MUST NOT send subsequent messages whose payload exceeds the receiver's declared `max_payload_bytes`. -* Implementations SHOULD cap `max_payload_bytes` to roughly 1/4 of the BOLT #1 custom message maximum (`length=65535 bytes`), e.g. 16384 bytes. +* Maximum custom-message payload size the sender can receive. +* The peer MUST NOT send messages whose payload exceeds the receiver’s `max_payload_bytes`. +* Implementations SHOULD cap `max_payload_bytes` to roughly 1/4 of the BOLT #1 custom message maximum (65535), e.g. 16384 bytes. -### type: 14 (`max_stream_bytes`) +#### type: 14 (`max_stream_bytes`) * data: [`tu64`:`max_stream_bytes`] Meaning: -* Declares the maximum total decoded bytes the sender can receive for a single stream (`lcp_stream_begin` … `lcp_stream_end`). +* Maximum total decoded bytes the sender can receive for a single stream. -### type: 15 (`max_job_bytes`) +#### type: 15 (`max_call_bytes`) -* data: [`tu64`:`max_job_bytes`] +* data: [`tu64`:`max_call_bytes`] Meaning: -* Declares the maximum total decoded bytes the sender can receive across all streams in a job (input + result + any future streams). +* Maximum total decoded bytes the sender can receive across all streams in a call (request + response). -### type: 16 (`max_inflight_jobs`) (optional) +#### type: 16 (`max_inflight_calls`) (optional) -* data: [`u16`:`max_inflight_jobs`] +* data: [`u16`:`max_inflight_calls`] Meaning: -* Declares the maximum concurrent active jobs the sender is willing to handle per connection. +* Maximum concurrent active calls per connection the sender is willing to handle. -### type: 12 (`supported_tasks`) (optional) +#### type: 12 (`supported_methods`) (optional) -* data: [`byte`:`supported_tasks`] (`bytes_list`) +* data: [`byte`:`supported_methods`] (`bytes_list`) Meaning: -* `supported_tasks` enumerates the task templates that the Provider accepts in `lcp_quote_request`. -* Each element is a TLV stream called `task_tlvs`, corresponding to `lcp_quote_request`'s `task_kind` / `params`. -* The Requester SHOULD select a task template from this list when constructing `lcp_quote_request`. +* `supported_methods` enumerates method descriptors the Provider accepts in `lcp_call`. +* Each element is a TLV stream called `method_tlvs`. -`task_tlvs` (the element TLV stream): +`method_tlvs` (element TLV stream): -* type: 20 (`task_kind`) +* type: 20 (`method`): UTF-8 string (MUST) +* type: 23 (`request_content_types`) (optional): `string_list` +* type: 24 (`response_content_types`) (optional): `string_list` +* type: 26 (`docs_uri`) (optional): UTF-8 string +* type: 27 (`docs_sha256`) (optional): 32 bytes +* type: 28 (`policy_notice`) (optional): UTF-8 string - * data: [`byte`:`task_kind`] (UTF-8 string) +Recommended matching rule: -* type: 22 (`params_template`) +* If `supported_methods` is present and a received `lcp_call.method` does not match, the Provider SHOULD return `lcp_error(code=unsupported_method)`. - * data: [`byte`:`params_template`] (bytes depending on `task_kind`; standardized kinds include §5.2.1) - -Matching against `lcp_quote_request` (recommended): +--- -* If the Provider declares `supported_tasks`, and the received `lcp_quote_request` does not match at least one task template, the Provider SHOULD return `lcp_error(code=unsupported_task)`. -* Task template matching: - * `task_kind` MUST match as a string. - * `params` handling depends on `task_kind`. For standardized kinds defined in §5.2.1, the peer MUST follow the encoding rules there. +## 5. Call lifecycle ---- +LCP v0.3 is **streaming-only** for payloads: -## 5.2 type: 42083 (`lcp_quote_request`) +* The Requester MUST send the call request payload as a request stream. +* The Provider MUST send the call response payload as a response stream. +* There is no inline request/response body in control messages. -Starts a job negotiation and declares `task_kind` and optional parameters. +### 5.1 `lcp_call` -Input is NOT carried inline in v0.2. The Requester MUST send an input stream after the `lcp_quote_request` (§5.5). +Type: 42103 (`lcp_call`) -* data: [`tlv_stream`:`tlvs`] +Starts a call negotiation and declares the method name and an optional small parameter blob. -Required TLVs (minimum): +Required TLVs: -* type: 20 (`task_kind`): [`byte`:`task_kind`] (UTF-8 string, e.g., `openai.chat_completions.v1`) +* type: 20 (`method`): [`byte`:`method`] (UTF-8 string) Optional TLVs: -* type: 22 (`params`): [`byte`:`params`] (bytes depending on `task_kind`; standardized kinds include §5.2.1) +* type: 22 (`params`): [`byte`:`params`] (opaque bytes) +* type: 25 (`params_content_type`): [`byte`:`params_content_type`] (UTF-8 media type string) -Meaning: +Rules: -* The Provider MAY reject unsupported tasks via `lcp_error`. -* For standardized kinds defined in §5.2.1, the sender MUST follow the encoding rules there. -* If the Provider declares `lcp_manifest.supported_tasks`, the Requester SHOULD choose matching `task_kind` / `params`. +* The Provider MAY reject unsupported methods via `lcp_error`. +* If the Provider advertised `supported_methods`, the Requester SHOULD choose a matching method. -Input requirement (MUST): +Request stream requirement (MUST): -* After sending `lcp_quote_request`, the Requester MUST send exactly one input stream with `stream_kind=input` (§5.5.1). -* The Provider MUST NOT send `lcp_quote_response` until it has received and validated the input stream. +* After sending `lcp_call`, the Requester MUST send exactly one request stream with `stream_kind=request` (§6.1). +* The Provider MUST NOT send `lcp_quote` until it has received and validated the request stream. Idempotency (recommended): -* If the Provider receives multiple `lcp_quote_request` messages for the same `job_id`, it SHOULD return the same `lcp_quote_response` (if still valid) or SHOULD return an `lcp_error` indicating `quote_expired`. - ---- - -### 5.2.1 Standardized `task_kind` (v0.2) +* If the Provider receives multiple `lcp_call` messages for the same `call_id`, it SHOULD return the same `lcp_quote` if still valid; otherwise it SHOULD return `lcp_error(code=quote_expired)`. -LCP v0.2 standardizes the following `task_kind` values: +### 5.2 `lcp_quote` -* `task_kind = "openai.chat_completions.v1"` -* `task_kind = "openai.responses.v1"` +Type: 42105 (`lcp_quote`) -The standardized kinds in this section define only how bytes are carried over LCP: +Provides a price and a BOLT11 invoice bound to the call terms. -* LCP does not interpret, validate, or re-encode the HTTP bodies (JSON or SSE). They are treated as opaque bytes. -* Result stream chunk boundaries have no semantic meaning (they do not align with JSON tokens nor SSE events). +Required TLVs: -#### `task_kind = "openai.chat_completions.v1"` +* type: 30 (`price_msat`): [`tu64`:`price_msat`] +* type: 31 (`quote_expiry`): [`tu64`:`quote_expiry`] +* type: 32 (`terms_hash`): [`32*byte`:`terms_hash`] +* type: 33 (`payment_request`): [`byte`:`payment_request`] (BOLT11 invoice string, UTF-8) -Input stream interpretation: +Optional TLVs (commitment to response encoding): -* The decoded input stream bytes MUST be the exact HTTP request body bytes for an OpenAI-compatible `POST /v1/chat/completions` call. -* The Requester MUST set the input stream `content_type="application/json; charset=utf-8"` and `content_encoding="identity"`. +* type: 34 (`response_content_type`): UTF-8 string +* type: 35 (`response_content_encoding`): UTF-8 string -`params` encoding: +Invoice binding (MUST): -* `params` MUST be a TLV stream called `openai_chat_completions_v1_params_tlvs`. -* The TLV encoding for `openai_chat_completions_v1_params_tlvs` is the same as the TLV stream in §4. -* `openai_chat_completions_v1_params_tlvs` MUST include at least `model`. +* Provider MUST set invoice `description_hash == terms_hash`. +* Before paying, Requester MUST verify: -`openai_chat_completions_v1_params_tlvs` (standard TLVs): + * `description_hash == terms_hash` + * invoice payee pubkey == Provider’s peer pubkey + * invoice amount == `price_msat` (amount-less invoices MUST be rejected) + * invoice expiry time does not exceed `quote_expiry` -* type: 1 (`model`): [`byte`:`model`] (UTF-8 string) - Meaning: - * `model` MUST identify the execution target (for example, an OpenAI model ID). +Invoice time rules (MUST): -Constraint (recommended): +* Let `invoice_timestamp_unix` be the invoice timestamp (Unix epoch seconds). +* Let `invoice_expiry_seconds` be invoice expiry (seconds). +* Define `invoice_expiry_unix = invoice_timestamp_unix + invoice_expiry_seconds`. +* Requester MUST verify `invoice_expiry_unix <= quote_expiry`. -* Providers SHOULD reject `openai_chat_completions_v1_params_tlvs` containing unknown param types with `lcp_error(code=unsupported_params)`. + * Clock skew allowance MAY be applied (RECOMMENDED: 5 seconds). -Result: +Execution rule (MUST): -* The decoded result stream bytes MUST be the exact HTTP response body bytes for the corresponding OpenAI-compatible `POST /v1/chat/completions` call. -* If the request JSON field `stream` is omitted or false (non-streaming): - * The Provider MUST set the result stream `content_type="application/json; charset=utf-8"` and `content_encoding="identity"`. -* If the request JSON field `stream` is true (streaming): - * The decoded result stream bytes MUST be the exact `text/event-stream` (SSE) response body bytes, without interpretation. - * The Provider MUST set the result stream `content_type="text/event-stream; charset=utf-8"` and `content_encoding="identity"`. +* Provider MUST NOT start executing or delivering response payloads until invoice settlement. -Streaming note: +#### 5.2.1 `terms_hash` computation -* In the streaming case, the Provider MAY omit `total_len` / `sha256` in `lcp_stream_begin` (if unknown) and MUST provide them in `lcp_stream_end`. -* The Requester MAY consume or forward decoded bytes before validating `lcp_stream_end`. If validation fails, the Requester SHOULD treat the stream as invalid and SHOULD NOT treat the job as successful. +`terms_hash` (MUST): -#### `task_kind = "openai.responses.v1"` +* `terms_hash = SHA256(tlv_encode(terms_tlvs))` -Input stream interpretation: +`terms_tlvs` is a canonical TLV stream containing only: -* The decoded input stream bytes MUST be the exact HTTP request body bytes for an OpenAI-compatible `POST /v1/responses` call. -* The Requester MUST set the input stream `content_type="application/json; charset=utf-8"` and `content_encoding="identity"`. +* type: 1 (`protocol_version`): `u16` +* type: 2 (`call_id`): `32*byte` +* type: 20 (`method`): UTF-8 string +* type: 30 (`price_msat`): `tu64` +* type: 31 (`quote_expiry`): `tu64` +* type: 50 (`request_hash`): `32*byte` (`SHA256(request_decoded_bytes)`) +* type: 52 (`request_len`): `tu64` +* type: 53 (`request_content_type`): UTF-8 string +* type: 54 (`request_content_encoding`): UTF-8 string +* type: 51 (`params_hash`): `32*byte` (`SHA256(params_bytes)` or `SHA256(empty)`) +* type: 55 (`response_content_type`) (optional): UTF-8 string +* type: 56 (`response_content_encoding`) (optional): UTF-8 string -`params` encoding: +Rules: -* `params` MUST be a TLV stream called `openai_responses_v1_params_tlvs`. -* The TLV encoding for `openai_responses_v1_params_tlvs` is the same as the TLV stream in §4. -* `openai_responses_v1_params_tlvs` MUST include at least `model`. +* If `params` is present, `params_hash = SHA256(params_bytes)`. Otherwise `SHA256(empty)`. +* If `lcp_quote` includes 34/35, Provider MUST include 55/56 with identical values. -`openai_responses_v1_params_tlvs` (standard TLVs): +Request binding source (MUST): -* type: 1 (`model`): [`byte`:`model`] (UTF-8 string) - Meaning: - * `model` MUST identify the execution target (for example, an OpenAI model ID). +* Provider MUST compute request hash/len/content metadata from the **validated request stream** (§6). +* If the request stream cannot be validated, Provider MUST NOT issue a quote. -Constraint (recommended): +### 5.3 `lcp_complete` -* Providers SHOULD reject `openai_responses_v1_params_tlvs` containing unknown param types with `lcp_error(code=unsupported_params)`. +Type: 42107 (`lcp_complete`) -Result: +Terminal call completion message. -* The decoded result stream bytes MUST be the exact HTTP response body bytes for the corresponding OpenAI-compatible `POST /v1/responses` call. -* If the request JSON field `stream` is omitted or false (non-streaming): - * The Provider MUST set the result stream `content_type="application/json; charset=utf-8"` and `content_encoding="identity"`. -* If the request JSON field `stream` is true (streaming): - * The decoded result stream bytes MUST be the exact `text/event-stream` (SSE) response body bytes, without interpretation. - * The Provider MUST set the result stream `content_type="text/event-stream; charset=utf-8"` and `content_encoding="identity"`. +Required TLVs: -Extensions: +* type: 100 (`status`): `u16` -* Implementations MAY support additional `task_kind` values beyond the above. -* If the parties have not pre-agreed on the encoding of `input` / `params`, the Provider SHOULD return `lcp_error` with `unsupported_task`. + * 0: `ok` + * 1: `failed` + * 2: `cancelled` ---- +If a response stream was delivered, include: -## 5.3 type: 42085 (`lcp_quote_response`) +* type: 101 (`response_stream_id`): 32 bytes +* type: 102 (`response_hash`): 32 bytes (SHA256 of decoded response bytes) +* type: 103 (`response_len`): `tu64` +* type: 104 (`response_content_type`): UTF-8 string +* type: 105 (`response_content_encoding`): UTF-8 string -Provides a price and a BOLT11 invoice bound to the job terms. +If `status != ok`, optional: -* data: [`tlv_stream`:`tlvs`] +* type: 81 (`message`): UTF-8 string -Required TLVs: +Rules: -* type: 30 (`price_msat`): [`tu64`:`price_msat`] -* type: 31 (`quote_expiry`): [`tu64`:`quote_expiry`] -* type: 32 (`terms_hash`): [`32*byte`:`terms_hash`] -* type: 33 (`payment_request`): [`byte`:`payment_request`] (BOLT11 invoice string, UTF-8) +* After post-payment execution begins, Provider SHOULD deliver a response stream even for failures (e.g., an error event stream) and then send `lcp_complete(status=failed)` including response stream metadata. +* After receiving `lcp_complete`, Requester SHOULD treat the call as terminal. -Invoice binding (MUST): +### 5.4 Minimal state machine (informative) -* The Provider MUST set the BOLT11 invoice `description_hash` to exactly equal `terms_hash`. -* Before paying, the Requester MUST verify: - * `description_hash == terms_hash` - * the invoice payee (destination node pubkey) matches the LCP peer's (Provider's) pubkey - * the invoice amount equals `price_msat` (amount-less invoices MUST be rejected) - * the invoice expiry time does not exceed `quote_expiry` +Requester: -Invoice time rules (MUST): +1. Exchange `lcp_manifest`. +2. Send `lcp_call`. +3. Send request stream. +4. Receive `lcp_quote`, verify, pay. +5. Receive response stream. +6. Receive `lcp_complete`. -* Let `invoice_timestamp_unix` be the BOLT11 invoice timestamp (Unix epoch seconds). -* Let `invoice_expiry_seconds` be the BOLT11 invoice expiry (seconds). -* Define `invoice_expiry_unix = invoice_timestamp_unix + invoice_expiry_seconds`. -* The Requester MUST verify `invoice_expiry_unix <= quote_expiry`. - * Implementations MAY include a small allowance for clock skew (RECOMMENDED: `ALLOWED_CLOCK_SKEW_SECONDS = 5`). +Provider: -Execution rule (MUST): +1. Exchange `lcp_manifest`. +2. Receive `lcp_call`. +3. Receive/validate request stream. +4. Send `lcp_quote`. +5. Wait for invoice settlement. +6. Execute method. +7. Send response stream. +8. Send `lcp_complete`. +9. On `lcp_cancel`, stop best-effort. -* The Provider MUST NOT start executing or delivering results for `job_id` until the invoice is settled. +--- -`terms_hash` computation (MUST): +## 6. Stream transfer -* `terms_hash = SHA256(tlv_encode(terms_tlvs))` -* `terms_tlvs` is a canonical TLV stream containing only: - * type: 1 (`protocol_version`): `u16` - * type: 2 (`job_id`): `32*byte` - * type: 3 (`price_msat`): `tu64` - * type: 4 (`quote_expiry`): `tu64` - * type: 20 (`task_kind`): UTF-8 string - * type: 50 (`input_hash`): `32*byte` (`SHA256(input_decoded_bytes)`) - * type: 52 (`input_len`): `tu64` (byte length of `input_decoded_bytes`) - * type: 53 (`input_content_type`): UTF-8 string - * type: 54 (`input_content_encoding`): UTF-8 string - * type: 51 (`params_hash`): `32*byte` (`SHA256(canonical_tlv(params))`) - -`canonical_tlv(params)` (MUST): - -* If `params` is present, it MUST be interpreted as a TLV stream. -* The receiver MUST compute `params_hash` by: - 1. decoding the TLV stream (`params`) and rejecting invalid TLV encodings - 2. re-encoding it in canonical TLV stream form (ascending type, no duplicates) - 3. hashing the canonical re-encoding with SHA256 -* If `params` is absent, `params_hash = SHA256(empty)` (empty byte string). - -Input binding source (MUST): - -* The Provider MUST compute (`input_hash`, `input_len`, `input_content_type`, `input_content_encoding`) - from the validated input stream (§5.5). If the input stream cannot be validated, the Provider MUST NOT issue a quote. +Streams are transferred with: ---- +* `lcp_stream_begin` (42109) +* `lcp_stream_chunk` (42111) +* `lcp_stream_end` (42113) -## 5.4 type: 42087 (`lcp_result`) +### 6.1 Stream kinds -Terminal job completion message. +* 1: `request` +* 2: `response` -* data: [`tlv_stream`:`tlvs`] +Constraints: -Required TLVs: +* Exactly one request stream MUST be sent after `lcp_call` and before `lcp_quote`. +* Exactly one response stream SHOULD be sent after payment and before `lcp_complete`. -* type: 100 (`status`): [`u16`:`status`] - * 0: `ok` - * 1: `failed` - * 2: `cancelled` +### 6.2 Content type / encoding -If `status = ok`, required additional TLVs: +`content_type`: -* type: 101 (`result_stream_id`): [`32*byte`:`result_stream_id`] -* type: 102 (`result_hash`): [`32*byte`:`result_hash`] (`SHA256(result_decoded_bytes)`) -* type: 103 (`result_len`): [`tu64`:`result_len`] (byte length of `result_decoded_bytes`) -* type: 104 (`result_content_type`): [`byte`:`result_content_type`] (UTF-8 string) -* type: 105 (`result_content_encoding`): [`byte`:`result_content_encoding`] (UTF-8 string) +* UTF-8 media type string (e.g., `application/json; charset=utf-8`). -If `status != ok`, optional TLVs: +`content_encoding`: -* type: 81 (`message`): [`byte`:`message`] (UTF-8 string) +* UTF-8 identifier describing decoding of chunk bytes into decoded stream bytes. -Meaning: +This spec defines: -* If `status=ok`, the Provider MUST have sent exactly one validated result stream (§5.5) matching the metadata in this message. -* After receiving `lcp_result`, the Requester SHOULD treat the job as terminal and stop accepting further job-scope messages for that `job_id` (except duplicates within the replay window). +* `identity` (MUST be supported) ---- +Receivers MUST reject unknown `content_encoding` with `lcp_error(code=unsupported_encoding)`. -## 5.5 Stream transfer (chunking) +All `sha256` and `total_len` fields refer to **decoded** stream bytes. -Streams are used for large payloads (input and result) and are carried with: +### 6.3 `lcp_stream_begin` -* `lcp_stream_begin` (42089) -* `lcp_stream_chunk` (42091) -* `lcp_stream_end` (42093) +Type: 42109 -### 5.5.1 Stream kinds +Required TLVs: -Stream kinds: +* type: 90 (`stream_id`): 32 bytes +* type: 91 (`stream_kind`): `u16` +* type: 94 (`content_type`): UTF-8 string +* type: 95 (`content_encoding`): UTF-8 string -* 1: `input` -* 2: `result` +Optional (if known at stream start): -Constraints (v0.2): +* type: 92 (`total_len`): `tu64` +* type: 93 (`sha256`): 32 bytes -* Exactly one input stream MUST be sent by the Requester after `lcp_quote_request` and before `lcp_quote_response`. -* Exactly one result stream MUST be sent by the Provider after invoice settlement and before `lcp_result(status=ok)`. +Rules: -Receivers MUST reject any additional streams of the same kind for a job with `lcp_error(code=invalid_state)`. +* Sender MAY omit `total_len` and `sha256` at begin if unknown. +* Sender MUST provide `total_len` and `sha256` in `lcp_stream_end`. -### 5.5.2 Content type / encoding +Limit enforcement (MUST): -`content_type`: +* If `total_len` is present, receiver MUST enforce `total_len <= max_stream_bytes`. +* Receiver MUST enforce received decoded bytes do not exceed `max_stream_bytes` even if omitted. +* Receiver MUST enforce total decoded bytes across streams do not exceed `max_call_bytes`. -* A UTF-8 media type string (for example, `text/plain; charset=utf-8`). +### 6.4 `lcp_stream_chunk` -`content_encoding`: +Type: 42111 -* A UTF-8 identifier describing how stream chunk bytes are decoded into the logical stream bytes. +Required TLVs: -This specification defines: +* type: 90 (`stream_id`): 32 bytes +* type: 96 (`seq`): `tu32` (0,1,2,…) +* type: 97 (`data`): bytes -* `identity`: chunk bytes are the decoded bytes (MUST be supported) +Ordering (MUST): -Receivers MUST reject unknown `content_encoding` with `lcp_error(code=unsupported_encoding)`. +* `seq` MUST start at 0 and increase by exactly 1 for each accepted chunk. +* If `seq < expected_seq`, receiver MUST treat as duplicate and ignore. +* If `seq > expected_seq`, receiver MUST reject with `lcp_error(code=chunk_out_of_order)`. -Unless explicitly stated otherwise, all `sha256` and `total_len` fields refer to the decoded stream bytes. +Deterministic `msg_id` (MUST): -### 5.5.3 type: 42089 (`lcp_stream_begin`) +* `msg_id = SHA256(stream_id || u32be(seq))` -* data: [`tlv_stream`:`tlvs`] +Size rule (MUST): -Required TLVs: +* Sender MUST ensure each message payload size ≤ peer’s `max_payload_bytes`. -* type: 90 (`stream_id`): [`32*byte`:`stream_id`] -* type: 91 (`stream_kind`): [`u16`:`stream_kind`] -* type: 94 (`content_type`): [`byte`:`content_type`] (UTF-8 string) -* type: 95 (`content_encoding`): [`byte`:`content_encoding`] (UTF-8 string) +### 6.5 `lcp_stream_end` -Input stream requirements (`stream_kind = input`) (MUST): +Type: 42113 -* type: 92 (`total_len`): [`tu64`:`total_len`] -* type: 93 (`sha256`): [`32*byte`:`sha256`] +Required TLVs: -Result stream (`stream_kind = result`): +* type: 90 (`stream_id`): 32 bytes +* type: 92 (`total_len`): `tu64` +* type: 93 (`sha256`): 32 bytes -* `total_len` and `sha256` MAY be omitted if unknown at start. - (They MUST be provided in `lcp_stream_end`.) +Validation (MUST): -Limit enforcement (MUST): +* Receiver MUST verify: -* The receiver MUST enforce `total_len <= max_stream_bytes` (from the sender's `lcp_manifest`) if `total_len` is present. -* The receiver MUST enforce that total decoded bytes received do not exceed `max_stream_bytes` even if `total_len` was omitted. -* The receiver MUST enforce that total decoded bytes across all streams in the job do not exceed `max_job_bytes`. + * decoded length == `total_len` + * `SHA256(decoded_bytes) == sha256` +* On failure, receiver MUST send `lcp_error(code=checksum_mismatch)` and SHOULD treat the call as failed. -### 5.5.4 type: 42091 (`lcp_stream_chunk`) +--- -* data: [`tlv_stream`:`tlvs`] +## 7. Event stream format -Required TLVs: +This section defines an **optional standardized streaming response encoding** for methods that want structured incremental output. -* type: 90 (`stream_id`): [`32*byte`:`stream_id`] -* type: 96 (`seq`): [`tu32`:`seq`] (0,1,2,…) -* type: 97 (`data`): [`byte`:`data`] (chunk payload) +If a method chooses this encoding, it MUST set the response stream: -Chunk ordering rules (MUST): +* `content_type = "application/lcp.events+jsonl; charset=utf-8"` +* `content_encoding = "identity"` -* `seq` MUST start at 0 and increase by exactly 1 for each subsequent accepted chunk. -* If `seq < expected_seq`, the receiver MUST treat the chunk as a duplicate and MUST ignore it. -* If `seq > expected_seq`, the receiver MUST reject with `lcp_error(code=chunk_out_of_order)`. +If another content type is used, response bytes are method-defined. -Deterministic `msg_id` (MUST): +### 7.1 Encoding -To keep replay state bounded, `lcp_stream_chunk` MUST set: +For `application/lcp.events+jsonl; charset=utf-8`: -* `msg_id = SHA256(stream_id || u32be(seq))` (32 bytes) +* Decoded bytes MUST be valid UTF-8. +* Response is **JSON Lines**: -Where `u32be(seq)` is `seq` encoded as a 4-byte big-endian unsigned integer. + * One JSON object per line + * Lines separated by `\n` (0x0A) + * Trailing newline is RECOMMENDED -Size rule (MUST): +### 7.2 Minimum frame schema -* The sender MUST ensure each `lcp_stream_chunk` message payload size does not exceed the peer's `max_payload_bytes`. -* The sender SHOULD choose `len(data)` such that the full message fits within `max_payload_bytes` after TLV overhead. +Each frame MUST be a JSON object with: -### 5.5.5 type: 42093 (`lcp_stream_end`) +* `type`: string +* `seq`: non-negative integer -* data: [`tlv_stream`:`tlvs`] +Optional fields: -Required TLVs: +* `time`: integer (Unix epoch seconds) +* `data`: any JSON value +* `error`: object -* type: 90 (`stream_id`): [`32*byte`:`stream_id`] -* type: 92 (`total_len`): [`tu64`:`total_len`] -* type: 93 (`sha256`): [`32*byte`:`sha256`] +### 7.3 Terminal frame rule -Validation (MUST): +* `seq` MUST start at 0 and increment by 1 per frame. +* Stream MUST include exactly one terminal frame: -* The receiver MUST verify: - * total decoded bytes length equals `total_len` - * `SHA256(decoded_bytes) == sha256` -* If validation fails, the receiver MUST send `lcp_error(code=checksum_mismatch)` and SHOULD treat the job as failed. + * `type="final"` (success) OR `type="error"` (application failure) +* After terminal frame, sender MUST NOT emit additional frames. --- -## 5.6 type: 42095 (`lcp_cancel`) +## 8. Error, cancel, and rate limiting + +### 8.1 `lcp_cancel` -* data: [`tlv_stream`:`tlvs`] +Type: 42115 Optional TLVs: -* type: 70 (`reason`): [`byte`:`reason`] (UTF-8 string) +* type: 70 (`reason`): UTF-8 string Meaning: -* On receiving `lcp_cancel`, the receiver SHOULD stop further work for `job_id` best-effort. -* If cancellation occurs after payment, refunds are out of scope. - ---- +* Receiver SHOULD stop work best-effort. +* Refunds are out of scope. -## 5.7 type: 42097 (`lcp_error`) +### 8.2 `lcp_error` -Protocol-level terminal error. +Type: 42117 -* data: [`tlv_stream`:`tlvs`] +Protocol-level terminal error (invalid sequencing, unsupported method, stream checksum failure, etc). Required TLVs: -* type: 80 (`code`): [`u16`:`code`] +* type: 80 (`code`): `u16` Optional TLVs: -* type: 81 (`message`): [`byte`:`message`] (UTF-8 string) +* type: 81 (`message`): UTF-8 string -Recommended error codes: +Recommended codes: * 1: `unsupported_version` -* 2: `unsupported_task` -* 3: `quote_expired` -* 4: `payment_required` -* 5: `payment_invalid` -* 6: `payload_too_large` -* 7: `rate_limited` -* 8: `unsupported_params` +* 2: `manifest_required` +* 3: `unsupported_method` +* 4: `quote_expired` +* 5: `payment_required` +* 6: `payment_invalid` +* 7: `payload_too_large` +* 8: `rate_limited` * 9: `unsupported_encoding` * 10: `invalid_state` * 11: `chunk_out_of_order` * 12: `checksum_mismatch` +* 13: `stream_limit_exceeded` -Note: +Rule of thumb: -* `message` is human-readable supplemental information. Receivers MAY display it. +* **Protocol** failures → `lcp_error` +* **Method execution** failures → response stream (e.g., terminal event `type="error"`) + `lcp_complete(status=failed)` --- -## 6. Minimal state machine - -### 6.1 Requester - -1. Connect to the Provider (out of scope). -2. Exchange `lcp_manifest` (MUST). -3. Send `lcp_quote_request`. -4. Send input stream: - 1. `lcp_stream_begin(stream_kind=input, total_len, sha256, content_type, content_encoding)` - 2. `lcp_stream_chunk` × N - 3. `lcp_stream_end(total_len, sha256)` -5. Receive `lcp_quote_response`. Verify invoice binding and payee. Pay the invoice. -6. Receive result stream: - 1. `lcp_stream_begin(stream_kind=result, …)` - 2. `lcp_stream_chunk` × N - 3. `lcp_stream_end(total_len, sha256)` -7. Receive `lcp_result`. - -### 6.2 Provider - -1. Exchange `lcp_manifest` (MUST). -2. Receive `lcp_quote_request`. -3. Receive and validate input stream. -4. Send `lcp_quote_response` with an invoice bound to terms. -5. Wait for invoice settlement. -6. Execute job. -7. Send result stream. -8. Send `lcp_result`. -9. If `lcp_cancel` is received, stop work best-effort. - ---- +## 9. Security and policy considerations -## 7. Security considerations +### 9.1 Replay protection -### 7.1 Replay protection +Receivers MUST de-duplicate by (`call_id`, `msg_id`) at least until `effective_expiry` (§3.5). +For `lcp_stream_chunk`, `msg_id` is deterministic (§6.4). -Receivers MUST de-duplicate by (`job_id`, `msg_id`) at least until `effective_expiry` (see §4.2). +### 9.2 DoS resistance -For `lcp_stream_chunk`, `msg_id` is deterministic (§5.5.4) and stream reassembly state can be used to keep memory bounded. +Implementations MUST enforce: -### 7.2 DoS resistance +* `max_payload_bytes` +* `max_stream_bytes` +* `max_call_bytes` -* Implementations MUST enforce payload size limits and stream/job limits via: - * `lcp_manifest.max_payload_bytes` - * `lcp_manifest.max_stream_bytes` - * `lcp_manifest.max_job_bytes` -* Implementations SHOULD enforce per-peer rate limits. -* Providers MAY restrict who they serve (for example pubkey allowlists, or peers with existing channels). -* Providers MAY return `rate_limited`. +Implementations SHOULD rate limit per peer. +Providers MAY restrict service (allowlists, channel requirements, etc). -### 7.3 Preventing invoice swapping +### 9.3 Invoice swapping prevention -The Requester MUST satisfy at least: +Requester MUST verify at least: -* `description_hash == terms_hash` -* the invoice payee (destination node pubkey) matches the LCP peer's (Provider's) pubkey +* invoice `description_hash == terms_hash` +* invoice payee pubkey matches Provider peer pubkey -This detects paying an invoice for a different node. +### 9.4 License and usage policy compliance ---- +Providers may invoke upstream models/APIs/datasets. -## 8. Trade-offs and limitations (direct peer connections) - -LCP v0.2 makes the following trade-offs explicit: +Rules (MUST): -* Direct connections. Privacy is not fully preserved compared to onion messages or blinded paths. -* Payment happens before execution. This is not a perfect atomic swap. -* Large payloads are supported via streaming, but are still subject to peer-declared limits. -* Provider receives full input stream before quoting, which increases pre-payment bandwidth exposure (bounded by manifest limits). +* Provider MUST comply with applicable licenses, terms of service, and usage policies of any upstream systems used to fulfill a method. +* Provider MUST reject or restrict calls that would violate those obligations. +* Provider SHOULD disclose relevant restrictions via `policy_notice` in method descriptors when feasible. -This specification does not define forwarding via onion messages or blinded paths. +--- diff --git a/go-lcpd/gen/go/lcpd/v1/lcpd.cobra.pb.go b/go-lcpd/gen/go/lcpd/v1/lcpd.cobra.pb.go index 87f70f4..530dd88 100644 --- a/go-lcpd/gen/go/lcpd/v1/lcpd.cobra.pb.go +++ b/go-lcpd/gen/go/lcpd/v1/lcpd.cobra.pb.go @@ -117,7 +117,7 @@ func _LCPDServiceRequestQuoteCommand(cfg *client.Config) *cobra.Command { cmd := &cobra.Command{ Use: cfg.CommandNamer("RequestQuote"), Short: "RequestQuote RPC client", - Long: "RequestQuote sends an `lcp_quote_request` to a specific peer.\n\n On success, it returns the provider's `lcp_quote_response` as `Terms`,\n including a BOLT11 invoice whose `description_hash` MUST equal `terms_hash`\n (invoice swapping defense).\n Errors:\n - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id` format, missing\n `task`, or invalid task spec).\n Impact: No wire message is sent to the peer.\n - NOT_FOUND: The target peer is unknown to the daemon.\n Impact: No wire message is sent to the peer.\n - FAILED_PRECONDITION: The target peer is not connected and ready for LCP\n (e.g. no manifest observed, protocol mismatch, or LCP disabled).\n Impact: No quote is returned; clients may retry after resolving the condition.\n - RESOURCE_EXHAUSTED: The request would exceed a payload/size limit (local\n or remote `max_payload_bytes`), or the daemon refuses due to local policy.\n Impact: No quote is returned.\n - DEADLINE_EXCEEDED: The peer did not respond before the client/server deadline.\n Impact: Quote outcome is unknown to the caller; retry MAY result in a new\n `job_id` and a different invoice unless the implementation provides idempotency.\n - UNAVAILABLE: Transient transport failure sending/receiving peer messages.\n Impact: Quote outcome is unknown; safe retries depend on idempotency.", + Long: "RequestQuote sends an `lcp_call` and a request stream to a specific peer.\n\n On success, it returns the provider's `lcp_quote` as `Quote`,\n including a BOLT11 invoice whose `description_hash` MUST equal `terms_hash`\n (invoice swapping defense).\n Errors:\n - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id` format, missing\n `call`, or invalid call spec).\n Impact: No wire message is sent to the peer.\n - NOT_FOUND: The target peer is unknown to the daemon.\n Impact: No wire message is sent to the peer.\n - FAILED_PRECONDITION: The target peer is not connected and ready for LCP\n (e.g. no manifest observed, protocol mismatch, or LCP disabled).\n Impact: No quote is returned; clients may retry after resolving the condition.\n - RESOURCE_EXHAUSTED: The request would exceed a payload/size limit (local\n or remote `max_payload_bytes`), or the daemon refuses due to local policy.\n Impact: No quote is returned.\n - DEADLINE_EXCEEDED: The peer did not respond before the client/server deadline.\n Impact: Quote outcome is unknown to the caller; retry MAY result in a new\n `call_id` and a different invoice unless the implementation provides idempotency.\n - UNAVAILABLE: Transient transport failure sending/receiving peer messages.\n Impact: Quote outcome is unknown; safe retries depend on idempotency.", RunE: func(cmd *cobra.Command, args []string) error { if cfg.UseEnvVars { if err := flag.SetFlagsFromEnv(cmd.Parent().PersistentFlags(), true, cfg.EnvVarNamer, cfg.EnvVarPrefix, "LCPDService"); err != nil { @@ -149,43 +149,19 @@ func _LCPDServiceRequestQuoteCommand(cfg *client.Config) *cobra.Command { } cmd.PersistentFlags().StringVar(&req.PeerId, cfg.FlagNamer("PeerId"), "", "The target peer's Node ID (Pubkey).") - _Task := &Task{} - _Task_OpenaiChatCompletionsV1 := &OpenAIChatCompletionsV1TaskSpec{} - cmd.PersistentFlags().Bool(cfg.FlagNamer("Task OpenaiChatCompletionsV1"), false, "") - flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Task OpenaiChatCompletionsV1"), func() { - req.Task = _Task - _Task.Spec = &Task_OpenaiChatCompletionsV1{OpenaiChatCompletionsV1: _Task_OpenaiChatCompletionsV1} - }) - flag.BytesBase64Var(cmd.PersistentFlags(), &_Task_OpenaiChatCompletionsV1.RequestJson, cfg.FlagNamer("Task OpenaiChatCompletionsV1 RequestJson"), "Raw HTTP request body bytes for `POST /v1/chat/completions`.") - flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Task OpenaiChatCompletionsV1 RequestJson"), func() { - req.Task = _Task - _Task.Spec = &Task_OpenaiChatCompletionsV1{OpenaiChatCompletionsV1: _Task_OpenaiChatCompletionsV1} - }) - _Task_OpenaiChatCompletionsV1_Params := &OpenAIChatCompletionsV1Params{} - cmd.PersistentFlags().StringVar(&_Task_OpenaiChatCompletionsV1_Params.Model, cfg.FlagNamer("Task OpenaiChatCompletionsV1 Params Model"), "", "\"model\" (TLV type 1).\n Identifier for the execution target (for example, an OpenAI model ID).") - flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Task OpenaiChatCompletionsV1 Params Model"), func() { - req.Task = _Task - _Task.Spec = &Task_OpenaiChatCompletionsV1{OpenaiChatCompletionsV1: _Task_OpenaiChatCompletionsV1} - _Task_OpenaiChatCompletionsV1.Params = _Task_OpenaiChatCompletionsV1_Params - }) - _Task_OpenaiResponsesV1 := &OpenAIResponsesV1TaskSpec{} - cmd.PersistentFlags().Bool(cfg.FlagNamer("Task OpenaiResponsesV1"), false, "") - flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Task OpenaiResponsesV1"), func() { - req.Task = _Task - _Task.Spec = &Task_OpenaiResponsesV1{OpenaiResponsesV1: _Task_OpenaiResponsesV1} - }) - flag.BytesBase64Var(cmd.PersistentFlags(), &_Task_OpenaiResponsesV1.RequestJson, cfg.FlagNamer("Task OpenaiResponsesV1 RequestJson"), "Raw HTTP request body bytes for `POST /v1/responses`.") - flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Task OpenaiResponsesV1 RequestJson"), func() { - req.Task = _Task - _Task.Spec = &Task_OpenaiResponsesV1{OpenaiResponsesV1: _Task_OpenaiResponsesV1} - }) - _Task_OpenaiResponsesV1_Params := &OpenAIResponsesV1Params{} - cmd.PersistentFlags().StringVar(&_Task_OpenaiResponsesV1_Params.Model, cfg.FlagNamer("Task OpenaiResponsesV1 Params Model"), "", "\"model\" (TLV type 1).\n Identifier for the execution target (for example, an OpenAI model ID).") - flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Task OpenaiResponsesV1 Params Model"), func() { - req.Task = _Task - _Task.Spec = &Task_OpenaiResponsesV1{OpenaiResponsesV1: _Task_OpenaiResponsesV1} - _Task_OpenaiResponsesV1.Params = _Task_OpenaiResponsesV1_Params - }) + _Call := &CallSpec{} + cmd.PersistentFlags().StringVar(&_Call.Method, cfg.FlagNamer("Call Method"), "", "Method name (wire mapping: `lcp_call.method`).") + flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Call Method"), func() { req.Call = _Call }) + flag.BytesBase64Var(cmd.PersistentFlags(), &_Call.Params, cfg.FlagNamer("Call Params"), "Opaque method params bytes (wire mapping: `lcp_call.params`).\n For LCP v0.3, params are method-defined opaque bytes (often a TLV stream).") + flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Call Params"), func() { req.Call = _Call }) + cmd.PersistentFlags().StringVar(&_Call.ParamsContentType, cfg.FlagNamer("Call ParamsContentType"), "", "Optional params content type hint (wire mapping: `lcp_call.params_content_type`).") + flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Call ParamsContentType"), func() { req.Call = _Call }) + flag.BytesBase64Var(cmd.PersistentFlags(), &_Call.RequestBytes, cfg.FlagNamer("Call RequestBytes"), "Decoded request bytes to send in the request stream.") + flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Call RequestBytes"), func() { req.Call = _Call }) + cmd.PersistentFlags().StringVar(&_Call.RequestContentType, cfg.FlagNamer("Call RequestContentType"), "", "Request stream content type (wire mapping: `lcp_stream_begin.content_type`).") + flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Call RequestContentType"), func() { req.Call = _Call }) + cmd.PersistentFlags().StringVar(&_Call.RequestContentEncoding, cfg.FlagNamer("Call RequestContentEncoding"), "", "Request stream content encoding (wire mapping: `lcp_stream_begin.content_encoding`).") + flag.WithPostSetHook(cmd.PersistentFlags(), cfg.FlagNamer("Call RequestContentEncoding"), func() { req.Call = _Call }) return cmd } @@ -196,7 +172,7 @@ func _LCPDServiceAcceptAndExecuteCommand(cfg *client.Config) *cobra.Command { cmd := &cobra.Command{ Use: cfg.CommandNamer("AcceptAndExecute"), Short: "AcceptAndExecute RPC client", - Long: "AcceptAndExecute pays the invoice associated with the quote and waits for\n the `lcp_result`.\n\n This is a blocking call. Clients SHOULD set a deadline and MAY cancel via\n context cancellation (gRPC) or `CancelJob`.\n\n Errors:\n - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format),\n or `pay_invoice=false`.\n Impact: No payment attempt is made and no execution is started.\n - NOT_FOUND: The daemon has no known quote/payment data for the given `job_id`.\n Impact: No payment attempt is made.\n - FAILED_PRECONDITION: The quote is expired, the job is not executable, or the\n daemon refuses to pay/execute due to local policy.\n Impact: No execution is started.\n - UNAVAILABLE: Transient failure interacting with the Lightning node for payment,\n or transient peer messaging failure for waiting on the result.\n Impact: The job/payment outcome may be unknown; callers SHOULD treat retries\n with caution because a payment could have succeeded while the result wait failed.\n - DEADLINE_EXCEEDED: The result did not arrive before the deadline.\n Impact: The job may still complete and deliver a result later; callers SHOULD\n avoid blindly retrying payment logic.\n - INTERNAL: Unexpected server error.\n Impact: Outcome may be unknown if failure happened after payment settlement.", + Long: "AcceptAndExecute pays the invoice associated with the quote and waits for\n the terminal `lcp_complete` and (when available) the validated response stream.\n\n This is a blocking call. Clients SHOULD set a deadline and MAY cancel via\n context cancellation (gRPC) or `CancelJob`.\n\n Errors:\n - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format),\n or `pay_invoice=false`.\n Impact: No payment attempt is made and no execution is started.\n - NOT_FOUND: The daemon has no known quote/payment data for the given `call_id`.\n Impact: No payment attempt is made.\n - FAILED_PRECONDITION: The quote is expired, the job is not executable, or the\n daemon refuses to pay/execute due to local policy.\n Impact: No execution is started.\n - UNAVAILABLE: Transient failure interacting with the Lightning node for payment,\n or transient peer messaging failure for waiting on the result.\n Impact: The job/payment outcome may be unknown; callers SHOULD treat retries\n with caution because a payment could have succeeded while the result wait failed.\n - DEADLINE_EXCEEDED: The result did not arrive before the deadline.\n Impact: The job may still complete and deliver a result later; callers SHOULD\n avoid blindly retrying payment logic.\n - INTERNAL: Unexpected server error.\n Impact: Outcome may be unknown if failure happened after payment settlement.", RunE: func(cmd *cobra.Command, args []string) error { if cfg.UseEnvVars { if err := flag.SetFlagsFromEnv(cmd.Parent().PersistentFlags(), true, cfg.EnvVarNamer, cfg.EnvVarPrefix, "LCPDService"); err != nil { @@ -228,7 +204,7 @@ func _LCPDServiceAcceptAndExecuteCommand(cfg *client.Config) *cobra.Command { } cmd.PersistentFlags().StringVar(&req.PeerId, cfg.FlagNamer("PeerId"), "", "The target peer's Node ID.") - flag.BytesBase64Var(cmd.PersistentFlags(), &req.JobId, cfg.FlagNamer("JobId"), "The job_id to execute (must match a previously received Quote).") + flag.BytesBase64Var(cmd.PersistentFlags(), &req.CallId, cfg.FlagNamer("CallId"), "The call_id to execute (must match a previously received Quote).") cmd.PersistentFlags().BoolVar(&req.PayInvoice, cfg.FlagNamer("PayInvoice"), false, "Authorization to pay the invoice.") return cmd @@ -240,7 +216,7 @@ func _LCPDServiceAcceptAndExecuteStreamCommand(cfg *client.Config) *cobra.Comman cmd := &cobra.Command{ Use: cfg.CommandNamer("AcceptAndExecuteStream"), Short: "AcceptAndExecuteStream RPC client", - Long: "AcceptAndExecuteStream pays the invoice associated with the quote and streams\n the decoded result stream bytes as they arrive.\n\n The stream ends with a terminal `Result` event that indicates the final job\n status and (when available) the validated stream metadata (hash/len).\n\n Errors follow the same model as `AcceptAndExecute`, but note that streaming\n responses may have already delivered partial bytes before an error is\n detected (for example, checksum validation failure at stream end).", + Long: "AcceptAndExecuteStream pays the invoice associated with the quote and streams\n the decoded response stream bytes as they arrive.\n\n The stream ends with a terminal `Complete` event that indicates the final job\n status and (when available) the validated stream metadata (hash/len).\n\n Errors follow the same model as `AcceptAndExecute`, but note that streaming\n responses may have already delivered partial bytes before an error is\n detected (for example, checksum validation failure at stream end).", RunE: func(cmd *cobra.Command, args []string) error { if cfg.UseEnvVars { if err := flag.SetFlagsFromEnv(cmd.Parent().PersistentFlags(), true, cfg.EnvVarNamer, cfg.EnvVarPrefix, "LCPDService"); err != nil { @@ -284,7 +260,7 @@ func _LCPDServiceAcceptAndExecuteStreamCommand(cfg *client.Config) *cobra.Comman } cmd.PersistentFlags().StringVar(&req.PeerId, cfg.FlagNamer("PeerId"), "", "The target peer's Node ID.") - flag.BytesBase64Var(cmd.PersistentFlags(), &req.JobId, cfg.FlagNamer("JobId"), "The job_id to execute (must match a previously received Quote).") + flag.BytesBase64Var(cmd.PersistentFlags(), &req.CallId, cfg.FlagNamer("CallId"), "The call_id to execute (must match a previously received Quote).") cmd.PersistentFlags().BoolVar(&req.PayInvoice, cfg.FlagNamer("PayInvoice"), false, "Authorization to pay the invoice.") return cmd @@ -296,7 +272,7 @@ func _LCPDServiceCancelJobCommand(cfg *client.Config) *cobra.Command { cmd := &cobra.Command{ Use: cfg.CommandNamer("CancelJob"), Short: "CancelJob RPC client", - Long: "CancelJob sends an `lcp_cancel` message to the provider.\n\n Cancellation is best-effort: the provider may have already completed the job.\n\n Errors:\n - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format).\n Impact: No cancel message is sent.\n - NOT_FOUND: The target peer is unknown to the daemon.\n Impact: No cancel message is sent.\n - FAILED_PRECONDITION: The daemon cannot currently send peer messages to the\n target peer (e.g. not connected).\n Impact: No cancel message is sent.\n - UNAVAILABLE: Transient transport failure sending the cancel message.\n Impact: Unknown whether the provider received the cancel; retries are generally safe.\n - DEADLINE_EXCEEDED: The cancel attempt did not complete before the deadline.\n Impact: Unknown whether the provider received the cancel; retries are generally safe.\n - INTERNAL: Unexpected server error.\n Impact: Unknown whether the provider received the cancel.", + Long: "CancelJob sends an `lcp_cancel` message to the provider.\n\n Cancellation is best-effort: the provider may have already completed the job.\n\n Errors:\n - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format).\n Impact: No cancel message is sent.\n - NOT_FOUND: The target peer is unknown to the daemon.\n Impact: No cancel message is sent.\n - FAILED_PRECONDITION: The daemon cannot currently send peer messages to the\n target peer (e.g. not connected).\n Impact: No cancel message is sent.\n - UNAVAILABLE: Transient transport failure sending the cancel message.\n Impact: Unknown whether the provider received the cancel; retries are generally safe.\n - DEADLINE_EXCEEDED: The cancel attempt did not complete before the deadline.\n Impact: Unknown whether the provider received the cancel; retries are generally safe.\n - INTERNAL: Unexpected server error.\n Impact: Unknown whether the provider received the cancel.", RunE: func(cmd *cobra.Command, args []string) error { if cfg.UseEnvVars { if err := flag.SetFlagsFromEnv(cmd.Parent().PersistentFlags(), true, cfg.EnvVarNamer, cfg.EnvVarPrefix, "LCPDService"); err != nil { @@ -328,7 +304,7 @@ func _LCPDServiceCancelJobCommand(cfg *client.Config) *cobra.Command { } cmd.PersistentFlags().StringVar(&req.PeerId, cfg.FlagNamer("PeerId"), "", "The target peer's Node ID.") - flag.BytesBase64Var(cmd.PersistentFlags(), &req.JobId, cfg.FlagNamer("JobId"), "The job_id to cancel.") + flag.BytesBase64Var(cmd.PersistentFlags(), &req.CallId, cfg.FlagNamer("CallId"), "The call_id to cancel.") cmd.PersistentFlags().StringVar(&req.Reason, cfg.FlagNamer("Reason"), "", "Optional human-readable cancellation reason.") return cmd diff --git a/go-lcpd/gen/go/lcpd/v1/lcpd.pb.go b/go-lcpd/gen/go/lcpd/v1/lcpd.pb.go index ddaa244..cad8831 100644 --- a/go-lcpd/gen/go/lcpd/v1/lcpd.pb.go +++ b/go-lcpd/gen/go/lcpd/v1/lcpd.pb.go @@ -13,9 +13,9 @@ // // LCP (Lightning Compute Protocol) is a Lightning L3 overlay protocol that // uses BOLT #1 custom messages carrying TLV streams to negotiate and execute -// compute jobs (quote → payment → execution → result delivery). +// method calls (manifest → call → quote → pay → stream → complete). // -// This API is designed to be compatible with the LCP v0.2 wire protocol +// This API is designed to be compatible with the LCP v0.3 wire protocol // described in `docs/protocol/protocol.md`. package lcpdv1 @@ -38,76 +38,25 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// LCPTaskKind enumerates the supported task types. -// This is primarily used for Manifests and debugging. -type LCPTaskKind int32 - -const ( - LCPTaskKind_LCP_TASK_KIND_UNSPECIFIED LCPTaskKind = 0 - LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1 LCPTaskKind = 2 // "openai.chat_completions.v1" - LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1 LCPTaskKind = 3 // "openai.responses.v1" -) - -// Enum value maps for LCPTaskKind. -var ( - LCPTaskKind_name = map[int32]string{ - 0: "LCP_TASK_KIND_UNSPECIFIED", - 2: "LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1", - 3: "LCP_TASK_KIND_OPENAI_RESPONSES_V1", - } - LCPTaskKind_value = map[string]int32{ - "LCP_TASK_KIND_UNSPECIFIED": 0, - "LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1": 2, - "LCP_TASK_KIND_OPENAI_RESPONSES_V1": 3, - } -) - -func (x LCPTaskKind) Enum() *LCPTaskKind { - p := new(LCPTaskKind) - *p = x - return p -} - -func (x LCPTaskKind) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (LCPTaskKind) Descriptor() protoreflect.EnumDescriptor { - return file_lcpd_v1_lcpd_proto_enumTypes[0].Descriptor() -} - -func (LCPTaskKind) Type() protoreflect.EnumType { - return &file_lcpd_v1_lcpd_proto_enumTypes[0] -} - -func (x LCPTaskKind) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use LCPTaskKind.Descriptor instead. -func (LCPTaskKind) EnumDescriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{0} -} - // Status represents the job completion state. -type Result_Status int32 +type Complete_Status int32 const ( - Result_STATUS_UNSPECIFIED Result_Status = 0 - Result_STATUS_OK Result_Status = 1 - Result_STATUS_FAILED Result_Status = 2 - Result_STATUS_CANCELLED Result_Status = 3 + Complete_STATUS_UNSPECIFIED Complete_Status = 0 + Complete_STATUS_OK Complete_Status = 1 + Complete_STATUS_FAILED Complete_Status = 2 + Complete_STATUS_CANCELLED Complete_Status = 3 ) -// Enum value maps for Result_Status. +// Enum value maps for Complete_Status. var ( - Result_Status_name = map[int32]string{ + Complete_Status_name = map[int32]string{ 0: "STATUS_UNSPECIFIED", 1: "STATUS_OK", 2: "STATUS_FAILED", 3: "STATUS_CANCELLED", } - Result_Status_value = map[string]int32{ + Complete_Status_value = map[string]int32{ "STATUS_UNSPECIFIED": 0, "STATUS_OK": 1, "STATUS_FAILED": 2, @@ -115,60 +64,69 @@ var ( } ) -func (x Result_Status) Enum() *Result_Status { - p := new(Result_Status) +func (x Complete_Status) Enum() *Complete_Status { + p := new(Complete_Status) *p = x return p } -func (x Result_Status) String() string { +func (x Complete_Status) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } -func (Result_Status) Descriptor() protoreflect.EnumDescriptor { - return file_lcpd_v1_lcpd_proto_enumTypes[1].Descriptor() +func (Complete_Status) Descriptor() protoreflect.EnumDescriptor { + return file_lcpd_v1_lcpd_proto_enumTypes[0].Descriptor() } -func (Result_Status) Type() protoreflect.EnumType { - return &file_lcpd_v1_lcpd_proto_enumTypes[1] +func (Complete_Status) Type() protoreflect.EnumType { + return &file_lcpd_v1_lcpd_proto_enumTypes[0] } -func (x Result_Status) Number() protoreflect.EnumNumber { +func (x Complete_Status) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Use Result_Status.Descriptor instead. -func (Result_Status) EnumDescriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{6, 0} +// Deprecated: Use Complete_Status.Descriptor instead. +func (Complete_Status) EnumDescriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{2, 0} } -// Task is the strict container for a job request. -// It uses `oneof` to enforce that only one valid task specification is provided. -type Task struct { +// CallSpec describes the Requester's method call and the corresponding request +// payload. The LCP wire protocol transmits the request payload via stream +// messages; this gRPC API accepts the decoded bytes and streams them over LCP. +type CallSpec struct { state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Spec: - // - // *Task_OpenaiChatCompletionsV1 - // *Task_OpenaiResponsesV1 - Spec isTask_Spec `protobuf_oneof:"spec"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Task) Reset() { - *x = Task{} + // Method name (wire mapping: `lcp_call.method`). + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // Opaque method params bytes (wire mapping: `lcp_call.params`). + // For LCP v0.3, params are method-defined opaque bytes (often a TLV stream). + Params []byte `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` + // Optional params content type hint (wire mapping: `lcp_call.params_content_type`). + ParamsContentType string `protobuf:"bytes,3,opt,name=params_content_type,json=paramsContentType,proto3" json:"params_content_type,omitempty"` + // Decoded request bytes to send in the request stream. + RequestBytes []byte `protobuf:"bytes,4,opt,name=request_bytes,json=requestBytes,proto3" json:"request_bytes,omitempty"` + // Request stream content type (wire mapping: `lcp_stream_begin.content_type`). + RequestContentType string `protobuf:"bytes,5,opt,name=request_content_type,json=requestContentType,proto3" json:"request_content_type,omitempty"` + // Request stream content encoding (wire mapping: `lcp_stream_begin.content_encoding`). + RequestContentEncoding string `protobuf:"bytes,6,opt,name=request_content_encoding,json=requestContentEncoding,proto3" json:"request_content_encoding,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallSpec) Reset() { + *x = CallSpec{} mi := &file_lcpd_v1_lcpd_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Task) String() string { +func (x *CallSpec) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Task) ProtoMessage() {} +func (*CallSpec) ProtoMessage() {} -func (x *Task) ProtoReflect() protoreflect.Message { +func (x *CallSpec) ProtoReflect() protoreflect.Message { mi := &file_lcpd_v1_lcpd_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -180,82 +138,95 @@ func (x *Task) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Task.ProtoReflect.Descriptor instead. -func (*Task) Descriptor() ([]byte, []int) { +// Deprecated: Use CallSpec.ProtoReflect.Descriptor instead. +func (*CallSpec) Descriptor() ([]byte, []int) { return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{0} } -func (x *Task) GetSpec() isTask_Spec { +func (x *CallSpec) GetMethod() string { if x != nil { - return x.Spec + return x.Method } - return nil + return "" } -func (x *Task) GetOpenaiChatCompletionsV1() *OpenAIChatCompletionsV1TaskSpec { +func (x *CallSpec) GetParams() []byte { if x != nil { - if x, ok := x.Spec.(*Task_OpenaiChatCompletionsV1); ok { - return x.OpenaiChatCompletionsV1 - } + return x.Params } return nil } -func (x *Task) GetOpenaiResponsesV1() *OpenAIResponsesV1TaskSpec { +func (x *CallSpec) GetParamsContentType() string { if x != nil { - if x, ok := x.Spec.(*Task_OpenaiResponsesV1); ok { - return x.OpenaiResponsesV1 - } + return x.ParamsContentType } - return nil + return "" } -type isTask_Spec interface { - isTask_Spec() +func (x *CallSpec) GetRequestBytes() []byte { + if x != nil { + return x.RequestBytes + } + return nil } -type Task_OpenaiChatCompletionsV1 struct { - // Structured specification for "openai.chat_completions.v1" - OpenaiChatCompletionsV1 *OpenAIChatCompletionsV1TaskSpec `protobuf:"bytes,2,opt,name=openai_chat_completions_v1,json=openaiChatCompletionsV1,proto3,oneof"` +func (x *CallSpec) GetRequestContentType() string { + if x != nil { + return x.RequestContentType + } + return "" } -type Task_OpenaiResponsesV1 struct { - // Structured specification for "openai.responses.v1" - OpenaiResponsesV1 *OpenAIResponsesV1TaskSpec `protobuf:"bytes,3,opt,name=openai_responses_v1,json=openaiResponsesV1,proto3,oneof"` +func (x *CallSpec) GetRequestContentEncoding() string { + if x != nil { + return x.RequestContentEncoding + } + return "" } -func (*Task_OpenaiChatCompletionsV1) isTask_Spec() {} - -func (*Task_OpenaiResponsesV1) isTask_Spec() {} - -// OpenAIChatCompletionsV1TaskSpec defines a passthrough OpenAI-compatible -// Chat Completions request. It maps to: -// - input: raw HTTP request body bytes (JSON) -// - params: TLV encoded parameters -type OpenAIChatCompletionsV1TaskSpec struct { +// Quote represents the commitment returned by the Provider in `lcp_quote`. +type Quote struct { state protoimpl.MessageState `protogen:"open.v1"` - // Raw HTTP request body bytes for `POST /v1/chat/completions`. - RequestJson []byte `protobuf:"bytes,1,opt,name=request_json,json=requestJson,proto3" json:"request_json,omitempty"` - // The configuration parameters for routing/validation. - Params *OpenAIChatCompletionsV1Params `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // LCP protocol version (v0.3 = 3; encoding: major*100 + minor). + ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` + // Call ID (32 bytes). + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` + // Price in millisatoshis. + PriceMsat uint64 `protobuf:"varint,3,opt,name=price_msat,json=priceMsat,proto3" json:"price_msat,omitempty"` + // Quote expiry time. + // + // Wire mapping: + // LCP v0.1 encodes quote expiry as Unix epoch seconds. Implementations that + // compute or verify `terms_hash` MUST use `quote_expiry.seconds` only and + // MUST treat `quote_expiry.nanos` as 0. + QuoteExpiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=quote_expiry,json=quoteExpiry,proto3" json:"quote_expiry,omitempty"` + // The SHA256 commitment over the canonical TLV stream of terms_tlvs. + // Must match the BOLT11 invoice's description_hash. + TermsHash []byte `protobuf:"bytes,5,opt,name=terms_hash,json=termsHash,proto3" json:"terms_hash,omitempty"` + // The BOLT11 invoice string provided by the peer. + PaymentRequest string `protobuf:"bytes,6,opt,name=payment_request,json=paymentRequest,proto3" json:"payment_request,omitempty"` + // Optional provider commitment to response stream encoding. + ResponseContentType *string `protobuf:"bytes,7,opt,name=response_content_type,json=responseContentType,proto3,oneof" json:"response_content_type,omitempty"` + ResponseContentEncoding *string `protobuf:"bytes,8,opt,name=response_content_encoding,json=responseContentEncoding,proto3,oneof" json:"response_content_encoding,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *OpenAIChatCompletionsV1TaskSpec) Reset() { - *x = OpenAIChatCompletionsV1TaskSpec{} +func (x *Quote) Reset() { + *x = Quote{} mi := &file_lcpd_v1_lcpd_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *OpenAIChatCompletionsV1TaskSpec) String() string { +func (x *Quote) String() string { return protoimpl.X.MessageStringOf(x) } -func (*OpenAIChatCompletionsV1TaskSpec) ProtoMessage() {} +func (*Quote) ProtoMessage() {} -func (x *OpenAIChatCompletionsV1TaskSpec) ProtoReflect() protoreflect.Message { +func (x *Quote) ProtoReflect() protoreflect.Message { mi := &file_lcpd_v1_lcpd_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -267,157 +238,103 @@ func (x *OpenAIChatCompletionsV1TaskSpec) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use OpenAIChatCompletionsV1TaskSpec.ProtoReflect.Descriptor instead. -func (*OpenAIChatCompletionsV1TaskSpec) Descriptor() ([]byte, []int) { +// Deprecated: Use Quote.ProtoReflect.Descriptor instead. +func (*Quote) Descriptor() ([]byte, []int) { return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{1} } -func (x *OpenAIChatCompletionsV1TaskSpec) GetRequestJson() []byte { +func (x *Quote) GetProtocolVersion() uint32 { if x != nil { - return x.RequestJson + return x.ProtocolVersion } - return nil + return 0 } -func (x *OpenAIChatCompletionsV1TaskSpec) GetParams() *OpenAIChatCompletionsV1Params { +func (x *Quote) GetCallId() []byte { if x != nil { - return x.Params + return x.CallId } return nil } -// OpenAIChatCompletionsV1Params defines the tunable parameters for -// "openai.chat_completions.v1". -type OpenAIChatCompletionsV1Params struct { - state protoimpl.MessageState `protogen:"open.v1"` - // "model" (TLV type 1). - // Identifier for the execution target (for example, an OpenAI model ID). - Model string `protobuf:"bytes,1,opt,name=model,proto3" json:"model,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *OpenAIChatCompletionsV1Params) Reset() { - *x = OpenAIChatCompletionsV1Params{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *OpenAIChatCompletionsV1Params) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*OpenAIChatCompletionsV1Params) ProtoMessage() {} - -func (x *OpenAIChatCompletionsV1Params) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] +func (x *Quote) GetPriceMsat() uint64 { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.PriceMsat } - return mi.MessageOf(x) -} - -// Deprecated: Use OpenAIChatCompletionsV1Params.ProtoReflect.Descriptor instead. -func (*OpenAIChatCompletionsV1Params) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{2} + return 0 } -func (x *OpenAIChatCompletionsV1Params) GetModel() string { +func (x *Quote) GetQuoteExpiry() *timestamppb.Timestamp { if x != nil { - return x.Model + return x.QuoteExpiry } - return "" -} - -// OpenAIResponsesV1TaskSpec defines a passthrough OpenAI-compatible -// Responses request. It maps to: -// - input: raw HTTP request body bytes (JSON) -// - params: TLV encoded parameters -type OpenAIResponsesV1TaskSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Raw HTTP request body bytes for `POST /v1/responses`. - RequestJson []byte `protobuf:"bytes,1,opt,name=request_json,json=requestJson,proto3" json:"request_json,omitempty"` - // The configuration parameters for routing/validation. - Params *OpenAIResponsesV1Params `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *OpenAIResponsesV1TaskSpec) Reset() { - *x = OpenAIResponsesV1TaskSpec{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *OpenAIResponsesV1TaskSpec) String() string { - return protoimpl.X.MessageStringOf(x) + return nil } -func (*OpenAIResponsesV1TaskSpec) ProtoMessage() {} - -func (x *OpenAIResponsesV1TaskSpec) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] +func (x *Quote) GetTermsHash() []byte { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.TermsHash } - return mi.MessageOf(x) + return nil } -// Deprecated: Use OpenAIResponsesV1TaskSpec.ProtoReflect.Descriptor instead. -func (*OpenAIResponsesV1TaskSpec) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{3} +func (x *Quote) GetPaymentRequest() string { + if x != nil { + return x.PaymentRequest + } + return "" } -func (x *OpenAIResponsesV1TaskSpec) GetRequestJson() []byte { - if x != nil { - return x.RequestJson +func (x *Quote) GetResponseContentType() string { + if x != nil && x.ResponseContentType != nil { + return *x.ResponseContentType } - return nil + return "" } -func (x *OpenAIResponsesV1TaskSpec) GetParams() *OpenAIResponsesV1Params { - if x != nil { - return x.Params +func (x *Quote) GetResponseContentEncoding() string { + if x != nil && x.ResponseContentEncoding != nil { + return *x.ResponseContentEncoding } - return nil + return "" } -// OpenAIResponsesV1Params defines the tunable parameters for -// "openai.responses.v1". -type OpenAIResponsesV1Params struct { +// Complete represents the terminal completion state from `lcp_complete`. +// For status=OK, it also includes the validated response stream metadata and +// (for unary RPCs) the reconstructed decoded response bytes. +type Complete struct { state protoimpl.MessageState `protogen:"open.v1"` - // "model" (TLV type 1). - // Identifier for the execution target (for example, an OpenAI model ID). - Model string `protobuf:"bytes,1,opt,name=model,proto3" json:"model,omitempty"` + // Completion status for the job. + Status Complete_Status `protobuf:"varint,1,opt,name=status,proto3,enum=lcpd.v1.Complete_Status" json:"status,omitempty"` + // If status=OK, the decoded response bytes reconstructed from the response stream. + ResponseBytes []byte `protobuf:"bytes,2,opt,name=response_bytes,json=responseBytes,proto3" json:"response_bytes,omitempty"` + // If status=OK, these fields reflect the validated response stream metadata. + ResponseStreamId []byte `protobuf:"bytes,3,opt,name=response_stream_id,json=responseStreamId,proto3,oneof" json:"response_stream_id,omitempty"` + ResponseHash []byte `protobuf:"bytes,4,opt,name=response_hash,json=responseHash,proto3,oneof" json:"response_hash,omitempty"` + ResponseLen *uint64 `protobuf:"varint,5,opt,name=response_len,json=responseLen,proto3,oneof" json:"response_len,omitempty"` + ResponseContentType *string `protobuf:"bytes,6,opt,name=response_content_type,json=responseContentType,proto3,oneof" json:"response_content_type,omitempty"` + ResponseContentEncoding *string `protobuf:"bytes,7,opt,name=response_content_encoding,json=responseContentEncoding,proto3,oneof" json:"response_content_encoding,omitempty"` + // If status!=OK, optional human-readable message. + Message string `protobuf:"bytes,8,opt,name=message,proto3" json:"message,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *OpenAIResponsesV1Params) Reset() { - *x = OpenAIResponsesV1Params{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] +func (x *Complete) Reset() { + *x = Complete{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *OpenAIResponsesV1Params) String() string { +func (x *Complete) String() string { return protoimpl.X.MessageStringOf(x) } -func (*OpenAIResponsesV1Params) ProtoMessage() {} +func (*Complete) ProtoMessage() {} -func (x *OpenAIResponsesV1Params) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] +func (x *Complete) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -428,149 +345,99 @@ func (x *OpenAIResponsesV1Params) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use OpenAIResponsesV1Params.ProtoReflect.Descriptor instead. -func (*OpenAIResponsesV1Params) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{4} +// Deprecated: Use Complete.ProtoReflect.Descriptor instead. +func (*Complete) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{2} } -func (x *OpenAIResponsesV1Params) GetModel() string { +func (x *Complete) GetStatus() Complete_Status { if x != nil { - return x.Model + return x.Status } - return "" -} - -// Terms represents the commitment returned by the Provider in `lcp_quote_response`. -type Terms struct { - state protoimpl.MessageState `protogen:"open.v1"` - // LCP protocol version (v0.2 = 2; encoding: major*100 + minor). - ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` - // Job ID (32 bytes). - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` - // Price in millisatoshis. - PriceMsat uint64 `protobuf:"varint,3,opt,name=price_msat,json=priceMsat,proto3" json:"price_msat,omitempty"` - // Quote expiry time. - // - // Wire mapping: - // LCP v0.1 encodes quote expiry as Unix epoch seconds. Implementations that - // compute or verify `terms_hash` MUST use `quote_expiry.seconds` only and - // MUST treat `quote_expiry.nanos` as 0. - QuoteExpiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=quote_expiry,json=quoteExpiry,proto3" json:"quote_expiry,omitempty"` - // The SHA256 commitment over the canonical TLV stream of Terms. - // Must match the BOLT11 invoice's description_hash. - TermsHash []byte `protobuf:"bytes,5,opt,name=terms_hash,json=termsHash,proto3" json:"terms_hash,omitempty"` - // The BOLT11 invoice string provided by the peer. - PaymentRequest string `protobuf:"bytes,6,opt,name=payment_request,json=paymentRequest,proto3" json:"payment_request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + return Complete_STATUS_UNSPECIFIED } -func (x *Terms) Reset() { - *x = Terms{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Terms) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Terms) ProtoMessage() {} - -func (x *Terms) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] +func (x *Complete) GetResponseBytes() []byte { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.ResponseBytes } - return mi.MessageOf(x) -} - -// Deprecated: Use Terms.ProtoReflect.Descriptor instead. -func (*Terms) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{5} + return nil } -func (x *Terms) GetProtocolVersion() uint32 { +func (x *Complete) GetResponseStreamId() []byte { if x != nil { - return x.ProtocolVersion + return x.ResponseStreamId } - return 0 + return nil } -func (x *Terms) GetJobId() []byte { +func (x *Complete) GetResponseHash() []byte { if x != nil { - return x.JobId + return x.ResponseHash } return nil } -func (x *Terms) GetPriceMsat() uint64 { - if x != nil { - return x.PriceMsat +func (x *Complete) GetResponseLen() uint64 { + if x != nil && x.ResponseLen != nil { + return *x.ResponseLen } return 0 } -func (x *Terms) GetQuoteExpiry() *timestamppb.Timestamp { - if x != nil { - return x.QuoteExpiry +func (x *Complete) GetResponseContentType() string { + if x != nil && x.ResponseContentType != nil { + return *x.ResponseContentType } - return nil + return "" } -func (x *Terms) GetTermsHash() []byte { - if x != nil { - return x.TermsHash +func (x *Complete) GetResponseContentEncoding() string { + if x != nil && x.ResponseContentEncoding != nil { + return *x.ResponseContentEncoding } - return nil + return "" } -func (x *Terms) GetPaymentRequest() string { +func (x *Complete) GetMessage() string { if x != nil { - return x.PaymentRequest + return x.Message } return "" } -// Result represents the output from `lcp_result`. -type Result struct { +// MethodDescriptor represents one entry of `supported_methods` in `lcp_manifest`. +type MethodDescriptor struct { state protoimpl.MessageState `protogen:"open.v1"` - // Completion status for the job. - Status Result_Status `protobuf:"varint,1,opt,name=status,proto3,enum=lcpd.v1.Result_Status" json:"status,omitempty"` - // If status=OK, the decoded result bytes reconstructed from the result stream. - // For "openai.chat_completions.v1", the client should interpret this as raw JSON bytes. - Result []byte `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` - // If status=OK, these fields reflect the validated result stream metadata. - ResultHash []byte `protobuf:"bytes,3,opt,name=result_hash,json=resultHash,proto3" json:"result_hash,omitempty"` - ResultLen uint64 `protobuf:"varint,4,opt,name=result_len,json=resultLen,proto3" json:"result_len,omitempty"` - ContentType string `protobuf:"bytes,5,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` - ContentEncoding string `protobuf:"bytes,6,opt,name=content_encoding,json=contentEncoding,proto3" json:"content_encoding,omitempty"` - // If status!=OK, optional human-readable message. - Message string `protobuf:"bytes,7,opt,name=message,proto3" json:"message,omitempty"` + // Method name. + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // Optional hints for request/response stream content type selection. + RequestContentTypes []string `protobuf:"bytes,2,rep,name=request_content_types,json=requestContentTypes,proto3" json:"request_content_types,omitempty"` + ResponseContentTypes []string `protobuf:"bytes,3,rep,name=response_content_types,json=responseContentTypes,proto3" json:"response_content_types,omitempty"` + // Optional documentation metadata. + DocsUri *string `protobuf:"bytes,4,opt,name=docs_uri,json=docsUri,proto3,oneof" json:"docs_uri,omitempty"` + DocsSha256 []byte `protobuf:"bytes,5,opt,name=docs_sha256,json=docsSha256,proto3,oneof" json:"docs_sha256,omitempty"` + // Optional human-readable policy notice (for example, pricing or limits). + PolicyNotice *string `protobuf:"bytes,6,opt,name=policy_notice,json=policyNotice,proto3,oneof" json:"policy_notice,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Result) Reset() { - *x = Result{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] +func (x *MethodDescriptor) Reset() { + *x = MethodDescriptor{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Result) String() string { +func (x *MethodDescriptor) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Result) ProtoMessage() {} +func (*MethodDescriptor) ProtoMessage() {} -func (x *Result) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] +func (x *MethodDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -581,56 +448,49 @@ func (x *Result) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Result.ProtoReflect.Descriptor instead. -func (*Result) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{6} +// Deprecated: Use MethodDescriptor.ProtoReflect.Descriptor instead. +func (*MethodDescriptor) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{3} } -func (x *Result) GetStatus() Result_Status { +func (x *MethodDescriptor) GetMethod() string { if x != nil { - return x.Status + return x.Method } - return Result_STATUS_UNSPECIFIED + return "" } -func (x *Result) GetResult() []byte { +func (x *MethodDescriptor) GetRequestContentTypes() []string { if x != nil { - return x.Result + return x.RequestContentTypes } return nil } -func (x *Result) GetResultHash() []byte { +func (x *MethodDescriptor) GetResponseContentTypes() []string { if x != nil { - return x.ResultHash + return x.ResponseContentTypes } return nil } -func (x *Result) GetResultLen() uint64 { - if x != nil { - return x.ResultLen - } - return 0 -} - -func (x *Result) GetContentType() string { - if x != nil { - return x.ContentType +func (x *MethodDescriptor) GetDocsUri() string { + if x != nil && x.DocsUri != nil { + return *x.DocsUri } return "" } -func (x *Result) GetContentEncoding() string { +func (x *MethodDescriptor) GetDocsSha256() []byte { if x != nil { - return x.ContentEncoding + return x.DocsSha256 } - return "" + return nil } -func (x *Result) GetMessage() string { - if x != nil { - return x.Message +func (x *MethodDescriptor) GetPolicyNotice() string { + if x != nil && x.PolicyNotice != nil { + return *x.PolicyNotice } return "" } @@ -650,7 +510,7 @@ type LCPPeer struct { func (x *LCPPeer) Reset() { *x = LCPPeer{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -662,7 +522,7 @@ func (x *LCPPeer) String() string { func (*LCPPeer) ProtoMessage() {} func (x *LCPPeer) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -675,7 +535,7 @@ func (x *LCPPeer) ProtoReflect() protoreflect.Message { // Deprecated: Use LCPPeer.ProtoReflect.Descriptor instead. func (*LCPPeer) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{7} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{4} } func (x *LCPPeer) GetPeerId() string { @@ -702,26 +562,26 @@ func (x *LCPPeer) GetRemoteManifest() *LCPManifest { // LCPManifest represents the capabilities declared by a peer via `lcp_manifest`. type LCPManifest struct { state protoimpl.MessageState `protogen:"open.v1"` - // LCP protocol version (v0.2 = 2; encoding: major*100 + minor). + // LCP protocol version (v0.3 = 3; encoding: major*100 + minor). ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` // Maximum supported BOLT #1 custom message payload size in bytes. MaxPayloadBytes uint32 `protobuf:"varint,11,opt,name=max_payload_bytes,json=maxPayloadBytes,proto3" json:"max_payload_bytes,omitempty"` // Maximum total decoded bytes supported for a single stream. MaxStreamBytes uint64 `protobuf:"varint,14,opt,name=max_stream_bytes,json=maxStreamBytes,proto3" json:"max_stream_bytes,omitempty"` // Maximum total decoded bytes supported across all streams in a job. - MaxJobBytes uint64 `protobuf:"varint,15,opt,name=max_job_bytes,json=maxJobBytes,proto3" json:"max_job_bytes,omitempty"` + MaxCallBytes uint64 `protobuf:"varint,15,opt,name=max_call_bytes,json=maxCallBytes,proto3" json:"max_call_bytes,omitempty"` // Optional maximum concurrent in-flight jobs supported per connection. // 0 means "unspecified". - MaxInflightJobs uint32 `protobuf:"varint,16,opt,name=max_inflight_jobs,json=maxInflightJobs,proto3" json:"max_inflight_jobs,omitempty"` - // Task templates supported by this peer. - SupportedTasks []*LCPTaskTemplate `protobuf:"bytes,12,rep,name=supported_tasks,json=supportedTasks,proto3" json:"supported_tasks,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + MaxInflightCalls uint32 `protobuf:"varint,16,opt,name=max_inflight_calls,json=maxInflightCalls,proto3" json:"max_inflight_calls,omitempty"` + // Methods supported by this peer. + SupportedMethods []*MethodDescriptor `protobuf:"bytes,12,rep,name=supported_methods,json=supportedMethods,proto3" json:"supported_methods,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LCPManifest) Reset() { *x = LCPManifest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -733,7 +593,7 @@ func (x *LCPManifest) String() string { func (*LCPManifest) ProtoMessage() {} func (x *LCPManifest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -746,7 +606,7 @@ func (x *LCPManifest) ProtoReflect() protoreflect.Message { // Deprecated: Use LCPManifest.ProtoReflect.Descriptor instead. func (*LCPManifest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{8} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{5} } func (x *LCPManifest) GetProtocolVersion() uint32 { @@ -770,121 +630,27 @@ func (x *LCPManifest) GetMaxStreamBytes() uint64 { return 0 } -func (x *LCPManifest) GetMaxJobBytes() uint64 { +func (x *LCPManifest) GetMaxCallBytes() uint64 { if x != nil { - return x.MaxJobBytes + return x.MaxCallBytes } return 0 } -func (x *LCPManifest) GetMaxInflightJobs() uint32 { +func (x *LCPManifest) GetMaxInflightCalls() uint32 { if x != nil { - return x.MaxInflightJobs + return x.MaxInflightCalls } return 0 } -func (x *LCPManifest) GetSupportedTasks() []*LCPTaskTemplate { - if x != nil { - return x.SupportedTasks - } - return nil -} - -// LCPTaskTemplate describes a task kind supported by a peer. -type LCPTaskTemplate struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The kind of task supported. - Kind LCPTaskKind `protobuf:"varint,1,opt,name=kind,proto3,enum=lcpd.v1.LCPTaskKind" json:"kind,omitempty"` - // The parameter template/defaults supported for this kind. - // - // Types that are valid to be assigned to ParamsTemplate: - // - // *LCPTaskTemplate_OpenaiChatCompletionsV1 - // *LCPTaskTemplate_OpenaiResponsesV1 - ParamsTemplate isLCPTaskTemplate_ParamsTemplate `protobuf_oneof:"params_template"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LCPTaskTemplate) Reset() { - *x = LCPTaskTemplate{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LCPTaskTemplate) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LCPTaskTemplate) ProtoMessage() {} - -func (x *LCPTaskTemplate) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LCPTaskTemplate.ProtoReflect.Descriptor instead. -func (*LCPTaskTemplate) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{9} -} - -func (x *LCPTaskTemplate) GetKind() LCPTaskKind { - if x != nil { - return x.Kind - } - return LCPTaskKind_LCP_TASK_KIND_UNSPECIFIED -} - -func (x *LCPTaskTemplate) GetParamsTemplate() isLCPTaskTemplate_ParamsTemplate { - if x != nil { - return x.ParamsTemplate - } - return nil -} - -func (x *LCPTaskTemplate) GetOpenaiChatCompletionsV1() *OpenAIChatCompletionsV1Params { - if x != nil { - if x, ok := x.ParamsTemplate.(*LCPTaskTemplate_OpenaiChatCompletionsV1); ok { - return x.OpenaiChatCompletionsV1 - } - } - return nil -} - -func (x *LCPTaskTemplate) GetOpenaiResponsesV1() *OpenAIResponsesV1Params { +func (x *LCPManifest) GetSupportedMethods() []*MethodDescriptor { if x != nil { - if x, ok := x.ParamsTemplate.(*LCPTaskTemplate_OpenaiResponsesV1); ok { - return x.OpenaiResponsesV1 - } + return x.SupportedMethods } return nil } -type isLCPTaskTemplate_ParamsTemplate interface { - isLCPTaskTemplate_ParamsTemplate() -} - -type LCPTaskTemplate_OpenaiChatCompletionsV1 struct { - OpenaiChatCompletionsV1 *OpenAIChatCompletionsV1Params `protobuf:"bytes,3,opt,name=openai_chat_completions_v1,json=openaiChatCompletionsV1,proto3,oneof"` -} - -type LCPTaskTemplate_OpenaiResponsesV1 struct { - OpenaiResponsesV1 *OpenAIResponsesV1Params `protobuf:"bytes,4,opt,name=openai_responses_v1,json=openaiResponsesV1,proto3,oneof"` -} - -func (*LCPTaskTemplate_OpenaiChatCompletionsV1) isLCPTaskTemplate_ParamsTemplate() {} - -func (*LCPTaskTemplate_OpenaiResponsesV1) isLCPTaskTemplate_ParamsTemplate() {} - // ListLCPPeersRequest is the request message for `LCPDService.ListLCPPeers`. type ListLCPPeersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -894,7 +660,7 @@ type ListLCPPeersRequest struct { func (x *ListLCPPeersRequest) Reset() { *x = ListLCPPeersRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -906,7 +672,7 @@ func (x *ListLCPPeersRequest) String() string { func (*ListLCPPeersRequest) ProtoMessage() {} func (x *ListLCPPeersRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -919,7 +685,7 @@ func (x *ListLCPPeersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLCPPeersRequest.ProtoReflect.Descriptor instead. func (*ListLCPPeersRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{10} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{6} } // ListLCPPeersResponse is the response message for `LCPDService.ListLCPPeers`. @@ -932,7 +698,7 @@ type ListLCPPeersResponse struct { func (x *ListLCPPeersResponse) Reset() { *x = ListLCPPeersResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -944,7 +710,7 @@ func (x *ListLCPPeersResponse) String() string { func (*ListLCPPeersResponse) ProtoMessage() {} func (x *ListLCPPeersResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -957,7 +723,7 @@ func (x *ListLCPPeersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLCPPeersResponse.ProtoReflect.Descriptor instead. func (*ListLCPPeersResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{11} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{7} } func (x *ListLCPPeersResponse) GetPeers() []*LCPPeer { @@ -976,7 +742,7 @@ type GetLocalInfoRequest struct { func (x *GetLocalInfoRequest) Reset() { *x = GetLocalInfoRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -988,7 +754,7 @@ func (x *GetLocalInfoRequest) String() string { func (*GetLocalInfoRequest) ProtoMessage() {} func (x *GetLocalInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1001,7 +767,7 @@ func (x *GetLocalInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLocalInfoRequest.ProtoReflect.Descriptor instead. func (*GetLocalInfoRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{12} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{8} } // GetLocalInfoResponse is the response message for `LCPDService.GetLocalInfo`. @@ -1017,7 +783,7 @@ type GetLocalInfoResponse struct { func (x *GetLocalInfoResponse) Reset() { *x = GetLocalInfoResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1029,7 +795,7 @@ func (x *GetLocalInfoResponse) String() string { func (*GetLocalInfoResponse) ProtoMessage() {} func (x *GetLocalInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1042,7 +808,7 @@ func (x *GetLocalInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLocalInfoResponse.ProtoReflect.Descriptor instead. func (*GetLocalInfoResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{13} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{9} } func (x *GetLocalInfoResponse) GetNodeId() string { @@ -1064,15 +830,15 @@ type RequestQuoteRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID (Pubkey). PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The strict task definition. - Task *Task `protobuf:"bytes,2,opt,name=task,proto3" json:"task,omitempty"` + // The method call specification. + Call *CallSpec `protobuf:"bytes,2,opt,name=call,proto3" json:"call,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RequestQuoteRequest) Reset() { *x = RequestQuoteRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1084,7 +850,7 @@ func (x *RequestQuoteRequest) String() string { func (*RequestQuoteRequest) ProtoMessage() {} func (x *RequestQuoteRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1097,7 +863,7 @@ func (x *RequestQuoteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestQuoteRequest.ProtoReflect.Descriptor instead. func (*RequestQuoteRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{14} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{10} } func (x *RequestQuoteRequest) GetPeerId() string { @@ -1107,9 +873,9 @@ func (x *RequestQuoteRequest) GetPeerId() string { return "" } -func (x *RequestQuoteRequest) GetTask() *Task { +func (x *RequestQuoteRequest) GetCall() *CallSpec { if x != nil { - return x.Task + return x.Call } return nil } @@ -1119,15 +885,15 @@ type RequestQuoteResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The peer sending the quote. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The agreed terms and invoice. - Terms *Terms `protobuf:"bytes,2,opt,name=terms,proto3" json:"terms,omitempty"` + // The agreed quote and invoice. + Quote *Quote `protobuf:"bytes,2,opt,name=quote,proto3" json:"quote,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RequestQuoteResponse) Reset() { *x = RequestQuoteResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1139,7 +905,7 @@ func (x *RequestQuoteResponse) String() string { func (*RequestQuoteResponse) ProtoMessage() {} func (x *RequestQuoteResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1152,7 +918,7 @@ func (x *RequestQuoteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestQuoteResponse.ProtoReflect.Descriptor instead. func (*RequestQuoteResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{15} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{11} } func (x *RequestQuoteResponse) GetPeerId() string { @@ -1162,9 +928,9 @@ func (x *RequestQuoteResponse) GetPeerId() string { return "" } -func (x *RequestQuoteResponse) GetTerms() *Terms { +func (x *RequestQuoteResponse) GetQuote() *Quote { if x != nil { - return x.Terms + return x.Quote } return nil } @@ -1174,8 +940,8 @@ type AcceptAndExecuteRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The job_id to execute (must match a previously received Quote). - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + // The call_id to execute (must match a previously received Quote). + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // Authorization to pay the invoice. PayInvoice bool `protobuf:"varint,3,opt,name=pay_invoice,json=payInvoice,proto3" json:"pay_invoice,omitempty"` unknownFields protoimpl.UnknownFields @@ -1184,7 +950,7 @@ type AcceptAndExecuteRequest struct { func (x *AcceptAndExecuteRequest) Reset() { *x = AcceptAndExecuteRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1196,7 +962,7 @@ func (x *AcceptAndExecuteRequest) String() string { func (*AcceptAndExecuteRequest) ProtoMessage() {} func (x *AcceptAndExecuteRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1209,7 +975,7 @@ func (x *AcceptAndExecuteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteRequest.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{16} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{12} } func (x *AcceptAndExecuteRequest) GetPeerId() string { @@ -1219,9 +985,9 @@ func (x *AcceptAndExecuteRequest) GetPeerId() string { return "" } -func (x *AcceptAndExecuteRequest) GetJobId() []byte { +func (x *AcceptAndExecuteRequest) GetCallId() []byte { if x != nil { - return x.JobId + return x.CallId } return nil } @@ -1238,8 +1004,8 @@ type AcceptAndExecuteStreamRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The job_id to execute (must match a previously received Quote). - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + // The call_id to execute (must match a previously received Quote). + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // Authorization to pay the invoice. PayInvoice bool `protobuf:"varint,3,opt,name=pay_invoice,json=payInvoice,proto3" json:"pay_invoice,omitempty"` unknownFields protoimpl.UnknownFields @@ -1248,7 +1014,7 @@ type AcceptAndExecuteStreamRequest struct { func (x *AcceptAndExecuteStreamRequest) Reset() { *x = AcceptAndExecuteStreamRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1260,7 +1026,7 @@ func (x *AcceptAndExecuteStreamRequest) String() string { func (*AcceptAndExecuteStreamRequest) ProtoMessage() {} func (x *AcceptAndExecuteStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1273,7 +1039,7 @@ func (x *AcceptAndExecuteStreamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteStreamRequest.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteStreamRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{17} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{13} } func (x *AcceptAndExecuteStreamRequest) GetPeerId() string { @@ -1283,9 +1049,9 @@ func (x *AcceptAndExecuteStreamRequest) GetPeerId() string { return "" } -func (x *AcceptAndExecuteStreamRequest) GetJobId() []byte { +func (x *AcceptAndExecuteStreamRequest) GetCallId() []byte { if x != nil { - return x.JobId + return x.CallId } return nil } @@ -1301,14 +1067,14 @@ func (x *AcceptAndExecuteStreamRequest) GetPayInvoice() bool { type AcceptAndExecuteResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The execution result. - Result *Result `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Complete *Complete `protobuf:"bytes,1,opt,name=complete,proto3" json:"complete,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AcceptAndExecuteResponse) Reset() { *x = AcceptAndExecuteResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1320,7 +1086,7 @@ func (x *AcceptAndExecuteResponse) String() string { func (*AcceptAndExecuteResponse) ProtoMessage() {} func (x *AcceptAndExecuteResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1333,12 +1099,12 @@ func (x *AcceptAndExecuteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteResponse.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{18} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{14} } -func (x *AcceptAndExecuteResponse) GetResult() *Result { +func (x *AcceptAndExecuteResponse) GetComplete() *Complete { if x != nil { - return x.Result + return x.Complete } return nil } @@ -1349,10 +1115,10 @@ type AcceptAndExecuteStreamResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Event: // - // *AcceptAndExecuteStreamResponse_ResultBegin - // *AcceptAndExecuteStreamResponse_ResultChunk - // *AcceptAndExecuteStreamResponse_ResultEnd - // *AcceptAndExecuteStreamResponse_Result + // *AcceptAndExecuteStreamResponse_ResponseBegin + // *AcceptAndExecuteStreamResponse_ResponseChunk + // *AcceptAndExecuteStreamResponse_ResponseEnd + // *AcceptAndExecuteStreamResponse_Complete Event isAcceptAndExecuteStreamResponse_Event `protobuf_oneof:"event"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1360,7 +1126,7 @@ type AcceptAndExecuteStreamResponse struct { func (x *AcceptAndExecuteStreamResponse) Reset() { *x = AcceptAndExecuteStreamResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1372,7 +1138,7 @@ func (x *AcceptAndExecuteStreamResponse) String() string { func (*AcceptAndExecuteStreamResponse) ProtoMessage() {} func (x *AcceptAndExecuteStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1385,7 +1151,7 @@ func (x *AcceptAndExecuteStreamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteStreamResponse.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteStreamResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{19} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{15} } func (x *AcceptAndExecuteStreamResponse) GetEvent() isAcceptAndExecuteStreamResponse_Event { @@ -1395,37 +1161,37 @@ func (x *AcceptAndExecuteStreamResponse) GetEvent() isAcceptAndExecuteStreamResp return nil } -func (x *AcceptAndExecuteStreamResponse) GetResultBegin() *ResultStreamBegin { +func (x *AcceptAndExecuteStreamResponse) GetResponseBegin() *ResponseStreamBegin { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResultBegin); ok { - return x.ResultBegin + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResponseBegin); ok { + return x.ResponseBegin } } return nil } -func (x *AcceptAndExecuteStreamResponse) GetResultChunk() *ResultStreamChunk { +func (x *AcceptAndExecuteStreamResponse) GetResponseChunk() *ResponseStreamChunk { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResultChunk); ok { - return x.ResultChunk + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResponseChunk); ok { + return x.ResponseChunk } } return nil } -func (x *AcceptAndExecuteStreamResponse) GetResultEnd() *ResultStreamEnd { +func (x *AcceptAndExecuteStreamResponse) GetResponseEnd() *ResponseStreamEnd { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResultEnd); ok { - return x.ResultEnd + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResponseEnd); ok { + return x.ResponseEnd } } return nil } -func (x *AcceptAndExecuteStreamResponse) GetResult() *Result { +func (x *AcceptAndExecuteStreamResponse) GetComplete() *Complete { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_Result); ok { - return x.Result + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_Complete); ok { + return x.Complete } } return nil @@ -1435,56 +1201,56 @@ type isAcceptAndExecuteStreamResponse_Event interface { isAcceptAndExecuteStreamResponse_Event() } -type AcceptAndExecuteStreamResponse_ResultBegin struct { - ResultBegin *ResultStreamBegin `protobuf:"bytes,1,opt,name=result_begin,json=resultBegin,proto3,oneof"` +type AcceptAndExecuteStreamResponse_ResponseBegin struct { + ResponseBegin *ResponseStreamBegin `protobuf:"bytes,1,opt,name=response_begin,json=responseBegin,proto3,oneof"` } -type AcceptAndExecuteStreamResponse_ResultChunk struct { - ResultChunk *ResultStreamChunk `protobuf:"bytes,2,opt,name=result_chunk,json=resultChunk,proto3,oneof"` +type AcceptAndExecuteStreamResponse_ResponseChunk struct { + ResponseChunk *ResponseStreamChunk `protobuf:"bytes,2,opt,name=response_chunk,json=responseChunk,proto3,oneof"` } -type AcceptAndExecuteStreamResponse_ResultEnd struct { - ResultEnd *ResultStreamEnd `protobuf:"bytes,3,opt,name=result_end,json=resultEnd,proto3,oneof"` +type AcceptAndExecuteStreamResponse_ResponseEnd struct { + ResponseEnd *ResponseStreamEnd `protobuf:"bytes,3,opt,name=response_end,json=responseEnd,proto3,oneof"` } -type AcceptAndExecuteStreamResponse_Result struct { - Result *Result `protobuf:"bytes,4,opt,name=result,proto3,oneof"` +type AcceptAndExecuteStreamResponse_Complete struct { + Complete *Complete `protobuf:"bytes,4,opt,name=complete,proto3,oneof"` } -func (*AcceptAndExecuteStreamResponse_ResultBegin) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_ResponseBegin) isAcceptAndExecuteStreamResponse_Event() {} -func (*AcceptAndExecuteStreamResponse_ResultChunk) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_ResponseChunk) isAcceptAndExecuteStreamResponse_Event() {} -func (*AcceptAndExecuteStreamResponse_ResultEnd) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_ResponseEnd) isAcceptAndExecuteStreamResponse_Event() {} -func (*AcceptAndExecuteStreamResponse_Result) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_Complete) isAcceptAndExecuteStreamResponse_Event() {} -// ResultStreamBegin marks the start of the decoded result stream. -type ResultStreamBegin struct { +// ResponseStreamBegin marks the start of the decoded response stream. +type ResponseStreamBegin struct { state protoimpl.MessageState `protogen:"open.v1"` - // Content type of the decoded result stream bytes. + // Content type of the decoded response stream bytes. ContentType string `protobuf:"bytes,1,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` - // Content encoding of the decoded result stream bytes. + // Content encoding of the decoded response stream bytes. ContentEncoding string `protobuf:"bytes,2,opt,name=content_encoding,json=contentEncoding,proto3" json:"content_encoding,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ResultStreamBegin) Reset() { - *x = ResultStreamBegin{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] +func (x *ResponseStreamBegin) Reset() { + *x = ResponseStreamBegin{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ResultStreamBegin) String() string { +func (x *ResponseStreamBegin) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResultStreamBegin) ProtoMessage() {} +func (*ResponseStreamBegin) ProtoMessage() {} -func (x *ResultStreamBegin) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] +func (x *ResponseStreamBegin) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1495,48 +1261,48 @@ func (x *ResultStreamBegin) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResultStreamBegin.ProtoReflect.Descriptor instead. -func (*ResultStreamBegin) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{20} +// Deprecated: Use ResponseStreamBegin.ProtoReflect.Descriptor instead. +func (*ResponseStreamBegin) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{16} } -func (x *ResultStreamBegin) GetContentType() string { +func (x *ResponseStreamBegin) GetContentType() string { if x != nil { return x.ContentType } return "" } -func (x *ResultStreamBegin) GetContentEncoding() string { +func (x *ResponseStreamBegin) GetContentEncoding() string { if x != nil { return x.ContentEncoding } return "" } -// ResultStreamChunk carries a contiguous slice of decoded result stream bytes. -type ResultStreamChunk struct { +// ResponseStreamChunk carries a contiguous slice of decoded response stream bytes. +type ResponseStreamChunk struct { state protoimpl.MessageState `protogen:"open.v1"` Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ResultStreamChunk) Reset() { - *x = ResultStreamChunk{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[21] +func (x *ResponseStreamChunk) Reset() { + *x = ResponseStreamChunk{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ResultStreamChunk) String() string { +func (x *ResponseStreamChunk) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResultStreamChunk) ProtoMessage() {} +func (*ResponseStreamChunk) ProtoMessage() {} -func (x *ResultStreamChunk) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[21] +func (x *ResponseStreamChunk) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1547,43 +1313,43 @@ func (x *ResultStreamChunk) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResultStreamChunk.ProtoReflect.Descriptor instead. -func (*ResultStreamChunk) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{21} +// Deprecated: Use ResponseStreamChunk.ProtoReflect.Descriptor instead. +func (*ResponseStreamChunk) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{17} } -func (x *ResultStreamChunk) GetData() []byte { +func (x *ResponseStreamChunk) GetData() []byte { if x != nil { return x.Data } return nil } -// ResultStreamEnd marks the end of the decoded result stream and contains the +// ResponseStreamEnd marks the end of the decoded response stream and contains the // validated stream metadata. -type ResultStreamEnd struct { +type ResponseStreamEnd struct { state protoimpl.MessageState `protogen:"open.v1"` - ResultHash []byte `protobuf:"bytes,1,opt,name=result_hash,json=resultHash,proto3" json:"result_hash,omitempty"` - ResultLen uint64 `protobuf:"varint,2,opt,name=result_len,json=resultLen,proto3" json:"result_len,omitempty"` + ResponseHash []byte `protobuf:"bytes,1,opt,name=response_hash,json=responseHash,proto3" json:"response_hash,omitempty"` + ResponseLen uint64 `protobuf:"varint,2,opt,name=response_len,json=responseLen,proto3" json:"response_len,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ResultStreamEnd) Reset() { - *x = ResultStreamEnd{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[22] +func (x *ResponseStreamEnd) Reset() { + *x = ResponseStreamEnd{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ResultStreamEnd) String() string { +func (x *ResponseStreamEnd) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResultStreamEnd) ProtoMessage() {} +func (*ResponseStreamEnd) ProtoMessage() {} -func (x *ResultStreamEnd) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[22] +func (x *ResponseStreamEnd) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1594,21 +1360,21 @@ func (x *ResultStreamEnd) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResultStreamEnd.ProtoReflect.Descriptor instead. -func (*ResultStreamEnd) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{22} +// Deprecated: Use ResponseStreamEnd.ProtoReflect.Descriptor instead. +func (*ResponseStreamEnd) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{18} } -func (x *ResultStreamEnd) GetResultHash() []byte { +func (x *ResponseStreamEnd) GetResponseHash() []byte { if x != nil { - return x.ResultHash + return x.ResponseHash } return nil } -func (x *ResultStreamEnd) GetResultLen() uint64 { +func (x *ResponseStreamEnd) GetResponseLen() uint64 { if x != nil { - return x.ResultLen + return x.ResponseLen } return 0 } @@ -1618,8 +1384,8 @@ type CancelJobRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The job_id to cancel. - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + // The call_id to cancel. + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // Optional human-readable cancellation reason. Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` unknownFields protoimpl.UnknownFields @@ -1628,7 +1394,7 @@ type CancelJobRequest struct { func (x *CancelJobRequest) Reset() { *x = CancelJobRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[23] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1640,7 +1406,7 @@ func (x *CancelJobRequest) String() string { func (*CancelJobRequest) ProtoMessage() {} func (x *CancelJobRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[23] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1653,7 +1419,7 @@ func (x *CancelJobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelJobRequest.ProtoReflect.Descriptor instead. func (*CancelJobRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{23} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{19} } func (x *CancelJobRequest) GetPeerId() string { @@ -1663,9 +1429,9 @@ func (x *CancelJobRequest) GetPeerId() string { return "" } -func (x *CancelJobRequest) GetJobId() []byte { +func (x *CancelJobRequest) GetCallId() []byte { if x != nil { - return x.JobId + return x.CallId } return nil } @@ -1687,7 +1453,7 @@ type CancelJobResponse struct { func (x *CancelJobResponse) Reset() { *x = CancelJobResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[24] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1699,7 +1465,7 @@ func (x *CancelJobResponse) String() string { func (*CancelJobResponse) ProtoMessage() {} func (x *CancelJobResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[24] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1712,7 +1478,7 @@ func (x *CancelJobResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelJobResponse.ProtoReflect.Descriptor instead. func (*CancelJobResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{24} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{20} } func (x *CancelJobResponse) GetSuccess() bool { @@ -1732,291 +1498,294 @@ var file_lcpd_v1_lcpd_proto_rawDesc = string([]byte{ 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x01, 0x0a, - 0x04, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x67, 0x0a, 0x1a, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x5f, - 0x63, 0x68, 0x61, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x5f, 0x76, 0x31, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6c, 0x63, 0x70, 0x64, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x54, 0x61, 0x73, 0x6b, 0x53, - 0x70, 0x65, 0x63, 0x48, 0x00, 0x52, 0x17, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x43, 0x68, 0x61, - 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x12, 0x54, - 0x0a, 0x13, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x73, 0x5f, 0x76, 0x31, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6c, 0x63, - 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x70, 0x65, 0x63, 0x48, - 0x00, 0x52, 0x11, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x73, 0x56, 0x31, 0x42, 0x0d, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x05, 0xba, 0x48, - 0x02, 0x08, 0x01, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x08, 0x6c, 0x6c, 0x6d, 0x5f, 0x63, - 0x68, 0x61, 0x74, 0x22, 0x9b, 0x01, 0x0a, 0x1f, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, - 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x54, - 0x61, 0x73, 0x6b, 0x53, 0x70, 0x65, 0x63, 0x12, 0x2d, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, - 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x49, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, 0x09, - 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, - 0x73, 0x22, 0x41, 0x0a, 0x1d, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x73, 0x12, 0x20, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6d, - 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x8f, 0x01, 0x0a, 0x19, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x70, - 0x65, 0x63, 0x12, 0x2d, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6a, 0x73, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, - 0x7a, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4a, 0x73, 0x6f, - 0x6e, 0x12, 0x43, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x6e, - 0x41, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x73, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x22, 0x3b, 0x0a, 0x17, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x73, 0x12, 0x20, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6d, 0x6f, - 0x64, 0x65, 0x6c, 0x22, 0xad, 0x02, 0x0a, 0x05, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x32, 0x0a, - 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x07, 0xba, 0x48, 0x04, 0x2a, 0x02, 0x08, 0x02, - 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x1e, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, - 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x61, 0x74, - 0x12, 0x57, 0x0a, 0x0c, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x42, 0x18, 0xba, 0x48, 0x15, 0xc8, 0x01, 0x01, 0xea, 0x01, 0x0f, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x52, 0x0b, 0x71, 0x75, - 0x6f, 0x74, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x26, 0x0a, 0x0a, 0x74, 0x65, 0x72, - 0x6d, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, - 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x09, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x48, 0x61, 0x73, - 0x68, 0x12, 0x30, 0x0a, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, - 0x02, 0x10, 0x01, 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0xe3, 0x02, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x36, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, - 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x28, - 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x18, 0x20, 0x52, 0x0a, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, - 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9f, 0x02, 0x0a, + 0x08, 0x43, 0x61, 0x6c, 0x6c, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1f, 0x0a, 0x06, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, + 0x10, 0x01, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, + 0x10, 0x01, 0x52, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x39, 0x0a, 0x14, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x12, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x41, 0x0a, 0x18, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x16, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0xf3, + 0x03, 0x0a, 0x05, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0d, 0x42, 0x07, 0xba, 0x48, 0x04, 0x2a, 0x02, 0x08, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x07, + 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x09, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x57, 0x0a, + 0x0c, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, + 0x18, 0xba, 0x48, 0x15, 0xc8, 0x01, 0x01, 0xea, 0x01, 0x0f, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, + 0x61, 0x6e, 0x6f, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x52, 0x0b, 0x71, 0x75, 0x6f, 0x74, 0x65, + 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x26, 0x0a, 0x0a, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x5f, + 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, + 0x02, 0x68, 0x20, 0x52, 0x09, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x30, + 0x0a, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, + 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x40, 0x0a, 0x15, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x13, 0x72, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x48, 0x0a, 0x19, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x01, + 0x52, 0x17, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x42, 0x18, 0x0a, 0x16, + 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x1c, 0x0a, 0x1a, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, + 0x64, 0x69, 0x6e, 0x67, 0x22, 0xf4, 0x04, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x18, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x06, 0xba, 0x48, 0x03, + 0xc8, 0x01, 0x01, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x48, 0x00, 0x52, 0x10, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x31, + 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x48, 0x01, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x61, 0x73, 0x68, 0x88, 0x01, + 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x6c, 0x65, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x48, 0x02, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x4c, 0x65, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x15, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, + 0x01, 0x48, 0x03, 0x52, 0x13, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x19, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, + 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x04, 0x52, 0x17, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, + 0x6e, 0x67, 0x88, 0x01, 0x01, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x58, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x4b, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, - 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4c, 0x43, - 0x50, 0x50, 0x65, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, - 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, - 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xe0, 0x41, - 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x12, 0x42, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, 0x70, - 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0xd4, 0x02, 0x0a, 0x0b, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, - 0x07, 0xba, 0x48, 0x04, 0x2a, 0x02, 0x08, 0x02, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x11, 0x6d, 0x61, 0x78, - 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x0d, 0x42, 0x0b, 0xba, 0x48, 0x08, 0x2a, 0x06, 0x18, 0xff, 0xff, 0x03, 0x20, - 0x00, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x31, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x32, 0x02, 0x20, 0x00, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x6a, 0x6f, 0x62, - 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x32, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x4a, 0x6f, 0x62, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x35, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x66, 0x6c, 0x69, 0x67, - 0x68, 0x74, 0x5f, 0x6a, 0x6f, 0x62, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x09, 0xba, - 0x48, 0x06, 0x2a, 0x04, 0x18, 0xff, 0xff, 0x03, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x49, 0x6e, 0x66, - 0x6c, 0x69, 0x67, 0x68, 0x74, 0x4a, 0x6f, 0x62, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x73, 0x75, 0x70, - 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x0c, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, - 0x54, 0x61, 0x73, 0x6b, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x73, 0x75, - 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x22, 0xa8, 0x02, 0x0a, - 0x0f, 0x4c, 0x43, 0x50, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x12, 0x30, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, - 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x54, 0x61, 0x73, 0x6b, - 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x6b, 0x69, - 0x6e, 0x64, 0x12, 0x65, 0x0a, 0x1a, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x5f, 0x63, 0x68, 0x61, - 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x76, 0x31, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x48, 0x00, - 0x52, 0x17, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x12, 0x52, 0x0a, 0x13, 0x6f, 0x70, 0x65, - 0x6e, 0x61, 0x69, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x5f, 0x76, 0x31, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, - 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x48, 0x00, 0x52, 0x11, 0x6f, 0x70, 0x65, 0x6e, - 0x61, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x42, 0x18, 0x0a, - 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x08, 0x6c, - 0x6c, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x74, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4c, - 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, - 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x05, 0x70, 0x65, - 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x89, 0x01, 0x0a, 0x14, 0x47, - 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, - 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, - 0x24, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, - 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x42, 0x09, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x6d, 0x61, - 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x79, 0x0a, 0x13, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, - 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, - 0xe0, 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, - 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x73, 0x6b, - 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x74, 0x61, 0x73, - 0x6b, 0x22, 0x7d, 0x0a, 0x14, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, - 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, - 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, - 0x2f, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x42, 0x09, - 0xe0, 0x41, 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, - 0x22, 0x9e, 0x01, 0x0a, 0x17, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, - 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, - 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, - 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, - 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, + 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, + 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x61, + 0x73, 0x68, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, + 0x6c, 0x65, 0x6e, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x1c, 0x0a, + 0x1a, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0xd7, 0x02, 0x0a, 0x10, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, + 0x12, 0x1f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x32, 0x0a, 0x15, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x13, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x08, 0x64, + 0x6f, 0x63, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x64, 0x6f, 0x63, 0x73, 0x55, 0x72, + 0x69, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x0b, 0x64, 0x6f, 0x63, 0x73, 0x5f, 0x73, 0x68, 0x61, + 0x32, 0x35, 0x36, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, + 0x68, 0x20, 0x48, 0x01, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x73, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, + 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6e, 0x6f, + 0x74, 0x69, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, + 0x02, 0x10, 0x01, 0x48, 0x02, 0x52, 0x0c, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4e, 0x6f, 0x74, + 0x69, 0x63, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x6f, 0x63, 0x73, 0x5f, + 0x75, 0x72, 0x69, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x6f, 0x63, 0x73, 0x5f, 0x73, 0x68, 0x61, + 0x32, 0x35, 0x36, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6e, + 0x6f, 0x74, 0x69, 0x63, 0x65, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, + 0x72, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, + 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, + 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, + 0x72, 0x02, 0x10, 0x01, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x42, 0x0a, + 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x42, 0x03, 0xe0, 0x41, + 0x03, 0x52, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x22, 0xdd, 0x02, 0x0a, 0x0b, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x12, 0x32, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x07, 0xba, 0x48, 0x04, + 0x2a, 0x02, 0x08, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, + 0x42, 0x0b, 0xba, 0x48, 0x08, 0x2a, 0x06, 0x18, 0xff, 0xff, 0x03, 0x20, 0x00, 0x52, 0x0f, 0x6d, + 0x61, 0x78, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x31, + 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, 0x04, 0x32, 0x02, 0x20, + 0x00, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x12, 0x2d, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, 0x04, 0x32, 0x02, + 0x20, 0x00, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x43, 0x61, 0x6c, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x37, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, + 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x09, 0xba, 0x48, + 0x06, 0x2a, 0x04, 0x18, 0xff, 0xff, 0x03, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x49, 0x6e, 0x66, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x43, 0x61, 0x6c, 0x6c, 0x73, 0x12, 0x46, 0x0a, 0x11, 0x73, 0x75, 0x70, + 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x0c, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, + 0x10, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, + 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2b, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, + 0x72, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a, + 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x89, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, + 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, + 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, + 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x6e, 0x6f, 0x64, + 0x65, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x42, 0x09, 0xe0, 0x41, 0x03, + 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x22, 0x7d, 0x0a, 0x13, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x15, + 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, + 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x30, 0x0a, + 0x04, 0x63, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x63, + 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x53, 0x70, 0x65, 0x63, 0x42, 0x09, + 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x63, 0x61, 0x6c, 0x6c, 0x22, + 0x7d, 0x0a, 0x14, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, + 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, + 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, + 0x05, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, + 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x42, 0x09, 0xe0, 0x41, + 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x05, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x22, 0xa0, + 0x01, 0x0a, 0x17, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, + 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, + 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x23, 0x0a, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, + 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x70, 0x61, 0x79, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x1d, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, + 0x65, 0x22, 0xa6, 0x01, 0x0a, 0x1d, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, - 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6a, 0x6f, 0x62, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, - 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x0b, - 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x70, 0x61, - 0x79, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x22, 0x4e, 0x0a, 0x18, 0x41, 0x63, 0x63, 0x65, - 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0x09, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, - 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x98, 0x02, 0x0a, 0x1e, 0x41, 0x63, 0x63, - 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0c, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x48, 0x00, 0x52, - 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x3f, 0x0a, 0x0c, - 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x48, 0x00, - 0x52, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x39, 0x0a, - 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x09, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x45, 0x6e, 0x64, 0x12, 0x29, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x42, 0x0e, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x05, 0xba, 0x48, - 0x02, 0x08, 0x01, 0x22, 0x79, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x2d, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, - 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x35, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0f, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x33, - 0x0a, 0x11, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x68, - 0x75, 0x6e, 0x6b, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x04, 0x64, - 0x61, 0x74, 0x61, 0x22, 0x62, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x45, 0x6e, 0x64, 0x12, 0x2b, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, - 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, - 0x61, 0x73, 0x68, 0x12, 0x22, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x6c, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x09, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x22, 0x8d, 0x01, 0x0a, 0x10, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, - 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, - 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, - 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, - 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x63, 0x61, 0x6c, + 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, + 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x2a, + 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, + 0x70, 0x61, 0x79, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x22, 0x54, 0x0a, 0x18, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x09, 0xe0, 0x41, 0x03, + 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x22, 0xb0, 0x02, 0x0a, 0x1e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, + 0x62, 0x65, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x63, + 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x45, 0x0a, 0x0e, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x68, 0x75, 0x6e, 0x6b, + 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x68, 0x75, 0x6e, + 0x6b, 0x12, 0x3f, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x65, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x45, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, + 0x6e, 0x64, 0x12, 0x2f, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x42, 0x0e, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x05, 0xba, 0x48, + 0x02, 0x08, 0x01, 0x22, 0x7b, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x2d, 0x0a, 0x0c, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x35, 0x0a, 0x10, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, + 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, + 0x22, 0x35, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x10, + 0x01, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x6c, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x6e, 0x64, 0x12, 0x2f, 0x0a, 0x0d, + 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, + 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x26, 0x0a, + 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x4c, 0x65, 0x6e, 0x22, 0x8f, 0x01, 0x0a, 0x10, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, + 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, + 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x23, 0x0a, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, + 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x02, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x32, 0x0a, 0x11, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x03, 0xe0, - 0x41, 0x03, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x2a, 0x9f, 0x01, 0x0a, 0x0b, - 0x4c, 0x43, 0x50, 0x54, 0x61, 0x73, 0x6b, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1d, 0x0a, 0x19, 0x4c, - 0x43, 0x50, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2c, 0x0a, 0x28, 0x4c, 0x43, - 0x50, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4f, 0x50, 0x45, 0x4e, - 0x41, 0x49, 0x5f, 0x43, 0x48, 0x41, 0x54, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x49, - 0x4f, 0x4e, 0x53, 0x5f, 0x56, 0x31, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x4c, 0x43, 0x50, 0x5f, - 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x41, 0x49, - 0x5f, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, 0x53, 0x5f, 0x56, 0x31, 0x10, 0x03, 0x22, - 0x04, 0x08, 0x01, 0x10, 0x01, 0x2a, 0x16, 0x4c, 0x43, 0x50, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, - 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4c, 0x4c, 0x4d, 0x5f, 0x43, 0x48, 0x41, 0x54, 0x32, 0xfe, 0x03, - 0x0a, 0x0b, 0x4c, 0x43, 0x50, 0x44, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4b, 0x0a, - 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, - 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, + 0x41, 0x03, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x32, 0xfe, 0x03, 0x0a, 0x0b, + 0x4c, 0x43, 0x50, 0x44, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, + 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x47, 0x65, - 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, - 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, - 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, + 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4c, + 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, - 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6c, 0x63, 0x70, - 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, - 0x16, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x27, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, - 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x43, 0x61, - 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x19, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, - 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, - 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x72, 0x75, - 0x77, 0x62, 0x69, 0x72, 0x64, 0x2f, 0x6c, 0x63, 0x70, 0x2f, 0x67, 0x6f, 0x2d, 0x6c, 0x63, 0x70, - 0x64, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x6c, 0x63, 0x70, 0x64, 0x2f, 0x76, 0x31, - 0x3b, 0x6c, 0x63, 0x70, 0x64, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x16, 0x41, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, + 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, + 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x19, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x72, 0x75, 0x77, 0x62, + 0x69, 0x72, 0x64, 0x2f, 0x6c, 0x63, 0x70, 0x2f, 0x67, 0x6f, 0x2d, 0x6c, 0x63, 0x70, 0x64, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x6c, 0x63, 0x70, 0x64, 0x2f, 0x76, 0x31, 0x3b, 0x6c, + 0x63, 0x70, 0x64, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -2031,76 +1800,64 @@ func file_lcpd_v1_lcpd_proto_rawDescGZIP() []byte { return file_lcpd_v1_lcpd_proto_rawDescData } -var file_lcpd_v1_lcpd_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_lcpd_v1_lcpd_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_lcpd_v1_lcpd_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_lcpd_v1_lcpd_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_lcpd_v1_lcpd_proto_goTypes = []any{ - (LCPTaskKind)(0), // 0: lcpd.v1.LCPTaskKind - (Result_Status)(0), // 1: lcpd.v1.Result.Status - (*Task)(nil), // 2: lcpd.v1.Task - (*OpenAIChatCompletionsV1TaskSpec)(nil), // 3: lcpd.v1.OpenAIChatCompletionsV1TaskSpec - (*OpenAIChatCompletionsV1Params)(nil), // 4: lcpd.v1.OpenAIChatCompletionsV1Params - (*OpenAIResponsesV1TaskSpec)(nil), // 5: lcpd.v1.OpenAIResponsesV1TaskSpec - (*OpenAIResponsesV1Params)(nil), // 6: lcpd.v1.OpenAIResponsesV1Params - (*Terms)(nil), // 7: lcpd.v1.Terms - (*Result)(nil), // 8: lcpd.v1.Result - (*LCPPeer)(nil), // 9: lcpd.v1.LCPPeer - (*LCPManifest)(nil), // 10: lcpd.v1.LCPManifest - (*LCPTaskTemplate)(nil), // 11: lcpd.v1.LCPTaskTemplate - (*ListLCPPeersRequest)(nil), // 12: lcpd.v1.ListLCPPeersRequest - (*ListLCPPeersResponse)(nil), // 13: lcpd.v1.ListLCPPeersResponse - (*GetLocalInfoRequest)(nil), // 14: lcpd.v1.GetLocalInfoRequest - (*GetLocalInfoResponse)(nil), // 15: lcpd.v1.GetLocalInfoResponse - (*RequestQuoteRequest)(nil), // 16: lcpd.v1.RequestQuoteRequest - (*RequestQuoteResponse)(nil), // 17: lcpd.v1.RequestQuoteResponse - (*AcceptAndExecuteRequest)(nil), // 18: lcpd.v1.AcceptAndExecuteRequest - (*AcceptAndExecuteStreamRequest)(nil), // 19: lcpd.v1.AcceptAndExecuteStreamRequest - (*AcceptAndExecuteResponse)(nil), // 20: lcpd.v1.AcceptAndExecuteResponse - (*AcceptAndExecuteStreamResponse)(nil), // 21: lcpd.v1.AcceptAndExecuteStreamResponse - (*ResultStreamBegin)(nil), // 22: lcpd.v1.ResultStreamBegin - (*ResultStreamChunk)(nil), // 23: lcpd.v1.ResultStreamChunk - (*ResultStreamEnd)(nil), // 24: lcpd.v1.ResultStreamEnd - (*CancelJobRequest)(nil), // 25: lcpd.v1.CancelJobRequest - (*CancelJobResponse)(nil), // 26: lcpd.v1.CancelJobResponse - (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp + (Complete_Status)(0), // 0: lcpd.v1.Complete.Status + (*CallSpec)(nil), // 1: lcpd.v1.CallSpec + (*Quote)(nil), // 2: lcpd.v1.Quote + (*Complete)(nil), // 3: lcpd.v1.Complete + (*MethodDescriptor)(nil), // 4: lcpd.v1.MethodDescriptor + (*LCPPeer)(nil), // 5: lcpd.v1.LCPPeer + (*LCPManifest)(nil), // 6: lcpd.v1.LCPManifest + (*ListLCPPeersRequest)(nil), // 7: lcpd.v1.ListLCPPeersRequest + (*ListLCPPeersResponse)(nil), // 8: lcpd.v1.ListLCPPeersResponse + (*GetLocalInfoRequest)(nil), // 9: lcpd.v1.GetLocalInfoRequest + (*GetLocalInfoResponse)(nil), // 10: lcpd.v1.GetLocalInfoResponse + (*RequestQuoteRequest)(nil), // 11: lcpd.v1.RequestQuoteRequest + (*RequestQuoteResponse)(nil), // 12: lcpd.v1.RequestQuoteResponse + (*AcceptAndExecuteRequest)(nil), // 13: lcpd.v1.AcceptAndExecuteRequest + (*AcceptAndExecuteStreamRequest)(nil), // 14: lcpd.v1.AcceptAndExecuteStreamRequest + (*AcceptAndExecuteResponse)(nil), // 15: lcpd.v1.AcceptAndExecuteResponse + (*AcceptAndExecuteStreamResponse)(nil), // 16: lcpd.v1.AcceptAndExecuteStreamResponse + (*ResponseStreamBegin)(nil), // 17: lcpd.v1.ResponseStreamBegin + (*ResponseStreamChunk)(nil), // 18: lcpd.v1.ResponseStreamChunk + (*ResponseStreamEnd)(nil), // 19: lcpd.v1.ResponseStreamEnd + (*CancelJobRequest)(nil), // 20: lcpd.v1.CancelJobRequest + (*CancelJobResponse)(nil), // 21: lcpd.v1.CancelJobResponse + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp } var file_lcpd_v1_lcpd_proto_depIdxs = []int32{ - 3, // 0: lcpd.v1.Task.openai_chat_completions_v1:type_name -> lcpd.v1.OpenAIChatCompletionsV1TaskSpec - 5, // 1: lcpd.v1.Task.openai_responses_v1:type_name -> lcpd.v1.OpenAIResponsesV1TaskSpec - 4, // 2: lcpd.v1.OpenAIChatCompletionsV1TaskSpec.params:type_name -> lcpd.v1.OpenAIChatCompletionsV1Params - 6, // 3: lcpd.v1.OpenAIResponsesV1TaskSpec.params:type_name -> lcpd.v1.OpenAIResponsesV1Params - 27, // 4: lcpd.v1.Terms.quote_expiry:type_name -> google.protobuf.Timestamp - 1, // 5: lcpd.v1.Result.status:type_name -> lcpd.v1.Result.Status - 10, // 6: lcpd.v1.LCPPeer.remote_manifest:type_name -> lcpd.v1.LCPManifest - 11, // 7: lcpd.v1.LCPManifest.supported_tasks:type_name -> lcpd.v1.LCPTaskTemplate - 0, // 8: lcpd.v1.LCPTaskTemplate.kind:type_name -> lcpd.v1.LCPTaskKind - 4, // 9: lcpd.v1.LCPTaskTemplate.openai_chat_completions_v1:type_name -> lcpd.v1.OpenAIChatCompletionsV1Params - 6, // 10: lcpd.v1.LCPTaskTemplate.openai_responses_v1:type_name -> lcpd.v1.OpenAIResponsesV1Params - 9, // 11: lcpd.v1.ListLCPPeersResponse.peers:type_name -> lcpd.v1.LCPPeer - 10, // 12: lcpd.v1.GetLocalInfoResponse.manifest:type_name -> lcpd.v1.LCPManifest - 2, // 13: lcpd.v1.RequestQuoteRequest.task:type_name -> lcpd.v1.Task - 7, // 14: lcpd.v1.RequestQuoteResponse.terms:type_name -> lcpd.v1.Terms - 8, // 15: lcpd.v1.AcceptAndExecuteResponse.result:type_name -> lcpd.v1.Result - 22, // 16: lcpd.v1.AcceptAndExecuteStreamResponse.result_begin:type_name -> lcpd.v1.ResultStreamBegin - 23, // 17: lcpd.v1.AcceptAndExecuteStreamResponse.result_chunk:type_name -> lcpd.v1.ResultStreamChunk - 24, // 18: lcpd.v1.AcceptAndExecuteStreamResponse.result_end:type_name -> lcpd.v1.ResultStreamEnd - 8, // 19: lcpd.v1.AcceptAndExecuteStreamResponse.result:type_name -> lcpd.v1.Result - 12, // 20: lcpd.v1.LCPDService.ListLCPPeers:input_type -> lcpd.v1.ListLCPPeersRequest - 14, // 21: lcpd.v1.LCPDService.GetLocalInfo:input_type -> lcpd.v1.GetLocalInfoRequest - 16, // 22: lcpd.v1.LCPDService.RequestQuote:input_type -> lcpd.v1.RequestQuoteRequest - 18, // 23: lcpd.v1.LCPDService.AcceptAndExecute:input_type -> lcpd.v1.AcceptAndExecuteRequest - 19, // 24: lcpd.v1.LCPDService.AcceptAndExecuteStream:input_type -> lcpd.v1.AcceptAndExecuteStreamRequest - 25, // 25: lcpd.v1.LCPDService.CancelJob:input_type -> lcpd.v1.CancelJobRequest - 13, // 26: lcpd.v1.LCPDService.ListLCPPeers:output_type -> lcpd.v1.ListLCPPeersResponse - 15, // 27: lcpd.v1.LCPDService.GetLocalInfo:output_type -> lcpd.v1.GetLocalInfoResponse - 17, // 28: lcpd.v1.LCPDService.RequestQuote:output_type -> lcpd.v1.RequestQuoteResponse - 20, // 29: lcpd.v1.LCPDService.AcceptAndExecute:output_type -> lcpd.v1.AcceptAndExecuteResponse - 21, // 30: lcpd.v1.LCPDService.AcceptAndExecuteStream:output_type -> lcpd.v1.AcceptAndExecuteStreamResponse - 26, // 31: lcpd.v1.LCPDService.CancelJob:output_type -> lcpd.v1.CancelJobResponse - 26, // [26:32] is the sub-list for method output_type - 20, // [20:26] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 22, // 0: lcpd.v1.Quote.quote_expiry:type_name -> google.protobuf.Timestamp + 0, // 1: lcpd.v1.Complete.status:type_name -> lcpd.v1.Complete.Status + 6, // 2: lcpd.v1.LCPPeer.remote_manifest:type_name -> lcpd.v1.LCPManifest + 4, // 3: lcpd.v1.LCPManifest.supported_methods:type_name -> lcpd.v1.MethodDescriptor + 5, // 4: lcpd.v1.ListLCPPeersResponse.peers:type_name -> lcpd.v1.LCPPeer + 6, // 5: lcpd.v1.GetLocalInfoResponse.manifest:type_name -> lcpd.v1.LCPManifest + 1, // 6: lcpd.v1.RequestQuoteRequest.call:type_name -> lcpd.v1.CallSpec + 2, // 7: lcpd.v1.RequestQuoteResponse.quote:type_name -> lcpd.v1.Quote + 3, // 8: lcpd.v1.AcceptAndExecuteResponse.complete:type_name -> lcpd.v1.Complete + 17, // 9: lcpd.v1.AcceptAndExecuteStreamResponse.response_begin:type_name -> lcpd.v1.ResponseStreamBegin + 18, // 10: lcpd.v1.AcceptAndExecuteStreamResponse.response_chunk:type_name -> lcpd.v1.ResponseStreamChunk + 19, // 11: lcpd.v1.AcceptAndExecuteStreamResponse.response_end:type_name -> lcpd.v1.ResponseStreamEnd + 3, // 12: lcpd.v1.AcceptAndExecuteStreamResponse.complete:type_name -> lcpd.v1.Complete + 7, // 13: lcpd.v1.LCPDService.ListLCPPeers:input_type -> lcpd.v1.ListLCPPeersRequest + 9, // 14: lcpd.v1.LCPDService.GetLocalInfo:input_type -> lcpd.v1.GetLocalInfoRequest + 11, // 15: lcpd.v1.LCPDService.RequestQuote:input_type -> lcpd.v1.RequestQuoteRequest + 13, // 16: lcpd.v1.LCPDService.AcceptAndExecute:input_type -> lcpd.v1.AcceptAndExecuteRequest + 14, // 17: lcpd.v1.LCPDService.AcceptAndExecuteStream:input_type -> lcpd.v1.AcceptAndExecuteStreamRequest + 20, // 18: lcpd.v1.LCPDService.CancelJob:input_type -> lcpd.v1.CancelJobRequest + 8, // 19: lcpd.v1.LCPDService.ListLCPPeers:output_type -> lcpd.v1.ListLCPPeersResponse + 10, // 20: lcpd.v1.LCPDService.GetLocalInfo:output_type -> lcpd.v1.GetLocalInfoResponse + 12, // 21: lcpd.v1.LCPDService.RequestQuote:output_type -> lcpd.v1.RequestQuoteResponse + 15, // 22: lcpd.v1.LCPDService.AcceptAndExecute:output_type -> lcpd.v1.AcceptAndExecuteResponse + 16, // 23: lcpd.v1.LCPDService.AcceptAndExecuteStream:output_type -> lcpd.v1.AcceptAndExecuteStreamResponse + 21, // 24: lcpd.v1.LCPDService.CancelJob:output_type -> lcpd.v1.CancelJobResponse + 19, // [19:25] is the sub-list for method output_type + 13, // [13:19] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_lcpd_v1_lcpd_proto_init() } @@ -2108,27 +1865,22 @@ func file_lcpd_v1_lcpd_proto_init() { if File_lcpd_v1_lcpd_proto != nil { return } - file_lcpd_v1_lcpd_proto_msgTypes[0].OneofWrappers = []any{ - (*Task_OpenaiChatCompletionsV1)(nil), - (*Task_OpenaiResponsesV1)(nil), - } - file_lcpd_v1_lcpd_proto_msgTypes[9].OneofWrappers = []any{ - (*LCPTaskTemplate_OpenaiChatCompletionsV1)(nil), - (*LCPTaskTemplate_OpenaiResponsesV1)(nil), - } - file_lcpd_v1_lcpd_proto_msgTypes[19].OneofWrappers = []any{ - (*AcceptAndExecuteStreamResponse_ResultBegin)(nil), - (*AcceptAndExecuteStreamResponse_ResultChunk)(nil), - (*AcceptAndExecuteStreamResponse_ResultEnd)(nil), - (*AcceptAndExecuteStreamResponse_Result)(nil), + file_lcpd_v1_lcpd_proto_msgTypes[1].OneofWrappers = []any{} + file_lcpd_v1_lcpd_proto_msgTypes[2].OneofWrappers = []any{} + file_lcpd_v1_lcpd_proto_msgTypes[3].OneofWrappers = []any{} + file_lcpd_v1_lcpd_proto_msgTypes[15].OneofWrappers = []any{ + (*AcceptAndExecuteStreamResponse_ResponseBegin)(nil), + (*AcceptAndExecuteStreamResponse_ResponseChunk)(nil), + (*AcceptAndExecuteStreamResponse_ResponseEnd)(nil), + (*AcceptAndExecuteStreamResponse_Complete)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_lcpd_v1_lcpd_proto_rawDesc), len(file_lcpd_v1_lcpd_proto_rawDesc)), - NumEnums: 2, - NumMessages: 25, + NumEnums: 1, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/go-lcpd/gen/go/lcpd/v1/lcpd_grpc.pb.go b/go-lcpd/gen/go/lcpd/v1/lcpd_grpc.pb.go index 00d9c8e..0551b23 100644 --- a/go-lcpd/gen/go/lcpd/v1/lcpd_grpc.pb.go +++ b/go-lcpd/gen/go/lcpd/v1/lcpd_grpc.pb.go @@ -13,9 +13,9 @@ // // LCP (Lightning Compute Protocol) is a Lightning L3 overlay protocol that // uses BOLT #1 custom messages carrying TLV streams to negotiate and execute -// compute jobs (quote → payment → execution → result delivery). +// method calls (manifest → call → quote → pay → stream → complete). // -// This API is designed to be compatible with the LCP v0.2 wire protocol +// This API is designed to be compatible with the LCP v0.3 wire protocol // described in `docs/protocol/protocol.md`. package lcpdv1 @@ -70,14 +70,14 @@ type LCPDServiceClient interface { // - INTERNAL: Unexpected server error. // Impact: No local info is returned. GetLocalInfo(ctx context.Context, in *GetLocalInfoRequest, opts ...grpc.CallOption) (*GetLocalInfoResponse, error) - // RequestQuote sends an `lcp_quote_request` to a specific peer. + // RequestQuote sends an `lcp_call` and a request stream to a specific peer. // - // On success, it returns the provider's `lcp_quote_response` as `Terms`, + // On success, it returns the provider's `lcp_quote` as `Quote`, // including a BOLT11 invoice whose `description_hash` MUST equal `terms_hash` // (invoice swapping defense). // Errors: // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id` format, missing - // `task`, or invalid task spec). + // `call`, or invalid call spec). // Impact: No wire message is sent to the peer. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No wire message is sent to the peer. @@ -89,21 +89,21 @@ type LCPDServiceClient interface { // Impact: No quote is returned. // - DEADLINE_EXCEEDED: The peer did not respond before the client/server deadline. // Impact: Quote outcome is unknown to the caller; retry MAY result in a new - // `job_id` and a different invoice unless the implementation provides idempotency. + // `call_id` and a different invoice unless the implementation provides idempotency. // - UNAVAILABLE: Transient transport failure sending/receiving peer messages. // Impact: Quote outcome is unknown; safe retries depend on idempotency. RequestQuote(ctx context.Context, in *RequestQuoteRequest, opts ...grpc.CallOption) (*RequestQuoteResponse, error) // AcceptAndExecute pays the invoice associated with the quote and waits for - // the `lcp_result`. + // the terminal `lcp_complete` and (when available) the validated response stream. // // This is a blocking call. Clients SHOULD set a deadline and MAY cancel via // context cancellation (gRPC) or `CancelJob`. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format), + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format), // or `pay_invoice=false`. // Impact: No payment attempt is made and no execution is started. - // - NOT_FOUND: The daemon has no known quote/payment data for the given `job_id`. + // - NOT_FOUND: The daemon has no known quote/payment data for the given `call_id`. // Impact: No payment attempt is made. // - FAILED_PRECONDITION: The quote is expired, the job is not executable, or the // daemon refuses to pay/execute due to local policy. @@ -119,9 +119,9 @@ type LCPDServiceClient interface { // Impact: Outcome may be unknown if failure happened after payment settlement. AcceptAndExecute(ctx context.Context, in *AcceptAndExecuteRequest, opts ...grpc.CallOption) (*AcceptAndExecuteResponse, error) // AcceptAndExecuteStream pays the invoice associated with the quote and streams - // the decoded result stream bytes as they arrive. + // the decoded response stream bytes as they arrive. // - // The stream ends with a terminal `Result` event that indicates the final job + // The stream ends with a terminal `Complete` event that indicates the final job // status and (when available) the validated stream metadata (hash/len). // // Errors follow the same model as `AcceptAndExecute`, but note that streaming @@ -133,7 +133,7 @@ type LCPDServiceClient interface { // Cancellation is best-effort: the provider may have already completed the job. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format). + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format). // Impact: No cancel message is sent. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No cancel message is sent. @@ -255,14 +255,14 @@ type LCPDServiceServer interface { // - INTERNAL: Unexpected server error. // Impact: No local info is returned. GetLocalInfo(context.Context, *GetLocalInfoRequest) (*GetLocalInfoResponse, error) - // RequestQuote sends an `lcp_quote_request` to a specific peer. + // RequestQuote sends an `lcp_call` and a request stream to a specific peer. // - // On success, it returns the provider's `lcp_quote_response` as `Terms`, + // On success, it returns the provider's `lcp_quote` as `Quote`, // including a BOLT11 invoice whose `description_hash` MUST equal `terms_hash` // (invoice swapping defense). // Errors: // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id` format, missing - // `task`, or invalid task spec). + // `call`, or invalid call spec). // Impact: No wire message is sent to the peer. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No wire message is sent to the peer. @@ -274,21 +274,21 @@ type LCPDServiceServer interface { // Impact: No quote is returned. // - DEADLINE_EXCEEDED: The peer did not respond before the client/server deadline. // Impact: Quote outcome is unknown to the caller; retry MAY result in a new - // `job_id` and a different invoice unless the implementation provides idempotency. + // `call_id` and a different invoice unless the implementation provides idempotency. // - UNAVAILABLE: Transient transport failure sending/receiving peer messages. // Impact: Quote outcome is unknown; safe retries depend on idempotency. RequestQuote(context.Context, *RequestQuoteRequest) (*RequestQuoteResponse, error) // AcceptAndExecute pays the invoice associated with the quote and waits for - // the `lcp_result`. + // the terminal `lcp_complete` and (when available) the validated response stream. // // This is a blocking call. Clients SHOULD set a deadline and MAY cancel via // context cancellation (gRPC) or `CancelJob`. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format), + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format), // or `pay_invoice=false`. // Impact: No payment attempt is made and no execution is started. - // - NOT_FOUND: The daemon has no known quote/payment data for the given `job_id`. + // - NOT_FOUND: The daemon has no known quote/payment data for the given `call_id`. // Impact: No payment attempt is made. // - FAILED_PRECONDITION: The quote is expired, the job is not executable, or the // daemon refuses to pay/execute due to local policy. @@ -304,9 +304,9 @@ type LCPDServiceServer interface { // Impact: Outcome may be unknown if failure happened after payment settlement. AcceptAndExecute(context.Context, *AcceptAndExecuteRequest) (*AcceptAndExecuteResponse, error) // AcceptAndExecuteStream pays the invoice associated with the quote and streams - // the decoded result stream bytes as they arrive. + // the decoded response stream bytes as they arrive. // - // The stream ends with a terminal `Result` event that indicates the final job + // The stream ends with a terminal `Complete` event that indicates the final job // status and (when available) the validated stream metadata (hash/len). // // Errors follow the same model as `AcceptAndExecute`, but note that streaming @@ -318,7 +318,7 @@ type LCPDServiceServer interface { // Cancellation is best-effort: the provider may have already completed the job. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format). + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format). // Impact: No cancel message is sent. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No cancel message is sent. diff --git a/go-lcpd/proto/lcpd/v1/lcpd.proto b/go-lcpd/proto/lcpd/v1/lcpd.proto index 60cc589..22f1e09 100644 --- a/go-lcpd/proto/lcpd/v1/lcpd.proto +++ b/go-lcpd/proto/lcpd/v1/lcpd.proto @@ -9,9 +9,9 @@ syntax = "proto3"; // // LCP (Lightning Compute Protocol) is a Lightning L3 overlay protocol that // uses BOLT #1 custom messages carrying TLV streams to negotiate and execute -// compute jobs (quote → payment → execution → result delivery). +// method calls (manifest → call → quote → pay → stream → complete). // -// This API is designed to be compatible with the LCP v0.2 wire protocol +// This API is designed to be compatible with the LCP v0.3 wire protocol // described in `docs/protocol/protocol.md`. package lcpd.v1; @@ -57,14 +57,14 @@ service LCPDService { // --- Core Flow --- - // RequestQuote sends an `lcp_quote_request` to a specific peer. + // RequestQuote sends an `lcp_call` and a request stream to a specific peer. // - // On success, it returns the provider's `lcp_quote_response` as `Terms`, + // On success, it returns the provider's `lcp_quote` as `Quote`, // including a BOLT11 invoice whose `description_hash` MUST equal `terms_hash` // (invoice swapping defense). // Errors: // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id` format, missing - // `task`, or invalid task spec). + // `call`, or invalid call spec). // Impact: No wire message is sent to the peer. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No wire message is sent to the peer. @@ -76,22 +76,22 @@ service LCPDService { // Impact: No quote is returned. // - DEADLINE_EXCEEDED: The peer did not respond before the client/server deadline. // Impact: Quote outcome is unknown to the caller; retry MAY result in a new - // `job_id` and a different invoice unless the implementation provides idempotency. + // `call_id` and a different invoice unless the implementation provides idempotency. // - UNAVAILABLE: Transient transport failure sending/receiving peer messages. // Impact: Quote outcome is unknown; safe retries depend on idempotency. rpc RequestQuote(RequestQuoteRequest) returns (RequestQuoteResponse); // AcceptAndExecute pays the invoice associated with the quote and waits for - // the `lcp_result`. + // the terminal `lcp_complete` and (when available) the validated response stream. // // This is a blocking call. Clients SHOULD set a deadline and MAY cancel via // context cancellation (gRPC) or `CancelJob`. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format), + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format), // or `pay_invoice=false`. // Impact: No payment attempt is made and no execution is started. - // - NOT_FOUND: The daemon has no known quote/payment data for the given `job_id`. + // - NOT_FOUND: The daemon has no known quote/payment data for the given `call_id`. // Impact: No payment attempt is made. // - FAILED_PRECONDITION: The quote is expired, the job is not executable, or the // daemon refuses to pay/execute due to local policy. @@ -108,9 +108,9 @@ service LCPDService { rpc AcceptAndExecute(AcceptAndExecuteRequest) returns (AcceptAndExecuteResponse); // AcceptAndExecuteStream pays the invoice associated with the quote and streams - // the decoded result stream bytes as they arrive. + // the decoded response stream bytes as they arrive. // - // The stream ends with a terminal `Result` event that indicates the final job + // The stream ends with a terminal `Complete` event that indicates the final job // status and (when available) the validated stream metadata (hash/len). // // Errors follow the same model as `AcceptAndExecute`, but note that streaming @@ -124,7 +124,7 @@ service LCPDService { // Cancellation is best-effort: the provider may have already completed the job. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format). + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format). // Impact: No cancel message is sent. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No cancel message is sent. @@ -141,105 +141,44 @@ service LCPDService { } // ----------------------------------------------------------------------------- -// 1. Task Definitions (Strict & Typed) +// 1. Call Definitions (Client-Facing) // ----------------------------------------------------------------------------- -// LCPTaskKind enumerates the supported task types. -// This is primarily used for Manifests and debugging. -enum LCPTaskKind { - LCP_TASK_KIND_UNSPECIFIED = 0; - reserved 1; - reserved "LCP_TASK_KIND_LLM_CHAT"; - LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1 = 2; // "openai.chat_completions.v1" - LCP_TASK_KIND_OPENAI_RESPONSES_V1 = 3; // "openai.responses.v1" -} - -// Task is the strict container for a job request. -// It uses `oneof` to enforce that only one valid task specification is provided. -message Task { - reserved 1; - reserved "llm_chat"; +// CallSpec describes the Requester's method call and the corresponding request +// payload. The LCP wire protocol transmits the request payload via stream +// messages; this gRPC API accepts the decoded bytes and streams them over LCP. +message CallSpec { + // Method name (wire mapping: `lcp_call.method`). + string method = 1 [(buf.validate.field).string.min_len = 1]; - oneof spec { - option (buf.validate.oneof).required = true; + // Opaque method params bytes (wire mapping: `lcp_call.params`). + // For LCP v0.3, params are method-defined opaque bytes (often a TLV stream). + bytes params = 2; - // Structured specification for "openai.chat_completions.v1" - OpenAIChatCompletionsV1TaskSpec openai_chat_completions_v1 = 2; + // Optional params content type hint (wire mapping: `lcp_call.params_content_type`). + string params_content_type = 3; - // Structured specification for "openai.responses.v1" - OpenAIResponsesV1TaskSpec openai_responses_v1 = 3; - } -} + // Decoded request bytes to send in the request stream. + bytes request_bytes = 4 [(buf.validate.field).bytes.min_len = 1]; -// OpenAIChatCompletionsV1TaskSpec defines a passthrough OpenAI-compatible -// Chat Completions request. It maps to: -// - input: raw HTTP request body bytes (JSON) -// - params: TLV encoded parameters -message OpenAIChatCompletionsV1TaskSpec { - // Raw HTTP request body bytes for `POST /v1/chat/completions`. - bytes request_json = 1 [ - (google.api.field_behavior) = REQUIRED, - (buf.validate.field).bytes.min_len = 1 - ]; - - // The configuration parameters for routing/validation. - OpenAIChatCompletionsV1Params params = 2 [ - (google.api.field_behavior) = REQUIRED, - (buf.validate.field).required = true - ]; -} + // Request stream content type (wire mapping: `lcp_stream_begin.content_type`). + string request_content_type = 5 [(buf.validate.field).string.min_len = 1]; -// OpenAIChatCompletionsV1Params defines the tunable parameters for -// "openai.chat_completions.v1". -message OpenAIChatCompletionsV1Params { - // "model" (TLV type 1). - // Identifier for the execution target (for example, an OpenAI model ID). - string model = 1 [ - (google.api.field_behavior) = REQUIRED, - (buf.validate.field).string.min_len = 1 - ]; -} - -// OpenAIResponsesV1TaskSpec defines a passthrough OpenAI-compatible -// Responses request. It maps to: -// - input: raw HTTP request body bytes (JSON) -// - params: TLV encoded parameters -message OpenAIResponsesV1TaskSpec { - // Raw HTTP request body bytes for `POST /v1/responses`. - bytes request_json = 1 [ - (google.api.field_behavior) = REQUIRED, - (buf.validate.field).bytes.min_len = 1 - ]; - - // The configuration parameters for routing/validation. - OpenAIResponsesV1Params params = 2 [ - (google.api.field_behavior) = REQUIRED, - (buf.validate.field).required = true - ]; -} - -// OpenAIResponsesV1Params defines the tunable parameters for -// "openai.responses.v1". -message OpenAIResponsesV1Params { - // "model" (TLV type 1). - // Identifier for the execution target (for example, an OpenAI model ID). - string model = 1 [ - (google.api.field_behavior) = REQUIRED, - (buf.validate.field).string.min_len = 1 - ]; + // Request stream content encoding (wire mapping: `lcp_stream_begin.content_encoding`). + string request_content_encoding = 6 [(buf.validate.field).string.min_len = 1]; } // ----------------------------------------------------------------------------- -// 2. Protocol Objects (Terms, Result, Manifest) +// 2. Protocol Objects (Quote, Complete, Manifest) // ----------------------------------------------------------------------------- -// Terms represents the commitment returned by the Provider in `lcp_quote_response`. -message Terms { - // LCP protocol version (v0.2 = 2; encoding: major*100 + minor). - uint32 protocol_version = 1 [(buf.validate.field).uint32.const = 2]; +// Quote represents the commitment returned by the Provider in `lcp_quote`. +message Quote { + // LCP protocol version (v0.3 = 3; encoding: major*100 + minor). + uint32 protocol_version = 1 [(buf.validate.field).uint32.const = 3]; - // Job ID (32 bytes). - bytes job_id = 2 [(buf.validate.field).bytes.len = 32]; + // Call ID (32 bytes). + bytes call_id = 2 [(buf.validate.field).bytes.len = 32]; // Price in millisatoshis. uint64 price_msat = 3; @@ -255,16 +194,22 @@ message Terms { (buf.validate.field).cel_expression = "this.nanos == 0" ]; - // The SHA256 commitment over the canonical TLV stream of Terms. + // The SHA256 commitment over the canonical TLV stream of terms_tlvs. // Must match the BOLT11 invoice's description_hash. bytes terms_hash = 5 [(buf.validate.field).bytes.len = 32]; // The BOLT11 invoice string provided by the peer. string payment_request = 6 [(buf.validate.field).string.min_len = 1]; + + // Optional provider commitment to response stream encoding. + optional string response_content_type = 7 [(buf.validate.field).string.min_len = 1]; + optional string response_content_encoding = 8 [(buf.validate.field).string.min_len = 1]; } -// Result represents the output from `lcp_result`. -message Result { +// Complete represents the terminal completion state from `lcp_complete`. +// For status=OK, it also includes the validated response stream metadata and +// (for unary RPCs) the reconstructed decoded response bytes. +message Complete { // Status represents the job completion state. enum Status { STATUS_UNSPECIFIED = 0; @@ -276,18 +221,35 @@ message Result { // Completion status for the job. Status status = 1 [(buf.validate.field).required = true]; - // If status=OK, the decoded result bytes reconstructed from the result stream. - // For "openai.chat_completions.v1", the client should interpret this as raw JSON bytes. - bytes result = 2; + // If status=OK, the decoded response bytes reconstructed from the response stream. + bytes response_bytes = 2; - // If status=OK, these fields reflect the validated result stream metadata. - bytes result_hash = 3 [(buf.validate.field).bytes.max_len = 32]; - uint64 result_len = 4; - string content_type = 5; - string content_encoding = 6; + // If status=OK, these fields reflect the validated response stream metadata. + optional bytes response_stream_id = 3 [(buf.validate.field).bytes.len = 32]; + optional bytes response_hash = 4 [(buf.validate.field).bytes.len = 32]; + optional uint64 response_len = 5; + optional string response_content_type = 6 [(buf.validate.field).string.min_len = 1]; + optional string response_content_encoding = 7 [(buf.validate.field).string.min_len = 1]; // If status!=OK, optional human-readable message. - string message = 7; + string message = 8; +} + +// MethodDescriptor represents one entry of `supported_methods` in `lcp_manifest`. +message MethodDescriptor { + // Method name. + string method = 1 [(buf.validate.field).string.min_len = 1]; + + // Optional hints for request/response stream content type selection. + repeated string request_content_types = 2; + repeated string response_content_types = 3; + + // Optional documentation metadata. + optional string docs_uri = 4 [(buf.validate.field).string.min_len = 1]; + optional bytes docs_sha256 = 5 [(buf.validate.field).bytes.len = 32]; + + // Optional human-readable policy notice (for example, pricing or limits). + optional string policy_notice = 6 [(buf.validate.field).string.min_len = 1]; } // LCPPeer represents a connected peer node. @@ -310,8 +272,8 @@ message LCPPeer { // LCPManifest represents the capabilities declared by a peer via `lcp_manifest`. message LCPManifest { - // LCP protocol version (v0.2 = 2; encoding: major*100 + minor). - uint32 protocol_version = 1 [(buf.validate.field).uint32.const = 2]; + // LCP protocol version (v0.3 = 3; encoding: major*100 + minor). + uint32 protocol_version = 1 [(buf.validate.field).uint32.const = 3]; // Maximum supported BOLT #1 custom message payload size in bytes. uint32 max_payload_bytes = 11 [ @@ -323,31 +285,14 @@ message LCPManifest { uint64 max_stream_bytes = 14 [(buf.validate.field).uint64.gt = 0]; // Maximum total decoded bytes supported across all streams in a job. - uint64 max_job_bytes = 15 [(buf.validate.field).uint64.gt = 0]; + uint64 max_call_bytes = 15 [(buf.validate.field).uint64.gt = 0]; // Optional maximum concurrent in-flight jobs supported per connection. // 0 means "unspecified". - uint32 max_inflight_jobs = 16 [(buf.validate.field).uint32.lte = 65535]; - - // Task templates supported by this peer. - repeated LCPTaskTemplate supported_tasks = 12; -} - -// LCPTaskTemplate describes a task kind supported by a peer. -message LCPTaskTemplate { - // The kind of task supported. - LCPTaskKind kind = 1 [(buf.validate.field).required = true]; - - // The parameter template/defaults supported for this kind. - oneof params_template { - option (buf.validate.oneof).required = true; - - OpenAIChatCompletionsV1Params openai_chat_completions_v1 = 3; - OpenAIResponsesV1Params openai_responses_v1 = 4; - } + uint32 max_inflight_calls = 16 [(buf.validate.field).uint32.lte = 65535]; - reserved 2; - reserved "llm_chat"; + // Methods supported by this peer. + repeated MethodDescriptor supported_methods = 12; } // ----------------------------------------------------------------------------- @@ -388,8 +333,8 @@ message RequestQuoteRequest { (buf.validate.field).string.pattern = "^[0-9a-fA-F]{66}$" ]; - // The strict task definition. - Task task = 2 [ + // The method call specification. + CallSpec call = 2 [ (google.api.field_behavior) = REQUIRED, (buf.validate.field).required = true ]; @@ -403,8 +348,8 @@ message RequestQuoteResponse { (buf.validate.field).string.pattern = "^[0-9a-fA-F]{66}$" ]; - // The agreed terms and invoice. - Terms terms = 2 [ + // The agreed quote and invoice. + Quote quote = 2 [ (google.api.field_behavior) = OUTPUT_ONLY, (buf.validate.field).required = true ]; @@ -418,8 +363,8 @@ message AcceptAndExecuteRequest { (buf.validate.field).string.pattern = "^[0-9a-fA-F]{66}$" ]; - // The job_id to execute (must match a previously received Quote). - bytes job_id = 2 [ + // The call_id to execute (must match a previously received Quote). + bytes call_id = 2 [ (google.api.field_behavior) = REQUIRED, (buf.validate.field).bytes.len = 32 ]; @@ -439,8 +384,8 @@ message AcceptAndExecuteStreamRequest { (buf.validate.field).string.pattern = "^[0-9a-fA-F]{66}$" ]; - // The job_id to execute (must match a previously received Quote). - bytes job_id = 2 [ + // The call_id to execute (must match a previously received Quote). + bytes call_id = 2 [ (google.api.field_behavior) = REQUIRED, (buf.validate.field).bytes.len = 32 ]; @@ -455,7 +400,7 @@ message AcceptAndExecuteStreamRequest { // AcceptAndExecuteResponse is the response message for `LCPDService.AcceptAndExecute`. message AcceptAndExecuteResponse { // The execution result. - Result result = 1 [ + Complete complete = 1 [ (google.api.field_behavior) = OUTPUT_ONLY, (buf.validate.field).required = true ]; @@ -467,45 +412,45 @@ message AcceptAndExecuteStreamResponse { oneof event { option (buf.validate.oneof).required = true; - ResultStreamBegin result_begin = 1; - ResultStreamChunk result_chunk = 2; - ResultStreamEnd result_end = 3; - Result result = 4; + ResponseStreamBegin response_begin = 1; + ResponseStreamChunk response_chunk = 2; + ResponseStreamEnd response_end = 3; + Complete complete = 4; } } -// ResultStreamBegin marks the start of the decoded result stream. -message ResultStreamBegin { - // Content type of the decoded result stream bytes. +// ResponseStreamBegin marks the start of the decoded response stream. +message ResponseStreamBegin { + // Content type of the decoded response stream bytes. string content_type = 1 [ (google.api.field_behavior) = OUTPUT_ONLY, (buf.validate.field).string.min_len = 1 ]; - // Content encoding of the decoded result stream bytes. + // Content encoding of the decoded response stream bytes. string content_encoding = 2 [ (google.api.field_behavior) = OUTPUT_ONLY, (buf.validate.field).string.min_len = 1 ]; } -// ResultStreamChunk carries a contiguous slice of decoded result stream bytes. -message ResultStreamChunk { +// ResponseStreamChunk carries a contiguous slice of decoded response stream bytes. +message ResponseStreamChunk { bytes data = 1 [ (google.api.field_behavior) = OUTPUT_ONLY, (buf.validate.field).bytes.min_len = 1 ]; } -// ResultStreamEnd marks the end of the decoded result stream and contains the +// ResponseStreamEnd marks the end of the decoded response stream and contains the // validated stream metadata. -message ResultStreamEnd { - bytes result_hash = 1 [ +message ResponseStreamEnd { + bytes response_hash = 1 [ (google.api.field_behavior) = OUTPUT_ONLY, (buf.validate.field).bytes.len = 32 ]; - uint64 result_len = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + uint64 response_len = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; } // CancelJobRequest is the request message for `LCPDService.CancelJob`. @@ -516,8 +461,8 @@ message CancelJobRequest { (buf.validate.field).string.pattern = "^[0-9a-fA-F]{66}$" ]; - // The job_id to cancel. - bytes job_id = 2 [ + // The call_id to cancel. + bytes call_id = 2 [ (google.api.field_behavior) = REQUIRED, (buf.validate.field).bytes.len = 32 ]; diff --git a/proto-go/lcpd/v1/lcpd.pb.go b/proto-go/lcpd/v1/lcpd.pb.go index ddaa244..cad8831 100644 --- a/proto-go/lcpd/v1/lcpd.pb.go +++ b/proto-go/lcpd/v1/lcpd.pb.go @@ -13,9 +13,9 @@ // // LCP (Lightning Compute Protocol) is a Lightning L3 overlay protocol that // uses BOLT #1 custom messages carrying TLV streams to negotiate and execute -// compute jobs (quote → payment → execution → result delivery). +// method calls (manifest → call → quote → pay → stream → complete). // -// This API is designed to be compatible with the LCP v0.2 wire protocol +// This API is designed to be compatible with the LCP v0.3 wire protocol // described in `docs/protocol/protocol.md`. package lcpdv1 @@ -38,76 +38,25 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// LCPTaskKind enumerates the supported task types. -// This is primarily used for Manifests and debugging. -type LCPTaskKind int32 - -const ( - LCPTaskKind_LCP_TASK_KIND_UNSPECIFIED LCPTaskKind = 0 - LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1 LCPTaskKind = 2 // "openai.chat_completions.v1" - LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1 LCPTaskKind = 3 // "openai.responses.v1" -) - -// Enum value maps for LCPTaskKind. -var ( - LCPTaskKind_name = map[int32]string{ - 0: "LCP_TASK_KIND_UNSPECIFIED", - 2: "LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1", - 3: "LCP_TASK_KIND_OPENAI_RESPONSES_V1", - } - LCPTaskKind_value = map[string]int32{ - "LCP_TASK_KIND_UNSPECIFIED": 0, - "LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1": 2, - "LCP_TASK_KIND_OPENAI_RESPONSES_V1": 3, - } -) - -func (x LCPTaskKind) Enum() *LCPTaskKind { - p := new(LCPTaskKind) - *p = x - return p -} - -func (x LCPTaskKind) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (LCPTaskKind) Descriptor() protoreflect.EnumDescriptor { - return file_lcpd_v1_lcpd_proto_enumTypes[0].Descriptor() -} - -func (LCPTaskKind) Type() protoreflect.EnumType { - return &file_lcpd_v1_lcpd_proto_enumTypes[0] -} - -func (x LCPTaskKind) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use LCPTaskKind.Descriptor instead. -func (LCPTaskKind) EnumDescriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{0} -} - // Status represents the job completion state. -type Result_Status int32 +type Complete_Status int32 const ( - Result_STATUS_UNSPECIFIED Result_Status = 0 - Result_STATUS_OK Result_Status = 1 - Result_STATUS_FAILED Result_Status = 2 - Result_STATUS_CANCELLED Result_Status = 3 + Complete_STATUS_UNSPECIFIED Complete_Status = 0 + Complete_STATUS_OK Complete_Status = 1 + Complete_STATUS_FAILED Complete_Status = 2 + Complete_STATUS_CANCELLED Complete_Status = 3 ) -// Enum value maps for Result_Status. +// Enum value maps for Complete_Status. var ( - Result_Status_name = map[int32]string{ + Complete_Status_name = map[int32]string{ 0: "STATUS_UNSPECIFIED", 1: "STATUS_OK", 2: "STATUS_FAILED", 3: "STATUS_CANCELLED", } - Result_Status_value = map[string]int32{ + Complete_Status_value = map[string]int32{ "STATUS_UNSPECIFIED": 0, "STATUS_OK": 1, "STATUS_FAILED": 2, @@ -115,60 +64,69 @@ var ( } ) -func (x Result_Status) Enum() *Result_Status { - p := new(Result_Status) +func (x Complete_Status) Enum() *Complete_Status { + p := new(Complete_Status) *p = x return p } -func (x Result_Status) String() string { +func (x Complete_Status) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } -func (Result_Status) Descriptor() protoreflect.EnumDescriptor { - return file_lcpd_v1_lcpd_proto_enumTypes[1].Descriptor() +func (Complete_Status) Descriptor() protoreflect.EnumDescriptor { + return file_lcpd_v1_lcpd_proto_enumTypes[0].Descriptor() } -func (Result_Status) Type() protoreflect.EnumType { - return &file_lcpd_v1_lcpd_proto_enumTypes[1] +func (Complete_Status) Type() protoreflect.EnumType { + return &file_lcpd_v1_lcpd_proto_enumTypes[0] } -func (x Result_Status) Number() protoreflect.EnumNumber { +func (x Complete_Status) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Use Result_Status.Descriptor instead. -func (Result_Status) EnumDescriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{6, 0} +// Deprecated: Use Complete_Status.Descriptor instead. +func (Complete_Status) EnumDescriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{2, 0} } -// Task is the strict container for a job request. -// It uses `oneof` to enforce that only one valid task specification is provided. -type Task struct { +// CallSpec describes the Requester's method call and the corresponding request +// payload. The LCP wire protocol transmits the request payload via stream +// messages; this gRPC API accepts the decoded bytes and streams them over LCP. +type CallSpec struct { state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Spec: - // - // *Task_OpenaiChatCompletionsV1 - // *Task_OpenaiResponsesV1 - Spec isTask_Spec `protobuf_oneof:"spec"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Task) Reset() { - *x = Task{} + // Method name (wire mapping: `lcp_call.method`). + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // Opaque method params bytes (wire mapping: `lcp_call.params`). + // For LCP v0.3, params are method-defined opaque bytes (often a TLV stream). + Params []byte `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` + // Optional params content type hint (wire mapping: `lcp_call.params_content_type`). + ParamsContentType string `protobuf:"bytes,3,opt,name=params_content_type,json=paramsContentType,proto3" json:"params_content_type,omitempty"` + // Decoded request bytes to send in the request stream. + RequestBytes []byte `protobuf:"bytes,4,opt,name=request_bytes,json=requestBytes,proto3" json:"request_bytes,omitempty"` + // Request stream content type (wire mapping: `lcp_stream_begin.content_type`). + RequestContentType string `protobuf:"bytes,5,opt,name=request_content_type,json=requestContentType,proto3" json:"request_content_type,omitempty"` + // Request stream content encoding (wire mapping: `lcp_stream_begin.content_encoding`). + RequestContentEncoding string `protobuf:"bytes,6,opt,name=request_content_encoding,json=requestContentEncoding,proto3" json:"request_content_encoding,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallSpec) Reset() { + *x = CallSpec{} mi := &file_lcpd_v1_lcpd_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Task) String() string { +func (x *CallSpec) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Task) ProtoMessage() {} +func (*CallSpec) ProtoMessage() {} -func (x *Task) ProtoReflect() protoreflect.Message { +func (x *CallSpec) ProtoReflect() protoreflect.Message { mi := &file_lcpd_v1_lcpd_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -180,82 +138,95 @@ func (x *Task) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Task.ProtoReflect.Descriptor instead. -func (*Task) Descriptor() ([]byte, []int) { +// Deprecated: Use CallSpec.ProtoReflect.Descriptor instead. +func (*CallSpec) Descriptor() ([]byte, []int) { return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{0} } -func (x *Task) GetSpec() isTask_Spec { +func (x *CallSpec) GetMethod() string { if x != nil { - return x.Spec + return x.Method } - return nil + return "" } -func (x *Task) GetOpenaiChatCompletionsV1() *OpenAIChatCompletionsV1TaskSpec { +func (x *CallSpec) GetParams() []byte { if x != nil { - if x, ok := x.Spec.(*Task_OpenaiChatCompletionsV1); ok { - return x.OpenaiChatCompletionsV1 - } + return x.Params } return nil } -func (x *Task) GetOpenaiResponsesV1() *OpenAIResponsesV1TaskSpec { +func (x *CallSpec) GetParamsContentType() string { if x != nil { - if x, ok := x.Spec.(*Task_OpenaiResponsesV1); ok { - return x.OpenaiResponsesV1 - } + return x.ParamsContentType } - return nil + return "" } -type isTask_Spec interface { - isTask_Spec() +func (x *CallSpec) GetRequestBytes() []byte { + if x != nil { + return x.RequestBytes + } + return nil } -type Task_OpenaiChatCompletionsV1 struct { - // Structured specification for "openai.chat_completions.v1" - OpenaiChatCompletionsV1 *OpenAIChatCompletionsV1TaskSpec `protobuf:"bytes,2,opt,name=openai_chat_completions_v1,json=openaiChatCompletionsV1,proto3,oneof"` +func (x *CallSpec) GetRequestContentType() string { + if x != nil { + return x.RequestContentType + } + return "" } -type Task_OpenaiResponsesV1 struct { - // Structured specification for "openai.responses.v1" - OpenaiResponsesV1 *OpenAIResponsesV1TaskSpec `protobuf:"bytes,3,opt,name=openai_responses_v1,json=openaiResponsesV1,proto3,oneof"` +func (x *CallSpec) GetRequestContentEncoding() string { + if x != nil { + return x.RequestContentEncoding + } + return "" } -func (*Task_OpenaiChatCompletionsV1) isTask_Spec() {} - -func (*Task_OpenaiResponsesV1) isTask_Spec() {} - -// OpenAIChatCompletionsV1TaskSpec defines a passthrough OpenAI-compatible -// Chat Completions request. It maps to: -// - input: raw HTTP request body bytes (JSON) -// - params: TLV encoded parameters -type OpenAIChatCompletionsV1TaskSpec struct { +// Quote represents the commitment returned by the Provider in `lcp_quote`. +type Quote struct { state protoimpl.MessageState `protogen:"open.v1"` - // Raw HTTP request body bytes for `POST /v1/chat/completions`. - RequestJson []byte `protobuf:"bytes,1,opt,name=request_json,json=requestJson,proto3" json:"request_json,omitempty"` - // The configuration parameters for routing/validation. - Params *OpenAIChatCompletionsV1Params `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // LCP protocol version (v0.3 = 3; encoding: major*100 + minor). + ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` + // Call ID (32 bytes). + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` + // Price in millisatoshis. + PriceMsat uint64 `protobuf:"varint,3,opt,name=price_msat,json=priceMsat,proto3" json:"price_msat,omitempty"` + // Quote expiry time. + // + // Wire mapping: + // LCP v0.1 encodes quote expiry as Unix epoch seconds. Implementations that + // compute or verify `terms_hash` MUST use `quote_expiry.seconds` only and + // MUST treat `quote_expiry.nanos` as 0. + QuoteExpiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=quote_expiry,json=quoteExpiry,proto3" json:"quote_expiry,omitempty"` + // The SHA256 commitment over the canonical TLV stream of terms_tlvs. + // Must match the BOLT11 invoice's description_hash. + TermsHash []byte `protobuf:"bytes,5,opt,name=terms_hash,json=termsHash,proto3" json:"terms_hash,omitempty"` + // The BOLT11 invoice string provided by the peer. + PaymentRequest string `protobuf:"bytes,6,opt,name=payment_request,json=paymentRequest,proto3" json:"payment_request,omitempty"` + // Optional provider commitment to response stream encoding. + ResponseContentType *string `protobuf:"bytes,7,opt,name=response_content_type,json=responseContentType,proto3,oneof" json:"response_content_type,omitempty"` + ResponseContentEncoding *string `protobuf:"bytes,8,opt,name=response_content_encoding,json=responseContentEncoding,proto3,oneof" json:"response_content_encoding,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *OpenAIChatCompletionsV1TaskSpec) Reset() { - *x = OpenAIChatCompletionsV1TaskSpec{} +func (x *Quote) Reset() { + *x = Quote{} mi := &file_lcpd_v1_lcpd_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *OpenAIChatCompletionsV1TaskSpec) String() string { +func (x *Quote) String() string { return protoimpl.X.MessageStringOf(x) } -func (*OpenAIChatCompletionsV1TaskSpec) ProtoMessage() {} +func (*Quote) ProtoMessage() {} -func (x *OpenAIChatCompletionsV1TaskSpec) ProtoReflect() protoreflect.Message { +func (x *Quote) ProtoReflect() protoreflect.Message { mi := &file_lcpd_v1_lcpd_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -267,157 +238,103 @@ func (x *OpenAIChatCompletionsV1TaskSpec) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use OpenAIChatCompletionsV1TaskSpec.ProtoReflect.Descriptor instead. -func (*OpenAIChatCompletionsV1TaskSpec) Descriptor() ([]byte, []int) { +// Deprecated: Use Quote.ProtoReflect.Descriptor instead. +func (*Quote) Descriptor() ([]byte, []int) { return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{1} } -func (x *OpenAIChatCompletionsV1TaskSpec) GetRequestJson() []byte { +func (x *Quote) GetProtocolVersion() uint32 { if x != nil { - return x.RequestJson + return x.ProtocolVersion } - return nil + return 0 } -func (x *OpenAIChatCompletionsV1TaskSpec) GetParams() *OpenAIChatCompletionsV1Params { +func (x *Quote) GetCallId() []byte { if x != nil { - return x.Params + return x.CallId } return nil } -// OpenAIChatCompletionsV1Params defines the tunable parameters for -// "openai.chat_completions.v1". -type OpenAIChatCompletionsV1Params struct { - state protoimpl.MessageState `protogen:"open.v1"` - // "model" (TLV type 1). - // Identifier for the execution target (for example, an OpenAI model ID). - Model string `protobuf:"bytes,1,opt,name=model,proto3" json:"model,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *OpenAIChatCompletionsV1Params) Reset() { - *x = OpenAIChatCompletionsV1Params{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *OpenAIChatCompletionsV1Params) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*OpenAIChatCompletionsV1Params) ProtoMessage() {} - -func (x *OpenAIChatCompletionsV1Params) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] +func (x *Quote) GetPriceMsat() uint64 { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.PriceMsat } - return mi.MessageOf(x) -} - -// Deprecated: Use OpenAIChatCompletionsV1Params.ProtoReflect.Descriptor instead. -func (*OpenAIChatCompletionsV1Params) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{2} + return 0 } -func (x *OpenAIChatCompletionsV1Params) GetModel() string { +func (x *Quote) GetQuoteExpiry() *timestamppb.Timestamp { if x != nil { - return x.Model + return x.QuoteExpiry } - return "" -} - -// OpenAIResponsesV1TaskSpec defines a passthrough OpenAI-compatible -// Responses request. It maps to: -// - input: raw HTTP request body bytes (JSON) -// - params: TLV encoded parameters -type OpenAIResponsesV1TaskSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Raw HTTP request body bytes for `POST /v1/responses`. - RequestJson []byte `protobuf:"bytes,1,opt,name=request_json,json=requestJson,proto3" json:"request_json,omitempty"` - // The configuration parameters for routing/validation. - Params *OpenAIResponsesV1Params `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *OpenAIResponsesV1TaskSpec) Reset() { - *x = OpenAIResponsesV1TaskSpec{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *OpenAIResponsesV1TaskSpec) String() string { - return protoimpl.X.MessageStringOf(x) + return nil } -func (*OpenAIResponsesV1TaskSpec) ProtoMessage() {} - -func (x *OpenAIResponsesV1TaskSpec) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] +func (x *Quote) GetTermsHash() []byte { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.TermsHash } - return mi.MessageOf(x) + return nil } -// Deprecated: Use OpenAIResponsesV1TaskSpec.ProtoReflect.Descriptor instead. -func (*OpenAIResponsesV1TaskSpec) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{3} +func (x *Quote) GetPaymentRequest() string { + if x != nil { + return x.PaymentRequest + } + return "" } -func (x *OpenAIResponsesV1TaskSpec) GetRequestJson() []byte { - if x != nil { - return x.RequestJson +func (x *Quote) GetResponseContentType() string { + if x != nil && x.ResponseContentType != nil { + return *x.ResponseContentType } - return nil + return "" } -func (x *OpenAIResponsesV1TaskSpec) GetParams() *OpenAIResponsesV1Params { - if x != nil { - return x.Params +func (x *Quote) GetResponseContentEncoding() string { + if x != nil && x.ResponseContentEncoding != nil { + return *x.ResponseContentEncoding } - return nil + return "" } -// OpenAIResponsesV1Params defines the tunable parameters for -// "openai.responses.v1". -type OpenAIResponsesV1Params struct { +// Complete represents the terminal completion state from `lcp_complete`. +// For status=OK, it also includes the validated response stream metadata and +// (for unary RPCs) the reconstructed decoded response bytes. +type Complete struct { state protoimpl.MessageState `protogen:"open.v1"` - // "model" (TLV type 1). - // Identifier for the execution target (for example, an OpenAI model ID). - Model string `protobuf:"bytes,1,opt,name=model,proto3" json:"model,omitempty"` + // Completion status for the job. + Status Complete_Status `protobuf:"varint,1,opt,name=status,proto3,enum=lcpd.v1.Complete_Status" json:"status,omitempty"` + // If status=OK, the decoded response bytes reconstructed from the response stream. + ResponseBytes []byte `protobuf:"bytes,2,opt,name=response_bytes,json=responseBytes,proto3" json:"response_bytes,omitempty"` + // If status=OK, these fields reflect the validated response stream metadata. + ResponseStreamId []byte `protobuf:"bytes,3,opt,name=response_stream_id,json=responseStreamId,proto3,oneof" json:"response_stream_id,omitempty"` + ResponseHash []byte `protobuf:"bytes,4,opt,name=response_hash,json=responseHash,proto3,oneof" json:"response_hash,omitempty"` + ResponseLen *uint64 `protobuf:"varint,5,opt,name=response_len,json=responseLen,proto3,oneof" json:"response_len,omitempty"` + ResponseContentType *string `protobuf:"bytes,6,opt,name=response_content_type,json=responseContentType,proto3,oneof" json:"response_content_type,omitempty"` + ResponseContentEncoding *string `protobuf:"bytes,7,opt,name=response_content_encoding,json=responseContentEncoding,proto3,oneof" json:"response_content_encoding,omitempty"` + // If status!=OK, optional human-readable message. + Message string `protobuf:"bytes,8,opt,name=message,proto3" json:"message,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *OpenAIResponsesV1Params) Reset() { - *x = OpenAIResponsesV1Params{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] +func (x *Complete) Reset() { + *x = Complete{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *OpenAIResponsesV1Params) String() string { +func (x *Complete) String() string { return protoimpl.X.MessageStringOf(x) } -func (*OpenAIResponsesV1Params) ProtoMessage() {} +func (*Complete) ProtoMessage() {} -func (x *OpenAIResponsesV1Params) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] +func (x *Complete) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -428,149 +345,99 @@ func (x *OpenAIResponsesV1Params) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use OpenAIResponsesV1Params.ProtoReflect.Descriptor instead. -func (*OpenAIResponsesV1Params) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{4} +// Deprecated: Use Complete.ProtoReflect.Descriptor instead. +func (*Complete) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{2} } -func (x *OpenAIResponsesV1Params) GetModel() string { +func (x *Complete) GetStatus() Complete_Status { if x != nil { - return x.Model + return x.Status } - return "" -} - -// Terms represents the commitment returned by the Provider in `lcp_quote_response`. -type Terms struct { - state protoimpl.MessageState `protogen:"open.v1"` - // LCP protocol version (v0.2 = 2; encoding: major*100 + minor). - ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` - // Job ID (32 bytes). - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` - // Price in millisatoshis. - PriceMsat uint64 `protobuf:"varint,3,opt,name=price_msat,json=priceMsat,proto3" json:"price_msat,omitempty"` - // Quote expiry time. - // - // Wire mapping: - // LCP v0.1 encodes quote expiry as Unix epoch seconds. Implementations that - // compute or verify `terms_hash` MUST use `quote_expiry.seconds` only and - // MUST treat `quote_expiry.nanos` as 0. - QuoteExpiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=quote_expiry,json=quoteExpiry,proto3" json:"quote_expiry,omitempty"` - // The SHA256 commitment over the canonical TLV stream of Terms. - // Must match the BOLT11 invoice's description_hash. - TermsHash []byte `protobuf:"bytes,5,opt,name=terms_hash,json=termsHash,proto3" json:"terms_hash,omitempty"` - // The BOLT11 invoice string provided by the peer. - PaymentRequest string `protobuf:"bytes,6,opt,name=payment_request,json=paymentRequest,proto3" json:"payment_request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + return Complete_STATUS_UNSPECIFIED } -func (x *Terms) Reset() { - *x = Terms{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Terms) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Terms) ProtoMessage() {} - -func (x *Terms) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] +func (x *Complete) GetResponseBytes() []byte { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.ResponseBytes } - return mi.MessageOf(x) -} - -// Deprecated: Use Terms.ProtoReflect.Descriptor instead. -func (*Terms) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{5} + return nil } -func (x *Terms) GetProtocolVersion() uint32 { +func (x *Complete) GetResponseStreamId() []byte { if x != nil { - return x.ProtocolVersion + return x.ResponseStreamId } - return 0 + return nil } -func (x *Terms) GetJobId() []byte { +func (x *Complete) GetResponseHash() []byte { if x != nil { - return x.JobId + return x.ResponseHash } return nil } -func (x *Terms) GetPriceMsat() uint64 { - if x != nil { - return x.PriceMsat +func (x *Complete) GetResponseLen() uint64 { + if x != nil && x.ResponseLen != nil { + return *x.ResponseLen } return 0 } -func (x *Terms) GetQuoteExpiry() *timestamppb.Timestamp { - if x != nil { - return x.QuoteExpiry +func (x *Complete) GetResponseContentType() string { + if x != nil && x.ResponseContentType != nil { + return *x.ResponseContentType } - return nil + return "" } -func (x *Terms) GetTermsHash() []byte { - if x != nil { - return x.TermsHash +func (x *Complete) GetResponseContentEncoding() string { + if x != nil && x.ResponseContentEncoding != nil { + return *x.ResponseContentEncoding } - return nil + return "" } -func (x *Terms) GetPaymentRequest() string { +func (x *Complete) GetMessage() string { if x != nil { - return x.PaymentRequest + return x.Message } return "" } -// Result represents the output from `lcp_result`. -type Result struct { +// MethodDescriptor represents one entry of `supported_methods` in `lcp_manifest`. +type MethodDescriptor struct { state protoimpl.MessageState `protogen:"open.v1"` - // Completion status for the job. - Status Result_Status `protobuf:"varint,1,opt,name=status,proto3,enum=lcpd.v1.Result_Status" json:"status,omitempty"` - // If status=OK, the decoded result bytes reconstructed from the result stream. - // For "openai.chat_completions.v1", the client should interpret this as raw JSON bytes. - Result []byte `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` - // If status=OK, these fields reflect the validated result stream metadata. - ResultHash []byte `protobuf:"bytes,3,opt,name=result_hash,json=resultHash,proto3" json:"result_hash,omitempty"` - ResultLen uint64 `protobuf:"varint,4,opt,name=result_len,json=resultLen,proto3" json:"result_len,omitempty"` - ContentType string `protobuf:"bytes,5,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` - ContentEncoding string `protobuf:"bytes,6,opt,name=content_encoding,json=contentEncoding,proto3" json:"content_encoding,omitempty"` - // If status!=OK, optional human-readable message. - Message string `protobuf:"bytes,7,opt,name=message,proto3" json:"message,omitempty"` + // Method name. + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // Optional hints for request/response stream content type selection. + RequestContentTypes []string `protobuf:"bytes,2,rep,name=request_content_types,json=requestContentTypes,proto3" json:"request_content_types,omitempty"` + ResponseContentTypes []string `protobuf:"bytes,3,rep,name=response_content_types,json=responseContentTypes,proto3" json:"response_content_types,omitempty"` + // Optional documentation metadata. + DocsUri *string `protobuf:"bytes,4,opt,name=docs_uri,json=docsUri,proto3,oneof" json:"docs_uri,omitempty"` + DocsSha256 []byte `protobuf:"bytes,5,opt,name=docs_sha256,json=docsSha256,proto3,oneof" json:"docs_sha256,omitempty"` + // Optional human-readable policy notice (for example, pricing or limits). + PolicyNotice *string `protobuf:"bytes,6,opt,name=policy_notice,json=policyNotice,proto3,oneof" json:"policy_notice,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Result) Reset() { - *x = Result{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] +func (x *MethodDescriptor) Reset() { + *x = MethodDescriptor{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Result) String() string { +func (x *MethodDescriptor) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Result) ProtoMessage() {} +func (*MethodDescriptor) ProtoMessage() {} -func (x *Result) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] +func (x *MethodDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -581,56 +448,49 @@ func (x *Result) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Result.ProtoReflect.Descriptor instead. -func (*Result) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{6} +// Deprecated: Use MethodDescriptor.ProtoReflect.Descriptor instead. +func (*MethodDescriptor) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{3} } -func (x *Result) GetStatus() Result_Status { +func (x *MethodDescriptor) GetMethod() string { if x != nil { - return x.Status + return x.Method } - return Result_STATUS_UNSPECIFIED + return "" } -func (x *Result) GetResult() []byte { +func (x *MethodDescriptor) GetRequestContentTypes() []string { if x != nil { - return x.Result + return x.RequestContentTypes } return nil } -func (x *Result) GetResultHash() []byte { +func (x *MethodDescriptor) GetResponseContentTypes() []string { if x != nil { - return x.ResultHash + return x.ResponseContentTypes } return nil } -func (x *Result) GetResultLen() uint64 { - if x != nil { - return x.ResultLen - } - return 0 -} - -func (x *Result) GetContentType() string { - if x != nil { - return x.ContentType +func (x *MethodDescriptor) GetDocsUri() string { + if x != nil && x.DocsUri != nil { + return *x.DocsUri } return "" } -func (x *Result) GetContentEncoding() string { +func (x *MethodDescriptor) GetDocsSha256() []byte { if x != nil { - return x.ContentEncoding + return x.DocsSha256 } - return "" + return nil } -func (x *Result) GetMessage() string { - if x != nil { - return x.Message +func (x *MethodDescriptor) GetPolicyNotice() string { + if x != nil && x.PolicyNotice != nil { + return *x.PolicyNotice } return "" } @@ -650,7 +510,7 @@ type LCPPeer struct { func (x *LCPPeer) Reset() { *x = LCPPeer{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -662,7 +522,7 @@ func (x *LCPPeer) String() string { func (*LCPPeer) ProtoMessage() {} func (x *LCPPeer) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -675,7 +535,7 @@ func (x *LCPPeer) ProtoReflect() protoreflect.Message { // Deprecated: Use LCPPeer.ProtoReflect.Descriptor instead. func (*LCPPeer) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{7} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{4} } func (x *LCPPeer) GetPeerId() string { @@ -702,26 +562,26 @@ func (x *LCPPeer) GetRemoteManifest() *LCPManifest { // LCPManifest represents the capabilities declared by a peer via `lcp_manifest`. type LCPManifest struct { state protoimpl.MessageState `protogen:"open.v1"` - // LCP protocol version (v0.2 = 2; encoding: major*100 + minor). + // LCP protocol version (v0.3 = 3; encoding: major*100 + minor). ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` // Maximum supported BOLT #1 custom message payload size in bytes. MaxPayloadBytes uint32 `protobuf:"varint,11,opt,name=max_payload_bytes,json=maxPayloadBytes,proto3" json:"max_payload_bytes,omitempty"` // Maximum total decoded bytes supported for a single stream. MaxStreamBytes uint64 `protobuf:"varint,14,opt,name=max_stream_bytes,json=maxStreamBytes,proto3" json:"max_stream_bytes,omitempty"` // Maximum total decoded bytes supported across all streams in a job. - MaxJobBytes uint64 `protobuf:"varint,15,opt,name=max_job_bytes,json=maxJobBytes,proto3" json:"max_job_bytes,omitempty"` + MaxCallBytes uint64 `protobuf:"varint,15,opt,name=max_call_bytes,json=maxCallBytes,proto3" json:"max_call_bytes,omitempty"` // Optional maximum concurrent in-flight jobs supported per connection. // 0 means "unspecified". - MaxInflightJobs uint32 `protobuf:"varint,16,opt,name=max_inflight_jobs,json=maxInflightJobs,proto3" json:"max_inflight_jobs,omitempty"` - // Task templates supported by this peer. - SupportedTasks []*LCPTaskTemplate `protobuf:"bytes,12,rep,name=supported_tasks,json=supportedTasks,proto3" json:"supported_tasks,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + MaxInflightCalls uint32 `protobuf:"varint,16,opt,name=max_inflight_calls,json=maxInflightCalls,proto3" json:"max_inflight_calls,omitempty"` + // Methods supported by this peer. + SupportedMethods []*MethodDescriptor `protobuf:"bytes,12,rep,name=supported_methods,json=supportedMethods,proto3" json:"supported_methods,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LCPManifest) Reset() { *x = LCPManifest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -733,7 +593,7 @@ func (x *LCPManifest) String() string { func (*LCPManifest) ProtoMessage() {} func (x *LCPManifest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -746,7 +606,7 @@ func (x *LCPManifest) ProtoReflect() protoreflect.Message { // Deprecated: Use LCPManifest.ProtoReflect.Descriptor instead. func (*LCPManifest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{8} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{5} } func (x *LCPManifest) GetProtocolVersion() uint32 { @@ -770,121 +630,27 @@ func (x *LCPManifest) GetMaxStreamBytes() uint64 { return 0 } -func (x *LCPManifest) GetMaxJobBytes() uint64 { +func (x *LCPManifest) GetMaxCallBytes() uint64 { if x != nil { - return x.MaxJobBytes + return x.MaxCallBytes } return 0 } -func (x *LCPManifest) GetMaxInflightJobs() uint32 { +func (x *LCPManifest) GetMaxInflightCalls() uint32 { if x != nil { - return x.MaxInflightJobs + return x.MaxInflightCalls } return 0 } -func (x *LCPManifest) GetSupportedTasks() []*LCPTaskTemplate { - if x != nil { - return x.SupportedTasks - } - return nil -} - -// LCPTaskTemplate describes a task kind supported by a peer. -type LCPTaskTemplate struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The kind of task supported. - Kind LCPTaskKind `protobuf:"varint,1,opt,name=kind,proto3,enum=lcpd.v1.LCPTaskKind" json:"kind,omitempty"` - // The parameter template/defaults supported for this kind. - // - // Types that are valid to be assigned to ParamsTemplate: - // - // *LCPTaskTemplate_OpenaiChatCompletionsV1 - // *LCPTaskTemplate_OpenaiResponsesV1 - ParamsTemplate isLCPTaskTemplate_ParamsTemplate `protobuf_oneof:"params_template"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LCPTaskTemplate) Reset() { - *x = LCPTaskTemplate{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LCPTaskTemplate) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LCPTaskTemplate) ProtoMessage() {} - -func (x *LCPTaskTemplate) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LCPTaskTemplate.ProtoReflect.Descriptor instead. -func (*LCPTaskTemplate) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{9} -} - -func (x *LCPTaskTemplate) GetKind() LCPTaskKind { - if x != nil { - return x.Kind - } - return LCPTaskKind_LCP_TASK_KIND_UNSPECIFIED -} - -func (x *LCPTaskTemplate) GetParamsTemplate() isLCPTaskTemplate_ParamsTemplate { - if x != nil { - return x.ParamsTemplate - } - return nil -} - -func (x *LCPTaskTemplate) GetOpenaiChatCompletionsV1() *OpenAIChatCompletionsV1Params { - if x != nil { - if x, ok := x.ParamsTemplate.(*LCPTaskTemplate_OpenaiChatCompletionsV1); ok { - return x.OpenaiChatCompletionsV1 - } - } - return nil -} - -func (x *LCPTaskTemplate) GetOpenaiResponsesV1() *OpenAIResponsesV1Params { +func (x *LCPManifest) GetSupportedMethods() []*MethodDescriptor { if x != nil { - if x, ok := x.ParamsTemplate.(*LCPTaskTemplate_OpenaiResponsesV1); ok { - return x.OpenaiResponsesV1 - } + return x.SupportedMethods } return nil } -type isLCPTaskTemplate_ParamsTemplate interface { - isLCPTaskTemplate_ParamsTemplate() -} - -type LCPTaskTemplate_OpenaiChatCompletionsV1 struct { - OpenaiChatCompletionsV1 *OpenAIChatCompletionsV1Params `protobuf:"bytes,3,opt,name=openai_chat_completions_v1,json=openaiChatCompletionsV1,proto3,oneof"` -} - -type LCPTaskTemplate_OpenaiResponsesV1 struct { - OpenaiResponsesV1 *OpenAIResponsesV1Params `protobuf:"bytes,4,opt,name=openai_responses_v1,json=openaiResponsesV1,proto3,oneof"` -} - -func (*LCPTaskTemplate_OpenaiChatCompletionsV1) isLCPTaskTemplate_ParamsTemplate() {} - -func (*LCPTaskTemplate_OpenaiResponsesV1) isLCPTaskTemplate_ParamsTemplate() {} - // ListLCPPeersRequest is the request message for `LCPDService.ListLCPPeers`. type ListLCPPeersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -894,7 +660,7 @@ type ListLCPPeersRequest struct { func (x *ListLCPPeersRequest) Reset() { *x = ListLCPPeersRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -906,7 +672,7 @@ func (x *ListLCPPeersRequest) String() string { func (*ListLCPPeersRequest) ProtoMessage() {} func (x *ListLCPPeersRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -919,7 +685,7 @@ func (x *ListLCPPeersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLCPPeersRequest.ProtoReflect.Descriptor instead. func (*ListLCPPeersRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{10} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{6} } // ListLCPPeersResponse is the response message for `LCPDService.ListLCPPeers`. @@ -932,7 +698,7 @@ type ListLCPPeersResponse struct { func (x *ListLCPPeersResponse) Reset() { *x = ListLCPPeersResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -944,7 +710,7 @@ func (x *ListLCPPeersResponse) String() string { func (*ListLCPPeersResponse) ProtoMessage() {} func (x *ListLCPPeersResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -957,7 +723,7 @@ func (x *ListLCPPeersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLCPPeersResponse.ProtoReflect.Descriptor instead. func (*ListLCPPeersResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{11} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{7} } func (x *ListLCPPeersResponse) GetPeers() []*LCPPeer { @@ -976,7 +742,7 @@ type GetLocalInfoRequest struct { func (x *GetLocalInfoRequest) Reset() { *x = GetLocalInfoRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -988,7 +754,7 @@ func (x *GetLocalInfoRequest) String() string { func (*GetLocalInfoRequest) ProtoMessage() {} func (x *GetLocalInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1001,7 +767,7 @@ func (x *GetLocalInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLocalInfoRequest.ProtoReflect.Descriptor instead. func (*GetLocalInfoRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{12} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{8} } // GetLocalInfoResponse is the response message for `LCPDService.GetLocalInfo`. @@ -1017,7 +783,7 @@ type GetLocalInfoResponse struct { func (x *GetLocalInfoResponse) Reset() { *x = GetLocalInfoResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1029,7 +795,7 @@ func (x *GetLocalInfoResponse) String() string { func (*GetLocalInfoResponse) ProtoMessage() {} func (x *GetLocalInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1042,7 +808,7 @@ func (x *GetLocalInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLocalInfoResponse.ProtoReflect.Descriptor instead. func (*GetLocalInfoResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{13} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{9} } func (x *GetLocalInfoResponse) GetNodeId() string { @@ -1064,15 +830,15 @@ type RequestQuoteRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID (Pubkey). PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The strict task definition. - Task *Task `protobuf:"bytes,2,opt,name=task,proto3" json:"task,omitempty"` + // The method call specification. + Call *CallSpec `protobuf:"bytes,2,opt,name=call,proto3" json:"call,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RequestQuoteRequest) Reset() { *x = RequestQuoteRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1084,7 +850,7 @@ func (x *RequestQuoteRequest) String() string { func (*RequestQuoteRequest) ProtoMessage() {} func (x *RequestQuoteRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1097,7 +863,7 @@ func (x *RequestQuoteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestQuoteRequest.ProtoReflect.Descriptor instead. func (*RequestQuoteRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{14} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{10} } func (x *RequestQuoteRequest) GetPeerId() string { @@ -1107,9 +873,9 @@ func (x *RequestQuoteRequest) GetPeerId() string { return "" } -func (x *RequestQuoteRequest) GetTask() *Task { +func (x *RequestQuoteRequest) GetCall() *CallSpec { if x != nil { - return x.Task + return x.Call } return nil } @@ -1119,15 +885,15 @@ type RequestQuoteResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The peer sending the quote. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The agreed terms and invoice. - Terms *Terms `protobuf:"bytes,2,opt,name=terms,proto3" json:"terms,omitempty"` + // The agreed quote and invoice. + Quote *Quote `protobuf:"bytes,2,opt,name=quote,proto3" json:"quote,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RequestQuoteResponse) Reset() { *x = RequestQuoteResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1139,7 +905,7 @@ func (x *RequestQuoteResponse) String() string { func (*RequestQuoteResponse) ProtoMessage() {} func (x *RequestQuoteResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1152,7 +918,7 @@ func (x *RequestQuoteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestQuoteResponse.ProtoReflect.Descriptor instead. func (*RequestQuoteResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{15} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{11} } func (x *RequestQuoteResponse) GetPeerId() string { @@ -1162,9 +928,9 @@ func (x *RequestQuoteResponse) GetPeerId() string { return "" } -func (x *RequestQuoteResponse) GetTerms() *Terms { +func (x *RequestQuoteResponse) GetQuote() *Quote { if x != nil { - return x.Terms + return x.Quote } return nil } @@ -1174,8 +940,8 @@ type AcceptAndExecuteRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The job_id to execute (must match a previously received Quote). - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + // The call_id to execute (must match a previously received Quote). + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // Authorization to pay the invoice. PayInvoice bool `protobuf:"varint,3,opt,name=pay_invoice,json=payInvoice,proto3" json:"pay_invoice,omitempty"` unknownFields protoimpl.UnknownFields @@ -1184,7 +950,7 @@ type AcceptAndExecuteRequest struct { func (x *AcceptAndExecuteRequest) Reset() { *x = AcceptAndExecuteRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1196,7 +962,7 @@ func (x *AcceptAndExecuteRequest) String() string { func (*AcceptAndExecuteRequest) ProtoMessage() {} func (x *AcceptAndExecuteRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1209,7 +975,7 @@ func (x *AcceptAndExecuteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteRequest.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{16} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{12} } func (x *AcceptAndExecuteRequest) GetPeerId() string { @@ -1219,9 +985,9 @@ func (x *AcceptAndExecuteRequest) GetPeerId() string { return "" } -func (x *AcceptAndExecuteRequest) GetJobId() []byte { +func (x *AcceptAndExecuteRequest) GetCallId() []byte { if x != nil { - return x.JobId + return x.CallId } return nil } @@ -1238,8 +1004,8 @@ type AcceptAndExecuteStreamRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The job_id to execute (must match a previously received Quote). - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + // The call_id to execute (must match a previously received Quote). + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // Authorization to pay the invoice. PayInvoice bool `protobuf:"varint,3,opt,name=pay_invoice,json=payInvoice,proto3" json:"pay_invoice,omitempty"` unknownFields protoimpl.UnknownFields @@ -1248,7 +1014,7 @@ type AcceptAndExecuteStreamRequest struct { func (x *AcceptAndExecuteStreamRequest) Reset() { *x = AcceptAndExecuteStreamRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1260,7 +1026,7 @@ func (x *AcceptAndExecuteStreamRequest) String() string { func (*AcceptAndExecuteStreamRequest) ProtoMessage() {} func (x *AcceptAndExecuteStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1273,7 +1039,7 @@ func (x *AcceptAndExecuteStreamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteStreamRequest.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteStreamRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{17} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{13} } func (x *AcceptAndExecuteStreamRequest) GetPeerId() string { @@ -1283,9 +1049,9 @@ func (x *AcceptAndExecuteStreamRequest) GetPeerId() string { return "" } -func (x *AcceptAndExecuteStreamRequest) GetJobId() []byte { +func (x *AcceptAndExecuteStreamRequest) GetCallId() []byte { if x != nil { - return x.JobId + return x.CallId } return nil } @@ -1301,14 +1067,14 @@ func (x *AcceptAndExecuteStreamRequest) GetPayInvoice() bool { type AcceptAndExecuteResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The execution result. - Result *Result `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Complete *Complete `protobuf:"bytes,1,opt,name=complete,proto3" json:"complete,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AcceptAndExecuteResponse) Reset() { *x = AcceptAndExecuteResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1320,7 +1086,7 @@ func (x *AcceptAndExecuteResponse) String() string { func (*AcceptAndExecuteResponse) ProtoMessage() {} func (x *AcceptAndExecuteResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1333,12 +1099,12 @@ func (x *AcceptAndExecuteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteResponse.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{18} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{14} } -func (x *AcceptAndExecuteResponse) GetResult() *Result { +func (x *AcceptAndExecuteResponse) GetComplete() *Complete { if x != nil { - return x.Result + return x.Complete } return nil } @@ -1349,10 +1115,10 @@ type AcceptAndExecuteStreamResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Event: // - // *AcceptAndExecuteStreamResponse_ResultBegin - // *AcceptAndExecuteStreamResponse_ResultChunk - // *AcceptAndExecuteStreamResponse_ResultEnd - // *AcceptAndExecuteStreamResponse_Result + // *AcceptAndExecuteStreamResponse_ResponseBegin + // *AcceptAndExecuteStreamResponse_ResponseChunk + // *AcceptAndExecuteStreamResponse_ResponseEnd + // *AcceptAndExecuteStreamResponse_Complete Event isAcceptAndExecuteStreamResponse_Event `protobuf_oneof:"event"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1360,7 +1126,7 @@ type AcceptAndExecuteStreamResponse struct { func (x *AcceptAndExecuteStreamResponse) Reset() { *x = AcceptAndExecuteStreamResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1372,7 +1138,7 @@ func (x *AcceptAndExecuteStreamResponse) String() string { func (*AcceptAndExecuteStreamResponse) ProtoMessage() {} func (x *AcceptAndExecuteStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1385,7 +1151,7 @@ func (x *AcceptAndExecuteStreamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptAndExecuteStreamResponse.ProtoReflect.Descriptor instead. func (*AcceptAndExecuteStreamResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{19} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{15} } func (x *AcceptAndExecuteStreamResponse) GetEvent() isAcceptAndExecuteStreamResponse_Event { @@ -1395,37 +1161,37 @@ func (x *AcceptAndExecuteStreamResponse) GetEvent() isAcceptAndExecuteStreamResp return nil } -func (x *AcceptAndExecuteStreamResponse) GetResultBegin() *ResultStreamBegin { +func (x *AcceptAndExecuteStreamResponse) GetResponseBegin() *ResponseStreamBegin { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResultBegin); ok { - return x.ResultBegin + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResponseBegin); ok { + return x.ResponseBegin } } return nil } -func (x *AcceptAndExecuteStreamResponse) GetResultChunk() *ResultStreamChunk { +func (x *AcceptAndExecuteStreamResponse) GetResponseChunk() *ResponseStreamChunk { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResultChunk); ok { - return x.ResultChunk + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResponseChunk); ok { + return x.ResponseChunk } } return nil } -func (x *AcceptAndExecuteStreamResponse) GetResultEnd() *ResultStreamEnd { +func (x *AcceptAndExecuteStreamResponse) GetResponseEnd() *ResponseStreamEnd { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResultEnd); ok { - return x.ResultEnd + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_ResponseEnd); ok { + return x.ResponseEnd } } return nil } -func (x *AcceptAndExecuteStreamResponse) GetResult() *Result { +func (x *AcceptAndExecuteStreamResponse) GetComplete() *Complete { if x != nil { - if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_Result); ok { - return x.Result + if x, ok := x.Event.(*AcceptAndExecuteStreamResponse_Complete); ok { + return x.Complete } } return nil @@ -1435,56 +1201,56 @@ type isAcceptAndExecuteStreamResponse_Event interface { isAcceptAndExecuteStreamResponse_Event() } -type AcceptAndExecuteStreamResponse_ResultBegin struct { - ResultBegin *ResultStreamBegin `protobuf:"bytes,1,opt,name=result_begin,json=resultBegin,proto3,oneof"` +type AcceptAndExecuteStreamResponse_ResponseBegin struct { + ResponseBegin *ResponseStreamBegin `protobuf:"bytes,1,opt,name=response_begin,json=responseBegin,proto3,oneof"` } -type AcceptAndExecuteStreamResponse_ResultChunk struct { - ResultChunk *ResultStreamChunk `protobuf:"bytes,2,opt,name=result_chunk,json=resultChunk,proto3,oneof"` +type AcceptAndExecuteStreamResponse_ResponseChunk struct { + ResponseChunk *ResponseStreamChunk `protobuf:"bytes,2,opt,name=response_chunk,json=responseChunk,proto3,oneof"` } -type AcceptAndExecuteStreamResponse_ResultEnd struct { - ResultEnd *ResultStreamEnd `protobuf:"bytes,3,opt,name=result_end,json=resultEnd,proto3,oneof"` +type AcceptAndExecuteStreamResponse_ResponseEnd struct { + ResponseEnd *ResponseStreamEnd `protobuf:"bytes,3,opt,name=response_end,json=responseEnd,proto3,oneof"` } -type AcceptAndExecuteStreamResponse_Result struct { - Result *Result `protobuf:"bytes,4,opt,name=result,proto3,oneof"` +type AcceptAndExecuteStreamResponse_Complete struct { + Complete *Complete `protobuf:"bytes,4,opt,name=complete,proto3,oneof"` } -func (*AcceptAndExecuteStreamResponse_ResultBegin) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_ResponseBegin) isAcceptAndExecuteStreamResponse_Event() {} -func (*AcceptAndExecuteStreamResponse_ResultChunk) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_ResponseChunk) isAcceptAndExecuteStreamResponse_Event() {} -func (*AcceptAndExecuteStreamResponse_ResultEnd) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_ResponseEnd) isAcceptAndExecuteStreamResponse_Event() {} -func (*AcceptAndExecuteStreamResponse_Result) isAcceptAndExecuteStreamResponse_Event() {} +func (*AcceptAndExecuteStreamResponse_Complete) isAcceptAndExecuteStreamResponse_Event() {} -// ResultStreamBegin marks the start of the decoded result stream. -type ResultStreamBegin struct { +// ResponseStreamBegin marks the start of the decoded response stream. +type ResponseStreamBegin struct { state protoimpl.MessageState `protogen:"open.v1"` - // Content type of the decoded result stream bytes. + // Content type of the decoded response stream bytes. ContentType string `protobuf:"bytes,1,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` - // Content encoding of the decoded result stream bytes. + // Content encoding of the decoded response stream bytes. ContentEncoding string `protobuf:"bytes,2,opt,name=content_encoding,json=contentEncoding,proto3" json:"content_encoding,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ResultStreamBegin) Reset() { - *x = ResultStreamBegin{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] +func (x *ResponseStreamBegin) Reset() { + *x = ResponseStreamBegin{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ResultStreamBegin) String() string { +func (x *ResponseStreamBegin) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResultStreamBegin) ProtoMessage() {} +func (*ResponseStreamBegin) ProtoMessage() {} -func (x *ResultStreamBegin) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] +func (x *ResponseStreamBegin) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1495,48 +1261,48 @@ func (x *ResultStreamBegin) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResultStreamBegin.ProtoReflect.Descriptor instead. -func (*ResultStreamBegin) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{20} +// Deprecated: Use ResponseStreamBegin.ProtoReflect.Descriptor instead. +func (*ResponseStreamBegin) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{16} } -func (x *ResultStreamBegin) GetContentType() string { +func (x *ResponseStreamBegin) GetContentType() string { if x != nil { return x.ContentType } return "" } -func (x *ResultStreamBegin) GetContentEncoding() string { +func (x *ResponseStreamBegin) GetContentEncoding() string { if x != nil { return x.ContentEncoding } return "" } -// ResultStreamChunk carries a contiguous slice of decoded result stream bytes. -type ResultStreamChunk struct { +// ResponseStreamChunk carries a contiguous slice of decoded response stream bytes. +type ResponseStreamChunk struct { state protoimpl.MessageState `protogen:"open.v1"` Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ResultStreamChunk) Reset() { - *x = ResultStreamChunk{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[21] +func (x *ResponseStreamChunk) Reset() { + *x = ResponseStreamChunk{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ResultStreamChunk) String() string { +func (x *ResponseStreamChunk) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResultStreamChunk) ProtoMessage() {} +func (*ResponseStreamChunk) ProtoMessage() {} -func (x *ResultStreamChunk) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[21] +func (x *ResponseStreamChunk) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1547,43 +1313,43 @@ func (x *ResultStreamChunk) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResultStreamChunk.ProtoReflect.Descriptor instead. -func (*ResultStreamChunk) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{21} +// Deprecated: Use ResponseStreamChunk.ProtoReflect.Descriptor instead. +func (*ResponseStreamChunk) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{17} } -func (x *ResultStreamChunk) GetData() []byte { +func (x *ResponseStreamChunk) GetData() []byte { if x != nil { return x.Data } return nil } -// ResultStreamEnd marks the end of the decoded result stream and contains the +// ResponseStreamEnd marks the end of the decoded response stream and contains the // validated stream metadata. -type ResultStreamEnd struct { +type ResponseStreamEnd struct { state protoimpl.MessageState `protogen:"open.v1"` - ResultHash []byte `protobuf:"bytes,1,opt,name=result_hash,json=resultHash,proto3" json:"result_hash,omitempty"` - ResultLen uint64 `protobuf:"varint,2,opt,name=result_len,json=resultLen,proto3" json:"result_len,omitempty"` + ResponseHash []byte `protobuf:"bytes,1,opt,name=response_hash,json=responseHash,proto3" json:"response_hash,omitempty"` + ResponseLen uint64 `protobuf:"varint,2,opt,name=response_len,json=responseLen,proto3" json:"response_len,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ResultStreamEnd) Reset() { - *x = ResultStreamEnd{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[22] +func (x *ResponseStreamEnd) Reset() { + *x = ResponseStreamEnd{} + mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ResultStreamEnd) String() string { +func (x *ResponseStreamEnd) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResultStreamEnd) ProtoMessage() {} +func (*ResponseStreamEnd) ProtoMessage() {} -func (x *ResultStreamEnd) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[22] +func (x *ResponseStreamEnd) ProtoReflect() protoreflect.Message { + mi := &file_lcpd_v1_lcpd_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1594,21 +1360,21 @@ func (x *ResultStreamEnd) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResultStreamEnd.ProtoReflect.Descriptor instead. -func (*ResultStreamEnd) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{22} +// Deprecated: Use ResponseStreamEnd.ProtoReflect.Descriptor instead. +func (*ResponseStreamEnd) Descriptor() ([]byte, []int) { + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{18} } -func (x *ResultStreamEnd) GetResultHash() []byte { +func (x *ResponseStreamEnd) GetResponseHash() []byte { if x != nil { - return x.ResultHash + return x.ResponseHash } return nil } -func (x *ResultStreamEnd) GetResultLen() uint64 { +func (x *ResponseStreamEnd) GetResponseLen() uint64 { if x != nil { - return x.ResultLen + return x.ResponseLen } return 0 } @@ -1618,8 +1384,8 @@ type CancelJobRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The target peer's Node ID. PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` - // The job_id to cancel. - JobId []byte `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + // The call_id to cancel. + CallId []byte `protobuf:"bytes,2,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // Optional human-readable cancellation reason. Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` unknownFields protoimpl.UnknownFields @@ -1628,7 +1394,7 @@ type CancelJobRequest struct { func (x *CancelJobRequest) Reset() { *x = CancelJobRequest{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[23] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1640,7 +1406,7 @@ func (x *CancelJobRequest) String() string { func (*CancelJobRequest) ProtoMessage() {} func (x *CancelJobRequest) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[23] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1653,7 +1419,7 @@ func (x *CancelJobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelJobRequest.ProtoReflect.Descriptor instead. func (*CancelJobRequest) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{23} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{19} } func (x *CancelJobRequest) GetPeerId() string { @@ -1663,9 +1429,9 @@ func (x *CancelJobRequest) GetPeerId() string { return "" } -func (x *CancelJobRequest) GetJobId() []byte { +func (x *CancelJobRequest) GetCallId() []byte { if x != nil { - return x.JobId + return x.CallId } return nil } @@ -1687,7 +1453,7 @@ type CancelJobResponse struct { func (x *CancelJobResponse) Reset() { *x = CancelJobResponse{} - mi := &file_lcpd_v1_lcpd_proto_msgTypes[24] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1699,7 +1465,7 @@ func (x *CancelJobResponse) String() string { func (*CancelJobResponse) ProtoMessage() {} func (x *CancelJobResponse) ProtoReflect() protoreflect.Message { - mi := &file_lcpd_v1_lcpd_proto_msgTypes[24] + mi := &file_lcpd_v1_lcpd_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1712,7 +1478,7 @@ func (x *CancelJobResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelJobResponse.ProtoReflect.Descriptor instead. func (*CancelJobResponse) Descriptor() ([]byte, []int) { - return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{24} + return file_lcpd_v1_lcpd_proto_rawDescGZIP(), []int{20} } func (x *CancelJobResponse) GetSuccess() bool { @@ -1732,291 +1498,294 @@ var file_lcpd_v1_lcpd_proto_rawDesc = string([]byte{ 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x01, 0x0a, - 0x04, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x67, 0x0a, 0x1a, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x5f, - 0x63, 0x68, 0x61, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x5f, 0x76, 0x31, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6c, 0x63, 0x70, 0x64, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x54, 0x61, 0x73, 0x6b, 0x53, - 0x70, 0x65, 0x63, 0x48, 0x00, 0x52, 0x17, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x43, 0x68, 0x61, - 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x12, 0x54, - 0x0a, 0x13, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x73, 0x5f, 0x76, 0x31, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6c, 0x63, - 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x70, 0x65, 0x63, 0x48, - 0x00, 0x52, 0x11, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x73, 0x56, 0x31, 0x42, 0x0d, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x05, 0xba, 0x48, - 0x02, 0x08, 0x01, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x08, 0x6c, 0x6c, 0x6d, 0x5f, 0x63, - 0x68, 0x61, 0x74, 0x22, 0x9b, 0x01, 0x0a, 0x1f, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, - 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x54, - 0x61, 0x73, 0x6b, 0x53, 0x70, 0x65, 0x63, 0x12, 0x2d, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, - 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x49, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42, 0x09, - 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, - 0x73, 0x22, 0x41, 0x0a, 0x1d, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x73, 0x12, 0x20, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6d, - 0x6f, 0x64, 0x65, 0x6c, 0x22, 0x8f, 0x01, 0x0a, 0x19, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x70, - 0x65, 0x63, 0x12, 0x2d, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6a, 0x73, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, - 0x7a, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4a, 0x73, 0x6f, - 0x6e, 0x12, 0x43, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x6e, - 0x41, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x73, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x22, 0x3b, 0x0a, 0x17, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x73, 0x12, 0x20, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6d, 0x6f, - 0x64, 0x65, 0x6c, 0x22, 0xad, 0x02, 0x0a, 0x05, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x32, 0x0a, - 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x07, 0xba, 0x48, 0x04, 0x2a, 0x02, 0x08, 0x02, - 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x1e, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, - 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x61, 0x74, - 0x12, 0x57, 0x0a, 0x0c, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x42, 0x18, 0xba, 0x48, 0x15, 0xc8, 0x01, 0x01, 0xea, 0x01, 0x0f, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x52, 0x0b, 0x71, 0x75, - 0x6f, 0x74, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x26, 0x0a, 0x0a, 0x74, 0x65, 0x72, - 0x6d, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, - 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x09, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x48, 0x61, 0x73, - 0x68, 0x12, 0x30, 0x0a, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, - 0x02, 0x10, 0x01, 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0xe3, 0x02, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x36, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, - 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x28, - 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x18, 0x20, 0x52, 0x0a, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, - 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9f, 0x02, 0x0a, + 0x08, 0x43, 0x61, 0x6c, 0x6c, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1f, 0x0a, 0x06, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, + 0x10, 0x01, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, + 0x10, 0x01, 0x52, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x39, 0x0a, 0x14, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x12, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x41, 0x0a, 0x18, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x16, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0xf3, + 0x03, 0x0a, 0x05, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0d, 0x42, 0x07, 0xba, 0x48, 0x04, 0x2a, 0x02, 0x08, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x07, + 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x09, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x57, 0x0a, + 0x0c, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, + 0x18, 0xba, 0x48, 0x15, 0xc8, 0x01, 0x01, 0xea, 0x01, 0x0f, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, + 0x61, 0x6e, 0x6f, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x30, 0x52, 0x0b, 0x71, 0x75, 0x6f, 0x74, 0x65, + 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x26, 0x0a, 0x0a, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x5f, + 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, + 0x02, 0x68, 0x20, 0x52, 0x09, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x30, + 0x0a, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, + 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x40, 0x0a, 0x15, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x13, 0x72, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x48, 0x0a, 0x19, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x01, + 0x52, 0x17, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x42, 0x18, 0x0a, 0x16, + 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x1c, 0x0a, 0x1a, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, + 0x64, 0x69, 0x6e, 0x67, 0x22, 0xf4, 0x04, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x18, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x06, 0xba, 0x48, 0x03, + 0xc8, 0x01, 0x01, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x79, 0x74, + 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x48, 0x00, 0x52, 0x10, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x31, + 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x48, 0x01, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x61, 0x73, 0x68, 0x88, 0x01, + 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x6c, 0x65, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x48, 0x02, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x4c, 0x65, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x15, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, + 0x01, 0x48, 0x03, 0x52, 0x13, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x19, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, + 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x04, 0x52, 0x17, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, + 0x6e, 0x67, 0x88, 0x01, 0x01, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x58, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x4b, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, - 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4c, 0x43, - 0x50, 0x50, 0x65, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, - 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, - 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xe0, 0x41, - 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x12, 0x42, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, 0x70, - 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0xd4, 0x02, 0x0a, 0x0b, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, - 0x07, 0xba, 0x48, 0x04, 0x2a, 0x02, 0x08, 0x02, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x11, 0x6d, 0x61, 0x78, - 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x0d, 0x42, 0x0b, 0xba, 0x48, 0x08, 0x2a, 0x06, 0x18, 0xff, 0xff, 0x03, 0x20, - 0x00, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x31, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x32, 0x02, 0x20, 0x00, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x6a, 0x6f, 0x62, - 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x32, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x4a, 0x6f, 0x62, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x35, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x66, 0x6c, 0x69, 0x67, - 0x68, 0x74, 0x5f, 0x6a, 0x6f, 0x62, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x09, 0xba, - 0x48, 0x06, 0x2a, 0x04, 0x18, 0xff, 0xff, 0x03, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x49, 0x6e, 0x66, - 0x6c, 0x69, 0x67, 0x68, 0x74, 0x4a, 0x6f, 0x62, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x73, 0x75, 0x70, - 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x0c, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, - 0x54, 0x61, 0x73, 0x6b, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x73, 0x75, - 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x22, 0xa8, 0x02, 0x0a, - 0x0f, 0x4c, 0x43, 0x50, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x12, 0x30, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, - 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x54, 0x61, 0x73, 0x6b, - 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x6b, 0x69, - 0x6e, 0x64, 0x12, 0x65, 0x0a, 0x1a, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x5f, 0x63, 0x68, 0x61, - 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x76, 0x31, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x48, 0x00, - 0x52, 0x17, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x69, 0x43, 0x68, 0x61, 0x74, 0x43, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x56, 0x31, 0x12, 0x52, 0x0a, 0x13, 0x6f, 0x70, 0x65, - 0x6e, 0x61, 0x69, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x5f, 0x76, 0x31, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, - 0x56, 0x31, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x48, 0x00, 0x52, 0x11, 0x6f, 0x70, 0x65, 0x6e, - 0x61, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x56, 0x31, 0x42, 0x18, 0x0a, - 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x08, 0x6c, - 0x6c, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x74, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4c, - 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, - 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x05, 0x70, 0x65, - 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x89, 0x01, 0x0a, 0x14, 0x47, - 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, - 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, - 0x24, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, - 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x42, 0x09, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x6d, 0x61, - 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x79, 0x0a, 0x13, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, - 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, - 0xe0, 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, - 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x73, 0x6b, - 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x74, 0x61, 0x73, - 0x6b, 0x22, 0x7d, 0x0a, 0x14, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, - 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, - 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, - 0x2f, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x42, 0x09, - 0xe0, 0x41, 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, - 0x22, 0x9e, 0x01, 0x0a, 0x17, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, - 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, - 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, - 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, - 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, + 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, + 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x61, + 0x73, 0x68, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, + 0x6c, 0x65, 0x6e, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x1c, 0x0a, + 0x1a, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0xd7, 0x02, 0x0a, 0x10, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, + 0x12, 0x1f, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x32, 0x0a, 0x15, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x13, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x08, 0x64, + 0x6f, 0x63, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x64, 0x6f, 0x63, 0x73, 0x55, 0x72, + 0x69, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x0b, 0x64, 0x6f, 0x63, 0x73, 0x5f, 0x73, 0x68, 0x61, + 0x32, 0x35, 0x36, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, + 0x68, 0x20, 0x48, 0x01, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x73, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, + 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6e, 0x6f, + 0x74, 0x69, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, + 0x02, 0x10, 0x01, 0x48, 0x02, 0x52, 0x0c, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4e, 0x6f, 0x74, + 0x69, 0x63, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x6f, 0x63, 0x73, 0x5f, + 0x75, 0x72, 0x69, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x6f, 0x63, 0x73, 0x5f, 0x73, 0x68, 0x61, + 0x32, 0x35, 0x36, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6e, + 0x6f, 0x74, 0x69, 0x63, 0x65, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, + 0x72, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, + 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, + 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, + 0x72, 0x02, 0x10, 0x01, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x42, 0x0a, + 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x42, 0x03, 0xe0, 0x41, + 0x03, 0x52, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x22, 0xdd, 0x02, 0x0a, 0x0b, 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x12, 0x32, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x07, 0xba, 0x48, 0x04, + 0x2a, 0x02, 0x08, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, + 0x42, 0x0b, 0xba, 0x48, 0x08, 0x2a, 0x06, 0x18, 0xff, 0xff, 0x03, 0x20, 0x00, 0x52, 0x0f, 0x6d, + 0x61, 0x78, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x31, + 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, 0x04, 0x32, 0x02, 0x20, + 0x00, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x12, 0x2d, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x42, 0x07, 0xba, 0x48, 0x04, 0x32, 0x02, + 0x20, 0x00, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x43, 0x61, 0x6c, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x37, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x66, 0x6c, 0x69, 0x67, 0x68, 0x74, + 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x09, 0xba, 0x48, + 0x06, 0x2a, 0x04, 0x18, 0xff, 0xff, 0x03, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x49, 0x6e, 0x66, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x43, 0x61, 0x6c, 0x6c, 0x73, 0x12, 0x46, 0x0a, 0x11, 0x73, 0x75, 0x70, + 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x0c, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, + 0x10, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, + 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2b, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, + 0x72, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a, + 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x89, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, + 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, + 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, + 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x6e, 0x6f, 0x64, + 0x65, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x43, 0x50, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x42, 0x09, 0xe0, 0x41, 0x03, + 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x22, 0x7d, 0x0a, 0x13, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x15, + 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, + 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x30, 0x0a, + 0x04, 0x63, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x63, + 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x53, 0x70, 0x65, 0x63, 0x42, 0x09, + 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x04, 0x63, 0x61, 0x6c, 0x6c, 0x22, + 0x7d, 0x0a, 0x14, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x15, + 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, + 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, + 0x05, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, + 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x42, 0x09, 0xe0, 0x41, + 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x05, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x22, 0xa0, + 0x01, 0x0a, 0x17, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, + 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, + 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x23, 0x0a, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, + 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x70, 0x61, 0x79, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x1d, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, + 0x65, 0x22, 0xa6, 0x01, 0x0a, 0x1d, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, - 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6a, 0x6f, 0x62, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, - 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x0b, - 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x70, 0x61, - 0x79, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x22, 0x4e, 0x0a, 0x18, 0x41, 0x63, 0x63, 0x65, - 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0x09, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, - 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x98, 0x02, 0x0a, 0x1e, 0x41, 0x63, 0x63, - 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0c, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x48, 0x00, 0x52, - 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x3f, 0x0a, 0x0c, - 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x48, 0x00, - 0x52, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x39, 0x0a, - 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x09, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x45, 0x6e, 0x64, 0x12, 0x29, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x42, 0x0e, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x05, 0xba, 0x48, - 0x02, 0x08, 0x01, 0x22, 0x79, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x2d, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, - 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x35, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0f, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x33, - 0x0a, 0x11, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x68, - 0x75, 0x6e, 0x6b, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x04, 0x64, - 0x61, 0x74, 0x61, 0x22, 0x62, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x45, 0x6e, 0x64, 0x12, 0x2b, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, - 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, - 0x61, 0x73, 0x68, 0x12, 0x22, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x6c, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x09, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x22, 0x8d, 0x01, 0x0a, 0x10, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, - 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, - 0x41, 0x02, 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, - 0x66, 0x41, 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x21, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x05, - 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x63, 0x61, 0x6c, + 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, + 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x2a, + 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x42, 0x09, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, + 0x70, 0x61, 0x79, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x22, 0x54, 0x0a, 0x18, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x09, 0xe0, 0x41, 0x03, + 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x22, 0xb0, 0x02, 0x0a, 0x1e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, + 0x62, 0x65, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x63, + 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x45, 0x0a, 0x0e, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x68, 0x75, 0x6e, 0x6b, + 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x68, 0x75, 0x6e, + 0x6b, 0x12, 0x3f, 0x0a, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x65, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x45, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, + 0x6e, 0x64, 0x12, 0x2f, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x42, 0x0e, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x05, 0xba, 0x48, + 0x02, 0x08, 0x01, 0x22, 0x7b, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x2d, 0x0a, 0x0c, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x35, 0x0a, 0x10, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, + 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, + 0x22, 0x35, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x10, + 0x01, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x6c, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x6e, 0x64, 0x12, 0x2f, 0x0a, 0x0d, + 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x03, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, + 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x26, 0x0a, + 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x42, 0x03, 0xe0, 0x41, 0x03, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x4c, 0x65, 0x6e, 0x22, 0x8f, 0x01, 0x0a, 0x10, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x70, 0x65, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1b, 0xe0, 0x41, 0x02, + 0xba, 0x48, 0x15, 0x72, 0x13, 0x32, 0x11, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, + 0x2d, 0x46, 0x5d, 0x7b, 0x36, 0x36, 0x7d, 0x24, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x23, 0x0a, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x42, 0x0a, 0xe0, 0x41, 0x02, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x06, 0x63, + 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x02, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x32, 0x0a, 0x11, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x03, 0xe0, - 0x41, 0x03, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x2a, 0x9f, 0x01, 0x0a, 0x0b, - 0x4c, 0x43, 0x50, 0x54, 0x61, 0x73, 0x6b, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1d, 0x0a, 0x19, 0x4c, - 0x43, 0x50, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2c, 0x0a, 0x28, 0x4c, 0x43, - 0x50, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4f, 0x50, 0x45, 0x4e, - 0x41, 0x49, 0x5f, 0x43, 0x48, 0x41, 0x54, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x49, - 0x4f, 0x4e, 0x53, 0x5f, 0x56, 0x31, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x4c, 0x43, 0x50, 0x5f, - 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x41, 0x49, - 0x5f, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, 0x53, 0x5f, 0x56, 0x31, 0x10, 0x03, 0x22, - 0x04, 0x08, 0x01, 0x10, 0x01, 0x2a, 0x16, 0x4c, 0x43, 0x50, 0x5f, 0x54, 0x41, 0x53, 0x4b, 0x5f, - 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4c, 0x4c, 0x4d, 0x5f, 0x43, 0x48, 0x41, 0x54, 0x32, 0xfe, 0x03, - 0x0a, 0x0b, 0x4c, 0x43, 0x50, 0x44, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4b, 0x0a, - 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, - 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, + 0x41, 0x03, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x32, 0xfe, 0x03, 0x0a, 0x0b, + 0x4c, 0x43, 0x50, 0x44, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, + 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x47, 0x65, - 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, - 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, - 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, + 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x43, 0x50, 0x50, 0x65, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x4c, + 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, - 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x57, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, + 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6c, 0x63, 0x70, - 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, - 0x16, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x27, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, - 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x43, 0x61, - 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x19, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, - 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, - 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x72, 0x75, - 0x77, 0x62, 0x69, 0x72, 0x64, 0x2f, 0x6c, 0x63, 0x70, 0x2f, 0x67, 0x6f, 0x2d, 0x6c, 0x63, 0x70, - 0x64, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x6c, 0x63, 0x70, 0x64, 0x2f, 0x76, 0x31, - 0x3b, 0x6c, 0x63, 0x70, 0x64, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x16, 0x41, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x26, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, + 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6e, + 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x19, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x6c, 0x63, 0x70, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x72, 0x75, 0x77, 0x62, + 0x69, 0x72, 0x64, 0x2f, 0x6c, 0x63, 0x70, 0x2f, 0x67, 0x6f, 0x2d, 0x6c, 0x63, 0x70, 0x64, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x6c, 0x63, 0x70, 0x64, 0x2f, 0x76, 0x31, 0x3b, 0x6c, + 0x63, 0x70, 0x64, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -2031,76 +1800,64 @@ func file_lcpd_v1_lcpd_proto_rawDescGZIP() []byte { return file_lcpd_v1_lcpd_proto_rawDescData } -var file_lcpd_v1_lcpd_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_lcpd_v1_lcpd_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_lcpd_v1_lcpd_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_lcpd_v1_lcpd_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_lcpd_v1_lcpd_proto_goTypes = []any{ - (LCPTaskKind)(0), // 0: lcpd.v1.LCPTaskKind - (Result_Status)(0), // 1: lcpd.v1.Result.Status - (*Task)(nil), // 2: lcpd.v1.Task - (*OpenAIChatCompletionsV1TaskSpec)(nil), // 3: lcpd.v1.OpenAIChatCompletionsV1TaskSpec - (*OpenAIChatCompletionsV1Params)(nil), // 4: lcpd.v1.OpenAIChatCompletionsV1Params - (*OpenAIResponsesV1TaskSpec)(nil), // 5: lcpd.v1.OpenAIResponsesV1TaskSpec - (*OpenAIResponsesV1Params)(nil), // 6: lcpd.v1.OpenAIResponsesV1Params - (*Terms)(nil), // 7: lcpd.v1.Terms - (*Result)(nil), // 8: lcpd.v1.Result - (*LCPPeer)(nil), // 9: lcpd.v1.LCPPeer - (*LCPManifest)(nil), // 10: lcpd.v1.LCPManifest - (*LCPTaskTemplate)(nil), // 11: lcpd.v1.LCPTaskTemplate - (*ListLCPPeersRequest)(nil), // 12: lcpd.v1.ListLCPPeersRequest - (*ListLCPPeersResponse)(nil), // 13: lcpd.v1.ListLCPPeersResponse - (*GetLocalInfoRequest)(nil), // 14: lcpd.v1.GetLocalInfoRequest - (*GetLocalInfoResponse)(nil), // 15: lcpd.v1.GetLocalInfoResponse - (*RequestQuoteRequest)(nil), // 16: lcpd.v1.RequestQuoteRequest - (*RequestQuoteResponse)(nil), // 17: lcpd.v1.RequestQuoteResponse - (*AcceptAndExecuteRequest)(nil), // 18: lcpd.v1.AcceptAndExecuteRequest - (*AcceptAndExecuteStreamRequest)(nil), // 19: lcpd.v1.AcceptAndExecuteStreamRequest - (*AcceptAndExecuteResponse)(nil), // 20: lcpd.v1.AcceptAndExecuteResponse - (*AcceptAndExecuteStreamResponse)(nil), // 21: lcpd.v1.AcceptAndExecuteStreamResponse - (*ResultStreamBegin)(nil), // 22: lcpd.v1.ResultStreamBegin - (*ResultStreamChunk)(nil), // 23: lcpd.v1.ResultStreamChunk - (*ResultStreamEnd)(nil), // 24: lcpd.v1.ResultStreamEnd - (*CancelJobRequest)(nil), // 25: lcpd.v1.CancelJobRequest - (*CancelJobResponse)(nil), // 26: lcpd.v1.CancelJobResponse - (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp + (Complete_Status)(0), // 0: lcpd.v1.Complete.Status + (*CallSpec)(nil), // 1: lcpd.v1.CallSpec + (*Quote)(nil), // 2: lcpd.v1.Quote + (*Complete)(nil), // 3: lcpd.v1.Complete + (*MethodDescriptor)(nil), // 4: lcpd.v1.MethodDescriptor + (*LCPPeer)(nil), // 5: lcpd.v1.LCPPeer + (*LCPManifest)(nil), // 6: lcpd.v1.LCPManifest + (*ListLCPPeersRequest)(nil), // 7: lcpd.v1.ListLCPPeersRequest + (*ListLCPPeersResponse)(nil), // 8: lcpd.v1.ListLCPPeersResponse + (*GetLocalInfoRequest)(nil), // 9: lcpd.v1.GetLocalInfoRequest + (*GetLocalInfoResponse)(nil), // 10: lcpd.v1.GetLocalInfoResponse + (*RequestQuoteRequest)(nil), // 11: lcpd.v1.RequestQuoteRequest + (*RequestQuoteResponse)(nil), // 12: lcpd.v1.RequestQuoteResponse + (*AcceptAndExecuteRequest)(nil), // 13: lcpd.v1.AcceptAndExecuteRequest + (*AcceptAndExecuteStreamRequest)(nil), // 14: lcpd.v1.AcceptAndExecuteStreamRequest + (*AcceptAndExecuteResponse)(nil), // 15: lcpd.v1.AcceptAndExecuteResponse + (*AcceptAndExecuteStreamResponse)(nil), // 16: lcpd.v1.AcceptAndExecuteStreamResponse + (*ResponseStreamBegin)(nil), // 17: lcpd.v1.ResponseStreamBegin + (*ResponseStreamChunk)(nil), // 18: lcpd.v1.ResponseStreamChunk + (*ResponseStreamEnd)(nil), // 19: lcpd.v1.ResponseStreamEnd + (*CancelJobRequest)(nil), // 20: lcpd.v1.CancelJobRequest + (*CancelJobResponse)(nil), // 21: lcpd.v1.CancelJobResponse + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp } var file_lcpd_v1_lcpd_proto_depIdxs = []int32{ - 3, // 0: lcpd.v1.Task.openai_chat_completions_v1:type_name -> lcpd.v1.OpenAIChatCompletionsV1TaskSpec - 5, // 1: lcpd.v1.Task.openai_responses_v1:type_name -> lcpd.v1.OpenAIResponsesV1TaskSpec - 4, // 2: lcpd.v1.OpenAIChatCompletionsV1TaskSpec.params:type_name -> lcpd.v1.OpenAIChatCompletionsV1Params - 6, // 3: lcpd.v1.OpenAIResponsesV1TaskSpec.params:type_name -> lcpd.v1.OpenAIResponsesV1Params - 27, // 4: lcpd.v1.Terms.quote_expiry:type_name -> google.protobuf.Timestamp - 1, // 5: lcpd.v1.Result.status:type_name -> lcpd.v1.Result.Status - 10, // 6: lcpd.v1.LCPPeer.remote_manifest:type_name -> lcpd.v1.LCPManifest - 11, // 7: lcpd.v1.LCPManifest.supported_tasks:type_name -> lcpd.v1.LCPTaskTemplate - 0, // 8: lcpd.v1.LCPTaskTemplate.kind:type_name -> lcpd.v1.LCPTaskKind - 4, // 9: lcpd.v1.LCPTaskTemplate.openai_chat_completions_v1:type_name -> lcpd.v1.OpenAIChatCompletionsV1Params - 6, // 10: lcpd.v1.LCPTaskTemplate.openai_responses_v1:type_name -> lcpd.v1.OpenAIResponsesV1Params - 9, // 11: lcpd.v1.ListLCPPeersResponse.peers:type_name -> lcpd.v1.LCPPeer - 10, // 12: lcpd.v1.GetLocalInfoResponse.manifest:type_name -> lcpd.v1.LCPManifest - 2, // 13: lcpd.v1.RequestQuoteRequest.task:type_name -> lcpd.v1.Task - 7, // 14: lcpd.v1.RequestQuoteResponse.terms:type_name -> lcpd.v1.Terms - 8, // 15: lcpd.v1.AcceptAndExecuteResponse.result:type_name -> lcpd.v1.Result - 22, // 16: lcpd.v1.AcceptAndExecuteStreamResponse.result_begin:type_name -> lcpd.v1.ResultStreamBegin - 23, // 17: lcpd.v1.AcceptAndExecuteStreamResponse.result_chunk:type_name -> lcpd.v1.ResultStreamChunk - 24, // 18: lcpd.v1.AcceptAndExecuteStreamResponse.result_end:type_name -> lcpd.v1.ResultStreamEnd - 8, // 19: lcpd.v1.AcceptAndExecuteStreamResponse.result:type_name -> lcpd.v1.Result - 12, // 20: lcpd.v1.LCPDService.ListLCPPeers:input_type -> lcpd.v1.ListLCPPeersRequest - 14, // 21: lcpd.v1.LCPDService.GetLocalInfo:input_type -> lcpd.v1.GetLocalInfoRequest - 16, // 22: lcpd.v1.LCPDService.RequestQuote:input_type -> lcpd.v1.RequestQuoteRequest - 18, // 23: lcpd.v1.LCPDService.AcceptAndExecute:input_type -> lcpd.v1.AcceptAndExecuteRequest - 19, // 24: lcpd.v1.LCPDService.AcceptAndExecuteStream:input_type -> lcpd.v1.AcceptAndExecuteStreamRequest - 25, // 25: lcpd.v1.LCPDService.CancelJob:input_type -> lcpd.v1.CancelJobRequest - 13, // 26: lcpd.v1.LCPDService.ListLCPPeers:output_type -> lcpd.v1.ListLCPPeersResponse - 15, // 27: lcpd.v1.LCPDService.GetLocalInfo:output_type -> lcpd.v1.GetLocalInfoResponse - 17, // 28: lcpd.v1.LCPDService.RequestQuote:output_type -> lcpd.v1.RequestQuoteResponse - 20, // 29: lcpd.v1.LCPDService.AcceptAndExecute:output_type -> lcpd.v1.AcceptAndExecuteResponse - 21, // 30: lcpd.v1.LCPDService.AcceptAndExecuteStream:output_type -> lcpd.v1.AcceptAndExecuteStreamResponse - 26, // 31: lcpd.v1.LCPDService.CancelJob:output_type -> lcpd.v1.CancelJobResponse - 26, // [26:32] is the sub-list for method output_type - 20, // [20:26] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 22, // 0: lcpd.v1.Quote.quote_expiry:type_name -> google.protobuf.Timestamp + 0, // 1: lcpd.v1.Complete.status:type_name -> lcpd.v1.Complete.Status + 6, // 2: lcpd.v1.LCPPeer.remote_manifest:type_name -> lcpd.v1.LCPManifest + 4, // 3: lcpd.v1.LCPManifest.supported_methods:type_name -> lcpd.v1.MethodDescriptor + 5, // 4: lcpd.v1.ListLCPPeersResponse.peers:type_name -> lcpd.v1.LCPPeer + 6, // 5: lcpd.v1.GetLocalInfoResponse.manifest:type_name -> lcpd.v1.LCPManifest + 1, // 6: lcpd.v1.RequestQuoteRequest.call:type_name -> lcpd.v1.CallSpec + 2, // 7: lcpd.v1.RequestQuoteResponse.quote:type_name -> lcpd.v1.Quote + 3, // 8: lcpd.v1.AcceptAndExecuteResponse.complete:type_name -> lcpd.v1.Complete + 17, // 9: lcpd.v1.AcceptAndExecuteStreamResponse.response_begin:type_name -> lcpd.v1.ResponseStreamBegin + 18, // 10: lcpd.v1.AcceptAndExecuteStreamResponse.response_chunk:type_name -> lcpd.v1.ResponseStreamChunk + 19, // 11: lcpd.v1.AcceptAndExecuteStreamResponse.response_end:type_name -> lcpd.v1.ResponseStreamEnd + 3, // 12: lcpd.v1.AcceptAndExecuteStreamResponse.complete:type_name -> lcpd.v1.Complete + 7, // 13: lcpd.v1.LCPDService.ListLCPPeers:input_type -> lcpd.v1.ListLCPPeersRequest + 9, // 14: lcpd.v1.LCPDService.GetLocalInfo:input_type -> lcpd.v1.GetLocalInfoRequest + 11, // 15: lcpd.v1.LCPDService.RequestQuote:input_type -> lcpd.v1.RequestQuoteRequest + 13, // 16: lcpd.v1.LCPDService.AcceptAndExecute:input_type -> lcpd.v1.AcceptAndExecuteRequest + 14, // 17: lcpd.v1.LCPDService.AcceptAndExecuteStream:input_type -> lcpd.v1.AcceptAndExecuteStreamRequest + 20, // 18: lcpd.v1.LCPDService.CancelJob:input_type -> lcpd.v1.CancelJobRequest + 8, // 19: lcpd.v1.LCPDService.ListLCPPeers:output_type -> lcpd.v1.ListLCPPeersResponse + 10, // 20: lcpd.v1.LCPDService.GetLocalInfo:output_type -> lcpd.v1.GetLocalInfoResponse + 12, // 21: lcpd.v1.LCPDService.RequestQuote:output_type -> lcpd.v1.RequestQuoteResponse + 15, // 22: lcpd.v1.LCPDService.AcceptAndExecute:output_type -> lcpd.v1.AcceptAndExecuteResponse + 16, // 23: lcpd.v1.LCPDService.AcceptAndExecuteStream:output_type -> lcpd.v1.AcceptAndExecuteStreamResponse + 21, // 24: lcpd.v1.LCPDService.CancelJob:output_type -> lcpd.v1.CancelJobResponse + 19, // [19:25] is the sub-list for method output_type + 13, // [13:19] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_lcpd_v1_lcpd_proto_init() } @@ -2108,27 +1865,22 @@ func file_lcpd_v1_lcpd_proto_init() { if File_lcpd_v1_lcpd_proto != nil { return } - file_lcpd_v1_lcpd_proto_msgTypes[0].OneofWrappers = []any{ - (*Task_OpenaiChatCompletionsV1)(nil), - (*Task_OpenaiResponsesV1)(nil), - } - file_lcpd_v1_lcpd_proto_msgTypes[9].OneofWrappers = []any{ - (*LCPTaskTemplate_OpenaiChatCompletionsV1)(nil), - (*LCPTaskTemplate_OpenaiResponsesV1)(nil), - } - file_lcpd_v1_lcpd_proto_msgTypes[19].OneofWrappers = []any{ - (*AcceptAndExecuteStreamResponse_ResultBegin)(nil), - (*AcceptAndExecuteStreamResponse_ResultChunk)(nil), - (*AcceptAndExecuteStreamResponse_ResultEnd)(nil), - (*AcceptAndExecuteStreamResponse_Result)(nil), + file_lcpd_v1_lcpd_proto_msgTypes[1].OneofWrappers = []any{} + file_lcpd_v1_lcpd_proto_msgTypes[2].OneofWrappers = []any{} + file_lcpd_v1_lcpd_proto_msgTypes[3].OneofWrappers = []any{} + file_lcpd_v1_lcpd_proto_msgTypes[15].OneofWrappers = []any{ + (*AcceptAndExecuteStreamResponse_ResponseBegin)(nil), + (*AcceptAndExecuteStreamResponse_ResponseChunk)(nil), + (*AcceptAndExecuteStreamResponse_ResponseEnd)(nil), + (*AcceptAndExecuteStreamResponse_Complete)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_lcpd_v1_lcpd_proto_rawDesc), len(file_lcpd_v1_lcpd_proto_rawDesc)), - NumEnums: 2, - NumMessages: 25, + NumEnums: 1, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/proto-go/lcpd/v1/lcpd_grpc.pb.go b/proto-go/lcpd/v1/lcpd_grpc.pb.go index 00d9c8e..0551b23 100644 --- a/proto-go/lcpd/v1/lcpd_grpc.pb.go +++ b/proto-go/lcpd/v1/lcpd_grpc.pb.go @@ -13,9 +13,9 @@ // // LCP (Lightning Compute Protocol) is a Lightning L3 overlay protocol that // uses BOLT #1 custom messages carrying TLV streams to negotiate and execute -// compute jobs (quote → payment → execution → result delivery). +// method calls (manifest → call → quote → pay → stream → complete). // -// This API is designed to be compatible with the LCP v0.2 wire protocol +// This API is designed to be compatible with the LCP v0.3 wire protocol // described in `docs/protocol/protocol.md`. package lcpdv1 @@ -70,14 +70,14 @@ type LCPDServiceClient interface { // - INTERNAL: Unexpected server error. // Impact: No local info is returned. GetLocalInfo(ctx context.Context, in *GetLocalInfoRequest, opts ...grpc.CallOption) (*GetLocalInfoResponse, error) - // RequestQuote sends an `lcp_quote_request` to a specific peer. + // RequestQuote sends an `lcp_call` and a request stream to a specific peer. // - // On success, it returns the provider's `lcp_quote_response` as `Terms`, + // On success, it returns the provider's `lcp_quote` as `Quote`, // including a BOLT11 invoice whose `description_hash` MUST equal `terms_hash` // (invoice swapping defense). // Errors: // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id` format, missing - // `task`, or invalid task spec). + // `call`, or invalid call spec). // Impact: No wire message is sent to the peer. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No wire message is sent to the peer. @@ -89,21 +89,21 @@ type LCPDServiceClient interface { // Impact: No quote is returned. // - DEADLINE_EXCEEDED: The peer did not respond before the client/server deadline. // Impact: Quote outcome is unknown to the caller; retry MAY result in a new - // `job_id` and a different invoice unless the implementation provides idempotency. + // `call_id` and a different invoice unless the implementation provides idempotency. // - UNAVAILABLE: Transient transport failure sending/receiving peer messages. // Impact: Quote outcome is unknown; safe retries depend on idempotency. RequestQuote(ctx context.Context, in *RequestQuoteRequest, opts ...grpc.CallOption) (*RequestQuoteResponse, error) // AcceptAndExecute pays the invoice associated with the quote and waits for - // the `lcp_result`. + // the terminal `lcp_complete` and (when available) the validated response stream. // // This is a blocking call. Clients SHOULD set a deadline and MAY cancel via // context cancellation (gRPC) or `CancelJob`. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format), + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format), // or `pay_invoice=false`. // Impact: No payment attempt is made and no execution is started. - // - NOT_FOUND: The daemon has no known quote/payment data for the given `job_id`. + // - NOT_FOUND: The daemon has no known quote/payment data for the given `call_id`. // Impact: No payment attempt is made. // - FAILED_PRECONDITION: The quote is expired, the job is not executable, or the // daemon refuses to pay/execute due to local policy. @@ -119,9 +119,9 @@ type LCPDServiceClient interface { // Impact: Outcome may be unknown if failure happened after payment settlement. AcceptAndExecute(ctx context.Context, in *AcceptAndExecuteRequest, opts ...grpc.CallOption) (*AcceptAndExecuteResponse, error) // AcceptAndExecuteStream pays the invoice associated with the quote and streams - // the decoded result stream bytes as they arrive. + // the decoded response stream bytes as they arrive. // - // The stream ends with a terminal `Result` event that indicates the final job + // The stream ends with a terminal `Complete` event that indicates the final job // status and (when available) the validated stream metadata (hash/len). // // Errors follow the same model as `AcceptAndExecute`, but note that streaming @@ -133,7 +133,7 @@ type LCPDServiceClient interface { // Cancellation is best-effort: the provider may have already completed the job. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format). + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format). // Impact: No cancel message is sent. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No cancel message is sent. @@ -255,14 +255,14 @@ type LCPDServiceServer interface { // - INTERNAL: Unexpected server error. // Impact: No local info is returned. GetLocalInfo(context.Context, *GetLocalInfoRequest) (*GetLocalInfoResponse, error) - // RequestQuote sends an `lcp_quote_request` to a specific peer. + // RequestQuote sends an `lcp_call` and a request stream to a specific peer. // - // On success, it returns the provider's `lcp_quote_response` as `Terms`, + // On success, it returns the provider's `lcp_quote` as `Quote`, // including a BOLT11 invoice whose `description_hash` MUST equal `terms_hash` // (invoice swapping defense). // Errors: // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id` format, missing - // `task`, or invalid task spec). + // `call`, or invalid call spec). // Impact: No wire message is sent to the peer. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No wire message is sent to the peer. @@ -274,21 +274,21 @@ type LCPDServiceServer interface { // Impact: No quote is returned. // - DEADLINE_EXCEEDED: The peer did not respond before the client/server deadline. // Impact: Quote outcome is unknown to the caller; retry MAY result in a new - // `job_id` and a different invoice unless the implementation provides idempotency. + // `call_id` and a different invoice unless the implementation provides idempotency. // - UNAVAILABLE: Transient transport failure sending/receiving peer messages. // Impact: Quote outcome is unknown; safe retries depend on idempotency. RequestQuote(context.Context, *RequestQuoteRequest) (*RequestQuoteResponse, error) // AcceptAndExecute pays the invoice associated with the quote and waits for - // the `lcp_result`. + // the terminal `lcp_complete` and (when available) the validated response stream. // // This is a blocking call. Clients SHOULD set a deadline and MAY cancel via // context cancellation (gRPC) or `CancelJob`. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format), + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format), // or `pay_invoice=false`. // Impact: No payment attempt is made and no execution is started. - // - NOT_FOUND: The daemon has no known quote/payment data for the given `job_id`. + // - NOT_FOUND: The daemon has no known quote/payment data for the given `call_id`. // Impact: No payment attempt is made. // - FAILED_PRECONDITION: The quote is expired, the job is not executable, or the // daemon refuses to pay/execute due to local policy. @@ -304,9 +304,9 @@ type LCPDServiceServer interface { // Impact: Outcome may be unknown if failure happened after payment settlement. AcceptAndExecute(context.Context, *AcceptAndExecuteRequest) (*AcceptAndExecuteResponse, error) // AcceptAndExecuteStream pays the invoice associated with the quote and streams - // the decoded result stream bytes as they arrive. + // the decoded response stream bytes as they arrive. // - // The stream ends with a terminal `Result` event that indicates the final job + // The stream ends with a terminal `Complete` event that indicates the final job // status and (when available) the validated stream metadata (hash/len). // // Errors follow the same model as `AcceptAndExecute`, but note that streaming @@ -318,7 +318,7 @@ type LCPDServiceServer interface { // Cancellation is best-effort: the provider may have already completed the job. // // Errors: - // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`job_id` format). + // - INVALID_ARGUMENT: Request validation failed (e.g. `peer_id`/`call_id` format). // Impact: No cancel message is sent. // - NOT_FOUND: The target peer is unknown to the daemon. // Impact: No cancel message is sent. From 73c9e3bc8f30153eec2f19353aed64797e17b6da Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 9 Jan 2026 16:26:57 +0900 Subject: [PATCH 2/4] go-lcpd: Implement v0.3 call lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the reference implementation to match the v0.3 wire + gRPC model. - Switch requester/provider flows to call → quote → complete. - Add v0.3 lcpwire message types (call/quote/complete, supported_methods). - Update gRPC service, job stores, tools, and tests accordingly. - Remove the legacy typed task helpers (lcptasks). --- go-lcpd/README.md | 5 +- .../grpcservice/lcpd/observability.go | 148 +++--- go-lcpd/internal/grpcservice/lcpd/service.go | 423 +++++++++--------- .../internal/grpcservice/lcpd/service_test.go | 407 +++++++++-------- go-lcpd/internal/inbounddispatch/handler.go | 8 +- .../internal/inbounddispatch/handler_test.go | 18 +- go-lcpd/internal/jobstore/store.go | 26 +- go-lcpd/internal/jobstore/store_test.go | 28 +- go-lcpd/internal/lcpmsgrouter/router.go | 40 +- go-lcpd/internal/lcpmsgrouter/router_test.go | 24 +- go-lcpd/internal/lcptasks/lcptasks.go | 265 ----------- go-lcpd/internal/lcptasks/lcptasks_test.go | 317 ------------- go-lcpd/internal/lcpwire/call.go | 95 ++++ go-lcpd/internal/lcpwire/cancel.go | 4 +- go-lcpd/internal/lcpwire/complete.go | 252 +++++++++++ go-lcpd/internal/lcpwire/envelope.go | 34 +- go-lcpd/internal/lcpwire/error.go | 4 +- go-lcpd/internal/lcpwire/lcpwire_test.go | 63 ++- go-lcpd/internal/lcpwire/manifest.go | 248 +++++++--- go-lcpd/internal/lcpwire/quote.go | 174 +++++++ go-lcpd/internal/lcpwire/quote_request.go | 67 --- go-lcpd/internal/lcpwire/quote_response.go | 109 ----- go-lcpd/internal/lcpwire/result.go | 252 ----------- go-lcpd/internal/lcpwire/stream_begin.go | 8 +- go-lcpd/internal/lcpwire/stream_chunk.go | 4 +- go-lcpd/internal/lcpwire/stream_end.go | 4 +- go-lcpd/internal/lcpwire/string_list.go | 102 +++++ go-lcpd/internal/lcpwire/tlv_helpers.go | 8 +- go-lcpd/internal/lcpwire/tlv_types.go | 6 +- go-lcpd/internal/lcpwire/types.go | 138 +++--- go-lcpd/internal/lndpeermsg/peermsg.go | 12 +- go-lcpd/internal/lndpeermsg/peermsg_test.go | 10 +- .../internal/lndpeermsg/standalone_test.go | 2 +- go-lcpd/internal/protocolcompat/terms.go | 189 ++++---- go-lcpd/internal/protocolcompat/terms_test.go | 32 +- go-lcpd/internal/provider/handler.go | 356 +++++++-------- go-lcpd/internal/provider/handler_test.go | 162 +++---- .../internal/provider/test_helpers_test.go | 32 +- go-lcpd/internal/provider/validation.go | 73 ++- go-lcpd/internal/provider/validation_test.go | 36 +- go-lcpd/internal/requesterjobstore/store.go | 80 ++-- .../internal/requesterjobstore/store_test.go | 146 +++--- go-lcpd/internal/requesterwait/waiter.go | 136 +++--- go-lcpd/internal/requesterwait/waiter_test.go | 72 +-- .../itest/e2e/regtest_requester_grpc_test.go | 112 ++--- go-lcpd/tools/lcpd-grpcd/main.go | 44 +- go-lcpd/tools/lcpd-oneshot/chat.go | 52 ++- go-lcpd/tools/lcpd-oneshot/main.go | 121 +++-- go-lcpd/tools/lcpd-oneshot/main_test.go | 80 ++-- 49 files changed, 2402 insertions(+), 2626 deletions(-) delete mode 100644 go-lcpd/internal/lcptasks/lcptasks.go delete mode 100644 go-lcpd/internal/lcptasks/lcptasks_test.go create mode 100644 go-lcpd/internal/lcpwire/call.go create mode 100644 go-lcpd/internal/lcpwire/complete.go create mode 100644 go-lcpd/internal/lcpwire/quote.go delete mode 100644 go-lcpd/internal/lcpwire/quote_request.go delete mode 100644 go-lcpd/internal/lcpwire/quote_response.go delete mode 100644 go-lcpd/internal/lcpwire/result.go create mode 100644 go-lcpd/internal/lcpwire/string_list.go diff --git a/go-lcpd/README.md b/go-lcpd/README.md index 7882127..4e5a832 100644 --- a/go-lcpd/README.md +++ b/go-lcpd/README.md @@ -1,9 +1,10 @@ # go-lcpd (Lightning Compute Protocol Daemon) -`go-lcpd` is a reference implementation of LCP, an application-layer protocol carried over Lightning for quote → pay → stream(result) → `lcp_result`. +`go-lcpd` is a reference implementation of LCP, an application-layer protocol carried over Lightning for +manifest → call → quote → pay → stream(response) → `lcp_complete`. It provides: -- `tools/lcpd-grpcd`: a gRPC daemon that speaks the LCP v0.2 wire protocol over BOLT #1 custom messages (via `lnd`) +- `tools/lcpd-grpcd`: a gRPC daemon that speaks the LCP v0.3 wire protocol over BOLT #1 custom messages (via `lnd`) - `tools/lcpdctl`: a generated CLI client for the daemon (dev/debug) - `tools/lcpd-oneshot`: a minimal Requester helper (one-shot quote/pay or interactive `-chat`) diff --git a/go-lcpd/internal/grpcservice/lcpd/observability.go b/go-lcpd/internal/grpcservice/lcpd/observability.go index 53c4b00..73181ac 100644 --- a/go-lcpd/internal/grpcservice/lcpd/observability.go +++ b/go-lcpd/internal/grpcservice/lcpd/observability.go @@ -18,38 +18,36 @@ import ( const taskKindOpenAIChatCompletionsV1 = "openai.chat_completions.v1" -type taskLogSummary struct { - taskKind string +type callLogSummary struct { + method string model string requestBytes int maxOutputTokens uint32 } -func summarizeTask(task *lcpdv1.Task) taskLogSummary { - if task == nil { - return taskLogSummary{} +func summarizeCall(call *lcpdv1.CallSpec) callLogSummary { + if call == nil { + return callLogSummary{} } - spec := task.GetOpenaiChatCompletionsV1() - if spec == nil { - return taskLogSummary{} + method := call.GetMethod() + summary := callLogSummary{ + method: method, + requestBytes: len(call.GetRequestBytes()), } - model := "" - if params := spec.GetParams(); params != nil { - model = params.GetModel() + if method == taskKindOpenAIChatCompletionsV1 { + model, maxTokens := openAIChatCompletionsLogInfoFromRequestJSON(call.GetRequestBytes()) + summary.model = model + summary.maxOutputTokens = maxTokens } - return taskLogSummary{ - taskKind: taskKindOpenAIChatCompletionsV1, - model: model, - requestBytes: len(spec.GetRequestJson()), - maxOutputTokens: maxOutputTokensFromOpenAIChatCompletionsRequestJSON(spec.GetRequestJson()), - } + return summary } func maxOutputTokensFromOpenAIChatCompletionsRequestJSON(b []byte) uint32 { var parsed struct { + Model string `json:"model,omitempty"` MaxCompletionTokens *uint32 `json:"max_completion_tokens,omitempty"` MaxTokens *uint32 `json:"max_tokens,omitempty"` MaxOutputTokens *uint32 `json:"max_output_tokens,omitempty"` @@ -69,15 +67,38 @@ func maxOutputTokensFromOpenAIChatCompletionsRequestJSON(b []byte) uint32 { return 0 } -func (s *Service) summarizeStoredTask(peerID string, jobID lcp.JobID) taskLogSummary { +func openAIChatCompletionsLogInfoFromRequestJSON(b []byte) (string, uint32) { + var parsed struct { + Model string `json:"model,omitempty"` + MaxCompletionTokens *uint32 `json:"max_completion_tokens,omitempty"` + MaxTokens *uint32 `json:"max_tokens,omitempty"` + MaxOutputTokens *uint32 `json:"max_output_tokens,omitempty"` + } + if err := json.Unmarshal(b, &parsed); err != nil { + return "", 0 + } + + maxTokens := uint32(0) + switch { + case parsed.MaxCompletionTokens != nil: + maxTokens = *parsed.MaxCompletionTokens + case parsed.MaxTokens != nil: + maxTokens = *parsed.MaxTokens + case parsed.MaxOutputTokens != nil: + maxTokens = *parsed.MaxOutputTokens + } + return parsed.Model, maxTokens +} + +func (s *Service) summarizeStoredCall(peerID string, jobID lcp.JobID) callLogSummary { if s.jobs == nil { - return taskLogSummary{} + return callLogSummary{} } job, ok := s.jobs.Get(peerID, jobID) if !ok { - return taskLogSummary{} + return callLogSummary{} } - return summarizeTask(job.Task) + return summarizeCall(job.Call) } func unixSeconds(ts *timestamppb.Timestamp) int64 { @@ -90,50 +111,51 @@ func unixSeconds(ts *timestamppb.Timestamp) int64 { func (s *Service) logQuoteReceived( peerID string, jobID lcp.JobID, - summary taskLogSummary, + summary callLogSummary, payloadBytes int, - terms *lcpdv1.Terms, + quote *lcpdv1.Quote, started time.Time, ) { s.logger.Infow( "quote received", "peer_id", peerID, - "job_id", jobID.String(), - "task_kind", summary.taskKind, + "call_id", jobID.String(), + "method", summary.method, "model", summary.model, "request_bytes", summary.requestBytes, "max_output_tokens", summary.maxOutputTokens, "payload_bytes", payloadBytes, - "price_msat", terms.GetPriceMsat(), - "quote_expiry_unix", unixSeconds(terms.GetQuoteExpiry()), - "terms_hash", hex.EncodeToString(terms.GetTermsHash()), + "price_msat", quote.GetPriceMsat(), + "quote_expiry_unix", unixSeconds(quote.GetQuoteExpiry()), + "terms_hash", hex.EncodeToString(quote.GetTermsHash()), "total_ms", time.Since(started).Milliseconds(), ) } -func (s *Service) logResultReceived( +func (s *Service) logCompleteReceived( peerID string, jobID lcp.JobID, - summary taskLogSummary, - terms *lcpdv1.Terms, + summary callLogSummary, + quote *lcpdv1.Quote, payDuration time.Duration, waitDuration time.Duration, - res *lcpdv1.Result, + res *lcpdv1.Complete, started time.Time, ) { - contentType, resultBytes := resultLoggingFields(res) + contentType, resultBytes := completeLoggingFields(res) s.logger.Infow( - "result received", + "complete received", "peer_id", peerID, - "job_id", jobID.String(), - "task_kind", summary.taskKind, + "call_id", jobID.String(), + "method", summary.method, "model", summary.model, "request_bytes", summary.requestBytes, "max_output_tokens", summary.maxOutputTokens, - "price_msat", terms.GetPriceMsat(), - "quote_expiry_unix", unixSeconds(terms.GetQuoteExpiry()), - "terms_hash", hex.EncodeToString(terms.GetTermsHash()), + "price_msat", quote.GetPriceMsat(), + "quote_expiry_unix", unixSeconds(quote.GetQuoteExpiry()), + "terms_hash", hex.EncodeToString(quote.GetTermsHash()), + "status", res.GetStatus(), "result_bytes", resultBytes, "content_type", contentType, "pay_ms", payDuration.Milliseconds(), @@ -142,36 +164,36 @@ func (s *Service) logResultReceived( ) } -func resultLoggingFields(res *lcpdv1.Result) (string, int) { +func completeLoggingFields(res *lcpdv1.Complete) (string, int) { if res == nil { return "", 0 } - if res.GetStatus() == lcpdv1.Result_STATUS_OK && res.GetResultLen() != 0 { - resultLen := res.GetResultLen() + if res.GetStatus() == lcpdv1.Complete_STATUS_OK && res.GetResponseLen() != 0 { + resultLen := res.GetResponseLen() if resultLen > math.MaxInt { - return res.GetContentType(), math.MaxInt + return res.GetResponseContentType(), math.MaxInt } - return res.GetContentType(), int(resultLen) + return res.GetResponseContentType(), int(resultLen) } - return res.GetContentType(), len(res.GetResult()) + return res.GetResponseContentType(), len(res.GetResponseBytes()) } func (s *Service) requestQuoteWaiterError( ctx context.Context, peerID string, jobID lcp.JobID, - summary taskLogSummary, + summary callLogSummary, err error, ) error { st := grpcStatusFromWaiterError(ctx, err) s.logger.Warnw( "request_quote failed", "peer_id", peerID, - "job_id", jobID.String(), + "call_id", jobID.String(), "grpc_code", status.Code(st), - "task_kind", summary.taskKind, + "method", summary.method, "model", summary.model, ) return st @@ -180,16 +202,16 @@ func (s *Service) requestQuoteWaiterError( func (s *Service) requestQuoteLCPError( peerID string, jobID lcp.JobID, - summary taskLogSummary, + summary callLogSummary, lcpErr *lcpwire.Error, ) error { s.markJobState(peerID, jobID, requesterjobstore.StateFailed) s.logger.Warnw( "request_quote failed", "peer_id", peerID, - "job_id", jobID.String(), + "call_id", jobID.String(), "grpc_code", codes.FailedPrecondition, - "task_kind", summary.taskKind, + "method", summary.method, "model", summary.model, "lcp_error_code", lcpErr.Code, ) @@ -199,7 +221,7 @@ func (s *Service) requestQuoteLCPError( func (s *Service) acceptAndExecuteVerifyError( peerID string, jobID lcp.JobID, - summary taskLogSummary, + summary callLogSummary, verifyErr error, ) error { s.markJobState(peerID, jobID, requesterjobstore.StateFailed) @@ -207,9 +229,9 @@ func (s *Service) acceptAndExecuteVerifyError( s.logger.Warnw( "accept_and_execute failed", "peer_id", peerID, - "job_id", jobID.String(), + "call_id", jobID.String(), "grpc_code", st.Code(), - "task_kind", summary.taskKind, + "method", summary.method, "model", summary.model, ) } @@ -220,7 +242,7 @@ func (s *Service) acceptAndExecuteLightningError( ctx context.Context, peerID string, jobID lcp.JobID, - summary taskLogSummary, + summary callLogSummary, payErr error, ) error { s.markJobState(peerID, jobID, requesterjobstore.StateFailed) @@ -228,9 +250,9 @@ func (s *Service) acceptAndExecuteLightningError( s.logger.Warnw( "accept_and_execute failed", "peer_id", peerID, - "job_id", jobID.String(), + "call_id", jobID.String(), "grpc_code", status.Code(st), - "task_kind", summary.taskKind, + "method", summary.method, "model", summary.model, ) return st @@ -240,7 +262,7 @@ func (s *Service) acceptAndExecuteWaiterError( ctx context.Context, peerID string, jobID lcp.JobID, - summary taskLogSummary, + summary callLogSummary, err error, ) error { s.markJobState(peerID, jobID, requesterjobstore.StateFailed) @@ -248,9 +270,9 @@ func (s *Service) acceptAndExecuteWaiterError( s.logger.Warnw( "accept_and_execute failed", "peer_id", peerID, - "job_id", jobID.String(), + "call_id", jobID.String(), "grpc_code", status.Code(st), - "task_kind", summary.taskKind, + "method", summary.method, "model", summary.model, ) return st @@ -259,16 +281,16 @@ func (s *Service) acceptAndExecuteWaiterError( func (s *Service) acceptAndExecuteLCPError( peerID string, jobID lcp.JobID, - summary taskLogSummary, + summary callLogSummary, lcpErr *lcpwire.Error, ) error { s.markJobState(peerID, jobID, requesterjobstore.StateFailed) s.logger.Warnw( "accept_and_execute failed", "peer_id", peerID, - "job_id", jobID.String(), + "call_id", jobID.String(), "grpc_code", codes.FailedPrecondition, - "task_kind", summary.taskKind, + "method", summary.method, "model", summary.model, "lcp_error_code", lcpErr.Code, ) diff --git a/go-lcpd/internal/grpcservice/lcpd/service.go b/go-lcpd/internal/grpcservice/lcpd/service.go index b93b254..f39cb2e 100644 --- a/go-lcpd/internal/grpcservice/lcpd/service.go +++ b/go-lcpd/internal/grpcservice/lcpd/service.go @@ -12,7 +12,6 @@ import ( "buf.build/go/protovalidate" "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" "github.com/bruwbird/lcp/go-lcpd/internal/envconfig" - "github.com/bruwbird/lcp/go-lcpd/internal/lcptasks" "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" "github.com/bruwbird/lcp/go-lcpd/internal/lightningrpc" "github.com/bruwbird/lcp/go-lcpd/internal/peerdirectory" @@ -191,10 +190,7 @@ func (s *Service) RequestQuote( } peerID := strings.TrimSpace(req.GetPeerId()) - task := req.GetTask() - if err := lcptasks.ValidateTask(task); err != nil { - return nil, status.Error(codes.InvalidArgument, err.Error()) - } + call := req.GetCall() started := time.Now() @@ -204,15 +200,15 @@ func (s *Service) RequestQuote( } remoteManifest := peer.RemoteManifest - summary := summarizeTask(task) + summary := summarizeCall(call) - jobID, expiry, wireTask, inputStream, payload, err := s.buildQuoteRequestPayload(task) + jobID, expiry, callCommit, requestStream, payload, err := s.buildCallPayload(call) if err != nil { return nil, err } payloadBytes := len(payload) - if limitsErr := validateQuoteRequestRemoteLimits(remoteManifest, payload, inputStream); limitsErr != nil { + if limitsErr := validateQuoteRequestRemoteLimits(remoteManifest, payload, requestStream); limitsErr != nil { return nil, limitsErr } @@ -220,7 +216,7 @@ func (s *Service) RequestQuote( return nil, status.Error(codes.Unavailable, "peer messaging not configured") } - sendErr := s.messenger.SendCustomMessage(ctx, peerID, lcpwire.MessageTypeQuoteRequest, payload) + sendErr := s.messenger.SendCustomMessage(ctx, peerID, lcpwire.MessageTypeCall, payload) if sendErr != nil { return nil, grpcStatusFromPeerSendError(ctx, sendErr) } @@ -230,7 +226,7 @@ func (s *Service) RequestQuote( peerID, jobID, expiry, - inputStream, + requestStream, remoteManifest, ); sendStreamErr != nil { return nil, sendStreamErr @@ -244,21 +240,21 @@ func (s *Service) RequestQuote( if outcome.Error != nil { return nil, s.requestQuoteLCPError(peerID, jobID, summary, outcome.Error) } - if outcome.QuoteResponse == nil { + if outcome.Quote == nil { return nil, status.Error(codes.Internal, "quote waiter returned empty outcome") } - terms, err := termsFromQuoteResponse(wireTask, inputStream, *outcome.QuoteResponse) + quote, err := quoteFromWireQuoteResponse(callCommit, requestStream, *outcome.Quote) if err != nil { return nil, status.Error(codes.FailedPrecondition, err.Error()) } - if putErr := s.jobs.PutQuote(peerID, task, terms); putErr != nil { + if putErr := s.jobs.PutQuote(peerID, call, quote); putErr != nil { s.logger.Errorw( "put quote failed", "peer_id", peerID, - "job_id", + "call_id", jobID.String(), "err", putErr, @@ -266,45 +262,57 @@ func (s *Service) RequestQuote( return nil, status.Error(codes.Internal, "put quote failed") } - s.logQuoteReceived(peerID, jobID, summary, payloadBytes, terms, started) + s.logQuoteReceived(peerID, jobID, summary, payloadBytes, quote, started) return &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: terms, + Quote: quote, }, nil } func validateQuoteRequestRemoteLimits( remoteManifest lcpwire.Manifest, payload []byte, - inputStream lcptasks.InputStream, + requestStream requestStream, ) error { if remoteManifest.MaxPayloadBytes != 0 && len(payload) > int(remoteManifest.MaxPayloadBytes) { return status.Error( codes.ResourceExhausted, - "quote_request payload exceeds peer max_payload_bytes", + "call payload exceeds peer max_payload_bytes", ) } - inputLen := uint64(len(inputStream.DecodedBytes)) - if inputLen > remoteManifest.MaxStreamBytes || inputLen > remoteManifest.MaxJobBytes { + inputLen := uint64(len(requestStream.DecodedBytes)) + if inputLen > remoteManifest.MaxStreamBytes || inputLen > remoteManifest.MaxCallBytes { return status.Error( codes.ResourceExhausted, - "input stream exceeds peer max_stream_bytes/max_job_bytes", + "request stream exceeds peer max_stream_bytes/max_call_bytes", ) } return nil } -func (s *Service) buildQuoteRequestPayload( - task *lcpdv1.Task, -) (lcp.JobID, uint64, lcptasks.QuoteRequestTask, lcptasks.InputStream, []byte, error) { +type callCommit struct { + Method string + ParamsBytes []byte + ParamsContentType string +} + +type requestStream struct { + DecodedBytes []byte + ContentType string + ContentEncoding string +} + +func (s *Service) buildCallPayload( + call *lcpdv1.CallSpec, +) (lcp.JobID, uint64, callCommit, requestStream, []byte, error) { jobID, err := protocolcompat.NewJobID() if err != nil { s.logger.Errorw("generate job_id failed", "err", err) - return lcp.JobID{}, 0, lcptasks.QuoteRequestTask{}, lcptasks.InputStream{}, nil, status.Error( + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( codes.Internal, "generate job_id failed", ) @@ -313,7 +321,7 @@ func (s *Service) buildQuoteRequestPayload( msgID, err := newMsgID() if err != nil { s.logger.Errorw("generate msg_id failed", "err", err) - return lcp.JobID{}, 0, lcptasks.QuoteRequestTask{}, lcptasks.InputStream{}, nil, status.Error( + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( codes.Internal, "generate msg_id failed", ) @@ -322,49 +330,82 @@ func (s *Service) buildQuoteRequestPayload( expiry, err := s.newEnvelopeExpiry() if err != nil { s.logger.Errorw("generate envelope expiry failed", "err", err) - return lcp.JobID{}, 0, lcptasks.QuoteRequestTask{}, lcptasks.InputStream{}, nil, status.Error( + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( codes.Internal, "generate envelope expiry failed", ) } - wireTask, err := lcptasks.ToWireQuoteRequestTask(task) - if err != nil { - return lcp.JobID{}, 0, lcptasks.QuoteRequestTask{}, lcptasks.InputStream{}, nil, status.Error( + if call == nil { + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( codes.InvalidArgument, - err.Error(), + "call is required", + ) + } + method := call.GetMethod() + if strings.TrimSpace(method) == "" { + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( + codes.InvalidArgument, + "method is required", ) } - inputStream, err := lcptasks.ToWireInputStream(task) - if err != nil { - return lcp.JobID{}, 0, lcptasks.QuoteRequestTask{}, lcptasks.InputStream{}, nil, status.Error( + input := requestStream{ + DecodedBytes: append([]byte(nil), call.GetRequestBytes()...), + ContentType: strings.TrimSpace(call.GetRequestContentType()), + ContentEncoding: strings.TrimSpace(call.GetRequestContentEncoding()), + } + if input.ContentType == "" { + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( codes.InvalidArgument, - err.Error(), + "request_content_type is required", ) } + if input.ContentEncoding == "" { + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( + codes.InvalidArgument, + "request_content_encoding is required", + ) + } + + paramsBytes := append([]byte(nil), call.GetParams()...) + var paramsBytesPtr *[]byte + if len(paramsBytes) > 0 { + paramsBytesPtr = ¶msBytes + } + + paramsContentType := strings.TrimSpace(call.GetParamsContentType()) + var paramsContentTypePtr *string + if paramsContentType != "" { + paramsContentTypePtr = ¶msContentType + } - paramsBytes := wireTask.ParamsBytes - quoteReq := lcpwire.QuoteRequest{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + quoteReq := lcpwire.Call{ + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: msgID, Expiry: expiry, }, - TaskKind: wireTask.TaskKind, - ParamsBytes: ¶msBytes, + Method: method, + ParamsBytes: paramsBytesPtr, + ParamsContentType: paramsContentTypePtr, } - payload, err := lcpwire.EncodeQuoteRequest(quoteReq) + payload, err := lcpwire.EncodeCall(quoteReq) if err != nil { - return lcp.JobID{}, 0, lcptasks.QuoteRequestTask{}, lcptasks.InputStream{}, nil, status.Error( + return lcp.JobID{}, 0, callCommit{}, requestStream{}, nil, status.Error( codes.InvalidArgument, err.Error(), ) } - return jobID, expiry, wireTask, inputStream, payload, nil + commit := callCommit{ + Method: method, + ParamsBytes: paramsBytes, + ParamsContentType: paramsContentType, + } + return jobID, expiry, commit, input, payload, nil } func (s *Service) sendInputStream( @@ -372,7 +413,7 @@ func (s *Service) sendInputStream( peerID string, jobID lcp.JobID, expiry uint64, - inputStream lcptasks.InputStream, + inputStream requestStream, remoteManifest lcpwire.Manifest, ) error { if s.messenger == nil { @@ -437,7 +478,7 @@ func (s *Service) sendInputStreamBegin( peerID string, jobID lcp.JobID, expiry uint64, - inputStream lcptasks.InputStream, + inputStream requestStream, streamID lcp.Hash32, totalLen uint64, inputHash lcp.Hash32, @@ -449,14 +490,14 @@ func (s *Service) sendInputStreamBegin( } begin := lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: beginMsgID, Expiry: expiry, }, StreamID: streamID, - Kind: lcpwire.StreamKindInput, + Kind: lcpwire.StreamKindRequest, TotalLen: &totalLen, SHA256: &inputHash, ContentType: inputStream.ContentType, @@ -524,9 +565,9 @@ func (s *Service) sendInputStreamEnd( } end := lcpwire.StreamEnd{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: endMsgID, Expiry: expiry, }, @@ -562,9 +603,9 @@ func encodeFittingInputStreamChunk( for { chunk := lcpwire.StreamChunk{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, Expiry: expiry, }, StreamID: streamID, @@ -606,7 +647,7 @@ func (s *Service) AcceptAndExecute( return nil, status.Error(codes.InvalidArgument, "pay_invoice must be true") } - jobID, err := toJobID(req.GetJobId()) + jobID, err := toJobID(req.GetCallId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -617,12 +658,12 @@ func (s *Service) AcceptAndExecute( started := time.Now() - terms, err := s.jobs.GetTerms(peerID, jobID) + quote, err := s.jobs.GetQuote(peerID, jobID) if err != nil { return nil, grpcStatusFromJobStoreError(err) } - summary := s.summarizeStoredTask(peerID, jobID) + summary := s.summarizeStoredCall(peerID, jobID) s.markJobState(peerID, jobID, requesterjobstore.StatePaying) failed := true @@ -632,13 +673,13 @@ func (s *Service) AcceptAndExecute( } }() - verifyErr := verifyInvoiceBinding(ctx, s.lightning, peerID, terms) + verifyErr := verifyInvoiceBinding(ctx, s.lightning, peerID, quote) if verifyErr != nil { return nil, s.acceptAndExecuteVerifyError(peerID, jobID, summary, verifyErr) } payStart := time.Now() - _, payErr := s.lightning.PayInvoice(ctx, terms.GetPaymentRequest()) + _, payErr := s.lightning.PayInvoice(ctx, quote.GetPaymentRequest()) if payErr != nil { return nil, s.acceptAndExecuteLightningError(ctx, peerID, jobID, summary, payErr) } @@ -655,11 +696,11 @@ func (s *Service) AcceptAndExecute( if outcome.Error != nil { return nil, s.acceptAndExecuteLCPError(peerID, jobID, summary, outcome.Error) } - if outcome.Result == nil { + if outcome.Complete == nil { return nil, status.Error(codes.Internal, "result waiter returned empty outcome") } - res, err := protoResultFromTerminal(*outcome.Result, outcome.ResultBytes) + complete, err := protoCompleteFromTerminal(*outcome.Complete, outcome.ResponseBytes) if err != nil { return nil, err } @@ -667,9 +708,9 @@ func (s *Service) AcceptAndExecute( s.markJobState(peerID, jobID, requesterjobstore.StateDone) failed = false - s.logResultReceived(peerID, jobID, summary, terms, payDuration, waitDuration, res, started) + s.logCompleteReceived(peerID, jobID, summary, quote, payDuration, waitDuration, complete, started) - return &lcpdv1.AcceptAndExecuteResponse{Result: res}, nil + return &lcpdv1.AcceptAndExecuteResponse{Complete: complete}, nil } //nolint:gocognit,cyclop,funlen // Streaming flow handles many error branches; keep linear for clarity. @@ -692,7 +733,7 @@ func (s *Service) AcceptAndExecuteStream( return status.Error(codes.InvalidArgument, "pay_invoice must be true") } - jobID, err := toJobID(req.GetJobId()) + jobID, err := toJobID(req.GetCallId()) if err != nil { return status.Error(codes.InvalidArgument, err.Error()) } @@ -703,12 +744,12 @@ func (s *Service) AcceptAndExecuteStream( started := time.Now() - terms, err := s.jobs.GetTerms(peerID, jobID) + quote, err := s.jobs.GetQuote(peerID, jobID) if err != nil { return grpcStatusFromJobStoreError(err) } - summary := s.summarizeStoredTask(peerID, jobID) + summary := s.summarizeStoredCall(peerID, jobID) s.markJobState(peerID, jobID, requesterjobstore.StatePaying) failed := true @@ -718,7 +759,7 @@ func (s *Service) AcceptAndExecuteStream( } }() - verifyErr := verifyInvoiceBinding(ctx, s.lightning, peerID, terms) + verifyErr := verifyInvoiceBinding(ctx, s.lightning, peerID, quote) if verifyErr != nil { return s.acceptAndExecuteVerifyError(peerID, jobID, summary, verifyErr) } @@ -731,7 +772,7 @@ func (s *Service) AcceptAndExecuteStream( defer unsubscribe() payStart := time.Now() - _, payErr := s.lightning.PayInvoice(ctx, terms.GetPaymentRequest()) + _, payErr := s.lightning.PayInvoice(ctx, quote.GetPaymentRequest()) if payErr != nil { return s.acceptAndExecuteLightningError(ctx, peerID, jobID, summary, payErr) } @@ -757,8 +798,8 @@ func (s *Service) AcceptAndExecuteStream( return nil case requesterwait.ResultStreamEventKindBegin: return stream.Send(&lcpdv1.AcceptAndExecuteStreamResponse{ - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultBegin{ - ResultBegin: &lcpdv1.ResultStreamBegin{ + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseBegin{ + ResponseBegin: &lcpdv1.ResponseStreamBegin{ ContentType: ev.ContentType, ContentEncoding: ev.ContentEncoding, }, @@ -769,8 +810,8 @@ func (s *Service) AcceptAndExecuteStream( return nil } return stream.Send(&lcpdv1.AcceptAndExecuteStreamResponse{ - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultChunk{ - ResultChunk: &lcpdv1.ResultStreamChunk{Data: ev.Data}, + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseChunk{ + ResponseChunk: &lcpdv1.ResponseStreamChunk{Data: ev.Data}, }, }) default: @@ -793,7 +834,7 @@ func (s *Service) AcceptAndExecuteStream( if waited.outcome.Error != nil { return s.acceptAndExecuteLCPError(peerID, jobID, summary, waited.outcome.Error) } - if waited.outcome.Result == nil { + if waited.outcome.Complete == nil { return status.Error(codes.Internal, "result waiter returned empty outcome") } @@ -811,17 +852,17 @@ func (s *Service) AcceptAndExecuteStream( } drained: - res, protoErr := protoResultFromTerminalMetadataOnly(*waited.outcome.Result) + res, protoErr := protoCompleteFromTerminalMetadataOnly(*waited.outcome.Complete) if protoErr != nil { return protoErr } - if res.GetStatus() == lcpdv1.Result_STATUS_OK { + if res.GetStatus() == lcpdv1.Complete_STATUS_OK { if sendErr := stream.Send(&lcpdv1.AcceptAndExecuteStreamResponse{ - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultEnd{ - ResultEnd: &lcpdv1.ResultStreamEnd{ - ResultHash: res.GetResultHash(), - ResultLen: res.GetResultLen(), + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseEnd{ + ResponseEnd: &lcpdv1.ResponseStreamEnd{ + ResponseHash: res.GetResponseHash(), + ResponseLen: res.GetResponseLen(), }, }, }); sendErr != nil { @@ -830,7 +871,7 @@ func (s *Service) AcceptAndExecuteStream( } if sendErr := stream.Send(&lcpdv1.AcceptAndExecuteStreamResponse{ - Event: &lcpdv1.AcceptAndExecuteStreamResponse_Result{Result: res}, + Event: &lcpdv1.AcceptAndExecuteStreamResponse_Complete{Complete: res}, }); sendErr != nil { return sendErr } @@ -838,7 +879,7 @@ func (s *Service) AcceptAndExecuteStream( s.markJobState(peerID, jobID, requesterjobstore.StateDone) failed = false - s.logResultReceived(peerID, jobID, summary, terms, payDuration, waitDuration, res, started) + s.logCompleteReceived(peerID, jobID, summary, quote, payDuration, waitDuration, res, started) return nil case <-ctx.Done(): @@ -847,64 +888,47 @@ func (s *Service) AcceptAndExecuteStream( } } -func protoResultFromTerminal( - terminal lcpwire.Result, +func protoCompleteFromTerminal( + terminal lcpwire.Complete, resultBytes []byte, -) (*lcpdv1.Result, error) { - res := &lcpdv1.Result{} - switch terminal.Status { - case lcpwire.ResultStatusOK: - res.Status = lcpdv1.Result_STATUS_OK - if terminal.OK == nil { - return nil, status.Error(codes.Internal, "missing ok metadata in lcp_result") - } - res.Result = append([]byte(nil), resultBytes...) - res.ResultHash = append([]byte(nil), terminal.OK.ResultHash[:]...) - res.ResultLen = terminal.OK.ResultLen - res.ContentType = terminal.OK.ResultContentType - res.ContentEncoding = terminal.OK.ResultContentEncoding - case lcpwire.ResultStatusFailed: - res.Status = lcpdv1.Result_STATUS_FAILED - if terminal.Message != nil { - res.Message = *terminal.Message - } - case lcpwire.ResultStatusCancelled: - res.Status = lcpdv1.Result_STATUS_CANCELLED - if terminal.Message != nil { - res.Message = *terminal.Message - } - default: - return nil, status.Error(codes.Internal, "unknown lcp_result status") +) (*lcpdv1.Complete, error) { + complete, err := protoCompleteFromTerminalMetadataOnly(terminal) + if err != nil { + return nil, err } - return res, nil + if complete.GetStatus() == lcpdv1.Complete_STATUS_OK { + complete.ResponseBytes = append([]byte(nil), resultBytes...) + } + return complete, nil } -func protoResultFromTerminalMetadataOnly( - terminal lcpwire.Result, -) (*lcpdv1.Result, error) { - res := &lcpdv1.Result{} +func protoCompleteFromTerminalMetadataOnly( + terminal lcpwire.Complete, +) (*lcpdv1.Complete, error) { + res := &lcpdv1.Complete{} switch terminal.Status { - case lcpwire.ResultStatusOK: - res.Status = lcpdv1.Result_STATUS_OK + case lcpwire.CompleteStatusOK: + res.Status = lcpdv1.Complete_STATUS_OK if terminal.OK == nil { - return nil, status.Error(codes.Internal, "missing ok metadata in lcp_result") + return nil, status.Error(codes.Internal, "missing ok metadata in lcp_complete") } - res.ResultHash = append([]byte(nil), terminal.OK.ResultHash[:]...) - res.ResultLen = terminal.OK.ResultLen - res.ContentType = terminal.OK.ResultContentType - res.ContentEncoding = terminal.OK.ResultContentEncoding - case lcpwire.ResultStatusFailed: - res.Status = lcpdv1.Result_STATUS_FAILED + res.ResponseStreamId = append([]byte(nil), terminal.OK.ResponseStreamID[:]...) + res.ResponseHash = append([]byte(nil), terminal.OK.ResponseHash[:]...) + res.ResponseLen = proto.Uint64(terminal.OK.ResponseLen) + res.ResponseContentType = proto.String(terminal.OK.ResponseContentType) + res.ResponseContentEncoding = proto.String(terminal.OK.ResponseContentEncoding) + case lcpwire.CompleteStatusFailed: + res.Status = lcpdv1.Complete_STATUS_FAILED if terminal.Message != nil { res.Message = *terminal.Message } - case lcpwire.ResultStatusCancelled: - res.Status = lcpdv1.Result_STATUS_CANCELLED + case lcpwire.CompleteStatusCancelled: + res.Status = lcpdv1.Complete_STATUS_CANCELLED if terminal.Message != nil { res.Message = *terminal.Message } default: - return nil, status.Error(codes.Internal, "unknown lcp_result status") + return nil, status.Error(codes.Internal, "unknown lcp_complete status") } return res, nil } @@ -921,7 +945,7 @@ func (s *Service) CancelJob( } peerID := strings.TrimSpace(req.GetPeerId()) - jobID, err := toJobID(req.GetJobId()) + jobID, err := toJobID(req.GetCallId()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -960,9 +984,9 @@ func (s *Service) CancelJob( } cancel := lcpwire.Cancel{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: msgID, Expiry: expiry, }, @@ -989,60 +1013,36 @@ func toProtoManifest(m lcpwire.Manifest) *lcpdv1.LCPManifest { ProtocolVersion: uint32(m.ProtocolVersion), MaxPayloadBytes: m.MaxPayloadBytes, MaxStreamBytes: m.MaxStreamBytes, - MaxJobBytes: m.MaxJobBytes, + MaxCallBytes: m.MaxCallBytes, } - if m.MaxInflightJobs != nil { - out.MaxInflightJobs = uint32(*m.MaxInflightJobs) + if m.MaxInflightCalls != nil { + out.MaxInflightCalls = uint32(*m.MaxInflightCalls) } - - if len(m.SupportedTasks) == 0 { - return out + if len(m.SupportedMethods) != 0 { + out.SupportedMethods = make([]*lcpdv1.MethodDescriptor, 0, len(m.SupportedMethods)) + for _, d := range m.SupportedMethods { + out.SupportedMethods = append(out.SupportedMethods, toProtoMethodDescriptor(d)) + } } - out.SupportedTasks = make([]*lcpdv1.LCPTaskTemplate, 0, len(m.SupportedTasks)) - for _, tmpl := range m.SupportedTasks { - switch tmpl.TaskKind { - case lcptasks.TaskKindOpenAIChatCompletionsV1: - if tmpl.ParamsBytes == nil { - continue - } - - params, err := lcpwire.DecodeOpenAIChatCompletionsV1Params(*tmpl.ParamsBytes) - if err != nil { - continue - } - - out.SupportedTasks = append(out.SupportedTasks, &lcpdv1.LCPTaskTemplate{ - Kind: lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1, - ParamsTemplate: &lcpdv1.LCPTaskTemplate_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: params.Model, - }, - }, - }) - case lcptasks.TaskKindOpenAIResponsesV1: - if tmpl.ParamsBytes == nil { - continue - } - - params, err := lcpwire.DecodeOpenAIResponsesV1Params(*tmpl.ParamsBytes) - if err != nil { - continue - } + return out +} - out.SupportedTasks = append(out.SupportedTasks, &lcpdv1.LCPTaskTemplate{ - Kind: lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1, - ParamsTemplate: &lcpdv1.LCPTaskTemplate_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1Params{ - Model: params.Model, - }, - }, - }) - default: - continue - } +func toProtoMethodDescriptor(d lcpwire.MethodDescriptor) *lcpdv1.MethodDescriptor { + out := &lcpdv1.MethodDescriptor{ + Method: d.Method, + RequestContentTypes: append([]string(nil), d.RequestContentTypes...), + ResponseContentTypes: append([]string(nil), d.ResponseContentTypes...), + } + if d.DocsURI != nil { + out.DocsUri = proto.String(*d.DocsURI) + } + if d.DocsSHA256 != nil { + out.DocsSha256 = append([]byte(nil), d.DocsSHA256[:]...) + } + if d.PolicyNotice != nil { + out.PolicyNotice = proto.String(*d.PolicyNotice) } - return out } @@ -1085,7 +1085,7 @@ func (s *Service) requireReadyPeer(peerID string) (ReadyPeer, error) { return ReadyPeer{}, status.Error(codes.FailedPrecondition, "peer is not ready for lcp") } - if peer.RemoteManifest.ProtocolVersion != lcpwire.ProtocolVersionV02 { + if peer.RemoteManifest.ProtocolVersion != lcpwire.ProtocolVersionV03 { return ReadyPeer{}, status.Error(codes.FailedPrecondition, "peer protocol_version mismatch") } @@ -1115,22 +1115,24 @@ func (s *Service) markJobState(peerID string, jobID lcp.JobID, state requesterjo _ = s.jobs.MarkState(peerID, jobID, state) } -func termsFromQuoteResponse( - wireTask lcptasks.QuoteRequestTask, - inputStream lcptasks.InputStream, - resp lcpwire.QuoteResponse, -) (*lcpdv1.Terms, error) { +func quoteFromWireQuoteResponse( + call callCommit, + requestStream requestStream, + resp lcpwire.Quote, +) (*lcpdv1.Quote, error) { wantHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ ProtocolVersion: resp.Envelope.ProtocolVersion, - JobID: resp.Envelope.JobID, + JobID: resp.Envelope.CallID, PriceMsat: resp.PriceMsat, QuoteExpiry: resp.QuoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: wireTask.TaskKind, - Input: inputStream.DecodedBytes, - InputContentType: inputStream.ContentType, - InputContentEncoding: inputStream.ContentEncoding, - Params: wireTask.ParamsBytes, + Method: call.Method, + Request: requestStream.DecodedBytes, + RequestContentType: requestStream.ContentType, + RequestContentEncoding: requestStream.ContentEncoding, + Params: call.ParamsBytes, + ResponseContentType: resp.ResponseContentType, + ResponseContentEncoding: resp.ResponseContentEncoding, }) if err != nil { return nil, err @@ -1145,30 +1147,37 @@ func termsFromQuoteResponse( } termsHashBytes := resp.TermsHash[:] - jobIDBytes := resp.Envelope.JobID[:] + jobIDBytes := resp.Envelope.CallID[:] - return &lcpdv1.Terms{ + out := &lcpdv1.Quote{ ProtocolVersion: uint32(resp.Envelope.ProtocolVersion), - JobId: append([]byte(nil), jobIDBytes...), + CallId: append([]byte(nil), jobIDBytes...), PriceMsat: resp.PriceMsat, QuoteExpiry: ts, TermsHash: append([]byte(nil), termsHashBytes...), PaymentRequest: resp.PaymentRequest, - }, nil + } + if resp.ResponseContentType != nil { + out.ResponseContentType = proto.String(*resp.ResponseContentType) + } + if resp.ResponseContentEncoding != nil { + out.ResponseContentEncoding = proto.String(*resp.ResponseContentEncoding) + } + return out, nil } func verifyInvoiceBinding( ctx context.Context, ln LightningRPC, peerID string, - terms *lcpdv1.Terms, + quote *lcpdv1.Quote, ) error { - info, err := ln.DecodePaymentRequest(ctx, terms.GetPaymentRequest()) + info, err := ln.DecodePaymentRequest(ctx, quote.GetPaymentRequest()) if err != nil { return grpcStatusFromLightningError(ctx, err) } - termsHash, err := toHash32(terms.GetTermsHash()) + termsHash, err := toHash32(quote.GetTermsHash()) if err != nil { return status.Error(codes.Internal, "invalid stored terms_hash") } @@ -1183,7 +1192,7 @@ func verifyInvoiceBinding( return status.Error(codes.FailedPrecondition, "invoice destination does not match peer_id") } - priceMsat := terms.GetPriceMsat() + priceMsat := quote.GetPriceMsat() if priceMsat == 0 { return status.Error(codes.FailedPrecondition, "price_msat must be > 0") } @@ -1194,7 +1203,7 @@ func verifyInvoiceBinding( return status.Error(codes.FailedPrecondition, "invoice amount does not match price_msat") } - quoteExpiry := terms.GetQuoteExpiry() + quoteExpiry := quote.GetQuoteExpiry() if quoteExpiry == nil { return status.Error(codes.Internal, "invalid stored quote_expiry") } @@ -1268,11 +1277,11 @@ func grpcStatusFromLCPError(errMsg lcpwire.Error) error { func grpcCodeFromLCPErrorCode(code lcpwire.ErrorCode) codes.Code { switch code { - case lcpwire.ErrorCodePayloadTooLarge, lcpwire.ErrorCodeRateLimited: + case lcpwire.ErrorCodePayloadTooLarge, lcpwire.ErrorCodeRateLimited, lcpwire.ErrorCodeStreamLimitExceeded: return codes.ResourceExhausted case lcpwire.ErrorCodeUnsupportedVersion, - lcpwire.ErrorCodeUnsupportedTask, - lcpwire.ErrorCodeUnsupportedParams, + lcpwire.ErrorCodeManifestRequired, + lcpwire.ErrorCodeUnsupportedMethod, lcpwire.ErrorCodeUnsupportedEncoding, lcpwire.ErrorCodeInvalidState, lcpwire.ErrorCodeChunkOutOfOrder, @@ -1323,7 +1332,7 @@ func grpcStatusFromPeerSendError(ctx context.Context, err error) error { func toJobID(b []byte) (lcp.JobID, error) { if len(b) != lcp.Hash32Len { - return lcp.JobID{}, fmt.Errorf("job_id must be %d bytes, got %d", lcp.Hash32Len, len(b)) + return lcp.JobID{}, fmt.Errorf("call_id must be %d bytes, got %d", lcp.Hash32Len, len(b)) } var id lcp.JobID copy(id[:], b) @@ -1365,9 +1374,9 @@ const ( func defaultLocalManifest() lcpwire.Manifest { return lcpwire.Manifest{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, MaxPayloadBytes: defaultMaxPayloadBytes, MaxStreamBytes: defaultMaxStreamBytes, - MaxJobBytes: defaultMaxJobBytes, + MaxCallBytes: defaultMaxJobBytes, } } diff --git a/go-lcpd/internal/grpcservice/lcpd/service_test.go b/go-lcpd/internal/grpcservice/lcpd/service_test.go index fc9e0e1..635bd99 100644 --- a/go-lcpd/internal/grpcservice/lcpd/service_test.go +++ b/go-lcpd/internal/grpcservice/lcpd/service_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" - "github.com/bruwbird/lcp/go-lcpd/internal/lcptasks" "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" "github.com/bruwbird/lcp/go-lcpd/internal/lightningrpc" "github.com/bruwbird/lcp/go-lcpd/internal/lndpeermsg" @@ -34,6 +33,11 @@ type fakeClock struct { func (f fakeClock) Now() time.Time { return f.now } +const ( + requestContentTypeJSON = "application/json; charset=utf-8" + contentEncodingIdentity = "identity" +) + type fakeLightning struct { mu sync.Mutex getInfoFn func(context.Context) (lightningrpc.Info, error) @@ -162,10 +166,10 @@ func TestGetLocalInfo_ReturnsManifestAndNodeID(t *testing.T) { maxPayload := uint32(1234) localManifest := &lcpwire.Manifest{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, MaxPayloadBytes: maxPayload, MaxStreamBytes: 1024, - MaxJobBytes: 2048, + MaxCallBytes: 2048, } svc := mustNewService(t, Params{ @@ -195,10 +199,10 @@ func TestRequestQuote_SuccessStoresQuote(t *testing.T) { peers.MarkCustomMsgEnabled(peerID, true) peers.MarkManifestSent(peerID) remoteManifest := lcpwire.Manifest{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, MaxPayloadBytes: 65535, MaxStreamBytes: 4_194_304, - MaxJobBytes: 8_388_608, + MaxCallBytes: 8_388_608, } peers.MarkLCPReady(peerID, remoteManifest) @@ -234,14 +238,14 @@ func TestRequestQuote_SuccessStoresQuote(t *testing.T) { resp, err := svc.RequestQuote(ctx, &lcpdv1.RequestQuoteRequest{ PeerId: peerID, - Task: openaiChatTask("hello", "model-2"), + Call: openaiChatCall(t, "hello", "model-2"), }) if err != nil { t.Fatalf("RequestQuote: %v", err) } assertRequestQuoteResponse(t, resp, peerID, wantPriceMsat, wantPaymentRequest) - assertTermsStored(t, jobs, peerID, resp.GetTerms(), wantPaymentRequest) + assertQuoteStored(t, jobs, peerID, resp.GetQuote(), wantPaymentRequest) assertSentQuoteRequestAndInputStream(t, messenger.messages(), inputBytes) } @@ -254,10 +258,10 @@ func TestRequestQuote_RespectsRemoteMaxPayloadBytes(t *testing.T) { peers.MarkCustomMsgEnabled(peerID, true) peers.MarkManifestSent(peerID) peers.MarkLCPReady(peerID, lcpwire.Manifest{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, MaxPayloadBytes: 1, MaxStreamBytes: 1024, - MaxJobBytes: 2048, + MaxCallBytes: 2048, }) messenger := &fakeMessenger{} @@ -271,7 +275,7 @@ func TestRequestQuote_RespectsRemoteMaxPayloadBytes(t *testing.T) { _, err := svc.RequestQuote(context.Background(), &lcpdv1.RequestQuoteRequest{ PeerId: peerID, - Task: openaiChatTask("hi", "model-1"), + Call: openaiChatCall(t, "hi", "model-1"), }) assertStatusCode(t, err, codes.ResourceExhausted) if got := len(messenger.messages()); got != 0 { @@ -291,7 +295,7 @@ func TestRequestQuote_PeerNotFound(t *testing.T) { _, err := svc.RequestQuote(context.Background(), &lcpdv1.RequestQuoteRequest{ PeerId: "02" + strings.Repeat("c", 64), - Task: openaiChatTask("hello", "model-1"), + Call: openaiChatCall(t, "hello", "model-1"), }) assertStatusCode(t, err, codes.NotFound) } @@ -313,7 +317,7 @@ func TestRequestQuote_PeerNotReady(t *testing.T) { _, err := svc.RequestQuote(context.Background(), &lcpdv1.RequestQuoteRequest{ PeerId: peerID, - Task: openaiChatTask("hello", "model-1"), + Call: openaiChatCall(t, "hello", "model-1"), }) assertStatusCode(t, err, codes.FailedPrecondition) } @@ -340,31 +344,31 @@ func TestAcceptAndExecute_SuccessWaitsForResult(t *testing.T) { t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) } termsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, JobID: jobID, PriceMsat: 123, QuoteExpiry: quoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: "openai.chat_completions.v1", - Input: requestJSON, - InputContentType: lcptasks.ContentTypeApplicationJSONUTF8, - InputContentEncoding: lcptasks.ContentEncodingIdentity, - Params: paramsBytes, + Method: "openai.chat_completions.v1", + Request: requestJSON, + RequestContentType: requestContentTypeJSON, + RequestContentEncoding: contentEncodingIdentity, + Params: paramsBytes, }) if err != nil { t.Fatalf("ComputeTermsHash: %v", err) } - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: append([]byte(nil), jobID[:]...), + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: append([]byte(nil), jobID[:]...), PriceMsat: 123, QuoteExpiry: timestamppb.New(time.Unix(int64(quoteExpiry), 0)), TermsHash: append([]byte(nil), termsHash[:]...), PaymentRequest: "lnbc1dummy", } - if putErr := jobs.PutQuote(peerID, openaiChatTask("hello", "model-1"), terms); putErr != nil { + if putErr := jobs.PutQuote(peerID, openaiChatCall(t, "hello", "model-1"), quote); putErr != nil { t.Fatalf("PutQuote: %v", putErr) } @@ -393,20 +397,20 @@ func TestAcceptAndExecute_SuccessWaitsForResult(t *testing.T) { resultLen := uint64(len(resultBytes)) resultContentType := "text/plain" - resultContentEncoding := lcptasks.ContentEncodingIdentity + resultContentEncoding := contentEncodingIdentity var resultStreamID lcp.Hash32 resultStreamID[0] = 0x22 beginPayload, err := lcpwire.EncodeStreamBegin(lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: lcpwire.MsgID{}, Expiry: expiry, }, StreamID: resultStreamID, - Kind: lcpwire.StreamKindResult, + Kind: lcpwire.StreamKindResponse, TotalLen: &resultLen, SHA256: &resultHash, ContentType: resultContentType, @@ -417,9 +421,9 @@ func TestAcceptAndExecute_SuccessWaitsForResult(t *testing.T) { } chunkPayload, _, err := lcpwire.EncodeStreamChunk(lcpwire.StreamChunk{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, Expiry: expiry, }, StreamID: resultStreamID, @@ -431,9 +435,9 @@ func TestAcceptAndExecute_SuccessWaitsForResult(t *testing.T) { } endPayload, err := lcpwire.EncodeStreamEnd(lcpwire.StreamEnd{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: lcpwire.MsgID{}, Expiry: expiry, }, @@ -445,24 +449,24 @@ func TestAcceptAndExecute_SuccessWaitsForResult(t *testing.T) { t.Fatalf("EncodeStreamEnd: %v", err) } - terminalPayload, err := lcpwire.EncodeResult(lcpwire.Result{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + terminalPayload, err := lcpwire.EncodeComplete(lcpwire.Complete{ + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: lcpwire.MsgID{}, Expiry: expiry, }, - Status: lcpwire.ResultStatusOK, - OK: &lcpwire.ResultOK{ - ResultStreamID: resultStreamID, - ResultHash: resultHash, - ResultLen: resultLen, - ResultContentType: resultContentType, - ResultContentEncoding: resultContentEncoding, + Status: lcpwire.CompleteStatusOK, + OK: &lcpwire.CompleteOK{ + ResponseStreamID: resultStreamID, + ResponseHash: resultHash, + ResponseLen: resultLen, + ResponseContentType: resultContentType, + ResponseContentEncoding: resultContentEncoding, }, }) if err != nil { - t.Fatalf("EncodeResult: %v", err) + t.Fatalf("EncodeComplete: %v", err) } waiter.HandleInboundCustomMessage( @@ -474,7 +478,7 @@ func TestAcceptAndExecute_SuccessWaitsForResult(t *testing.T) { inbound(peerID, lcpwire.MessageTypeStreamChunk, chunkPayload), ) waiter.HandleInboundCustomMessage(context.Background(), inbound(peerID, lcpwire.MessageTypeStreamEnd, endPayload)) - waiter.HandleInboundCustomMessage(context.Background(), inbound(peerID, lcpwire.MessageTypeResult, terminalPayload)) + waiter.HandleInboundCustomMessage(context.Background(), inbound(peerID, lcpwire.MessageTypeComplete, terminalPayload)) svc := mustNewService(t, Params{ Clock: clock, @@ -486,24 +490,24 @@ func TestAcceptAndExecute_SuccessWaitsForResult(t *testing.T) { resp, err := svc.AcceptAndExecute(context.Background(), &lcpdv1.AcceptAndExecuteRequest{ PeerId: peerID, - JobId: append([]byte(nil), jobID[:]...), + CallId: append([]byte(nil), jobID[:]...), PayInvoice: true, }) if err != nil { t.Fatalf("AcceptAndExecute: %v", err) } - if got, want := resp.GetResult().GetStatus(), lcpdv1.Result_STATUS_OK; got != want { + if got, want := resp.GetComplete().GetStatus(), lcpdv1.Complete_STATUS_OK; got != want { t.Fatalf("status mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if got, want := string(resp.GetResult().GetResult()), "ok"; got != want { - t.Fatalf("result mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if diff := cmp.Diff(resultBytes, resp.GetComplete().GetResponseBytes()); diff != "" { + t.Fatalf("response_bytes mismatch (-want +got):\n%s", diff) } - if got, want := resp.GetResult().GetContentType(), resultContentType; got != want { - t.Fatalf("content_type mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if got, want := resp.GetComplete().GetResponseContentType(), resultContentType; got != want { + t.Fatalf("response_content_type mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if got, want := resp.GetResult().GetContentEncoding(), resultContentEncoding; got != want { - t.Fatalf("content_encoding mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if got, want := resp.GetComplete().GetResponseContentEncoding(), resultContentEncoding; got != want { + t.Fatalf("response_content_encoding mismatch (-want +got):\n%s", cmp.Diff(want, got)) } } @@ -530,31 +534,31 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) } termsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, JobID: jobID, PriceMsat: 123, QuoteExpiry: quoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: "openai.chat_completions.v1", - Input: requestJSON, - InputContentType: lcptasks.ContentTypeApplicationJSONUTF8, - InputContentEncoding: lcptasks.ContentEncodingIdentity, - Params: paramsBytes, + Method: "openai.chat_completions.v1", + Request: requestJSON, + RequestContentType: requestContentTypeJSON, + RequestContentEncoding: contentEncodingIdentity, + Params: paramsBytes, }) if err != nil { t.Fatalf("ComputeTermsHash: %v", err) } - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: append([]byte(nil), jobID[:]...), + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: append([]byte(nil), jobID[:]...), PriceMsat: 123, QuoteExpiry: timestamppb.New(time.Unix(int64(quoteExpiry), 0)), TermsHash: append([]byte(nil), termsHash[:]...), PaymentRequest: "lnbc1stream", } - if putErr := jobs.PutQuote(peerID, openaiChatTask("stream me", "model-stream"), terms); putErr != nil { + if putErr := jobs.PutQuote(peerID, openaiChatCall(t, "stream me", "model-stream"), quote); putErr != nil { t.Fatalf("PutQuote: %v", putErr) } @@ -589,20 +593,20 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { resultLen := uint64(len(resultBytes)) resultContentType := "text/event-stream; charset=utf-8" - resultContentEncoding := lcptasks.ContentEncodingIdentity + resultContentEncoding := contentEncodingIdentity var resultStreamID lcp.Hash32 resultStreamID[0] = 0x44 beginPayload, err := lcpwire.EncodeStreamBegin(lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: lcpwire.MsgID{}, Expiry: expiry, }, StreamID: resultStreamID, - Kind: lcpwire.StreamKindResult, + Kind: lcpwire.StreamKindResponse, ContentType: resultContentType, ContentEncoding: resultContentEncoding, }) @@ -611,9 +615,9 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { } chunkPayload, _, err := lcpwire.EncodeStreamChunk(lcpwire.StreamChunk{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, Expiry: expiry, }, StreamID: resultStreamID, @@ -625,9 +629,9 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { } endPayload, err := lcpwire.EncodeStreamEnd(lcpwire.StreamEnd{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: lcpwire.MsgID{}, Expiry: expiry, }, @@ -639,24 +643,24 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { t.Fatalf("EncodeStreamEnd: %v", err) } - terminalPayload, err := lcpwire.EncodeResult(lcpwire.Result{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + terminalPayload, err := lcpwire.EncodeComplete(lcpwire.Complete{ + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: lcpwire.MsgID{}, Expiry: expiry, }, - Status: lcpwire.ResultStatusOK, - OK: &lcpwire.ResultOK{ - ResultStreamID: resultStreamID, - ResultHash: resultHash, - ResultLen: resultLen, - ResultContentType: resultContentType, - ResultContentEncoding: resultContentEncoding, + Status: lcpwire.CompleteStatusOK, + OK: &lcpwire.CompleteOK{ + ResponseStreamID: resultStreamID, + ResponseHash: resultHash, + ResponseLen: resultLen, + ResponseContentType: resultContentType, + ResponseContentEncoding: resultContentEncoding, }, }) if err != nil { - t.Fatalf("EncodeResult: %v", err) + t.Fatalf("EncodeComplete: %v", err) } svc := mustNewService(t, Params{ @@ -673,7 +677,7 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { errCh <- svc.AcceptAndExecuteStream( &lcpdv1.AcceptAndExecuteStreamRequest{ PeerId: peerID, - JobId: append([]byte(nil), jobID[:]...), + CallId: append([]byte(nil), jobID[:]...), PayInvoice: true, }, stream, @@ -700,7 +704,7 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { ) waiter.HandleInboundCustomMessage( context.Background(), - inbound(peerID, lcpwire.MessageTypeResult, terminalPayload), + inbound(peerID, lcpwire.MessageTypeComplete, terminalPayload), ) select { @@ -716,13 +720,13 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { t.Fatalf("sent stream response count mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - begin := stream.sent[0].GetResultBegin() + begin := stream.sent[0].GetResponseBegin() if begin == nil { - t.Fatalf("expected first event to be result_begin") + t.Fatalf("expected first event to be response_begin") } if begin.GetContentType() != resultContentType || begin.GetContentEncoding() != resultContentEncoding { t.Fatalf( - "result_begin content type/encoding mismatch (-want +got):\n%s", + "response_begin content type/encoding mismatch (-want +got):\n%s", cmp.Diff( resultContentType+"/"+resultContentEncoding, begin.GetContentType()+"/"+begin.GetContentEncoding(), @@ -730,43 +734,43 @@ func TestAcceptAndExecuteStream_ForwardsResultStream(t *testing.T) { ) } - chunk := stream.sent[1].GetResultChunk() + chunk := stream.sent[1].GetResponseChunk() if chunk == nil { - t.Fatalf("expected second event to be result_chunk") + t.Fatalf("expected second event to be response_chunk") } if diff := cmp.Diff(resultBytes, chunk.GetData()); diff != "" { t.Fatalf("chunk data mismatch (-want +got):\n%s", diff) } - end := stream.sent[2].GetResultEnd() + end := stream.sent[2].GetResponseEnd() if end == nil { - t.Fatalf("expected third event to be result_end") + t.Fatalf("expected third event to be response_end") } - if end.GetResultLen() != resultLen { - t.Fatalf("result_end len mismatch (-want +got):\n%s", cmp.Diff(resultLen, end.GetResultLen())) + if end.GetResponseLen() != resultLen { + t.Fatalf("response_end len mismatch (-want +got):\n%s", cmp.Diff(resultLen, end.GetResponseLen())) } - if diff := cmp.Diff(resultHash[:], end.GetResultHash()); diff != "" { - t.Fatalf("result_end hash mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(resultHash[:], end.GetResponseHash()); diff != "" { + t.Fatalf("response_end hash mismatch (-want +got):\n%s", diff) } - result := stream.sent[3].GetResult() + result := stream.sent[3].GetComplete() if result == nil { - t.Fatalf("expected terminal result event") + t.Fatalf("expected terminal complete event") } - if got, want := result.GetStatus(), lcpdv1.Result_STATUS_OK; got != want { - t.Fatalf("result status mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if got, want := result.GetStatus(), lcpdv1.Complete_STATUS_OK; got != want { + t.Fatalf("complete status mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if diff := cmp.Diff(resultHash[:], result.GetResultHash()); diff != "" { - t.Fatalf("result hash mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(resultHash[:], result.GetResponseHash()); diff != "" { + t.Fatalf("complete response_hash mismatch (-want +got):\n%s", diff) } - if got := result.GetResultLen(); got != resultLen { - t.Fatalf("result len mismatch (-want +got):\n%s", cmp.Diff(resultLen, got)) + if got := result.GetResponseLen(); got != resultLen { + t.Fatalf("complete response_len mismatch (-want +got):\n%s", cmp.Diff(resultLen, got)) } - if got := result.GetContentType(); got != resultContentType { - t.Fatalf("result content_type mismatch (-want +got):\n%s", cmp.Diff(resultContentType, got)) + if got := result.GetResponseContentType(); got != resultContentType { + t.Fatalf("complete response_content_type mismatch (-want +got):\n%s", cmp.Diff(resultContentType, got)) } - if got := result.GetContentEncoding(); got != resultContentEncoding { - t.Fatalf("result content_encoding mismatch (-want +got):\n%s", cmp.Diff(resultContentEncoding, got)) + if got := result.GetResponseContentEncoding(); got != resultContentEncoding { + t.Fatalf("complete response_content_encoding mismatch (-want +got):\n%s", cmp.Diff(resultContentEncoding, got)) } } @@ -791,30 +795,30 @@ func TestAcceptAndExecute_FailsOnInvoiceAmountMismatch(t *testing.T) { t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) } termsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, JobID: jobID, PriceMsat: 123, QuoteExpiry: quoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: "openai.chat_completions.v1", - Input: requestJSON, - InputContentType: lcptasks.ContentTypeApplicationJSONUTF8, - InputContentEncoding: lcptasks.ContentEncodingIdentity, - Params: paramsBytes, + Method: "openai.chat_completions.v1", + Request: requestJSON, + RequestContentType: requestContentTypeJSON, + RequestContentEncoding: contentEncodingIdentity, + Params: paramsBytes, }) if err != nil { t.Fatalf("ComputeTermsHash: %v", err) } - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: append([]byte(nil), jobID[:]...), + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: append([]byte(nil), jobID[:]...), PriceMsat: 123, QuoteExpiry: timestamppb.New(time.Unix(int64(quoteExpiry), 0)), TermsHash: append([]byte(nil), termsHash[:]...), PaymentRequest: "lnbc1dummy", } - if putErr := jobs.PutQuote(peerID, openaiChatTask("hello", "model-1"), terms); putErr != nil { + if putErr := jobs.PutQuote(peerID, openaiChatCall(t, "hello", "model-1"), quote); putErr != nil { t.Fatalf("PutQuote: %v", putErr) } @@ -840,7 +844,7 @@ func TestAcceptAndExecute_FailsOnInvoiceAmountMismatch(t *testing.T) { _, err = svc.AcceptAndExecute(context.Background(), &lcpdv1.AcceptAndExecuteRequest{ PeerId: peerID, - JobId: append([]byte(nil), jobID[:]...), + CallId: append([]byte(nil), jobID[:]...), PayInvoice: true, }) assertStatusCode(t, err, codes.FailedPrecondition) @@ -867,30 +871,30 @@ func TestAcceptAndExecute_FailsOnInvoiceExpiryMismatch(t *testing.T) { t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) } termsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, JobID: jobID, PriceMsat: 123, QuoteExpiry: quoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: "openai.chat_completions.v1", - Input: requestJSON, - InputContentType: lcptasks.ContentTypeApplicationJSONUTF8, - InputContentEncoding: lcptasks.ContentEncodingIdentity, - Params: paramsBytes, + Method: "openai.chat_completions.v1", + Request: requestJSON, + RequestContentType: requestContentTypeJSON, + RequestContentEncoding: contentEncodingIdentity, + Params: paramsBytes, }) if err != nil { t.Fatalf("ComputeTermsHash: %v", err) } - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: append([]byte(nil), jobID[:]...), + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: append([]byte(nil), jobID[:]...), PriceMsat: 123, QuoteExpiry: timestamppb.New(time.Unix(int64(quoteExpiry), 0)), TermsHash: append([]byte(nil), termsHash[:]...), PaymentRequest: "lnbc1dummy", } - if putErr := jobs.PutQuote(peerID, openaiChatTask("hello", "model-1"), terms); putErr != nil { + if putErr := jobs.PutQuote(peerID, openaiChatCall(t, "hello", "model-1"), quote); putErr != nil { t.Fatalf("PutQuote: %v", putErr) } @@ -916,7 +920,7 @@ func TestAcceptAndExecute_FailsOnInvoiceExpiryMismatch(t *testing.T) { _, err = svc.AcceptAndExecute(context.Background(), &lcpdv1.AcceptAndExecuteRequest{ PeerId: peerID, - JobId: append([]byte(nil), jobID[:]...), + CallId: append([]byte(nil), jobID[:]...), PayInvoice: true, }) assertStatusCode(t, err, codes.FailedPrecondition) @@ -943,30 +947,30 @@ func TestAcceptAndExecute_AllowedClockSkewIsConfigurableViaEnv(t *testing.T) { t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) } termsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, JobID: jobID, PriceMsat: 123, QuoteExpiry: quoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: "openai.chat_completions.v1", - Input: requestJSON, - InputContentType: lcptasks.ContentTypeApplicationJSONUTF8, - InputContentEncoding: lcptasks.ContentEncodingIdentity, - Params: paramsBytes, + Method: "openai.chat_completions.v1", + Request: requestJSON, + RequestContentType: requestContentTypeJSON, + RequestContentEncoding: contentEncodingIdentity, + Params: paramsBytes, }) if err != nil { t.Fatalf("ComputeTermsHash: %v", err) } - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: append([]byte(nil), jobID[:]...), + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: append([]byte(nil), jobID[:]...), PriceMsat: 123, QuoteExpiry: timestamppb.New(time.Unix(int64(quoteExpiry), 0)), TermsHash: append([]byte(nil), termsHash[:]...), PaymentRequest: "lnbc1dummy", } - if putErr := jobs.PutQuote(peerID, openaiChatTask("hello", "model-1"), terms); putErr != nil { + if putErr := jobs.PutQuote(peerID, openaiChatCall(t, "hello", "model-1"), quote); putErr != nil { t.Fatalf("PutQuote: %v", putErr) } @@ -992,7 +996,7 @@ func TestAcceptAndExecute_AllowedClockSkewIsConfigurableViaEnv(t *testing.T) { _, err = svc.AcceptAndExecute(context.Background(), &lcpdv1.AcceptAndExecuteRequest{ PeerId: peerID, - JobId: append([]byte(nil), jobID[:]...), + CallId: append([]byte(nil), jobID[:]...), PayInvoice: true, }) assertStatusCode(t, err, codes.FailedPrecondition) @@ -1010,7 +1014,7 @@ func TestAcceptAndExecute_RejectsPayInvoiceFalse(t *testing.T) { _, err := svc.AcceptAndExecute(context.Background(), &lcpdv1.AcceptAndExecuteRequest{ PeerId: "02" + strings.Repeat("f", 64), - JobId: make([]byte, 32), + CallId: make([]byte, 32), PayInvoice: false, }) assertStatusCode(t, err, codes.InvalidArgument) @@ -1028,7 +1032,7 @@ func TestAcceptAndExecute_JobNotFound(t *testing.T) { _, err := svc.AcceptAndExecute(context.Background(), &lcpdv1.AcceptAndExecuteRequest{ PeerId: "02" + strings.Repeat("f", 64), - JobId: make([]byte, 32), + CallId: make([]byte, 32), PayInvoice: true, }) assertStatusCode(t, err, codes.NotFound) @@ -1056,7 +1060,7 @@ func TestCancelJob_SendsMessage(t *testing.T) { resp, err := svc.CancelJob(context.Background(), &lcpdv1.CancelJobRequest{ PeerId: peerID, - JobId: append([]byte(nil), jobID[:]...), + CallId: append([]byte(nil), jobID[:]...), Reason: "stop", }) if err != nil { @@ -1110,15 +1114,24 @@ func openaiChatCompletionsRequestJSON(prompt, model string) []byte { ) } -func openaiChatTask(prompt, model string) *lcpdv1.Task { +func openaiChatCall(t *testing.T, prompt, model string) *lcpdv1.CallSpec { + t.Helper() + + paramsBytes, err := lcpwire.EncodeOpenAIChatCompletionsV1Params( + lcpwire.OpenAIChatCompletionsV1Params{Model: model}, + ) + if err != nil { + t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) + } + requestJSON := openaiChatCompletionsRequestJSON(prompt, model) - return &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: model}, - }, - }, + + return &lcpdv1.CallSpec{ + Method: "openai.chat_completions.v1", + Params: paramsBytes, + RequestBytes: requestJSON, + RequestContentType: requestContentTypeJSON, + RequestContentEncoding: contentEncodingIdentity, } } @@ -1133,8 +1146,8 @@ func newQuoteRespondingFakeMessenger( return &fakeMessenger{ sendFn: func(_ context.Context, msg sentMsg) error { switch msg.msgType { - case lcpwire.MessageTypeQuoteRequest: - return respondToQuoteRequest( + case lcpwire.MessageTypeCall: + return respondToCall( peerID, clock, waiter, @@ -1147,11 +1160,11 @@ func newQuoteRespondingFakeMessenger( lcpwire.MessageTypeStreamChunk, lcpwire.MessageTypeStreamEnd: // Ignore the input stream; this fake provider is only validating - // quote_request/quote_response hashing for the test. + // call/quote hashing for the test. return nil case lcpwire.MessageTypeManifest, - lcpwire.MessageTypeQuoteResponse, - lcpwire.MessageTypeResult, + lcpwire.MessageTypeQuote, + lcpwire.MessageTypeComplete, lcpwire.MessageTypeCancel, lcpwire.MessageTypeError: return errors.New("unexpected outbound message type") @@ -1162,7 +1175,7 @@ func newQuoteRespondingFakeMessenger( } } -func respondToQuoteRequest( +func respondToCall( peerID string, clock fakeClock, waiter *requesterwait.Waiter, @@ -1171,7 +1184,7 @@ func respondToQuoteRequest( priceMsat uint64, paymentRequest string, ) error { - req, err := lcpwire.DecodeQuoteRequest(payload) + req, err := lcpwire.DecodeCall(payload) if err != nil { return err } @@ -1184,24 +1197,24 @@ func respondToQuoteRequest( termsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + JobID: req.Envelope.CallID, PriceMsat: priceMsat, QuoteExpiry: quoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: req.TaskKind, - Input: inputBytes, - InputContentType: lcptasks.ContentTypeApplicationJSONUTF8, - InputContentEncoding: lcptasks.ContentEncodingIdentity, - Params: paramsBytes, + Method: req.Method, + Request: inputBytes, + RequestContentType: requestContentTypeJSON, + RequestContentEncoding: contentEncodingIdentity, + Params: paramsBytes, }) if err != nil { return err } - respPayload, err := lcpwire.EncodeQuoteResponse(lcpwire.QuoteResponse{ - Envelope: lcpwire.JobEnvelope{ + respPayload, err := lcpwire.EncodeQuote(lcpwire.Quote{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: lcpwire.MsgID{}, Expiry: req.Envelope.Expiry, }, @@ -1216,7 +1229,7 @@ func respondToQuoteRequest( waiter.HandleInboundCustomMessage( context.Background(), - inbound(peerID, lcpwire.MessageTypeQuoteResponse, respPayload), + inbound(peerID, lcpwire.MessageTypeQuote, respPayload), ) return nil } @@ -1234,39 +1247,39 @@ func assertRequestQuoteResponse( t.Fatalf("peer_id mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - terms := resp.GetTerms() - if terms == nil { - t.Fatalf("RequestQuote: terms is nil") + quote := resp.GetQuote() + if quote == nil { + t.Fatalf("RequestQuote: quote is nil") } - if got, want := terms.GetPriceMsat(), wantPriceMsat; got != want { + if got, want := quote.GetPriceMsat(), wantPriceMsat; got != want { t.Fatalf("price_msat mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if got, want := terms.GetPaymentRequest(), wantPaymentRequest; got != want { + if got, want := quote.GetPaymentRequest(), wantPaymentRequest; got != want { t.Fatalf("payment_request mismatch (-want +got):\n%s", cmp.Diff(want, got)) } } -func assertTermsStored( +func assertQuoteStored( t *testing.T, jobs *requesterjobstore.Store, peerID string, - terms *lcpdv1.Terms, + quote *lcpdv1.Quote, wantPaymentRequest string, ) { t.Helper() - if terms == nil { - t.Fatalf("terms is nil") + if quote == nil { + t.Fatalf("quote is nil") } - storedJobID, err := toJobID(terms.GetJobId()) + storedJobID, err := toJobID(quote.GetCallId()) if err != nil { t.Fatalf("toJobID: %v", err) } - storedTerms, err := jobs.GetTerms(peerID, storedJobID) + storedQuote, err := jobs.GetQuote(peerID, storedJobID) if err != nil { - t.Fatalf("jobs.GetTerms: %v", err) + t.Fatalf("jobs.GetQuote: %v", err) } - if got, want := storedTerms.GetPaymentRequest(), wantPaymentRequest; got != want { + if got, want := storedQuote.GetPaymentRequest(), wantPaymentRequest; got != want { t.Fatalf("stored payment_request mismatch (-want +got):\n%s", cmp.Diff(want, got)) } } @@ -1275,7 +1288,7 @@ func assertSentQuoteRequestAndInputStream(t *testing.T, msgs []sentMsg, inputByt t.Helper() wantTypes := []lcpwire.MessageType{ - lcpwire.MessageTypeQuoteRequest, + lcpwire.MessageTypeCall, lcpwire.MessageTypeStreamBegin, lcpwire.MessageTypeStreamChunk, lcpwire.MessageTypeStreamEnd, @@ -1289,9 +1302,9 @@ func assertSentQuoteRequestAndInputStream(t *testing.T, msgs []sentMsg, inputByt } } - wireReq := mustDecodeQuoteRequest(t, msgs[0].payload) + wireReq := mustDecodeCall(t, msgs[0].payload) begin := mustDecodeStreamBegin(t, msgs[1].payload) - if got, want := begin.Kind, lcpwire.StreamKindInput; got != want { + if got, want := begin.Kind, lcpwire.StreamKindRequest; got != want { t.Fatalf("stream_begin.kind mismatch (-want +got):\n%s", cmp.Diff(want, got)) } @@ -1299,7 +1312,7 @@ func assertSentQuoteRequestAndInputStream(t *testing.T, msgs []sentMsg, inputByt wantInputHash := lcp.Hash32(inputSum) wantInputLen := uint64(len(inputBytes)) - if got, want := begin.Envelope.JobID, wireReq.Envelope.JobID; got != want { + if got, want := begin.Envelope.CallID, wireReq.Envelope.CallID; got != want { t.Fatalf("stream_begin.job_id mismatch (-want +got):\n%s", cmp.Diff(want, got)) } if begin.TotalLen == nil || *begin.TotalLen != wantInputLen { @@ -1308,15 +1321,15 @@ func assertSentQuoteRequestAndInputStream(t *testing.T, msgs []sentMsg, inputByt if begin.SHA256 == nil || *begin.SHA256 != wantInputHash { t.Fatalf("stream_begin.sha256 mismatch (-want +got):\n%s", cmp.Diff(wantInputHash, begin.SHA256)) } - if got, want := begin.ContentType, lcptasks.ContentTypeApplicationJSONUTF8; got != want { + if got, want := begin.ContentType, requestContentTypeJSON; got != want { t.Fatalf("stream_begin.content_type mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if got, want := begin.ContentEncoding, lcptasks.ContentEncodingIdentity; got != want { + if got, want := begin.ContentEncoding, contentEncodingIdentity; got != want { t.Fatalf("stream_begin.content_encoding mismatch (-want +got):\n%s", cmp.Diff(want, got)) } chunk := mustDecodeStreamChunk(t, msgs[2].payload) - if got, want := chunk.Envelope.JobID, wireReq.Envelope.JobID; got != want { + if got, want := chunk.Envelope.CallID, wireReq.Envelope.CallID; got != want { t.Fatalf("stream_chunk.job_id mismatch (-want +got):\n%s", cmp.Diff(want, got)) } if got, want := chunk.StreamID, begin.StreamID; got != want { @@ -1330,7 +1343,7 @@ func assertSentQuoteRequestAndInputStream(t *testing.T, msgs []sentMsg, inputByt } end := mustDecodeStreamEnd(t, msgs[3].payload) - if got, want := end.Envelope.JobID, wireReq.Envelope.JobID; got != want { + if got, want := end.Envelope.CallID, wireReq.Envelope.CallID; got != want { t.Fatalf("stream_end.job_id mismatch (-want +got):\n%s", cmp.Diff(want, got)) } if got, want := end.StreamID, begin.StreamID; got != want { @@ -1344,12 +1357,12 @@ func assertSentQuoteRequestAndInputStream(t *testing.T, msgs []sentMsg, inputByt } } -func mustDecodeQuoteRequest(t *testing.T, payload []byte) lcpwire.QuoteRequest { +func mustDecodeCall(t *testing.T, payload []byte) lcpwire.Call { t.Helper() - req, err := lcpwire.DecodeQuoteRequest(payload) + req, err := lcpwire.DecodeCall(payload) if err != nil { - t.Fatalf("DecodeQuoteRequest: %v", err) + t.Fatalf("DecodeCall: %v", err) } return req } diff --git a/go-lcpd/internal/inbounddispatch/handler.go b/go-lcpd/internal/inbounddispatch/handler.go index 98db1de..4ede6cd 100644 --- a/go-lcpd/internal/inbounddispatch/handler.go +++ b/go-lcpd/internal/inbounddispatch/handler.go @@ -30,16 +30,16 @@ func (h *Handler) HandleInboundCustomMessage( } // Note: lndpeermsg.PeerMessaging already handles `lcp_manifest` and unknown-even - // disconnect behavior via its Router. This handler only wires job-scope + // disconnect behavior via its Router. This handler only wires call-scope // messages to the correct subsystem per spec. switch lcpwire.MessageType(msg.MsgType) { - case lcpwire.MessageTypeQuoteRequest, + case lcpwire.MessageTypeCall, lcpwire.MessageTypeCancel: if h.provider != nil { h.provider.HandleInboundCustomMessage(ctx, msg) } - case lcpwire.MessageTypeQuoteResponse, - lcpwire.MessageTypeResult, + case lcpwire.MessageTypeQuote, + lcpwire.MessageTypeComplete, lcpwire.MessageTypeError: if h.requester != nil { h.requester.HandleInboundCustomMessage(ctx, msg) diff --git a/go-lcpd/internal/inbounddispatch/handler_test.go b/go-lcpd/internal/inbounddispatch/handler_test.go index fd7904b..6a7f043 100644 --- a/go-lcpd/internal/inbounddispatch/handler_test.go +++ b/go-lcpd/internal/inbounddispatch/handler_test.go @@ -41,8 +41,8 @@ func TestHandler_DispatchesPerSpec(t *testing.T) { wantRequester int }{ { - name: "quote_request -> provider", - msgType: uint16(lcpwire.MessageTypeQuoteRequest), + name: "call -> provider", + msgType: uint16(lcpwire.MessageTypeCall), wantProvider: 1, wantRequester: 0, }, @@ -53,14 +53,14 @@ func TestHandler_DispatchesPerSpec(t *testing.T) { wantRequester: 0, }, { - name: "quote_response -> requester", - msgType: uint16(lcpwire.MessageTypeQuoteResponse), + name: "quote -> requester", + msgType: uint16(lcpwire.MessageTypeQuote), wantProvider: 0, wantRequester: 1, }, { - name: "result -> requester", - msgType: uint16(lcpwire.MessageTypeResult), + name: "complete -> requester", + msgType: uint16(lcpwire.MessageTypeComplete), wantProvider: 0, wantRequester: 1, }, @@ -70,6 +70,12 @@ func TestHandler_DispatchesPerSpec(t *testing.T) { wantProvider: 0, wantRequester: 1, }, + { + name: "stream_begin -> both", + msgType: uint16(lcpwire.MessageTypeStreamBegin), + wantProvider: 1, + wantRequester: 1, + }, { name: "manifest -> nobody (handled upstream)", msgType: uint16(lcpwire.MessageTypeManifest), diff --git a/go-lcpd/internal/jobstore/store.go b/go-lcpd/internal/jobstore/store.go index 62a23da..0502ec9 100644 --- a/go-lcpd/internal/jobstore/store.go +++ b/go-lcpd/internal/jobstore/store.go @@ -33,7 +33,7 @@ type Job struct { JobID lcp.JobID State State - QuoteRequest *lcpwire.QuoteRequest + Call *lcpwire.Call InputStream *InputStreamState @@ -42,7 +42,7 @@ type Job struct { PaymentHash *lcp.Hash32 InvoiceAddIndex *uint64 - QuoteResponse *lcpwire.QuoteResponse + Quote *lcpwire.Quote CreatedAt time.Time } @@ -141,7 +141,7 @@ func (s *Store) UpdateState(key Key, state State) bool { return true } -func (s *Store) SetQuoteResponse(key Key, resp lcpwire.QuoteResponse) bool { +func (s *Store) SetQuote(key Key, quote lcpwire.Quote) bool { s.mu.Lock() defer s.mu.Unlock() @@ -150,11 +150,11 @@ func (s *Store) SetQuoteResponse(key Key, resp lcpwire.QuoteResponse) bool { return false } - respCopy := resp - job.QuoteResponse = &respCopy - job.QuoteExpiry = resp.QuoteExpiry + quoteCopy := quote + job.Quote = "eCopy + job.QuoteExpiry = quote.QuoteExpiry - termsCopy := resp.TermsHash + termsCopy := quote.TermsHash job.TermsHash = &termsCopy s.jobs[key] = job @@ -184,13 +184,13 @@ func (s *Store) Len() int { } func cloneJob(job Job) Job { - if job.QuoteRequest != nil { - reqCopy := *job.QuoteRequest + if job.Call != nil { + reqCopy := *job.Call if reqCopy.ParamsBytes != nil { paramsCopy := append([]byte(nil), (*reqCopy.ParamsBytes)...) reqCopy.ParamsBytes = ¶msCopy } - job.QuoteRequest = &reqCopy + job.Call = &reqCopy } if job.InputStream != nil { streamCopy := *job.InputStream @@ -211,9 +211,9 @@ func cloneJob(job Job) Job { addIndexCopy := *job.InvoiceAddIndex job.InvoiceAddIndex = &addIndexCopy } - if job.QuoteResponse != nil { - respCopy := *job.QuoteResponse - job.QuoteResponse = &respCopy + if job.Quote != nil { + quoteCopy := *job.Quote + job.Quote = "eCopy } return job } diff --git a/go-lcpd/internal/jobstore/store_test.go b/go-lcpd/internal/jobstore/store_test.go index 88f57ac..b03df97 100644 --- a/go-lcpd/internal/jobstore/store_test.go +++ b/go-lcpd/internal/jobstore/store_test.go @@ -67,7 +67,7 @@ func TestStore_UpdateState(t *testing.T) { } } -func TestStore_SetQuoteResponse(t *testing.T) { +func TestStore_SetQuote(t *testing.T) { t.Parallel() store := jobstore.New() @@ -81,10 +81,10 @@ func TestStore_SetQuoteResponse(t *testing.T) { State: jobstore.StateQuoted, }) - quoteResp := lcpwire.QuoteResponse{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + quote := lcpwire.Quote{ + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: jobID, MsgID: msgID, Expiry: 77, }, @@ -94,22 +94,22 @@ func TestStore_SetQuoteResponse(t *testing.T) { PaymentRequest: "bolt11", } - if ok := store.SetQuoteResponse(key, quoteResp); !ok { - t.Fatalf("SetQuoteResponse returned false for existing job") + if ok := store.SetQuote(key, quote); !ok { + t.Fatalf("SetQuote returned false for existing job") } got, ok := store.Get(key) if !ok { - t.Fatalf("job not found after SetQuoteResponse") + t.Fatalf("job not found after SetQuote") } - if got.QuoteExpiry != quoteResp.QuoteExpiry { - t.Fatalf("QuoteExpiry mismatch: got %d want %d", got.QuoteExpiry, quoteResp.QuoteExpiry) + if got.QuoteExpiry != quote.QuoteExpiry { + t.Fatalf("QuoteExpiry mismatch: got %d want %d", got.QuoteExpiry, quote.QuoteExpiry) } - if got.QuoteResponse == nil { - t.Fatalf("QuoteResponse not stored") + if got.Quote == nil { + t.Fatalf("Quote not stored") } - if diff := cmp.Diff(quoteResp, *got.QuoteResponse); diff != "" { - t.Fatalf("QuoteResponse mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(quote, *got.Quote); diff != "" { + t.Fatalf("Quote mismatch (-want +got):\n%s", diff) } if got.TermsHash == nil { t.Fatalf("TermsHash not stored") diff --git a/go-lcpd/internal/lcpmsgrouter/router.go b/go-lcpd/internal/lcpmsgrouter/router.go index b02145c..dbe7367 100644 --- a/go-lcpd/internal/lcpmsgrouter/router.go +++ b/go-lcpd/internal/lcpmsgrouter/router.go @@ -5,17 +5,17 @@ import "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" type RouteAction string const ( - RouteActionDispatchManifest RouteAction = "dispatch_manifest" - RouteActionDispatchQuoteRequest RouteAction = "dispatch_quote_request" - RouteActionDispatchQuoteResponse RouteAction = "dispatch_quote_response" - RouteActionDispatchStreamBegin RouteAction = "dispatch_stream_begin" - RouteActionDispatchStreamChunk RouteAction = "dispatch_stream_chunk" - RouteActionDispatchStreamEnd RouteAction = "dispatch_stream_end" - RouteActionDispatchResult RouteAction = "dispatch_result" - RouteActionDispatchCancel RouteAction = "dispatch_cancel" - RouteActionDispatchError RouteAction = "dispatch_error" - RouteActionIgnore RouteAction = "ignore" - RouteActionDisconnect RouteAction = "disconnect" + RouteActionDispatchManifest RouteAction = "dispatch_manifest" + RouteActionDispatchCall RouteAction = "dispatch_call" + RouteActionDispatchQuote RouteAction = "dispatch_quote" + RouteActionDispatchStreamBegin RouteAction = "dispatch_stream_begin" + RouteActionDispatchStreamChunk RouteAction = "dispatch_stream_chunk" + RouteActionDispatchStreamEnd RouteAction = "dispatch_stream_end" + RouteActionDispatchComplete RouteAction = "dispatch_complete" + RouteActionDispatchCancel RouteAction = "dispatch_cancel" + RouteActionDispatchError RouteAction = "dispatch_error" + RouteActionIgnore RouteAction = "ignore" + RouteActionDisconnect RouteAction = "disconnect" ) type RouteDecision struct { @@ -46,15 +46,15 @@ func (router) Route(msg CustomMessage) RouteDecision { Action: RouteActionDispatchManifest, Reason: "lcp_manifest", } - case lcpwire.MessageTypeQuoteRequest: + case lcpwire.MessageTypeCall: return RouteDecision{ - Action: RouteActionDispatchQuoteRequest, - Reason: "lcp_quote_request", + Action: RouteActionDispatchCall, + Reason: "lcp_call", } - case lcpwire.MessageTypeQuoteResponse: + case lcpwire.MessageTypeQuote: return RouteDecision{ - Action: RouteActionDispatchQuoteResponse, - Reason: "lcp_quote_response", + Action: RouteActionDispatchQuote, + Reason: "lcp_quote", } case lcpwire.MessageTypeStreamBegin: return RouteDecision{ @@ -71,10 +71,10 @@ func (router) Route(msg CustomMessage) RouteDecision { Action: RouteActionDispatchStreamEnd, Reason: "lcp_stream_end", } - case lcpwire.MessageTypeResult: + case lcpwire.MessageTypeComplete: return RouteDecision{ - Action: RouteActionDispatchResult, - Reason: "lcp_result", + Action: RouteActionDispatchComplete, + Reason: "lcp_complete", } case lcpwire.MessageTypeCancel: return RouteDecision{ diff --git a/go-lcpd/internal/lcpmsgrouter/router_test.go b/go-lcpd/internal/lcpmsgrouter/router_test.go index ae0d395..a6bc28d 100644 --- a/go-lcpd/internal/lcpmsgrouter/router_test.go +++ b/go-lcpd/internal/lcpmsgrouter/router_test.go @@ -27,19 +27,19 @@ func TestRouter_Route_KnownMessages(t *testing.T) { }, }, { - name: "quote_request", - msgType: lcpwire.MessageTypeQuoteRequest, + name: "call", + msgType: lcpwire.MessageTypeCall, want: lcpmsgrouter.RouteDecision{ - Action: lcpmsgrouter.RouteActionDispatchQuoteRequest, - Reason: "lcp_quote_request", + Action: lcpmsgrouter.RouteActionDispatchCall, + Reason: "lcp_call", }, }, { - name: "quote_response", - msgType: lcpwire.MessageTypeQuoteResponse, + name: "quote", + msgType: lcpwire.MessageTypeQuote, want: lcpmsgrouter.RouteDecision{ - Action: lcpmsgrouter.RouteActionDispatchQuoteResponse, - Reason: "lcp_quote_response", + Action: lcpmsgrouter.RouteActionDispatchQuote, + Reason: "lcp_quote", }, }, { @@ -67,11 +67,11 @@ func TestRouter_Route_KnownMessages(t *testing.T) { }, }, { - name: "result", - msgType: lcpwire.MessageTypeResult, + name: "complete", + msgType: lcpwire.MessageTypeComplete, want: lcpmsgrouter.RouteDecision{ - Action: lcpmsgrouter.RouteActionDispatchResult, - Reason: "lcp_result", + Action: lcpmsgrouter.RouteActionDispatchComplete, + Reason: "lcp_complete", }, }, { diff --git a/go-lcpd/internal/lcptasks/lcptasks.go b/go-lcpd/internal/lcptasks/lcptasks.go deleted file mode 100644 index a5c24b9..0000000 --- a/go-lcpd/internal/lcptasks/lcptasks.go +++ /dev/null @@ -1,265 +0,0 @@ -package lcptasks - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "unicode/utf8" - - "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" - lcpdv1 "github.com/bruwbird/lcp/proto-go/lcpd/v1" -) - -const ( - TaskKindOpenAIChatCompletionsV1 = "openai.chat_completions.v1" - TaskKindOpenAIResponsesV1 = "openai.responses.v1" -) - -var ( - ErrTaskRequired = errors.New("task is required") - ErrTaskSpecRequired = errors.New("task spec is required") - ErrUnsupportedTask = errors.New("unsupported task") - - ErrOpenAIChatCompletionsV1TaskRequired = errors.New( - "openai_chat_completions_v1 task is required", - ) - ErrOpenAIChatCompletionsV1RequestJSONRequired = errors.New( - "openai_chat_completions_v1.request_json is required", - ) - ErrOpenAIChatCompletionsV1RequestJSONInvalid = errors.New( - "openai_chat_completions_v1.request_json must be valid json", - ) - ErrOpenAIChatCompletionsV1ParamsRequired = errors.New( - "openai_chat_completions_v1.params is required", - ) - ErrOpenAIChatCompletionsV1ModelRequired = errors.New( - "openai_chat_completions_v1.params.model is required", - ) - ErrOpenAIChatCompletionsV1ModelInvalid = errors.New( - "openai_chat_completions_v1.params.model must be valid UTF-8", - ) - ErrOpenAIChatCompletionsV1RequestModelRequired = errors.New( - "openai_chat_completions_v1.request_json.model is required", - ) - ErrOpenAIChatCompletionsV1RequestMessagesRequired = errors.New( - "openai_chat_completions_v1.request_json.messages is required", - ) - ErrOpenAIChatCompletionsV1ModelMismatch = errors.New( - "openai_chat_completions_v1.params.model must match request_json.model", - ) - - ErrOpenAIResponsesV1TaskRequired = errors.New( - "openai_responses_v1 task is required", - ) - ErrOpenAIResponsesV1RequestJSONRequired = errors.New( - "openai_responses_v1.request_json is required", - ) - ErrOpenAIResponsesV1RequestJSONInvalid = errors.New( - "openai_responses_v1.request_json must be valid json", - ) - ErrOpenAIResponsesV1ParamsRequired = errors.New( - "openai_responses_v1.params is required", - ) - ErrOpenAIResponsesV1ModelRequired = errors.New( - "openai_responses_v1.params.model is required", - ) - ErrOpenAIResponsesV1ModelInvalid = errors.New( - "openai_responses_v1.params.model must be valid UTF-8", - ) - ErrOpenAIResponsesV1RequestModelRequired = errors.New( - "openai_responses_v1.request_json.model is required", - ) - ErrOpenAIResponsesV1RequestInputRequired = errors.New( - "openai_responses_v1.request_json.input is required", - ) - ErrOpenAIResponsesV1ModelMismatch = errors.New( - "openai_responses_v1.params.model must match request_json.model", - ) -) - -type QuoteRequestTask struct { - TaskKind string - - ParamsBytes []byte -} - -type InputStream struct { - DecodedBytes []byte - ContentType string - ContentEncoding string -} - -const ( - ContentTypeTextPlainUTF8 = "text/plain; charset=utf-8" - ContentTypeApplicationJSONUTF8 = "application/json; charset=utf-8" - ContentEncodingIdentity = "identity" -) - -// ValidateTask ensures the proto Task adheres to the strict LCP v0.2 rules. -func ValidateTask(task *lcpdv1.Task) error { - if task == nil { - return ErrTaskRequired - } - - switch spec := task.GetSpec().(type) { - case nil: - return ErrTaskSpecRequired - case *lcpdv1.Task_OpenaiChatCompletionsV1: - return validateOpenAIChatCompletionsV1(spec.OpenaiChatCompletionsV1) - case *lcpdv1.Task_OpenaiResponsesV1: - return validateOpenAIResponsesV1(spec.OpenaiResponsesV1) - default: - return ErrUnsupportedTask - } -} - -// ToWireQuoteRequestTask converts a proto Task into the wire representation used -// by lcp_quote_request (task_kind/params_bytes). -func ToWireQuoteRequestTask(task *lcpdv1.Task) (QuoteRequestTask, error) { - if err := ValidateTask(task); err != nil { - return QuoteRequestTask{}, err - } - - switch spec := task.GetSpec().(type) { - case *lcpdv1.Task_OpenaiChatCompletionsV1: - openaiTask := spec.OpenaiChatCompletionsV1 - model := openaiTask.GetParams().GetModel() - - openaiParams := lcpwire.OpenAIChatCompletionsV1Params{Model: model} - paramsBytes, err := lcpwire.EncodeOpenAIChatCompletionsV1Params(openaiParams) - if err != nil { - return QuoteRequestTask{}, fmt.Errorf("encode openai_chat_completions_v1 params: %w", err) - } - - return QuoteRequestTask{ - TaskKind: TaskKindOpenAIChatCompletionsV1, - ParamsBytes: paramsBytes, - }, nil - case *lcpdv1.Task_OpenaiResponsesV1: - openaiTask := spec.OpenaiResponsesV1 - model := openaiTask.GetParams().GetModel() - - openaiParams := lcpwire.OpenAIResponsesV1Params{Model: model} - paramsBytes, err := lcpwire.EncodeOpenAIResponsesV1Params(openaiParams) - if err != nil { - return QuoteRequestTask{}, fmt.Errorf("encode openai_responses_v1 params: %w", err) - } - - return QuoteRequestTask{ - TaskKind: TaskKindOpenAIResponsesV1, - ParamsBytes: paramsBytes, - }, nil - default: - return QuoteRequestTask{}, ErrUnsupportedTask - } -} - -// ToWireInputStream converts a proto Task into the input stream (decoded bytes -// plus metadata) as defined in `protocol/protocol.md` (LCP v0.2). -func ToWireInputStream(task *lcpdv1.Task) (InputStream, error) { - if err := ValidateTask(task); err != nil { - return InputStream{}, err - } - - switch spec := task.GetSpec().(type) { - case *lcpdv1.Task_OpenaiChatCompletionsV1: - openaiTask := spec.OpenaiChatCompletionsV1 - reqBytes := append([]byte(nil), openaiTask.GetRequestJson()...) - return InputStream{ - DecodedBytes: reqBytes, - ContentType: ContentTypeApplicationJSONUTF8, - ContentEncoding: ContentEncodingIdentity, - }, nil - case *lcpdv1.Task_OpenaiResponsesV1: - openaiTask := spec.OpenaiResponsesV1 - reqBytes := append([]byte(nil), openaiTask.GetRequestJson()...) - return InputStream{ - DecodedBytes: reqBytes, - ContentType: ContentTypeApplicationJSONUTF8, - ContentEncoding: ContentEncodingIdentity, - }, nil - default: - return InputStream{}, ErrUnsupportedTask - } -} - -func validateOpenAIChatCompletionsV1(spec *lcpdv1.OpenAIChatCompletionsV1TaskSpec) error { - if spec == nil { - return ErrOpenAIChatCompletionsV1TaskRequired - } - if len(spec.GetRequestJson()) == 0 { - return ErrOpenAIChatCompletionsV1RequestJSONRequired - } - - params := spec.GetParams() - if params == nil { - return ErrOpenAIChatCompletionsV1ParamsRequired - } - if params.GetModel() == "" { - return ErrOpenAIChatCompletionsV1ModelRequired - } - if !utf8.ValidString(params.GetModel()) { - return ErrOpenAIChatCompletionsV1ModelInvalid - } - - var parsed struct { - Model string `json:"model"` - Messages []json.RawMessage `json:"messages"` - Stream *bool `json:"stream,omitempty"` - } - if err := json.Unmarshal(spec.GetRequestJson(), &parsed); err != nil { - return fmt.Errorf("%w: %s", ErrOpenAIChatCompletionsV1RequestJSONInvalid, err.Error()) - } - if parsed.Model == "" { - return ErrOpenAIChatCompletionsV1RequestModelRequired - } - if len(parsed.Messages) == 0 { - return ErrOpenAIChatCompletionsV1RequestMessagesRequired - } - if parsed.Model != params.GetModel() { - return ErrOpenAIChatCompletionsV1ModelMismatch - } - - return nil -} - -func validateOpenAIResponsesV1(spec *lcpdv1.OpenAIResponsesV1TaskSpec) error { - if spec == nil { - return ErrOpenAIResponsesV1TaskRequired - } - if len(spec.GetRequestJson()) == 0 { - return ErrOpenAIResponsesV1RequestJSONRequired - } - - params := spec.GetParams() - if params == nil { - return ErrOpenAIResponsesV1ParamsRequired - } - if params.GetModel() == "" { - return ErrOpenAIResponsesV1ModelRequired - } - if !utf8.ValidString(params.GetModel()) { - return ErrOpenAIResponsesV1ModelInvalid - } - - var parsed struct { - Model string `json:"model"` - Input json.RawMessage `json:"input"` - Stream *bool `json:"stream,omitempty"` - } - if err := json.Unmarshal(spec.GetRequestJson(), &parsed); err != nil { - return fmt.Errorf("%w: %s", ErrOpenAIResponsesV1RequestJSONInvalid, err.Error()) - } - if parsed.Model == "" { - return ErrOpenAIResponsesV1RequestModelRequired - } - if len(bytes.TrimSpace(parsed.Input)) == 0 || bytes.Equal(bytes.TrimSpace(parsed.Input), []byte("null")) { - return ErrOpenAIResponsesV1RequestInputRequired - } - if parsed.Model != params.GetModel() { - return ErrOpenAIResponsesV1ModelMismatch - } - - return nil -} diff --git a/go-lcpd/internal/lcptasks/lcptasks_test.go b/go-lcpd/internal/lcptasks/lcptasks_test.go deleted file mode 100644 index d56ffc0..0000000 --- a/go-lcpd/internal/lcptasks/lcptasks_test.go +++ /dev/null @@ -1,317 +0,0 @@ -package lcptasks_test - -import ( - "errors" - "testing" - - "github.com/bruwbird/lcp/go-lcpd/internal/lcptasks" - "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" - lcpdv1 "github.com/bruwbird/lcp/proto-go/lcpd/v1" - "github.com/google/go-cmp/cmp" -) - -func TestValidateTask_OpenAIChatCompletionsV1(t *testing.T) { - t.Parallel() - - requestJSON := []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"hi"}]}`) - - task := &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: "gpt-5.2", - }, - }, - }, - } - - if err := lcptasks.ValidateTask(task); err != nil { - t.Fatalf("ValidateTask: %v", err) - } -} - -func TestValidateTask_Errors(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - task *lcpdv1.Task - wantErr error - }{ - { - name: "nil task", - task: nil, - wantErr: lcptasks.ErrTaskRequired, - }, - { - name: "missing spec", - task: &lcpdv1.Task{}, - wantErr: lcptasks.ErrTaskSpecRequired, - }, - { - name: "openai_chat_completions_v1 spec nil", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{}, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1TaskRequired, - }, - { - name: "openai_chat_completions_v1 request_json empty", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}, - }, - }, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1RequestJSONRequired, - }, - { - name: "openai_chat_completions_v1 params nil", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{"model":"gpt-5.2"}`), - }, - }, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1ParamsRequired, - }, - { - name: "openai_chat_completions_v1 params.model empty", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{"model":"gpt-5.2"}`), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{}, - }, - }, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1ModelRequired, - }, - { - name: "openai_chat_completions_v1 request_json invalid json", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{`), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}, - }, - }, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1RequestJSONInvalid, - }, - { - name: "openai_chat_completions_v1 request_json model missing", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{"stream":false}`), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}, - }, - }, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1RequestModelRequired, - }, - { - name: "openai_chat_completions_v1 request_json messages missing", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{"model":"gpt-5.2"}`), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}, - }, - }, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1RequestMessagesRequired, - }, - { - name: "openai_chat_completions_v1 model mismatch", - task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{"model":"gpt-5.2","messages":[{}]}`), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "other"}, - }, - }, - }, - wantErr: lcptasks.ErrOpenAIChatCompletionsV1ModelMismatch, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - err := lcptasks.ValidateTask(tc.task) - if !errors.Is(err, tc.wantErr) { - t.Fatalf("ValidateTask error mismatch (-want +got):\n%s", cmp.Diff(tc.wantErr, err)) - } - }) - } -} - -func TestValidateTask_OpenAIChatCompletionsV1_AllowsStreaming(t *testing.T) { - t.Parallel() - - requestJSON := []byte(`{"model":"gpt-5.2","stream":true,"messages":[{"role":"user","content":"hi"}]}`) - - task := &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: "gpt-5.2", - }, - }, - }, - } - - if err := lcptasks.ValidateTask(task); err != nil { - t.Fatalf("ValidateTask: %v", err) - } -} - -func TestValidateTask_OpenAIResponsesV1(t *testing.T) { - t.Parallel() - - requestJSON := []byte(`{"model":"gpt-5.2","input":"hi"}`) - - task := &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIResponsesV1Params{ - Model: "gpt-5.2", - }, - }, - }, - } - - if err := lcptasks.ValidateTask(task); err != nil { - t.Fatalf("ValidateTask: %v", err) - } -} - -func TestToWireQuoteRequestTask_OpenAIChatCompletionsV1(t *testing.T) { - t.Parallel() - - requestJSON := []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"hi"}]}`) - task := &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}, - }, - }, - } - - got, err := lcptasks.ToWireQuoteRequestTask(task) - if err != nil { - t.Fatalf("ToWireQuoteRequestTask: %v", err) - } - - if diff := cmp.Diff(lcptasks.TaskKindOpenAIChatCompletionsV1, got.TaskKind); diff != "" { - t.Fatalf("TaskKind mismatch (-want +got):\n%s", diff) - } - - decoded, err := lcpwire.DecodeOpenAIChatCompletionsV1Params(got.ParamsBytes) - if err != nil { - t.Fatalf("DecodeOpenAIChatCompletionsV1Params: %v", err) - } - - wantParams := lcpwire.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"} - if diff := cmp.Diff(wantParams, decoded); diff != "" { - t.Fatalf("params mismatch (-want +got):\n%s", diff) - } -} - -func TestToWireQuoteRequestTask_OpenAIResponsesV1(t *testing.T) { - t.Parallel() - - requestJSON := []byte(`{"model":"gpt-5.2","input":"hi"}`) - task := &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIResponsesV1Params{Model: "gpt-5.2"}, - }, - }, - } - - got, err := lcptasks.ToWireQuoteRequestTask(task) - if err != nil { - t.Fatalf("ToWireQuoteRequestTask: %v", err) - } - - if diff := cmp.Diff(lcptasks.TaskKindOpenAIResponsesV1, got.TaskKind); diff != "" { - t.Fatalf("TaskKind mismatch (-want +got):\n%s", diff) - } - - decoded, err := lcpwire.DecodeOpenAIResponsesV1Params(got.ParamsBytes) - if err != nil { - t.Fatalf("DecodeOpenAIResponsesV1Params: %v", err) - } - - wantParams := lcpwire.OpenAIResponsesV1Params{Model: "gpt-5.2"} - if diff := cmp.Diff(wantParams, decoded); diff != "" { - t.Fatalf("params mismatch (-want +got):\n%s", diff) - } -} - -func TestToWireInputStream_OpenAIChatCompletionsV1(t *testing.T) { - t.Parallel() - - requestJSON := []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"hi"}]}`) - task := &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}, - }, - }, - } - - got, err := lcptasks.ToWireInputStream(task) - if err != nil { - t.Fatalf("ToWireInputStream: %v", err) - } - - want := lcptasks.InputStream{ - DecodedBytes: requestJSON, - ContentType: lcptasks.ContentTypeApplicationJSONUTF8, - ContentEncoding: lcptasks.ContentEncodingIdentity, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Fatalf("input stream mismatch (-want +got):\n%s", diff) - } -} - -func TestToWireInputStream_OpenAIResponsesV1(t *testing.T) { - t.Parallel() - - requestJSON := []byte(`{"model":"gpt-5.2","input":"hi"}`) - task := &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIResponsesV1Params{Model: "gpt-5.2"}, - }, - }, - } - - got, err := lcptasks.ToWireInputStream(task) - if err != nil { - t.Fatalf("ToWireInputStream: %v", err) - } - - want := lcptasks.InputStream{ - DecodedBytes: requestJSON, - ContentType: lcptasks.ContentTypeApplicationJSONUTF8, - ContentEncoding: lcptasks.ContentEncodingIdentity, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Fatalf("input stream mismatch (-want +got):\n%s", diff) - } -} diff --git a/go-lcpd/internal/lcpwire/call.go b/go-lcpd/internal/lcpwire/call.go new file mode 100644 index 0000000..1e0ebd9 --- /dev/null +++ b/go-lcpd/internal/lcpwire/call.go @@ -0,0 +1,95 @@ +package lcpwire + +import ( + "errors" + + "github.com/lightningnetwork/lnd/tlv" +) + +func EncodeCall(c Call) ([]byte, error) { + envelopeRecords, err := encodeCallEnvelope(c.Envelope) + if err != nil { + return nil, err + } + + if c.Method == "" { + return nil, errors.New("method is required") + } + if validateErr := validateUTF8String(c.Method, "method"); validateErr != nil { + return nil, validateErr + } + + methodBytes := []byte(c.Method) + + records := append([]tlv.Record(nil), envelopeRecords...) + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(tlvTypeMethod), &methodBytes)) + + if c.ParamsBytes != nil { + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(tlvTypeParams), c.ParamsBytes)) + } + + if c.ParamsContentType != nil { + if *c.ParamsContentType == "" { + return nil, errors.New("params_content_type must be non-empty when present") + } + if validateErr := validateUTF8String(*c.ParamsContentType, "params_content_type"); validateErr != nil { + return nil, validateErr + } + + paramsContentTypeBytes := []byte(*c.ParamsContentType) + records = append(records, tlv.MakePrimitiveRecord( + tlv.Type(tlvTypeParamsContentType), + ¶msContentTypeBytes, + )) + } + + return encodeTLVStream(records) +} + +func DecodeCall(payload []byte) (Call, error) { + m, err := decodeTLVMap(payload) + if err != nil { + return Call{}, err + } + + env, err := decodeCallEnvelope(m) + if err != nil { + return Call{}, err + } + + methodBytes, err := requireTLV(m, tlvTypeMethod) + if err != nil { + return Call{}, err + } + method, err := readUTF8String(methodBytes, "method") + if err != nil { + return Call{}, err + } + if method == "" { + return Call{}, errors.New("method is required") + } + + var paramsPtr *[]byte + if b, ok := m[tlvTypeParams]; ok { + paramsPtr = ptrCopyBytes(b) + } + + var paramsContentTypePtr *string + if b, ok := m[tlvTypeParamsContentType]; ok { + s, sErr := readUTF8String(b, "params_content_type") + if sErr != nil { + return Call{}, sErr + } + if s == "" { + return Call{}, errors.New("params_content_type must be non-empty when present") + } + paramsContentTypePtr = &s + } + + return Call{ + Envelope: env, + Method: method, + ParamsBytes: paramsPtr, + ParamsContentType: paramsContentTypePtr, + }, nil +} diff --git a/go-lcpd/internal/lcpwire/cancel.go b/go-lcpd/internal/lcpwire/cancel.go index eb5957a..73d8d72 100644 --- a/go-lcpd/internal/lcpwire/cancel.go +++ b/go-lcpd/internal/lcpwire/cancel.go @@ -7,7 +7,7 @@ const ( ) func EncodeCancel(c Cancel) ([]byte, error) { - envelopeRecords, err := encodeJobEnvelope(c.Envelope) + envelopeRecords, err := encodeCallEnvelope(c.Envelope) if err != nil { return nil, err } @@ -34,7 +34,7 @@ func DecodeCancel(payload []byte) (Cancel, error) { return Cancel{}, err } - env, err := decodeJobEnvelope(m) + env, err := decodeCallEnvelope(m) if err != nil { return Cancel{}, err } diff --git a/go-lcpd/internal/lcpwire/complete.go b/go-lcpd/internal/lcpwire/complete.go new file mode 100644 index 0000000..c235308 --- /dev/null +++ b/go-lcpd/internal/lcpwire/complete.go @@ -0,0 +1,252 @@ +package lcpwire + +import ( + "errors" + "fmt" + + "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + completeTLVTypeStatus = uint64(100) + + completeTLVTypeResponseStreamID = uint64(101) + completeTLVTypeResponseHash = uint64(102) + completeTLVTypeResponseLen = uint64(103) + completeTLVTypeResponseContentType = uint64(104) + completeTLVTypeResponseContentEncode = uint64(105) + + // Reuse the same TLV type number as lcp_error.message. + completeTLVTypeMessage = uint64(81) +) + +func EncodeComplete(c Complete) ([]byte, error) { + envelopeRecords, err := encodeCallEnvelope(c.Envelope) + if err != nil { + return nil, err + } + + switch c.Status { + case CompleteStatusOK, CompleteStatusFailed, CompleteStatusCancelled: + default: + return nil, fmt.Errorf("invalid status: %d", c.Status) + } + + status := uint16(c.Status) + + records := append([]tlv.Record(nil), envelopeRecords...) + + if c.Message != nil { + if c.Status == CompleteStatusOK { + return nil, errors.New("message must be omitted for status=ok") + } + if validateErr := validateUTF8String(*c.Message, "message"); validateErr != nil { + return nil, validateErr + } + msgBytes := []byte(*c.Message) + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(completeTLVTypeMessage), &msgBytes)) + } + + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(completeTLVTypeStatus), &status)) + + if c.Status == CompleteStatusOK && c.OK == nil { + return nil, errors.New("response metadata is required for status=ok") + } + + if c.OK == nil { + return encodeTLVStream(records) + } + + if c.OK.ResponseContentType == "" { + return nil, errors.New("response_content_type is required when response metadata is present") + } + if validateErr := validateUTF8String(c.OK.ResponseContentType, "response_content_type"); validateErr != nil { + return nil, validateErr + } + if c.OK.ResponseContentEncoding == "" { + return nil, errors.New("response_content_encoding is required when response metadata is present") + } + if validateErr := validateUTF8String(c.OK.ResponseContentEncoding, "response_content_encoding"); validateErr != nil { + return nil, validateErr + } + + responseStreamID := [32]byte(c.OK.ResponseStreamID) + responseHash := [32]byte(c.OK.ResponseHash) + responseLen := c.OK.ResponseLen + responseContentTypeBytes := []byte(c.OK.ResponseContentType) + responseContentEncodingBytes := []byte(c.OK.ResponseContentEncoding) + + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(completeTLVTypeResponseStreamID), &responseStreamID)) + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(completeTLVTypeResponseHash), &responseHash)) + records = append(records, makeTU64Record(completeTLVTypeResponseLen, &responseLen)) + records = append( + records, + tlv.MakePrimitiveRecord(tlv.Type(completeTLVTypeResponseContentType), &responseContentTypeBytes), + ) + records = append( + records, + tlv.MakePrimitiveRecord(tlv.Type(completeTLVTypeResponseContentEncode), &responseContentEncodingBytes), + ) + + return encodeTLVStream(records) +} + +func DecodeComplete(payload []byte) (Complete, error) { + m, err := decodeTLVMap(payload) + if err != nil { + return Complete{}, err + } + + env, err := decodeCallEnvelope(m) + if err != nil { + return Complete{}, err + } + + status, err := decodeCompleteStatus(m) + if err != nil { + return Complete{}, err + } + + msg, hasMsg, err := decodeOptionalCompleteMessage(m) + if err != nil { + return Complete{}, err + } + + okMeta, err := decodeOptionalCompleteResponseMetadata(m) + if err != nil { + return Complete{}, err + } + + if status == CompleteStatusOK { + if hasMsg { + return Complete{}, errors.New("message must be omitted for status=ok") + } + if okMeta == nil { + return Complete{}, errors.New("response metadata is required for status=ok") + } + return Complete{ + Envelope: env, + Status: status, + OK: okMeta, + }, nil + } + + var msgPtr *string + if hasMsg { + msgPtr = &msg + } + + return Complete{ + Envelope: env, + Status: status, + OK: okMeta, + Message: msgPtr, + }, nil +} + +func decodeCompleteStatus(m map[uint64][]byte) (CompleteStatus, error) { + statusBytes, err := requireTLV(m, completeTLVTypeStatus) + if err != nil { + return 0, err + } + statusU16, err := readU16(statusBytes) + if err != nil { + return 0, fmt.Errorf("status: %w", err) + } + status := CompleteStatus(statusU16) + switch status { + case CompleteStatusOK, CompleteStatusFailed, CompleteStatusCancelled: + return status, nil + default: + return 0, fmt.Errorf("invalid status: %d", statusU16) + } +} + +func decodeOptionalCompleteMessage(m map[uint64][]byte) (string, bool, error) { + b, ok := m[completeTLVTypeMessage] + if !ok { + return "", false, nil + } + s, err := readUTF8String(b, "message") + if err != nil { + return "", false, err + } + return s, true, nil +} + +func decodeOptionalCompleteResponseMetadata(m map[uint64][]byte) (*CompleteOK, error) { + _, hasStreamID := m[completeTLVTypeResponseStreamID] + _, hasHash := m[completeTLVTypeResponseHash] + _, hasLen := m[completeTLVTypeResponseLen] + _, hasCT := m[completeTLVTypeResponseContentType] + _, hasCE := m[completeTLVTypeResponseContentEncode] + + hasAny := hasStreamID || hasHash || hasLen || hasCT || hasCE + if !hasAny { + return nil, nil + } + + if !(hasStreamID && hasHash && hasLen && hasCT && hasCE) { + return nil, errors.New("incomplete response metadata: all of stream_id/hash/len/content_type/content_encoding are required when any are present") + } + + streamIDBytes, err := requireTLV(m, completeTLVTypeResponseStreamID) + if err != nil { + return nil, err + } + streamID32, err := readBytes32(streamIDBytes) + if err != nil { + return nil, fmt.Errorf("response_stream_id: %w", err) + } + + hashBytes, err := requireTLV(m, completeTLVTypeResponseHash) + if err != nil { + return nil, err + } + hash32, err := readBytes32(hashBytes) + if err != nil { + return nil, fmt.Errorf("response_hash: %w", err) + } + + lenBytes, err := requireTLV(m, completeTLVTypeResponseLen) + if err != nil { + return nil, err + } + responseLen, err := readTU64(lenBytes) + if err != nil { + return nil, fmt.Errorf("response_len: %w", err) + } + + ctBytes, err := requireTLV(m, completeTLVTypeResponseContentType) + if err != nil { + return nil, err + } + ct, err := readUTF8String(ctBytes, "response_content_type") + if err != nil { + return nil, err + } + if ct == "" { + return nil, errors.New("response_content_type must be non-empty when response metadata is present") + } + + ceBytes, err := requireTLV(m, completeTLVTypeResponseContentEncode) + if err != nil { + return nil, err + } + ce, err := readUTF8String(ceBytes, "response_content_encoding") + if err != nil { + return nil, err + } + if ce == "" { + return nil, errors.New("response_content_encoding must be non-empty when response metadata is present") + } + + return &CompleteOK{ + ResponseStreamID: lcp.Hash32(streamID32), + ResponseHash: lcp.Hash32(hash32), + ResponseLen: responseLen, + ResponseContentType: ct, + ResponseContentEncoding: ce, + }, nil +} diff --git a/go-lcpd/internal/lcpwire/envelope.go b/go-lcpd/internal/lcpwire/envelope.go index 79a5808..bc44388 100644 --- a/go-lcpd/internal/lcpwire/envelope.go +++ b/go-lcpd/internal/lcpwire/envelope.go @@ -8,67 +8,67 @@ import ( "github.com/lightningnetwork/lnd/tlv" ) -func decodeJobEnvelope(m map[uint64][]byte) (JobEnvelope, error) { +func decodeCallEnvelope(m map[uint64][]byte) (CallEnvelope, error) { pvBytes, err := requireTLV(m, tlvTypeProtocolVersion) if err != nil { - return JobEnvelope{}, err + return CallEnvelope{}, err } pv, err := readU16(pvBytes) if err != nil { - return JobEnvelope{}, fmt.Errorf("protocol_version: %w", err) + return CallEnvelope{}, fmt.Errorf("protocol_version: %w", err) } - jobIDBytes, err := requireTLV(m, tlvTypeJobID) + callIDBytes, err := requireTLV(m, tlvTypeCallID) if err != nil { - return JobEnvelope{}, err + return CallEnvelope{}, err } - jobID32, err := readBytes32(jobIDBytes) + callID32, err := readBytes32(callIDBytes) if err != nil { - return JobEnvelope{}, fmt.Errorf("job_id: %w", err) + return CallEnvelope{}, fmt.Errorf("call_id: %w", err) } - jobID := lcp.JobID(jobID32) + callID := lcp.JobID(callID32) msgIDBytes, err := requireTLV(m, tlvTypeMsgID) if err != nil { - return JobEnvelope{}, err + return CallEnvelope{}, err } msgID32, err := readBytes32(msgIDBytes) if err != nil { - return JobEnvelope{}, fmt.Errorf("msg_id: %w", err) + return CallEnvelope{}, fmt.Errorf("msg_id: %w", err) } var msgID MsgID copy(msgID[:], msgID32[:]) expiryBytes, err := requireTLV(m, tlvTypeExpiry) if err != nil { - return JobEnvelope{}, err + return CallEnvelope{}, err } expiry, err := readTU64(expiryBytes) if err != nil { - return JobEnvelope{}, fmt.Errorf("expiry: %w", err) + return CallEnvelope{}, fmt.Errorf("expiry: %w", err) } - return JobEnvelope{ + return CallEnvelope{ ProtocolVersion: pv, - JobID: jobID, + CallID: callID, MsgID: msgID, Expiry: expiry, }, nil } -func encodeJobEnvelope(env JobEnvelope) ([]tlv.Record, error) { +func encodeCallEnvelope(env CallEnvelope) ([]tlv.Record, error) { if env.ProtocolVersion == 0 { return nil, errors.New("protocol_version is required") } pv := env.ProtocolVersion - jobID := [32]byte(env.JobID) + callID := [32]byte(env.CallID) msgID := [32]byte(env.MsgID) expiry := env.Expiry return []tlv.Record{ tlv.MakePrimitiveRecord(tlv.Type(tlvTypeProtocolVersion), &pv), - tlv.MakePrimitiveRecord(tlv.Type(tlvTypeJobID), &jobID), + tlv.MakePrimitiveRecord(tlv.Type(tlvTypeCallID), &callID), tlv.MakePrimitiveRecord(tlv.Type(tlvTypeMsgID), &msgID), makeTU64Record(tlvTypeExpiry, &expiry), }, nil diff --git a/go-lcpd/internal/lcpwire/error.go b/go-lcpd/internal/lcpwire/error.go index 1195cf9..15d9577 100644 --- a/go-lcpd/internal/lcpwire/error.go +++ b/go-lcpd/internal/lcpwire/error.go @@ -13,7 +13,7 @@ const ( ) func EncodeError(e Error) ([]byte, error) { - envelopeRecords, err := encodeJobEnvelope(e.Envelope) + envelopeRecords, err := encodeCallEnvelope(e.Envelope) if err != nil { return nil, err } @@ -44,7 +44,7 @@ func DecodeError(payload []byte) (Error, error) { return Error{}, err } - env, err := decodeJobEnvelope(m) + env, err := decodeCallEnvelope(m) if err != nil { return Error{}, err } diff --git a/go-lcpd/internal/lcpwire/lcpwire_test.go b/go-lcpd/internal/lcpwire/lcpwire_test.go index fcbeed1..ddffac3 100644 --- a/go-lcpd/internal/lcpwire/lcpwire_test.go +++ b/go-lcpd/internal/lcpwire/lcpwire_test.go @@ -17,30 +17,21 @@ func TestEncodeDecode_Manifest_RoundTrip(t *testing.T) { var ( maxPayload uint32 = 16384 maxStream uint64 = 4_194_304 - maxJob uint64 = 8_388_608 + maxCall uint64 = 8_388_608 ) - openaiParams := lcpwire.OpenAIChatCompletionsV1Params{Model: "test-model"} - openaiParamsBytes, err := lcpwire.EncodeOpenAIChatCompletionsV1Params(openaiParams) - if err != nil { - t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) - } - - customParams := []byte{0x01, 0x02, 0x03} - want := lcpwire.Manifest{ - ProtocolVersion: lcpwire.ProtocolVersionV02, + ProtocolVersion: lcpwire.ProtocolVersionV03, MaxPayloadBytes: maxPayload, MaxStreamBytes: maxStream, - MaxJobBytes: maxJob, - SupportedTasks: []lcpwire.TaskTemplate{ + MaxCallBytes: maxCall, + SupportedMethods: []lcpwire.MethodDescriptor{ { - TaskKind: "openai.chat_completions.v1", - ParamsBytes: &openaiParamsBytes, + Method: "openai.chat_completions.v1", + RequestContentTypes: []string{"application/json; charset=utf-8"}, }, { - TaskKind: "custom.task", - ParamsBytes: &customParams, + Method: "custom.method", }, }, } @@ -64,11 +55,11 @@ func TestEncodeDecode_QuoteRequest_OpenAIChatCompletionsV1_RoundTrip(t *testing. t.Parallel() var ( - jobID lcp.JobID - msgID lcpwire.MsgID + callID lcp.JobID + msgID lcpwire.MsgID ) - for i := range jobID { - jobID[i] = 0x11 + for i := range callID { + callID[i] = 0x11 } for i := range msgID { msgID[i] = 0x22 @@ -84,44 +75,44 @@ func TestEncodeDecode_QuoteRequest_OpenAIChatCompletionsV1_RoundTrip(t *testing. t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) } - want := lcpwire.QuoteRequest{ - Envelope: lcpwire.JobEnvelope{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + want := lcpwire.Call{ + Envelope: lcpwire.CallEnvelope{ + ProtocolVersion: lcpwire.ProtocolVersionV03, + CallID: callID, MsgID: msgID, Expiry: expiry, }, - TaskKind: "openai.chat_completions.v1", + Method: "openai.chat_completions.v1", ParamsBytes: &openaiParamsBytes, } - encoded, err := lcpwire.EncodeQuoteRequest(want) + encoded, err := lcpwire.EncodeCall(want) if err != nil { - t.Fatalf("EncodeQuoteRequest: %v", err) + t.Fatalf("EncodeCall: %v", err) } - got, err := lcpwire.DecodeQuoteRequest(encoded) + got, err := lcpwire.DecodeCall(encoded) if err != nil { - t.Fatalf("DecodeQuoteRequest: %v", err) + t.Fatalf("DecodeCall: %v", err) } if diff := cmp.Diff(want, got); diff != "" { - t.Fatalf("quote_request mismatch (-want +got):\n%s", diff) + t.Fatalf("call mismatch (-want +got):\n%s", diff) } } func TestDecodeManifest_RejectsJobEnvelopeTLVs(t *testing.T) { t.Parallel() - pv := lcpwire.ProtocolVersionV02 - var jobID [32]byte - for i := range jobID { - jobID[i] = 0x11 + pv := lcpwire.ProtocolVersionV03 + var callID [32]byte + for i := range callID { + callID[i] = 0x11 } stream := tlv.MustNewStream( tlv.MakePrimitiveRecord(1, &pv), - tlv.MakePrimitiveRecord(2, &jobID), + tlv.MakePrimitiveRecord(2, &callID), ) var b bytes.Buffer @@ -157,7 +148,7 @@ func TestDecodeManifest_ErrStreamNotCanonical(t *testing.T) { if err := tlv.WriteVarInt(&buf, 2, &scratch); err != nil { t.Fatalf("write len 2: %v", err) } - if _, err := buf.Write([]byte{0x00, byte(lcpwire.ProtocolVersionV02)}); err != nil { + if _, err := buf.Write([]byte{0x00, byte(lcpwire.ProtocolVersionV03)}); err != nil { t.Fatalf("write protocol_version: %v", err) } diff --git a/go-lcpd/internal/lcpwire/manifest.go b/go-lcpd/internal/lcpwire/manifest.go index b68023e..6ef4944 100644 --- a/go-lcpd/internal/lcpwire/manifest.go +++ b/go-lcpd/internal/lcpwire/manifest.go @@ -4,15 +4,25 @@ import ( "errors" "fmt" + "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" "github.com/lightningnetwork/lnd/tlv" ) const ( - manifestTLVTypeMaxPayloadBytes = uint64(11) - manifestTLVTypeMaxStreamBytes = uint64(14) - manifestTLVTypeMaxJobBytes = uint64(15) - manifestTLVTypeMaxInflightJobs = uint64(16) - manifestTLVTypeSupportedTasks = uint64(12) + manifestTLVTypeMaxPayloadBytes = uint64(11) + manifestTLVTypeSupportedMethods = uint64(12) + manifestTLVTypeMaxStreamBytes = uint64(14) + manifestTLVTypeMaxCallBytes = uint64(15) + manifestTLVTypeMaxInflightCalls = uint64(16) +) + +const ( + methodTLVTypeMethod = uint64(20) + methodTLVTypeRequestContentTypes = uint64(23) + methodTLVTypeResponseContentTypes = uint64(24) + methodTLVTypeDocsURI = uint64(26) + methodTLVTypeDocsSHA256 = uint64(27) + methodTLVTypePolicyNotice = uint64(28) ) func EncodeManifest(m Manifest) ([]byte, error) { @@ -25,8 +35,8 @@ func EncodeManifest(m Manifest) ([]byte, error) { if m.MaxStreamBytes == 0 { return nil, errors.New("max_stream_bytes is required") } - if m.MaxJobBytes == 0 { - return nil, errors.New("max_job_bytes is required") + if m.MaxCallBytes == 0 { + return nil, errors.New("max_call_bytes is required") } pv := m.ProtocolVersion @@ -36,37 +46,37 @@ func EncodeManifest(m Manifest) ([]byte, error) { maxPayloadBytes := m.MaxPayloadBytes records = append(records, makeTU32Record(manifestTLVTypeMaxPayloadBytes, &maxPayloadBytes)) - if len(m.SupportedTasks) != 0 { - encodedTasks := make([][]byte, 0, len(m.SupportedTasks)) - for i, tmpl := range m.SupportedTasks { - taskBytes, err := EncodeTaskTemplate(tmpl) + if len(m.SupportedMethods) != 0 { + encodedMethods := make([][]byte, 0, len(m.SupportedMethods)) + for i, desc := range m.SupportedMethods { + methodBytes, err := EncodeMethodDescriptor(desc) if err != nil { - return nil, fmt.Errorf("encode supported_tasks[%d]: %w", i, err) + return nil, fmt.Errorf("encode supported_methods[%d]: %w", i, err) } - encodedTasks = append(encodedTasks, taskBytes) + encodedMethods = append(encodedMethods, methodBytes) } - listBytes, err := EncodeBytesList(encodedTasks) + listBytes, err := EncodeBytesList(encodedMethods) if err != nil { - return nil, fmt.Errorf("encode supported_tasks bytes_list: %w", err) + return nil, fmt.Errorf("encode supported_methods bytes_list: %w", err) } // tlv.MakePrimitiveRecord expects a *[]byte for varbytes. records = append( records, - tlv.MakePrimitiveRecord(tlv.Type(manifestTLVTypeSupportedTasks), &listBytes), + tlv.MakePrimitiveRecord(tlv.Type(manifestTLVTypeSupportedMethods), &listBytes), ) } maxStreamBytes := m.MaxStreamBytes records = append(records, makeTU64Record(manifestTLVTypeMaxStreamBytes, &maxStreamBytes)) - maxJobBytes := m.MaxJobBytes - records = append(records, makeTU64Record(manifestTLVTypeMaxJobBytes, &maxJobBytes)) + maxCallBytes := m.MaxCallBytes + records = append(records, makeTU64Record(manifestTLVTypeMaxCallBytes, &maxCallBytes)) - if m.MaxInflightJobs != nil { - maxInflight := *m.MaxInflightJobs - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(manifestTLVTypeMaxInflightJobs), &maxInflight)) + if m.MaxInflightCalls != nil { + maxInflight := *m.MaxInflightCalls + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(manifestTLVTypeMaxInflightCalls), &maxInflight)) } return encodeTLVStream(records) @@ -77,7 +87,7 @@ func DecodeManifest(payload []byte) (Manifest, error) { if err != nil { return Manifest{}, err } - if validateErr := validateJobScopeNotPresent(m); validateErr != nil { + if validateErr := validateCallScopeNotPresent(m); validateErr != nil { return Manifest{}, validateErr } @@ -96,32 +106,32 @@ func DecodeManifest(payload []byte) (Manifest, error) { return Manifest{}, err } - maxJobBytes, err := decodeManifestMaxJobBytes(m) + maxCallBytes, err := decodeManifestMaxCallBytes(m) if err != nil { return Manifest{}, err } - maxInflight, hasMaxInflight, err := decodeManifestMaxInflightJobs(m) + maxInflight, hasMaxInflight, err := decodeManifestMaxInflightCalls(m) if err != nil { return Manifest{}, err } - var maxInflightJobs *uint16 + var maxInflightCalls *uint16 if hasMaxInflight { - maxInflightJobs = &maxInflight + maxInflightCalls = &maxInflight } - supportedTasks, err := decodeManifestSupportedTasks(m) + supportedMethods, err := decodeManifestSupportedMethods(m) if err != nil { return Manifest{}, err } return Manifest{ - ProtocolVersion: pv, - MaxPayloadBytes: maxPayloadBytes, - MaxStreamBytes: maxStreamBytes, - MaxJobBytes: maxJobBytes, - MaxInflightJobs: maxInflightJobs, - SupportedTasks: supportedTasks, + ProtocolVersion: pv, + MaxPayloadBytes: maxPayloadBytes, + MaxStreamBytes: maxStreamBytes, + MaxCallBytes: maxCallBytes, + MaxInflightCalls: maxInflightCalls, + SupportedMethods: supportedMethods, }, nil } @@ -155,98 +165,188 @@ func decodeManifestMaxStreamBytes(m map[uint64][]byte) (uint64, error) { return maxStreamBytes, nil } -func decodeManifestMaxJobBytes(m map[uint64][]byte) (uint64, error) { - maxJobBytesRaw, err := requireTLV(m, manifestTLVTypeMaxJobBytes) +func decodeManifestMaxCallBytes(m map[uint64][]byte) (uint64, error) { + maxCallBytesRaw, err := requireTLV(m, manifestTLVTypeMaxCallBytes) if err != nil { return 0, err } - maxJobBytes, err := readTU64(maxJobBytesRaw) + maxCallBytes, err := readTU64(maxCallBytesRaw) if err != nil { - return 0, fmt.Errorf("max_job_bytes: %w", err) + return 0, fmt.Errorf("max_call_bytes: %w", err) } - if maxJobBytes == 0 { - return 0, errors.New("max_job_bytes is required") + if maxCallBytes == 0 { + return 0, errors.New("max_call_bytes is required") } - return maxJobBytes, nil + return maxCallBytes, nil } -func decodeManifestMaxInflightJobs(m map[uint64][]byte) (uint16, bool, error) { - b, ok := m[manifestTLVTypeMaxInflightJobs] +func decodeManifestMaxInflightCalls(m map[uint64][]byte) (uint16, bool, error) { + b, ok := m[manifestTLVTypeMaxInflightCalls] if !ok { return 0, false, nil } v, err := readU16(b) if err != nil { - return 0, false, fmt.Errorf("max_inflight_jobs: %w", err) + return 0, false, fmt.Errorf("max_inflight_calls: %w", err) } return v, true, nil } -func decodeManifestSupportedTasks(m map[uint64][]byte) ([]TaskTemplate, error) { - b, ok := m[manifestTLVTypeSupportedTasks] +func decodeManifestSupportedMethods(m map[uint64][]byte) ([]MethodDescriptor, error) { + b, ok := m[manifestTLVTypeSupportedMethods] if !ok { return nil, nil } items, err := DecodeBytesList(b) if err != nil { - return nil, fmt.Errorf("supported_tasks: %w", err) + return nil, fmt.Errorf("supported_methods: %w", err) } - supportedTasks := make([]TaskTemplate, 0, len(items)) + supportedMethods := make([]MethodDescriptor, 0, len(items)) for i, item := range items { - tmpl, tmplErr := DecodeTaskTemplate(item) - if tmplErr != nil { - return nil, fmt.Errorf("supported_tasks[%d]: %w", i, tmplErr) + desc, descErr := DecodeMethodDescriptor(item) + if descErr != nil { + return nil, fmt.Errorf("supported_methods[%d]: %w", i, descErr) } - supportedTasks = append(supportedTasks, tmpl) + supportedMethods = append(supportedMethods, desc) } - return supportedTasks, nil + return supportedMethods, nil } -func EncodeTaskTemplate(t TaskTemplate) ([]byte, error) { - if t.TaskKind == "" { - return nil, errors.New("task_kind is required") +func EncodeMethodDescriptor(d MethodDescriptor) ([]byte, error) { + if d.Method == "" { + return nil, errors.New("method is required") } - if err := validateUTF8String(t.TaskKind, "task_kind"); err != nil { + if err := validateUTF8String(d.Method, "method"); err != nil { return nil, err } - taskKindBytes := []byte(t.TaskKind) + methodBytes := []byte(d.Method) - records := []tlv.Record{tlv.MakePrimitiveRecord(tlv.Type(tlvTypeTaskKind), &taskKindBytes)} + records := []tlv.Record{tlv.MakePrimitiveRecord(tlv.Type(methodTLVTypeMethod), &methodBytes)} + + if len(d.RequestContentTypes) != 0 { + listBytes, err := EncodeStringList(d.RequestContentTypes) + if err != nil { + return nil, fmt.Errorf("encode request_content_types: %w", err) + } + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(methodTLVTypeRequestContentTypes), &listBytes)) + } + + if len(d.ResponseContentTypes) != 0 { + listBytes, err := EncodeStringList(d.ResponseContentTypes) + if err != nil { + return nil, fmt.Errorf("encode response_content_types: %w", err) + } + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(methodTLVTypeResponseContentTypes), &listBytes)) + } + + if d.DocsURI != nil { + if *d.DocsURI == "" { + return nil, errors.New("docs_uri must be non-empty when present") + } + if err := validateUTF8String(*d.DocsURI, "docs_uri"); err != nil { + return nil, err + } + docsURIBytes := []byte(*d.DocsURI) + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(methodTLVTypeDocsURI), &docsURIBytes)) + } + + if d.DocsSHA256 != nil { + sum := [32]byte(*d.DocsSHA256) + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(methodTLVTypeDocsSHA256), &sum)) + } - if t.ParamsBytes != nil { - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(tlvTypeParams), t.ParamsBytes)) + if d.PolicyNotice != nil { + if *d.PolicyNotice == "" { + return nil, errors.New("policy_notice must be non-empty when present") + } + if err := validateUTF8String(*d.PolicyNotice, "policy_notice"); err != nil { + return nil, err + } + policyNoticeBytes := []byte(*d.PolicyNotice) + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(methodTLVTypePolicyNotice), &policyNoticeBytes)) } return encodeTLVStream(records) } -func DecodeTaskTemplate(payload []byte) (TaskTemplate, error) { +func DecodeMethodDescriptor(payload []byte) (MethodDescriptor, error) { m, err := decodeTLVMap(payload) if err != nil { - return TaskTemplate{}, err + return MethodDescriptor{}, err } - taskKindBytes, err := requireTLV(m, tlvTypeTaskKind) + methodRaw, err := requireTLV(m, methodTLVTypeMethod) if err != nil { - return TaskTemplate{}, err + return MethodDescriptor{}, err } - taskKind, err := readUTF8String(taskKindBytes, "task_kind") + method, err := readUTF8String(methodRaw, "method") if err != nil { - return TaskTemplate{}, err + return MethodDescriptor{}, err + } + if method == "" { + return MethodDescriptor{}, errors.New("method is required") } - if taskKind == "" { - return TaskTemplate{}, errors.New("task_kind is required") + + var requestContentTypes []string + if b, ok := m[methodTLVTypeRequestContentTypes]; ok { + items, listErr := DecodeStringList(b) + if listErr != nil { + return MethodDescriptor{}, fmt.Errorf("request_content_types: %w", listErr) + } + requestContentTypes = items } - var paramsPtr *[]byte - if b, ok := m[tlvTypeParams]; ok { - paramsPtr = ptrCopyBytes(b) + var responseContentTypes []string + if b, ok := m[methodTLVTypeResponseContentTypes]; ok { + items, listErr := DecodeStringList(b) + if listErr != nil { + return MethodDescriptor{}, fmt.Errorf("response_content_types: %w", listErr) + } + responseContentTypes = items + } + + var docsURIPtr *string + if b, ok := m[methodTLVTypeDocsURI]; ok { + s, sErr := readUTF8String(b, "docs_uri") + if sErr != nil { + return MethodDescriptor{}, sErr + } + if s == "" { + return MethodDescriptor{}, errors.New("docs_uri must be non-empty when present") + } + docsURIPtr = &s + } + + var docsSHA256Ptr *lcp.Hash32 + if b, ok := m[methodTLVTypeDocsSHA256]; ok { + sum32, sumErr := readBytes32(b) + if sumErr != nil { + return MethodDescriptor{}, fmt.Errorf("docs_sha256: %w", sumErr) + } + sum := lcp.Hash32(sum32) + docsSHA256Ptr = &sum + } + + var policyNoticePtr *string + if b, ok := m[methodTLVTypePolicyNotice]; ok { + s, sErr := readUTF8String(b, "policy_notice") + if sErr != nil { + return MethodDescriptor{}, sErr + } + if s == "" { + return MethodDescriptor{}, errors.New("policy_notice must be non-empty when present") + } + policyNoticePtr = &s } - return TaskTemplate{ - TaskKind: taskKind, - ParamsBytes: paramsPtr, + return MethodDescriptor{ + Method: method, + RequestContentTypes: requestContentTypes, + ResponseContentTypes: responseContentTypes, + DocsURI: docsURIPtr, + DocsSHA256: docsSHA256Ptr, + PolicyNotice: policyNoticePtr, }, nil } diff --git a/go-lcpd/internal/lcpwire/quote.go b/go-lcpd/internal/lcpwire/quote.go new file mode 100644 index 0000000..ca4d009 --- /dev/null +++ b/go-lcpd/internal/lcpwire/quote.go @@ -0,0 +1,174 @@ +package lcpwire + +import ( + "errors" + "fmt" + + "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + quoteTLVTypePriceMsat = uint64(30) + quoteTLVTypeQuoteExpiry = uint64(31) + quoteTLVTypeTermsHash = uint64(32) + quoteTLVTypePaymentRequest = uint64(33) + + quoteTLVTypeResponseContentType = uint64(34) + quoteTLVTypeResponseContentEncoding = uint64(35) +) + +func EncodeQuote(q Quote) ([]byte, error) { + envelopeRecords, err := encodeCallEnvelope(q.Envelope) + if err != nil { + return nil, err + } + + if q.PaymentRequest == "" { + return nil, errors.New("payment_request is required") + } + if validateErr := validateUTF8String(q.PaymentRequest, "payment_request"); validateErr != nil { + return nil, validateErr + } + + if (q.ResponseContentType != nil) != (q.ResponseContentEncoding != nil) { + return nil, errors.New("response_content_type and response_content_encoding must be set together") + } + if q.ResponseContentType != nil { + if *q.ResponseContentType == "" { + return nil, errors.New("response_content_type must be non-empty when present") + } + if validateErr := validateUTF8String(*q.ResponseContentType, "response_content_type"); validateErr != nil { + return nil, validateErr + } + } + if q.ResponseContentEncoding != nil { + if *q.ResponseContentEncoding == "" { + return nil, errors.New("response_content_encoding must be non-empty when present") + } + if validateErr := validateUTF8String(*q.ResponseContentEncoding, "response_content_encoding"); validateErr != nil { + return nil, validateErr + } + } + + priceMsat := q.PriceMsat + quoteExpiry := q.QuoteExpiry + termsHash := [32]byte(q.TermsHash) + paymentRequestBytes := []byte(q.PaymentRequest) + + records := append([]tlv.Record(nil), envelopeRecords...) + records = append(records, makeTU64Record(quoteTLVTypePriceMsat, &priceMsat)) + records = append(records, makeTU64Record(quoteTLVTypeQuoteExpiry, "eExpiry)) + records = append(records, tlv.MakePrimitiveRecord(tlv.Type(quoteTLVTypeTermsHash), &termsHash)) + records = append(records, tlv.MakePrimitiveRecord( + tlv.Type(quoteTLVTypePaymentRequest), + &paymentRequestBytes, + )) + + if q.ResponseContentType != nil { + responseContentTypeBytes := []byte(*q.ResponseContentType) + records = append(records, tlv.MakePrimitiveRecord( + tlv.Type(quoteTLVTypeResponseContentType), + &responseContentTypeBytes, + )) + } + if q.ResponseContentEncoding != nil { + responseContentEncodingBytes := []byte(*q.ResponseContentEncoding) + records = append(records, tlv.MakePrimitiveRecord( + tlv.Type(quoteTLVTypeResponseContentEncoding), + &responseContentEncodingBytes, + )) + } + + return encodeTLVStream(records) +} + +func DecodeQuote(payload []byte) (Quote, error) { + m, err := decodeTLVMap(payload) + if err != nil { + return Quote{}, err + } + + env, err := decodeCallEnvelope(m) + if err != nil { + return Quote{}, err + } + + priceBytes, err := requireTLV(m, quoteTLVTypePriceMsat) + if err != nil { + return Quote{}, err + } + priceMsat, err := readTU64(priceBytes) + if err != nil { + return Quote{}, fmt.Errorf("price_msat: %w", err) + } + + expiryBytes, err := requireTLV(m, quoteTLVTypeQuoteExpiry) + if err != nil { + return Quote{}, err + } + quoteExpiry, err := readTU64(expiryBytes) + if err != nil { + return Quote{}, fmt.Errorf("quote_expiry: %w", err) + } + + termsHashBytes, err := requireTLV(m, quoteTLVTypeTermsHash) + if err != nil { + return Quote{}, err + } + termsHash32, err := readBytes32(termsHashBytes) + if err != nil { + return Quote{}, fmt.Errorf("terms_hash: %w", err) + } + termsHash := lcp.Hash32(termsHash32) + + paymentRequestBytes, err := requireTLV(m, quoteTLVTypePaymentRequest) + if err != nil { + return Quote{}, err + } + paymentRequest, err := readUTF8String(paymentRequestBytes, "payment_request") + if err != nil { + return Quote{}, err + } + if paymentRequest == "" { + return Quote{}, errors.New("payment_request is required") + } + + var responseContentTypePtr *string + if b, ok := m[quoteTLVTypeResponseContentType]; ok { + s, sErr := readUTF8String(b, "response_content_type") + if sErr != nil { + return Quote{}, sErr + } + if s == "" { + return Quote{}, errors.New("response_content_type must be non-empty when present") + } + responseContentTypePtr = &s + } + + var responseContentEncodingPtr *string + if b, ok := m[quoteTLVTypeResponseContentEncoding]; ok { + s, sErr := readUTF8String(b, "response_content_encoding") + if sErr != nil { + return Quote{}, sErr + } + if s == "" { + return Quote{}, errors.New("response_content_encoding must be non-empty when present") + } + responseContentEncodingPtr = &s + } + + if (responseContentTypePtr != nil) != (responseContentEncodingPtr != nil) { + return Quote{}, errors.New("response_content_type and response_content_encoding must be set together") + } + + return Quote{ + Envelope: env, + PriceMsat: priceMsat, + QuoteExpiry: quoteExpiry, + TermsHash: termsHash, + PaymentRequest: paymentRequest, + ResponseContentType: responseContentTypePtr, + ResponseContentEncoding: responseContentEncodingPtr, + }, nil +} diff --git a/go-lcpd/internal/lcpwire/quote_request.go b/go-lcpd/internal/lcpwire/quote_request.go deleted file mode 100644 index b278c89..0000000 --- a/go-lcpd/internal/lcpwire/quote_request.go +++ /dev/null @@ -1,67 +0,0 @@ -package lcpwire - -import ( - "errors" - - "github.com/lightningnetwork/lnd/tlv" -) - -func EncodeQuoteRequest(q QuoteRequest) ([]byte, error) { - envelopeRecords, err := encodeJobEnvelope(q.Envelope) - if err != nil { - return nil, err - } - - if q.TaskKind == "" { - return nil, errors.New("task_kind is required") - } - if validateErr := validateUTF8String(q.TaskKind, "task_kind"); validateErr != nil { - return nil, validateErr - } - - taskKindBytes := []byte(q.TaskKind) - - records := append([]tlv.Record(nil), envelopeRecords...) - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(tlvTypeTaskKind), &taskKindBytes)) - - if q.ParamsBytes != nil { - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(tlvTypeParams), q.ParamsBytes)) - } - - return encodeTLVStream(records) -} - -func DecodeQuoteRequest(payload []byte) (QuoteRequest, error) { - m, err := decodeTLVMap(payload) - if err != nil { - return QuoteRequest{}, err - } - - env, err := decodeJobEnvelope(m) - if err != nil { - return QuoteRequest{}, err - } - - taskKindBytes, err := requireTLV(m, tlvTypeTaskKind) - if err != nil { - return QuoteRequest{}, err - } - taskKind, err := readUTF8String(taskKindBytes, "task_kind") - if err != nil { - return QuoteRequest{}, err - } - if taskKind == "" { - return QuoteRequest{}, errors.New("task_kind is required") - } - - var paramsPtr *[]byte - if b, ok := m[tlvTypeParams]; ok { - paramsPtr = ptrCopyBytes(b) - } - - return QuoteRequest{ - Envelope: env, - TaskKind: taskKind, - ParamsBytes: paramsPtr, - }, nil -} diff --git a/go-lcpd/internal/lcpwire/quote_response.go b/go-lcpd/internal/lcpwire/quote_response.go deleted file mode 100644 index b88f6ec..0000000 --- a/go-lcpd/internal/lcpwire/quote_response.go +++ /dev/null @@ -1,109 +0,0 @@ -package lcpwire - -import ( - "errors" - "fmt" - - "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" - "github.com/lightningnetwork/lnd/tlv" -) - -const ( - quoteResponseTLVTypePriceMsat = uint64(30) - quoteResponseTLVTypeQuoteExpiry = uint64(31) - quoteResponseTLVTypeTermsHash = uint64(32) - quoteResponseTLVTypePaymentRequest = uint64(33) -) - -func EncodeQuoteResponse(q QuoteResponse) ([]byte, error) { - envelopeRecords, err := encodeJobEnvelope(q.Envelope) - if err != nil { - return nil, err - } - - if q.PaymentRequest == "" { - return nil, errors.New("payment_request is required") - } - if validateErr := validateUTF8String(q.PaymentRequest, "payment_request"); validateErr != nil { - return nil, validateErr - } - - priceMsat := q.PriceMsat - quoteExpiry := q.QuoteExpiry - termsHash := [32]byte(q.TermsHash) - paymentRequestBytes := []byte(q.PaymentRequest) - - records := append([]tlv.Record(nil), envelopeRecords...) - records = append(records, makeTU64Record(quoteResponseTLVTypePriceMsat, &priceMsat)) - records = append(records, makeTU64Record(quoteResponseTLVTypeQuoteExpiry, "eExpiry)) - records = append( - records, - tlv.MakePrimitiveRecord(tlv.Type(quoteResponseTLVTypeTermsHash), &termsHash), - ) - records = append(records, tlv.MakePrimitiveRecord( - tlv.Type(quoteResponseTLVTypePaymentRequest), - &paymentRequestBytes, - )) - - return encodeTLVStream(records) -} - -func DecodeQuoteResponse(payload []byte) (QuoteResponse, error) { - m, err := decodeTLVMap(payload) - if err != nil { - return QuoteResponse{}, err - } - - env, err := decodeJobEnvelope(m) - if err != nil { - return QuoteResponse{}, err - } - - priceBytes, err := requireTLV(m, quoteResponseTLVTypePriceMsat) - if err != nil { - return QuoteResponse{}, err - } - priceMsat, err := readTU64(priceBytes) - if err != nil { - return QuoteResponse{}, fmt.Errorf("price_msat: %w", err) - } - - expiryBytes, err := requireTLV(m, quoteResponseTLVTypeQuoteExpiry) - if err != nil { - return QuoteResponse{}, err - } - quoteExpiry, err := readTU64(expiryBytes) - if err != nil { - return QuoteResponse{}, fmt.Errorf("quote_expiry: %w", err) - } - - termsHashBytes, err := requireTLV(m, quoteResponseTLVTypeTermsHash) - if err != nil { - return QuoteResponse{}, err - } - termsHash32, err := readBytes32(termsHashBytes) - if err != nil { - return QuoteResponse{}, fmt.Errorf("terms_hash: %w", err) - } - termsHash := lcp.Hash32(termsHash32) - - paymentRequestBytes, err := requireTLV(m, quoteResponseTLVTypePaymentRequest) - if err != nil { - return QuoteResponse{}, err - } - paymentRequest, err := readUTF8String(paymentRequestBytes, "payment_request") - if err != nil { - return QuoteResponse{}, err - } - if paymentRequest == "" { - return QuoteResponse{}, errors.New("payment_request is required") - } - - return QuoteResponse{ - Envelope: env, - PriceMsat: priceMsat, - QuoteExpiry: quoteExpiry, - TermsHash: termsHash, - PaymentRequest: paymentRequest, - }, nil -} diff --git a/go-lcpd/internal/lcpwire/result.go b/go-lcpd/internal/lcpwire/result.go deleted file mode 100644 index e3957ff..0000000 --- a/go-lcpd/internal/lcpwire/result.go +++ /dev/null @@ -1,252 +0,0 @@ -package lcpwire - -import ( - "errors" - "fmt" - - "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" - "github.com/lightningnetwork/lnd/tlv" -) - -const ( - resultTLVTypeStatus = uint64(100) - - resultTLVTypeResultStreamID = uint64(101) - resultTLVTypeResultHash = uint64(102) - resultTLVTypeResultLen = uint64(103) - resultTLVTypeResultContentType = uint64(104) - resultTLVTypeResultContentEncode = uint64(105) - - // Reuse the same TLV type number as lcp_error.message. - resultTLVTypeMessage = uint64(81) -) - -func EncodeResult(r Result) ([]byte, error) { - envelopeRecords, err := encodeJobEnvelope(r.Envelope) - if err != nil { - return nil, err - } - - switch r.Status { - case ResultStatusOK, ResultStatusFailed, ResultStatusCancelled: - default: - return nil, fmt.Errorf("invalid status: %d", r.Status) - } - - status := uint16(r.Status) - - records := append([]tlv.Record(nil), envelopeRecords...) - - if r.Status != ResultStatusOK && r.Message != nil { - if validateErr := validateUTF8String(*r.Message, "message"); validateErr != nil { - return nil, validateErr - } - msgBytes := []byte(*r.Message) - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(resultTLVTypeMessage), &msgBytes)) - } - - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(resultTLVTypeStatus), &status)) - - if r.Status != ResultStatusOK { - if r.OK != nil { - return nil, errors.New("ok metadata must be omitted for status!=ok") - } - return encodeTLVStream(records) - } - - if r.OK == nil { - return nil, errors.New("ok metadata is required for status=ok") - } - if r.Message != nil { - return nil, errors.New("message must be omitted for status=ok") - } - - if r.OK.ResultContentType == "" { - return nil, errors.New("result_content_type is required for status=ok") - } - if validateErr := validateUTF8String(r.OK.ResultContentType, "result_content_type"); validateErr != nil { - return nil, validateErr - } - if r.OK.ResultContentEncoding == "" { - return nil, errors.New("result_content_encoding is required for status=ok") - } - if validateErr := validateUTF8String(r.OK.ResultContentEncoding, "result_content_encoding"); validateErr != nil { - return nil, validateErr - } - - resultStreamID := [32]byte(r.OK.ResultStreamID) - resultHash := [32]byte(r.OK.ResultHash) - resultLen := r.OK.ResultLen - resultContentTypeBytes := []byte(r.OK.ResultContentType) - resultContentEncodingBytes := []byte(r.OK.ResultContentEncoding) - - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(resultTLVTypeResultStreamID), &resultStreamID)) - records = append(records, tlv.MakePrimitiveRecord(tlv.Type(resultTLVTypeResultHash), &resultHash)) - records = append(records, makeTU64Record(resultTLVTypeResultLen, &resultLen)) - records = append( - records, - tlv.MakePrimitiveRecord(tlv.Type(resultTLVTypeResultContentType), &resultContentTypeBytes), - ) - records = append( - records, - tlv.MakePrimitiveRecord(tlv.Type(resultTLVTypeResultContentEncode), &resultContentEncodingBytes), - ) - - return encodeTLVStream(records) -} - -func DecodeResult(payload []byte) (Result, error) { - m, err := decodeTLVMap(payload) - if err != nil { - return Result{}, err - } - - env, err := decodeJobEnvelope(m) - if err != nil { - return Result{}, err - } - - status, err := decodeResultStatus(m) - if err != nil { - return Result{}, err - } - - msg, hasMsg, err := decodeOptionalResultMessage(m) - if err != nil { - return Result{}, err - } - - if status != ResultStatusOK { - var msgPtr *string - if hasMsg { - msgPtr = &msg - } - if validateErr := ensureNoResultOKMetadata(m); validateErr != nil { - return Result{}, validateErr - } - - return Result{ - Envelope: env, - Status: status, - Message: msgPtr, - }, nil - } - - okMeta, err := decodeResultOKMetadata(m) - if err != nil { - return Result{}, err - } - - return Result{ - Envelope: env, - Status: status, - OK: okMeta, - }, nil -} - -func decodeResultStatus(m map[uint64][]byte) (ResultStatus, error) { - statusBytes, err := requireTLV(m, resultTLVTypeStatus) - if err != nil { - return 0, err - } - statusU16, err := readU16(statusBytes) - if err != nil { - return 0, fmt.Errorf("status: %w", err) - } - status := ResultStatus(statusU16) - switch status { - case ResultStatusOK, ResultStatusFailed, ResultStatusCancelled: - return status, nil - default: - return 0, fmt.Errorf("invalid status: %d", statusU16) - } -} - -func decodeOptionalResultMessage(m map[uint64][]byte) (string, bool, error) { - b, ok := m[resultTLVTypeMessage] - if !ok { - return "", false, nil - } - s, err := readUTF8String(b, "message") - if err != nil { - return "", false, err - } - return s, true, nil -} - -func ensureNoResultOKMetadata(m map[uint64][]byte) error { - for _, t := range []uint64{ - resultTLVTypeResultStreamID, - resultTLVTypeResultHash, - resultTLVTypeResultLen, - resultTLVTypeResultContentType, - resultTLVTypeResultContentEncode, - } { - if _, ok := m[t]; ok { - return errors.New("ok metadata must be omitted for status!=ok") - } - } - return nil -} - -func decodeResultOKMetadata(m map[uint64][]byte) (*ResultOK, error) { - // status=ok requires ok metadata. - streamIDBytes, err := requireTLV(m, resultTLVTypeResultStreamID) - if err != nil { - return nil, err - } - streamID32, err := readBytes32(streamIDBytes) - if err != nil { - return nil, fmt.Errorf("result_stream_id: %w", err) - } - - hashBytes, err := requireTLV(m, resultTLVTypeResultHash) - if err != nil { - return nil, err - } - hash32, err := readBytes32(hashBytes) - if err != nil { - return nil, fmt.Errorf("result_hash: %w", err) - } - - lenBytes, err := requireTLV(m, resultTLVTypeResultLen) - if err != nil { - return nil, err - } - resultLen, err := readTU64(lenBytes) - if err != nil { - return nil, fmt.Errorf("result_len: %w", err) - } - - ctBytes, err := requireTLV(m, resultTLVTypeResultContentType) - if err != nil { - return nil, err - } - ct, err := readUTF8String(ctBytes, "result_content_type") - if err != nil { - return nil, err - } - if ct == "" { - return nil, errors.New("result_content_type is required for status=ok") - } - - ceBytes, err := requireTLV(m, resultTLVTypeResultContentEncode) - if err != nil { - return nil, err - } - ce, err := readUTF8String(ceBytes, "result_content_encoding") - if err != nil { - return nil, err - } - if ce == "" { - return nil, errors.New("result_content_encoding is required for status=ok") - } - - return &ResultOK{ - ResultStreamID: lcp.Hash32(streamID32), - ResultHash: lcp.Hash32(hash32), - ResultLen: resultLen, - ResultContentType: ct, - ResultContentEncoding: ce, - }, nil -} diff --git a/go-lcpd/internal/lcpwire/stream_begin.go b/go-lcpd/internal/lcpwire/stream_begin.go index 9977659..9cb835d 100644 --- a/go-lcpd/internal/lcpwire/stream_begin.go +++ b/go-lcpd/internal/lcpwire/stream_begin.go @@ -21,12 +21,12 @@ const ( ) func EncodeStreamBegin(b StreamBegin) ([]byte, error) { - envelopeRecords, err := encodeJobEnvelope(b.Envelope) + envelopeRecords, err := encodeCallEnvelope(b.Envelope) if err != nil { return nil, err } - if b.Kind != StreamKindInput && b.Kind != StreamKindResult { + if b.Kind != StreamKindRequest && b.Kind != StreamKindResponse { return nil, fmt.Errorf("invalid stream_kind: %d", b.Kind) } if b.ContentType == "" { @@ -72,7 +72,7 @@ func DecodeStreamBegin(payload []byte) (StreamBegin, error) { return StreamBegin{}, err } - env, err := decodeJobEnvelope(m) + env, err := decodeCallEnvelope(m) if err != nil { return StreamBegin{}, err } @@ -105,7 +105,7 @@ func decodeStreamBeginBody(m map[uint64][]byte) (StreamBegin, error) { return StreamBegin{}, fmt.Errorf("stream_kind: %w", err) } kind := StreamKind(streamKindU16) - if kind != StreamKindInput && kind != StreamKindResult { + if kind != StreamKindRequest && kind != StreamKindResponse { return StreamBegin{}, fmt.Errorf("invalid stream_kind: %d", streamKindU16) } diff --git a/go-lcpd/internal/lcpwire/stream_chunk.go b/go-lcpd/internal/lcpwire/stream_chunk.go index cb5bf8f..486a708 100644 --- a/go-lcpd/internal/lcpwire/stream_chunk.go +++ b/go-lcpd/internal/lcpwire/stream_chunk.go @@ -32,7 +32,7 @@ func EncodeStreamChunk(c StreamChunk) ([]byte, MsgID, error) { env := c.Envelope env.MsgID = msgID - envelopeRecords, err := encodeJobEnvelope(env) + envelopeRecords, err := encodeCallEnvelope(env) if err != nil { return nil, MsgID{}, err } @@ -60,7 +60,7 @@ func DecodeStreamChunk(payload []byte) (StreamChunk, error) { return StreamChunk{}, err } - env, err := decodeJobEnvelope(m) + env, err := decodeCallEnvelope(m) if err != nil { return StreamChunk{}, err } diff --git a/go-lcpd/internal/lcpwire/stream_end.go b/go-lcpd/internal/lcpwire/stream_end.go index eda83f9..f208bc0 100644 --- a/go-lcpd/internal/lcpwire/stream_end.go +++ b/go-lcpd/internal/lcpwire/stream_end.go @@ -8,7 +8,7 @@ import ( ) func EncodeStreamEnd(e StreamEnd) ([]byte, error) { - envelopeRecords, err := encodeJobEnvelope(e.Envelope) + envelopeRecords, err := encodeCallEnvelope(e.Envelope) if err != nil { return nil, err } @@ -31,7 +31,7 @@ func DecodeStreamEnd(payload []byte) (StreamEnd, error) { return StreamEnd{}, err } - env, err := decodeJobEnvelope(m) + env, err := decodeCallEnvelope(m) if err != nil { return StreamEnd{}, err } diff --git a/go-lcpd/internal/lcpwire/string_list.go b/go-lcpd/internal/lcpwire/string_list.go new file mode 100644 index 0000000..1ce1518 --- /dev/null +++ b/go-lcpd/internal/lcpwire/string_list.go @@ -0,0 +1,102 @@ +package lcpwire + +import ( + "bytes" + "fmt" + "io" + "unicode/utf8" + + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // maxStringListCount is a soft limit used when decoding untrusted payloads. + maxStringListCount = 1024 + + // maxStringListTotalBytes is a soft limit used when decoding untrusted payloads. + maxStringListTotalBytes = 1 << 20 // 1MiB +) + +// EncodeStringList encodes a `string_list` as defined in `docs/protocol/protocol.md`. +// +// data: bigsize(count) + count*(bigsize(len_i) + len_i bytes) +func EncodeStringList(items []string) ([]byte, error) { + var ( + buf bytes.Buffer + scratch [8]byte + ) + + if err := tlv.WriteVarInt(&buf, uint64(len(items)), &scratch); err != nil { + return nil, fmt.Errorf("write string_list count: %w", err) + } + + for i, item := range items { + if !utf8.ValidString(item) { + return nil, fmt.Errorf("string_list[%d] must be valid UTF-8", i) + } + + b := []byte(item) + if err := tlv.WriteVarInt(&buf, uint64(len(b)), &scratch); err != nil { + return nil, fmt.Errorf("write string_list[%d] length: %w", i, err) + } + if _, err := buf.Write(b); err != nil { + return nil, fmt.Errorf("write string_list[%d] bytes: %w", i, err) + } + } + + return buf.Bytes(), nil +} + +// DecodeStringList decodes a `string_list` as defined in `docs/protocol/protocol.md`. +func DecodeStringList(b []byte) ([]string, error) { + var ( + r = bytes.NewReader(b) + scratch [8]byte + ) + + countU64, countErr := tlv.ReadVarInt(r, &scratch) + if countErr != nil { + return nil, fmt.Errorf("read string_list count: %w", countErr) + } + if countU64 > maxStringListCount { + return nil, fmt.Errorf("string_list count too large: %d", countU64) + } + count := int(countU64) + + items := make([]string, 0, count) + var total uint64 + for i := range count { + nU64, lenErr := tlv.ReadVarInt(r, &scratch) + if lenErr != nil { + return nil, fmt.Errorf("read string_list[%d] length: %w", i, lenErr) + } + n, convErr := uint64ToInt(nU64) + if convErr != nil { + return nil, fmt.Errorf("read string_list[%d] length: %w", i, convErr) + } + if n > r.Len() { + return nil, fmt.Errorf("string_list[%d] length %d exceeds remaining %d", i, n, r.Len()) + } + + total += nU64 + if total > maxStringListTotalBytes { + return nil, fmt.Errorf("string_list too large: %d bytes", total) + } + + itemBytes := make([]byte, n) + if _, readErr := io.ReadFull(r, itemBytes); readErr != nil { + return nil, fmt.Errorf("read string_list[%d] bytes: %w", i, readErr) + } + if !utf8.Valid(itemBytes) { + return nil, fmt.Errorf("string_list[%d] must be valid UTF-8", i) + } + + items = append(items, string(itemBytes)) + } + + if r.Len() != 0 { + return nil, fmt.Errorf("string_list has trailing bytes: %d", r.Len()) + } + + return items, nil +} diff --git a/go-lcpd/internal/lcpwire/tlv_helpers.go b/go-lcpd/internal/lcpwire/tlv_helpers.go index 7d1d15c..89ffba5 100644 --- a/go-lcpd/internal/lcpwire/tlv_helpers.go +++ b/go-lcpd/internal/lcpwire/tlv_helpers.go @@ -12,7 +12,7 @@ import ( const ( tlvTypeProtocolVersion = uint64(1) - tlvTypeJobID = uint64(2) + tlvTypeCallID = uint64(2) tlvTypeMsgID = uint64(3) tlvTypeExpiry = uint64(4) @@ -149,10 +149,10 @@ func ptrCopyBytes(b []byte) *[]byte { return &c } -func validateJobScopeNotPresent(m map[uint64][]byte) error { - for _, t := range []uint64{tlvTypeJobID, tlvTypeMsgID, tlvTypeExpiry} { +func validateCallScopeNotPresent(m map[uint64][]byte) error { + for _, t := range []uint64{tlvTypeCallID, tlvTypeMsgID, tlvTypeExpiry} { if _, ok := m[t]; ok { - return errors.New("job-scope envelope tlvs must not be present") + return errors.New("call-scope envelope tlvs must not be present") } } return nil diff --git a/go-lcpd/internal/lcpwire/tlv_types.go b/go-lcpd/internal/lcpwire/tlv_types.go index b1cce63..34082e9 100644 --- a/go-lcpd/internal/lcpwire/tlv_types.go +++ b/go-lcpd/internal/lcpwire/tlv_types.go @@ -1,6 +1,8 @@ package lcpwire const ( - tlvTypeTaskKind = uint64(20) - tlvTypeParams = uint64(22) + tlvTypeMethod = uint64(20) + tlvTypeParams = uint64(22) + + tlvTypeParamsContentType = uint64(25) ) diff --git a/go-lcpd/internal/lcpwire/types.go b/go-lcpd/internal/lcpwire/types.go index 32e1637..7f2aefa 100644 --- a/go-lcpd/internal/lcpwire/types.go +++ b/go-lcpd/internal/lcpwire/types.go @@ -4,31 +4,35 @@ import "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" // MessageType is a BOLT #1 custom message type. // -// LCP v0.2 uses odd types >= 32768. +// LCP v0.3 uses odd types >= 32768. type MessageType uint16 const ( - // ProtocolVersionV02 is the protocol_version for LCP v0.2 as defined in + // ProtocolVersionV03 is the protocol_version for LCP v0.3 as defined in // `docs/protocol/protocol.md`. - // v0.2 encodes to 2 (major*100 + minor). - ProtocolVersionV02 = uint16(2) - - MessageTypeManifest MessageType = 42081 - MessageTypeQuoteRequest MessageType = 42083 - MessageTypeQuoteResponse MessageType = 42085 - MessageTypeResult MessageType = 42087 - MessageTypeStreamBegin MessageType = 42089 - MessageTypeStreamChunk MessageType = 42091 - MessageTypeStreamEnd MessageType = 42093 - MessageTypeCancel MessageType = 42095 - MessageTypeError MessageType = 42097 + // v0.3 encodes to 3 (major*100 + minor). + ProtocolVersionV03 = uint16(3) + + // ProtocolVersionV02 is a deprecated name kept for compatibility with + // older code in this repo. LCP v0.3 uses protocol_version=3. + ProtocolVersionV02 = ProtocolVersionV03 + + MessageTypeManifest MessageType = 42101 + MessageTypeCall MessageType = 42103 + MessageTypeQuote MessageType = 42105 + MessageTypeComplete MessageType = 42107 + MessageTypeStreamBegin MessageType = 42109 + MessageTypeStreamChunk MessageType = 42111 + MessageTypeStreamEnd MessageType = 42113 + MessageTypeCancel MessageType = 42115 + MessageTypeError MessageType = 42117 ) type MsgID [32]byte -type JobEnvelope struct { +type CallEnvelope struct { ProtocolVersion uint16 - JobID lcp.JobID + CallID lcp.JobID MsgID MsgID Expiry uint64 // tu64 (unix epoch seconds) } @@ -36,50 +40,59 @@ type JobEnvelope struct { type Manifest struct { ProtocolVersion uint16 - MaxPayloadBytes uint32 - MaxStreamBytes uint64 - MaxJobBytes uint64 - MaxInflightJobs *uint16 - SupportedTasks []TaskTemplate + MaxPayloadBytes uint32 + MaxStreamBytes uint64 + MaxCallBytes uint64 + MaxInflightCalls *uint16 + SupportedMethods []MethodDescriptor } -type TaskTemplate struct { - TaskKind string +type MethodDescriptor struct { + Method string - // ParamsBytes is task_kind dependent. For standardized kinds, this is a TLV - // stream as defined in `docs/protocol/protocol.md`. - ParamsBytes *[]byte + // RequestContentTypes and ResponseContentTypes are optional hints for + // request/response stream content type selection. + RequestContentTypes []string + ResponseContentTypes []string + + DocsURI *string + DocsSHA256 *lcp.Hash32 + PolicyNotice *string } -type QuoteRequest struct { - Envelope JobEnvelope +type Call struct { + Envelope CallEnvelope - TaskKind string + Method string - // ParamsBytes is task_kind dependent. For standardized kinds, this is a TLV - // stream as defined in `docs/protocol/protocol.md`. + // ParamsBytes is method-dependent opaque bytes. ParamsBytes *[]byte + + ParamsContentType *string } -type QuoteResponse struct { - Envelope JobEnvelope +type Quote struct { + Envelope CallEnvelope PriceMsat uint64 // tu64 QuoteExpiry uint64 // tu64 TermsHash lcp.Hash32 PaymentRequest string // BOLT11 invoice string (UTF-8) + + ResponseContentType *string + ResponseContentEncoding *string } type StreamKind uint16 const ( - StreamKindInput StreamKind = 1 - StreamKindResult StreamKind = 2 + StreamKindRequest StreamKind = 1 + StreamKindResponse StreamKind = 2 ) type StreamBegin struct { - Envelope JobEnvelope + Envelope CallEnvelope StreamID lcp.Hash32 Kind StreamKind @@ -91,7 +104,7 @@ type StreamBegin struct { } type StreamChunk struct { - Envelope JobEnvelope + Envelope CallEnvelope StreamID lcp.Hash32 Seq uint32 // tu32 @@ -99,41 +112,41 @@ type StreamChunk struct { } type StreamEnd struct { - Envelope JobEnvelope + Envelope CallEnvelope StreamID lcp.Hash32 TotalLen uint64 // tu64 SHA256 lcp.Hash32 } -type ResultStatus uint16 +type CompleteStatus uint16 const ( - ResultStatusOK ResultStatus = 0 - ResultStatusFailed ResultStatus = 1 - ResultStatusCancelled ResultStatus = 2 + CompleteStatusOK CompleteStatus = 0 + CompleteStatusFailed CompleteStatus = 1 + CompleteStatusCancelled CompleteStatus = 2 ) -type ResultOK struct { - ResultStreamID lcp.Hash32 - ResultHash lcp.Hash32 - ResultLen uint64 // tu64 +type CompleteOK struct { + ResponseStreamID lcp.Hash32 + ResponseHash lcp.Hash32 + ResponseLen uint64 // tu64 - ResultContentType string - ResultContentEncoding string + ResponseContentType string + ResponseContentEncoding string } -// Result represents the v0.2 terminal job completion (`lcp_result`). -type Result struct { - Envelope JobEnvelope +// Complete represents the v0.3 terminal call completion (`lcp_complete`). +type Complete struct { + Envelope CallEnvelope - Status ResultStatus - OK *ResultOK + Status CompleteStatus + OK *CompleteOK Message *string // UTF-8 (only used when Status != ok) } type Cancel struct { - Envelope JobEnvelope + Envelope CallEnvelope Reason *string // UTF-8 } @@ -142,21 +155,22 @@ type ErrorCode uint16 const ( ErrorCodeUnsupportedVersion ErrorCode = 1 - ErrorCodeUnsupportedTask ErrorCode = 2 - ErrorCodeQuoteExpired ErrorCode = 3 - ErrorCodePaymentRequired ErrorCode = 4 - ErrorCodePaymentInvalid ErrorCode = 5 - ErrorCodePayloadTooLarge ErrorCode = 6 - ErrorCodeRateLimited ErrorCode = 7 - ErrorCodeUnsupportedParams ErrorCode = 8 + ErrorCodeManifestRequired ErrorCode = 2 + ErrorCodeUnsupportedMethod ErrorCode = 3 + ErrorCodeQuoteExpired ErrorCode = 4 + ErrorCodePaymentRequired ErrorCode = 5 + ErrorCodePaymentInvalid ErrorCode = 6 + ErrorCodePayloadTooLarge ErrorCode = 7 + ErrorCodeRateLimited ErrorCode = 8 ErrorCodeUnsupportedEncoding ErrorCode = 9 ErrorCodeInvalidState ErrorCode = 10 ErrorCodeChunkOutOfOrder ErrorCode = 11 ErrorCodeChecksumMismatch ErrorCode = 12 + ErrorCodeStreamLimitExceeded ErrorCode = 13 ) type Error struct { - Envelope JobEnvelope + Envelope CallEnvelope Code ErrorCode Message *string // UTF-8 diff --git a/go-lcpd/internal/lndpeermsg/peermsg.go b/go-lcpd/internal/lndpeermsg/peermsg.go index 68fc97e..4bd568c 100644 --- a/go-lcpd/internal/lndpeermsg/peermsg.go +++ b/go-lcpd/internal/lndpeermsg/peermsg.go @@ -111,10 +111,10 @@ type PeerMessaging struct { } const ( - defaultProtocolVersion = lcpwire.ProtocolVersionV02 + defaultProtocolVersion = lcpwire.ProtocolVersionV03 defaultMaxPayloadBytes = uint32(16384) defaultMaxStreamBytes = uint64(4_194_304) - defaultMaxJobBytes = uint64(8_388_608) + defaultMaxCallBytes = uint64(8_388_608) subscriptionLoops = 2 ) @@ -135,7 +135,7 @@ func New(p Params) (*PeerMessaging, error) { ProtocolVersion: defaultProtocolVersion, MaxPayloadBytes: maxPayloadBytes, MaxStreamBytes: defaultMaxStreamBytes, - MaxJobBytes: defaultMaxJobBytes, + MaxCallBytes: defaultMaxCallBytes, } if p.LocalManifest != nil { localManifest = *p.LocalManifest @@ -469,12 +469,12 @@ func (p *PeerMessaging) HandleCustomMessage(ctx context.Context, msg *lnrpc.Cust "err", err, ) } - case lcpmsgrouter.RouteActionDispatchQuoteRequest, - lcpmsgrouter.RouteActionDispatchQuoteResponse, + case lcpmsgrouter.RouteActionDispatchCall, + lcpmsgrouter.RouteActionDispatchQuote, lcpmsgrouter.RouteActionDispatchStreamBegin, lcpmsgrouter.RouteActionDispatchStreamChunk, lcpmsgrouter.RouteActionDispatchStreamEnd, - lcpmsgrouter.RouteActionDispatchResult, + lcpmsgrouter.RouteActionDispatchComplete, lcpmsgrouter.RouteActionDispatchCancel, lcpmsgrouter.RouteActionDispatchError: p.dispatchInbound(ctx, customMsg, decision) diff --git a/go-lcpd/internal/lndpeermsg/peermsg_test.go b/go-lcpd/internal/lndpeermsg/peermsg_test.go index 9fd5307..28c6298 100644 --- a/go-lcpd/internal/lndpeermsg/peermsg_test.go +++ b/go-lcpd/internal/lndpeermsg/peermsg_test.go @@ -126,7 +126,7 @@ func TestPeerMessaging_HandleCustomMessage_DispatchesJobMessages(t *testing.T) { pm.HandleCustomMessage(context.Background(), &lnrpc.CustomMessage{ Peer: peerBytes, - Type: uint32(lcpwire.MessageTypeQuoteRequest), + Type: uint32(lcpwire.MessageTypeCall), Data: payload, }) @@ -137,7 +137,7 @@ func TestPeerMessaging_HandleCustomMessage_DispatchesJobMessages(t *testing.T) { } want := InboundCustomMessage{ PeerPubKey: peerPubKey, - MsgType: uint16(lcpwire.MessageTypeQuoteRequest), + MsgType: uint16(lcpwire.MessageTypeCall), Payload: payload, } if diff := cmp.Diff(want, received[0]); diff != "" { @@ -305,7 +305,7 @@ func TestPeerMessaging_HandleCustomMessage_LCPManifestMarksReadyAndRepliesOnce(t ProtocolVersion: lcpwire.ProtocolVersionV02, MaxPayloadBytes: 123, MaxStreamBytes: 1024, - MaxJobBytes: 2048, + MaxCallBytes: 2048, }) if err != nil { t.Fatalf("encode remote manifest: %v", err) @@ -362,7 +362,7 @@ func TestPeerMessaging_HandleCustomMessage_LCPManifestRepliesOnceEvenIfAlreadySe ProtocolVersion: lcpwire.ProtocolVersionV02, MaxPayloadBytes: 123, MaxStreamBytes: 1024, - MaxJobBytes: 2048, + MaxCallBytes: 2048, }, ) if err != nil { @@ -406,7 +406,7 @@ func TestPeerMessaging_HandlePeerEvent_OfflineResetsState(t *testing.T) { ProtocolVersion: lcpwire.ProtocolVersionV02, MaxPayloadBytes: 123, MaxStreamBytes: 1024, - MaxJobBytes: 2048, + MaxCallBytes: 2048, }, ) if err != nil { diff --git a/go-lcpd/internal/lndpeermsg/standalone_test.go b/go-lcpd/internal/lndpeermsg/standalone_test.go index daf8c8e..0653e52 100644 --- a/go-lcpd/internal/lndpeermsg/standalone_test.go +++ b/go-lcpd/internal/lndpeermsg/standalone_test.go @@ -41,7 +41,7 @@ func NewStandalone( ProtocolVersion: defaultProtocolVersion, MaxPayloadBytes: defaultMaxPayloadBytes, MaxStreamBytes: defaultMaxStreamBytes, - MaxJobBytes: defaultMaxJobBytes, + MaxCallBytes: defaultMaxCallBytes, } if localManifest != nil { manifest = *localManifest diff --git a/go-lcpd/internal/protocolcompat/terms.go b/go-lcpd/internal/protocolcompat/terms.go index d89c2b0..29be2d6 100644 --- a/go-lcpd/internal/protocolcompat/terms.go +++ b/go-lcpd/internal/protocolcompat/terms.go @@ -1,38 +1,45 @@ package protocolcompat import ( - "bytes" "crypto/rand" "crypto/sha256" "encoding/binary" "errors" "fmt" - "slices" "unicode/utf8" "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" - "github.com/lightningnetwork/lnd/tlv" ) const ( - termsTLVTypeProtocolVersion = 1 - termsTLVTypeJobID = 2 - termsTLVTypePriceMsat = 3 - termsTLVTypeQuoteExpiry = 4 - termsTLVTypeTaskKind = 20 - termsTLVTypeInputHash = 50 - termsTLVTypeParamsHash = 51 - termsTLVTypeInputLen = 52 - termsTLVTypeInputContentType = 53 - termsTLVTypeInputContentEncoding = 54 + termsTLVTypeProtocolVersion = 1 + termsTLVTypeCallID = 2 + termsTLVTypeMethod = 20 + + termsTLVTypePriceMsat = 30 + termsTLVTypeQuoteExpiry = 31 + + termsTLVTypeRequestHash = 50 + termsTLVTypeParamsHash = 51 + termsTLVTypeRequestLen = 52 + termsTLVTypeRequestContentType = 53 + termsTLVTypeRequestContentEncoding = 54 + + termsTLVTypeResponseContentType = 55 + termsTLVTypeResponseContentEncoding = 56 ) type TermsCommit struct { - TaskKind string - Input []byte - InputContentType string - InputContentEncoding string - Params []byte // nil means "absent" (hash is SHA256(empty)) + Method string + + Request []byte + RequestContentType string + RequestContentEncoding string + + Params []byte // nil means "absent" (hash is SHA256(empty)) + + ResponseContentType *string + ResponseContentEncoding *string } func NewJobID() (lcp.JobID, error) { @@ -45,46 +52,64 @@ func NewJobID() (lcp.JobID, error) { } func ComputeTermsHash(terms lcp.Terms, commit TermsCommit) (lcp.Hash32, error) { - if commit.TaskKind == "" { - return lcp.Hash32{}, errors.New("task_kind is required for terms_hash") + if commit.Method == "" { + return lcp.Hash32{}, errors.New("method is required for terms_hash") } - if !utf8.ValidString(commit.TaskKind) { - return lcp.Hash32{}, errors.New("task_kind must be valid UTF-8 for terms_hash") + if !utf8.ValidString(commit.Method) { + return lcp.Hash32{}, errors.New("method must be valid UTF-8 for terms_hash") } - if commit.InputContentType == "" { - return lcp.Hash32{}, errors.New("input_content_type is required for terms_hash") + if commit.RequestContentType == "" { + return lcp.Hash32{}, errors.New("request_content_type is required for terms_hash") } - if !utf8.ValidString(commit.InputContentType) { - return lcp.Hash32{}, errors.New("input_content_type must be valid UTF-8 for terms_hash") + if !utf8.ValidString(commit.RequestContentType) { + return lcp.Hash32{}, errors.New("request_content_type must be valid UTF-8 for terms_hash") } - if commit.InputContentEncoding == "" { - return lcp.Hash32{}, errors.New("input_content_encoding is required for terms_hash") + if commit.RequestContentEncoding == "" { + return lcp.Hash32{}, errors.New("request_content_encoding is required for terms_hash") } - if !utf8.ValidString(commit.InputContentEncoding) { - return lcp.Hash32{}, errors.New("input_content_encoding must be valid UTF-8 for terms_hash") + if !utf8.ValidString(commit.RequestContentEncoding) { + return lcp.Hash32{}, errors.New("request_content_encoding must be valid UTF-8 for terms_hash") } - inputHash := sha256.Sum256(commit.Input) - inputLen := uint64(len(commit.Input)) - - paramsCanonical := commit.Params - if commit.Params != nil { - canonical, err := canonicalizeTLVStream(commit.Params) - if err != nil { - return lcp.Hash32{}, fmt.Errorf("canonicalize params tlv: %w", err) + if (commit.ResponseContentType != nil) != (commit.ResponseContentEncoding != nil) { + return lcp.Hash32{}, errors.New("response_content_type and response_content_encoding must be set together for terms_hash") + } + if commit.ResponseContentType != nil { + if *commit.ResponseContentType == "" { + return lcp.Hash32{}, errors.New("response_content_type must be non-empty when present for terms_hash") } - paramsCanonical = canonical + if !utf8.ValidString(*commit.ResponseContentType) { + return lcp.Hash32{}, errors.New("response_content_type must be valid UTF-8 for terms_hash") + } + } + if commit.ResponseContentEncoding != nil { + if *commit.ResponseContentEncoding == "" { + return lcp.Hash32{}, errors.New("response_content_encoding must be non-empty when present for terms_hash") + } + if !utf8.ValidString(*commit.ResponseContentEncoding) { + return lcp.Hash32{}, errors.New("response_content_encoding must be valid UTF-8 for terms_hash") + } + } + + requestHash := sha256.Sum256(commit.Request) + requestLen := uint64(len(commit.Request)) + + paramsHashBytes := commit.Params + if paramsHashBytes == nil { + paramsHashBytes = nil } - paramsHash := sha256.Sum256(paramsCanonical) + paramsHash := sha256.Sum256(paramsHashBytes) encoded := encodeTermsTLVStream( terms, - commit.TaskKind, - inputHash, + commit.Method, + requestHash, paramsHash, - inputLen, - commit.InputContentType, - commit.InputContentEncoding, + requestLen, + commit.RequestContentType, + commit.RequestContentEncoding, + commit.ResponseContentType, + commit.ResponseContentEncoding, ) sum := sha256.Sum256(encoded) return lcp.Hash32(sum), nil @@ -92,12 +117,14 @@ func ComputeTermsHash(terms lcp.Terms, commit TermsCommit) (lcp.Hash32, error) { func encodeTermsTLVStream( terms lcp.Terms, - taskKind string, - inputHash [32]byte, + method string, + requestHash [32]byte, paramsHash [32]byte, - inputLen uint64, - inputContentType string, - inputContentEncoding string, + requestLen uint64, + requestContentType string, + requestContentEncoding string, + responseContentType *string, + responseContentEncoding *string, ) []byte { const u16ByteLen = 2 @@ -105,27 +132,42 @@ func encodeTermsTLVStream( binary.BigEndian.PutUint16(v1, terms.ProtocolVersion) t1 := encodeTLV(termsTLVTypeProtocolVersion, v1) - t2 := encodeTLV(termsTLVTypeJobID, terms.JobID[:]) - t3 := encodeTLV(termsTLVTypePriceMsat, encodeTU64(terms.PriceMsat)) - t4 := encodeTLV(termsTLVTypeQuoteExpiry, encodeTU64(terms.QuoteExpiry)) - t20 := encodeTLV(termsTLVTypeTaskKind, []byte(taskKind)) - t50 := encodeTLV(termsTLVTypeInputHash, inputHash[:]) + t2 := encodeTLV(termsTLVTypeCallID, terms.JobID[:]) + t20 := encodeTLV(termsTLVTypeMethod, []byte(method)) + t30 := encodeTLV(termsTLVTypePriceMsat, encodeTU64(terms.PriceMsat)) + t31 := encodeTLV(termsTLVTypeQuoteExpiry, encodeTU64(terms.QuoteExpiry)) + t50 := encodeTLV(termsTLVTypeRequestHash, requestHash[:]) t51 := encodeTLV(termsTLVTypeParamsHash, paramsHash[:]) - t52 := encodeTLV(termsTLVTypeInputLen, encodeTU64(inputLen)) - t53 := encodeTLV(termsTLVTypeInputContentType, []byte(inputContentType)) - t54 := encodeTLV(termsTLVTypeInputContentEncoding, []byte(inputContentEncoding)) + t52 := encodeTLV(termsTLVTypeRequestLen, encodeTU64(requestLen)) + t53 := encodeTLV(termsTLVTypeRequestContentType, []byte(requestContentType)) + t54 := encodeTLV(termsTLVTypeRequestContentEncoding, []byte(requestContentEncoding)) - out := make([]byte, 0, len(t1)+len(t2)+len(t3)+len(t4)+len(t20)+len(t50)+len(t51)+len(t52)+len(t53)+len(t54)) + var t55 []byte + if responseContentType != nil { + t55 = encodeTLV(termsTLVTypeResponseContentType, []byte(*responseContentType)) + } + var t56 []byte + if responseContentEncoding != nil { + t56 = encodeTLV(termsTLVTypeResponseContentEncoding, []byte(*responseContentEncoding)) + } + + out := make([]byte, 0, len(t1)+len(t2)+len(t20)+len(t30)+len(t31)+len(t50)+len(t51)+len(t52)+len(t53)+len(t54)+len(t55)+len(t56)) out = append(out, t1...) out = append(out, t2...) - out = append(out, t3...) - out = append(out, t4...) out = append(out, t20...) + out = append(out, t30...) + out = append(out, t31...) out = append(out, t50...) out = append(out, t51...) out = append(out, t52...) out = append(out, t53...) out = append(out, t54...) + if len(t55) > 0 { + out = append(out, t55...) + } + if len(t56) > 0 { + out = append(out, t56...) + } return out } @@ -197,28 +239,3 @@ func encodeBigSize(v uint64) []byte { return append([]byte{prefix64}, b[:]...) } } - -func canonicalizeTLVStream(payload []byte) ([]byte, error) { - stream, err := tlv.NewStream() - if err != nil { - return nil, fmt.Errorf("new empty tlv stream: %w", err) - } - - parsed, err := stream.DecodeWithParsedTypesP2P(bytes.NewReader(payload)) - if err != nil { - return nil, fmt.Errorf("decode tlv stream: %w", err) - } - - types := make([]uint64, 0, len(parsed)) - for typ := range parsed { - types = append(types, uint64(typ)) - } - slices.Sort(types) - - out := make([]byte, 0, len(payload)) - for _, typ := range types { - v := parsed[tlv.Type(typ)] - out = append(out, encodeTLV(typ, v)...) - } - return out, nil -} diff --git a/go-lcpd/internal/protocolcompat/terms_test.go b/go-lcpd/internal/protocolcompat/terms_test.go index 4a6a26c..59482a7 100644 --- a/go-lcpd/internal/protocolcompat/terms_test.go +++ b/go-lcpd/internal/protocolcompat/terms_test.go @@ -27,36 +27,36 @@ func TestComputeTermsHash_MatchesSpecEncodingVector(t *testing.T) { } commit := protocolcompat.TermsCommit{ - TaskKind: "openai.chat_completions.v1", - Input: []byte(`{"model":"a","messages":[{"role":"user","content":"hi"}]}`), - InputContentType: "application/json; charset=utf-8", - InputContentEncoding: "identity", - Params: []byte{0x01, 0x01, 0x61}, // type=1(model), len=1, value="a" + Method: "openai.chat_completions.v1", + Request: []byte(`{"model":"a","messages":[{"role":"user","content":"hi"}]}`), + RequestContentType: "application/json; charset=utf-8", + RequestContentEncoding: "identity", + Params: []byte{0x01, 0x01, 0x61}, // type=1(model), len=1, value="a" } - inputHash := sha256.Sum256(commit.Input) + requestHash := sha256.Sum256(commit.Request) paramsHash := sha256.Sum256(commit.Params) expectedTLV := make([]byte, 0, 122) - expectedTLV = append(expectedTLV, 0x01, 0x02, 0x00, 0x02) + expectedTLV = append(expectedTLV, 0x01, 0x02, 0x00, 0x03) expectedTLV = append(expectedTLV, 0x02, 0x20) expectedTLV = append(expectedTLV, bytes.Repeat([]byte{0x11}, 32)...) - expectedTLV = append(expectedTLV, 0x03, 0x02, 0x03, 0xE8) - expectedTLV = append(expectedTLV, 0x04, 0x02, 0x01, 0xF4) expectedTLV = append(expectedTLV, 0x14, 0x1a) - expectedTLV = append(expectedTLV, []byte(commit.TaskKind)...) + expectedTLV = append(expectedTLV, []byte(commit.Method)...) + expectedTLV = append(expectedTLV, 0x1e, 0x02, 0x03, 0xE8) + expectedTLV = append(expectedTLV, 0x1f, 0x02, 0x01, 0xF4) expectedTLV = append(expectedTLV, 0x32, 0x20) - expectedTLV = append(expectedTLV, inputHash[:]...) + expectedTLV = append(expectedTLV, requestHash[:]...) expectedTLV = append(expectedTLV, 0x33, 0x20) expectedTLV = append(expectedTLV, paramsHash[:]...) - if len(commit.Input) == 0 || len(commit.Input) > 0xfc { - t.Fatalf("unexpected input length for test vector: %d", len(commit.Input)) + if len(commit.Request) == 0 || len(commit.Request) > 0xfc { + t.Fatalf("unexpected request length for test vector: %d", len(commit.Request)) } - expectedTLV = append(expectedTLV, 0x34, 0x01, byte(len(commit.Input))) + expectedTLV = append(expectedTLV, 0x34, 0x01, byte(len(commit.Request))) expectedTLV = append(expectedTLV, 0x35, 0x1f) - expectedTLV = append(expectedTLV, []byte(commit.InputContentType)...) + expectedTLV = append(expectedTLV, []byte(commit.RequestContentType)...) expectedTLV = append(expectedTLV, 0x36, 0x08) - expectedTLV = append(expectedTLV, []byte(commit.InputContentEncoding)...) + expectedTLV = append(expectedTLV, []byte(commit.RequestContentEncoding)...) expectedSum := sha256.Sum256(expectedTLV) expectedHash := lcp.Hash32(expectedSum) diff --git a/go-lcpd/internal/provider/handler.go b/go-lcpd/internal/provider/handler.go index e431cdd..c25a6cd 100644 --- a/go-lcpd/internal/provider/handler.go +++ b/go-lcpd/internal/provider/handler.go @@ -35,7 +35,7 @@ const ( defaultMaxEnvelopeExpiryWindowSeconds = uint64(600) defaultMaxStreamBytes = uint64(4_194_304) - defaultMaxJobBytes = uint64(8_388_608) + defaultMaxCallBytes = uint64(8_388_608) contentTypeTextPlain = "text/plain; charset=utf-8" contentTypeApplicationJSON = "application/json; charset=utf-8" @@ -147,7 +147,7 @@ func (h *Handler) HandleInboundCustomMessage( msg lndpeermsg.InboundCustomMessage, ) { switch lcpwire.MessageType(msg.MsgType) { - case lcpwire.MessageTypeQuoteRequest: + case lcpwire.MessageTypeCall: h.handleQuoteRequest(ctx, msg) case lcpwire.MessageTypeStreamBegin: h.handleStreamBegin(ctx, msg) @@ -158,8 +158,8 @@ func (h *Handler) HandleInboundCustomMessage( case lcpwire.MessageTypeCancel: h.handleCancel(ctx, msg) case lcpwire.MessageTypeManifest, - lcpwire.MessageTypeQuoteResponse, - lcpwire.MessageTypeResult, + lcpwire.MessageTypeQuote, + lcpwire.MessageTypeComplete, lcpwire.MessageTypeError: return default: @@ -168,7 +168,7 @@ func (h *Handler) HandleInboundCustomMessage( } func (h *Handler) handleQuoteRequest(ctx context.Context, msg lndpeermsg.InboundCustomMessage) { - req, err := lcpwire.DecodeQuoteRequest(msg.Payload) + req, err := lcpwire.DecodeCall(msg.Payload) if err != nil { h.logger.Warnw( "decode lcp_quote_request failed", @@ -206,7 +206,7 @@ func (h *Handler) handleQuoteRequest(ctx context.Context, msg lndpeermsg.Inbound return } - key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: req.Envelope.JobID} + key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: req.Envelope.CallID} if h.jobs == nil { h.logger.Warnw("drop lcp_quote_request: job store is not configured") @@ -233,15 +233,15 @@ func (h *Handler) handleQuoteRequest(ctx context.Context, msg lndpeermsg.Inbound reqCopy := req h.jobs.Upsert(jobstore.Job{ - PeerPubKey: msg.PeerPubKey, - JobID: req.Envelope.JobID, - State: jobstore.StateAwaitingInput, - QuoteRequest: &reqCopy, - InputStream: nil, - QuoteExpiry: 0, - TermsHash: nil, - PaymentHash: nil, - QuoteResponse: nil, + PeerPubKey: msg.PeerPubKey, + JobID: req.Envelope.CallID, + State: jobstore.StateAwaitingInput, + Call: &reqCopy, + InputStream: nil, + QuoteExpiry: 0, + TermsHash: nil, + PaymentHash: nil, + Quote: nil, }) } @@ -249,7 +249,7 @@ func (h *Handler) handleExistingQuoteRequest( ctx context.Context, peerPubKey string, key jobstore.Key, - req lcpwire.QuoteRequest, + req lcpwire.Call, now uint64, remoteMaxPayload uint32, existing jobstore.Job, @@ -261,7 +261,7 @@ func (h *Handler) handleExistingQuoteRequest( return } - if existing.QuoteRequest != nil && !quoteRequestEqual(*existing.QuoteRequest, req) { + if existing.Call != nil && !quoteRequestEqual(*existing.Call, req) { h.sendError( ctx, peerPubKey, @@ -275,29 +275,29 @@ func (h *Handler) handleExistingQuoteRequest( // Update stored quote_request (first writer wins semantics are fine here; // streams are validated independently). existingReq := req - existing.QuoteRequest = &existingReq + existing.Call = &existingReq if existing.State == "" { existing.State = jobstore.StateAwaitingInput } h.jobs.Upsert(existing) - if existing.QuoteResponse == nil { + if existing.Quote == nil { return } h.logger.Debugw( - "resend stored lcp_quote_response", + "resend stored lcp_quote", "peer_pub_key", peerPubKey, - "job_id", req.Envelope.JobID.String(), + "job_id", req.Envelope.CallID.String(), ) - if !h.sendQuoteResponse(ctx, peerPubKey, *existing.QuoteResponse, remoteMaxPayload) { + if !h.sendQuoteResponse(ctx, peerPubKey, *existing.Quote, remoteMaxPayload) { return } if existing.InputStream == nil || !existing.InputStream.Validated { return } - if existing.QuoteRequest == nil { + if existing.Call == nil { return } @@ -305,9 +305,9 @@ func (h *Handler) handleExistingQuoteRequest( h.startJobRunner( ctx, key, - *existing.QuoteRequest, + *existing.Call, *existing.InputStream, - *existing.QuoteResponse, + *existing.Quote, invoice, remoteMaxPayload, ) @@ -325,7 +325,7 @@ func (h *Handler) handleStreamBegin(ctx context.Context, msg lndpeermsg.InboundC } // Provider only accepts the input stream. - if begin.Kind != lcpwire.StreamKindInput { + if begin.Kind != lcpwire.StreamKindRequest { return } @@ -366,15 +366,15 @@ func (h *Handler) handleStreamBegin(ctx context.Context, msg lndpeermsg.InboundC return } - key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: begin.Envelope.JobID} + key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: begin.Envelope.CallID} job, ok := h.jobs.Get(key) - if !ok || job.QuoteRequest == nil { + if !ok || job.Call == nil { h.sendError( ctx, msg.PeerPubKey, begin.Envelope, lcpwire.ErrorCodeInvalidState, - "no quote_request for job", + "no call for job", ) return } @@ -390,7 +390,7 @@ func (h *Handler) handleStreamBegin(ctx context.Context, msg lndpeermsg.InboundC return } - expectedContentType := inputContentTypeForTaskKind(job.QuoteRequest.TaskKind) + expectedContentType := inputContentTypeForTaskKind(job.Call.Method) if expectedContentType != "" && begin.ContentType != expectedContentType { h.sendError( ctx, @@ -439,9 +439,9 @@ func (h *Handler) handleStreamChunk(ctx context.Context, msg lndpeermsg.InboundC return } - key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: chunk.Envelope.JobID} + key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: chunk.Envelope.CallID} job, ok := h.jobs.Get(key) - if !ok || job.InputStream == nil || job.QuoteRequest == nil { + if !ok || job.InputStream == nil || job.Call == nil { return } if job.State != jobstore.StateAwaitingInput || job.InputStream.Validated { @@ -468,7 +468,7 @@ func (h *Handler) handleStreamChunk(ctx context.Context, msg lndpeermsg.InboundC nextLen := uint64(len(job.InputStream.Buf)) + uint64(len(chunk.Data)) if nextLen > job.InputStream.TotalLen || nextLen > defaultMaxStreamBytes || - nextLen > defaultMaxJobBytes { + nextLen > defaultMaxCallBytes { h.sendError( ctx, msg.PeerPubKey, @@ -512,9 +512,9 @@ func (h *Handler) handleStreamEnd(ctx context.Context, msg lndpeermsg.InboundCus return } - key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: end.Envelope.JobID} + key := jobstore.Key{PeerPubKey: msg.PeerPubKey, JobID: end.Envelope.CallID} job, ok := h.jobs.Get(key) - if !ok || job.InputStream == nil || job.QuoteRequest == nil { + if !ok || job.InputStream == nil || job.Call == nil { h.sendError( ctx, msg.PeerPubKey, @@ -588,14 +588,14 @@ func (h *Handler) handleValidatedInputStreamEnd( job jobstore.Job, stream jobstore.InputStreamState, now uint64, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, ) { - if job.QuoteResponse != nil { + if job.Quote != nil { h.jobs.Upsert(job) return } - if vErr := validateInputStream(*job.QuoteRequest, stream); vErr != nil { + if vErr := validateInputStream(*job.Call, stream); vErr != nil { h.sendError(ctx, peerPubKey, env, vErr.Code, vErr.Message) return } @@ -603,19 +603,19 @@ func (h *Handler) handleValidatedInputStreamEnd( quoteTTL := h.quoteTTLSeconds() quoteExpiry := now + quoteTTL - price, err := h.quotePrice(*job.QuoteRequest, stream.Buf) + price, err := h.quotePrice(*job.Call, stream.Buf) if err != nil { h.sendError(ctx, peerPubKey, env, quoteErrorCode(err), err.Error()) return } - termsHash, err := h.computeTermsHash(*job.QuoteRequest, stream, quoteExpiry, price.PriceMsat) + termsHash, err := h.computeTermsHash(*job.Call, stream, quoteExpiry, price.PriceMsat) if err != nil { h.sendError( ctx, peerPubKey, env, - lcpwire.ErrorCodeUnsupportedTask, + lcpwire.ErrorCodeUnsupportedMethod, "failed to compute terms_hash", ) return @@ -632,7 +632,7 @@ func (h *Handler) handleValidatedInputStreamEnd( ctx, peerPubKey, key, - *job.QuoteRequest, + *job.Call, stream, quoteExpiry, quoteTTL, @@ -643,14 +643,14 @@ func (h *Handler) handleValidatedInputStreamEnd( } func (h *Handler) computeTermsHash( - req lcpwire.QuoteRequest, + req lcpwire.Call, input jobstore.InputStreamState, quoteExpiry uint64, priceMsat uint64, ) (lcp.Hash32, error) { terms := lcp.Terms{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + JobID: req.Envelope.CallID, PriceMsat: priceMsat, QuoteExpiry: quoteExpiry, } @@ -661,11 +661,11 @@ func (h *Handler) computeTermsHash( } return protocolcompat.ComputeTermsHash(terms, protocolcompat.TermsCommit{ - TaskKind: req.TaskKind, - Input: input.Buf, - InputContentType: input.ContentType, - InputContentEncoding: input.ContentEncoding, - Params: paramsBytes, + Method: req.Method, + Request: input.Buf, + RequestContentType: input.ContentType, + RequestContentEncoding: input.ContentEncoding, + Params: paramsBytes, }) } @@ -673,7 +673,7 @@ func (h *Handler) createInvoiceAndStartQuoteJob( ctx context.Context, peerPubKey string, key jobstore.Key, - req lcpwire.QuoteRequest, + req lcpwire.Call, input jobstore.InputStreamState, quoteExpiry uint64, quoteTTL uint64, @@ -706,7 +706,7 @@ func (h *Handler) createInvoiceAndStartQuoteJob( h.logger.Warnw( "create invoice failed", "peer_pub_key", peerPubKey, - "job_id", req.Envelope.JobID.String(), + "job_id", req.Envelope.CallID.String(), "err", err, ) h.sendError( @@ -746,13 +746,13 @@ func (h *Handler) createInvoiceAndStartQuoteJob( PeerPubKey: key.PeerPubKey, JobID: key.JobID, State: jobstore.StateWaitingPayment, - QuoteRequest: &reqCopy, + Call: &reqCopy, InputStream: &inputCopy, QuoteExpiry: quoteExpiry, TermsHash: &termsCopy, PaymentHash: &paymentHash, InvoiceAddIndex: &addIndex, - QuoteResponse: "eRespCopy, + Quote: "eRespCopy, }) h.startJobRunner(ctx, key, reqCopy, inputCopy, quoteResp, invoice, remoteMaxPayload) @@ -761,10 +761,10 @@ func (h *Handler) createInvoiceAndStartQuoteJob( func (h *Handler) rejectQuoteRequestIfProviderUnavailable( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, ) bool { if !h.cfg.Enabled { - h.sendError(ctx, peerPubKey, env, lcpwire.ErrorCodeUnsupportedTask, "provider disabled") + h.sendError(ctx, peerPubKey, env, lcpwire.ErrorCodeUnsupportedMethod, "provider disabled") return true } @@ -773,7 +773,7 @@ func (h *Handler) rejectQuoteRequestIfProviderUnavailable( ctx, peerPubKey, env, - lcpwire.ErrorCodeUnsupportedTask, + lcpwire.ErrorCodeUnsupportedMethod, "compute backend disabled", ) return true @@ -785,9 +785,9 @@ func (h *Handler) rejectQuoteRequestIfProviderUnavailable( func (h *Handler) rejectQuoteRequestIfUnsupportedModel( ctx context.Context, peerPubKey string, - req lcpwire.QuoteRequest, + req lcpwire.Call, ) bool { - switch req.TaskKind { + switch req.Method { case taskKindOpenAIChatCompletionsV1, taskKindOpenAIResponsesV1: default: return false @@ -796,12 +796,12 @@ func (h *Handler) rejectQuoteRequestIfUnsupportedModel( return false } if req.ParamsBytes == nil { - h.sendError(ctx, peerPubKey, req.Envelope, lcpwire.ErrorCodeUnsupportedParams, "params are required") + h.sendError(ctx, peerPubKey, req.Envelope, lcpwire.ErrorCodeUnsupportedMethod, "params are required") return true } var model string - switch req.TaskKind { + switch req.Method { case taskKindOpenAIChatCompletionsV1: params, err := lcpwire.DecodeOpenAIChatCompletionsV1Params(*req.ParamsBytes) if err != nil { @@ -809,7 +809,7 @@ func (h *Handler) rejectQuoteRequestIfUnsupportedModel( ctx, peerPubKey, req.Envelope, - lcpwire.ErrorCodeUnsupportedParams, + lcpwire.ErrorCodeUnsupportedMethod, "openai.chat_completions.v1 params must be a valid TLV stream", ) return true @@ -822,7 +822,7 @@ func (h *Handler) rejectQuoteRequestIfUnsupportedModel( ctx, peerPubKey, req.Envelope, - lcpwire.ErrorCodeUnsupportedParams, + lcpwire.ErrorCodeUnsupportedMethod, "openai.responses.v1 params must be a valid TLV stream", ) return true @@ -841,7 +841,7 @@ func (h *Handler) rejectQuoteRequestIfUnsupportedModel( } slices.Sort(supported) msg = fmt.Sprintf("%s (supported: %s)", msg, strings.Join(supported, ", ")) - h.sendError(ctx, peerPubKey, req.Envelope, lcpwire.ErrorCodeUnsupportedTask, msg) + h.sendError(ctx, peerPubKey, req.Envelope, lcpwire.ErrorCodeUnsupportedMethod, msg) return true } @@ -866,7 +866,7 @@ func (h *Handler) handleCancel(_ context.Context, msg lndpeermsg.InboundCustomMe key := jobstore.Key{ PeerPubKey: msg.PeerPubKey, - JobID: cancelMsg.Envelope.JobID, + JobID: cancelMsg.Envelope.CallID, } if !h.checkReplay(msg.PeerPubKey, cancelMsg.Envelope, now, "lcp_cancel") { @@ -877,7 +877,7 @@ func (h *Handler) handleCancel(_ context.Context, msg lndpeermsg.InboundCustomMe h.logger.Infow( "job canceled by requester", "peer_pub_key", msg.PeerPubKey, - "job_id", cancelMsg.Envelope.JobID.String(), + "job_id", cancelMsg.Envelope.CallID.String(), ) } h.updateState(key, jobstore.StateCanceled) @@ -885,7 +885,7 @@ func (h *Handler) handleCancel(_ context.Context, msg lndpeermsg.InboundCustomMe func (h *Handler) dropIfExpired( peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, now uint64, logMessage string, ) bool { @@ -914,20 +914,20 @@ func (h *Handler) remoteManifestFor(peerPubKey string) *lcpwire.Manifest { } func (h *Handler) newQuoteResponse( - reqEnvelope lcpwire.JobEnvelope, + reqEnvelope lcpwire.CallEnvelope, quoteExpiry uint64, priceMsat uint64, termsHash lcp.Hash32, paymentRequest string, -) lcpwire.QuoteResponse { - respEnvelope := lcpwire.JobEnvelope{ +) lcpwire.Quote { + respEnvelope := lcpwire.CallEnvelope{ ProtocolVersion: reqEnvelope.ProtocolVersion, - JobID: reqEnvelope.JobID, + CallID: reqEnvelope.CallID, MsgID: h.mustNewMsgID(), Expiry: quoteExpiry, } - return lcpwire.QuoteResponse{ + return lcpwire.Quote{ Envelope: respEnvelope, PriceMsat: priceMsat, QuoteExpiry: quoteExpiry, @@ -938,7 +938,7 @@ func (h *Handler) newQuoteResponse( func invoiceFromJob(job jobstore.Job) InvoiceResult { invoice := InvoiceResult{ - PaymentRequest: job.QuoteResponse.PaymentRequest, + PaymentRequest: job.Quote.PaymentRequest, } if job.PaymentHash != nil { invoice.PaymentHash = *job.PaymentHash @@ -960,7 +960,7 @@ func (h *Handler) nowUnix() (uint64, bool) { func (h *Handler) checkReplay( peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, now uint64, msgName string, ) bool { @@ -984,7 +984,7 @@ func (h *Handler) checkReplay( result := h.replay.CheckAndRemember( replaystore.Key{ PeerPubKey: peerPubKey, - JobID: env.JobID, + JobID: env.CallID, MsgID: env.MsgID, }, effectiveExpiry, @@ -1015,15 +1015,15 @@ func (h *Handler) checkReplay( func (h *Handler) sendQuoteResponse( ctx context.Context, peerPubKey string, - resp lcpwire.QuoteResponse, + resp lcpwire.Quote, remoteMaxPayload uint32, ) bool { - payload, err := lcpwire.EncodeQuoteResponse(resp) + payload, err := lcpwire.EncodeQuote(resp) if err != nil { h.logger.Warnw( - "encode lcp_quote_response failed", + "encode lcp_quote failed", "peer_pub_key", peerPubKey, - "job_id", resp.Envelope.JobID.String(), + "job_id", resp.Envelope.CallID.String(), "err", err, ) return false @@ -1035,16 +1035,16 @@ func (h *Handler) sendQuoteResponse( peerPubKey, resp.Envelope, lcpwire.ErrorCodePayloadTooLarge, - "quote_response exceeds remote max_payload_bytes", + "quote exceeds remote max_payload_bytes", ) return false } - if sendErr := h.messenger.SendCustomMessage(ctx, peerPubKey, lcpwire.MessageTypeQuoteResponse, payload); sendErr != nil { + if sendErr := h.messenger.SendCustomMessage(ctx, peerPubKey, lcpwire.MessageTypeQuote, payload); sendErr != nil { h.logger.Warnw( - "send lcp_quote_response failed", + "send lcp_quote failed", "peer_pub_key", peerPubKey, - "job_id", resp.Envelope.JobID.String(), + "job_id", resp.Envelope.CallID.String(), "err", sendErr, ) return false @@ -1055,9 +1055,9 @@ func (h *Handler) sendQuoteResponse( func (h *Handler) startJobRunner( _ context.Context, key jobstore.Key, - req lcpwire.QuoteRequest, + req lcpwire.Call, input jobstore.InputStreamState, - quoteResp lcpwire.QuoteResponse, + quoteResp lcpwire.Quote, invoice InvoiceResult, remoteMaxPayload uint32, ) { @@ -1112,21 +1112,21 @@ func (h *Handler) startJobRunner( func (h *Handler) runJob( ctx context.Context, key jobstore.Key, - req lcpwire.QuoteRequest, + req lcpwire.Call, input jobstore.InputStreamState, - quoteResp lcpwire.QuoteResponse, + quoteResp lcpwire.Quote, invoice InvoiceResult, remoteMaxPayload uint32, ) { defer h.forgetJob(key) - jobID := quoteResp.Envelope.JobID.String() + jobID := quoteResp.Envelope.CallID.String() model := modelFromRequest(req) inputBytes := len(input.Buf) meta := jobLogMeta{ peerPubKey: key.PeerPubKey, jobID: jobID, - taskKind: req.TaskKind, + taskKind: req.Method, model: model, inputBytes: inputBytes, priceMsat: quoteResp.PriceMsat, @@ -1212,7 +1212,7 @@ func (h *Handler) runJob( ctx, key.PeerPubKey, quoteResp.Envelope, - req.TaskKind, + req.Method, inputLen, result.Output, remoteMaxPayload, @@ -1281,7 +1281,7 @@ func (h *Handler) runJob( ctx, key.PeerPubKey, quoteResp.Envelope, - req.TaskKind, + req.Method, wantStreaming, inputLen, result.OutputBytes, @@ -1298,7 +1298,7 @@ func (h *Handler) runJob( func (h *Handler) sendResult( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, taskKind string, streaming bool, inputLen uint64, @@ -1311,7 +1311,7 @@ func (h *Handler) sendResult( remoteManifest, remoteMaxPayload, ok := h.resolveRemoteManifestAndMaxPayload( peerPubKey, - env.JobID, + env.CallID, remoteMaxPayload, ) if !ok { @@ -1319,13 +1319,13 @@ func (h *Handler) sendResult( } totalLen := uint64(len(output)) - if totalLen > remoteManifest.MaxStreamBytes || inputLen+totalLen > remoteManifest.MaxJobBytes { + if totalLen > remoteManifest.MaxStreamBytes || inputLen+totalLen > remoteManifest.MaxCallBytes { msg := "result exceeds remote stream limits" return h.sendTerminalResultStatus( ctx, peerPubKey, env, - lcpwire.ResultStatusFailed, + lcpwire.CompleteStatusFailed, &msg, remoteMaxPayload, ) @@ -1377,7 +1377,7 @@ func (h *Handler) sendResult( func (h *Handler) sendResultStreaming( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, taskKind string, inputLen uint64, output io.Reader, @@ -1389,7 +1389,7 @@ func (h *Handler) sendResultStreaming( remoteManifest, remoteMaxPayload, ok := h.resolveRemoteManifestAndMaxPayload( peerPubKey, - env.JobID, + env.CallID, remoteMaxPayload, ) if !ok { @@ -1400,9 +1400,9 @@ func (h *Handler) sendResultStreaming( if maxStreamBytes == 0 { maxStreamBytes = defaultMaxStreamBytes } - maxJobBytes := remoteManifest.MaxJobBytes + maxJobBytes := remoteManifest.MaxCallBytes if maxJobBytes == 0 { - maxJobBytes = defaultMaxJobBytes + maxJobBytes = defaultMaxCallBytes } streamID, ok := h.newStreamID() @@ -1493,7 +1493,7 @@ func (h *Handler) sendResultStreaming( ctx, peerPubKey, env, - lcpwire.ResultStatusFailed, + lcpwire.CompleteStatusFailed, &msg, remoteMaxPayload, ) @@ -1597,7 +1597,7 @@ func (h *Handler) newStreamID() (lcp.Hash32, bool) { func (h *Handler) sendResultStream( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, expiry uint64, streamID lcp.Hash32, output []byte, @@ -1639,7 +1639,7 @@ func (h *Handler) sendResultStream( func (h *Handler) sendResultStreamBegin( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, expiry uint64, streamID lcp.Hash32, totalLen uint64, @@ -1649,14 +1649,14 @@ func (h *Handler) sendResultStreamBegin( remoteMaxPayload uint32, ) bool { begin := lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: env.ProtocolVersion, - JobID: env.JobID, + CallID: env.CallID, MsgID: h.mustNewMsgID(), Expiry: expiry, }, StreamID: streamID, - Kind: lcpwire.StreamKindResult, + Kind: lcpwire.StreamKindResponse, TotalLen: &totalLen, SHA256: &resultHash, ContentType: contentType, @@ -1694,7 +1694,7 @@ func (h *Handler) sendResultStreamBegin( func (h *Handler) sendResultStreamBeginUnknown( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, expiry uint64, streamID lcp.Hash32, contentType string, @@ -1702,14 +1702,14 @@ func (h *Handler) sendResultStreamBeginUnknown( remoteMaxPayload uint32, ) bool { begin := lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: env.ProtocolVersion, - JobID: env.JobID, + CallID: env.CallID, MsgID: h.mustNewMsgID(), Expiry: expiry, }, StreamID: streamID, - Kind: lcpwire.StreamKindResult, + Kind: lcpwire.StreamKindResponse, ContentType: contentType, ContentEncoding: contentEncoding, } @@ -1745,7 +1745,7 @@ func (h *Handler) sendResultStreamBeginUnknown( func (h *Handler) sendResultStreamChunks( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, expiry uint64, streamID lcp.Hash32, output []byte, @@ -1789,7 +1789,7 @@ func (h *Handler) sendResultStreamChunks( } func encodeFittingResultStreamChunk( - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, expiry uint64, streamID lcp.Hash32, seq uint32, @@ -1803,9 +1803,9 @@ func encodeFittingResultStreamChunk( for { chunk := lcpwire.StreamChunk{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: env.ProtocolVersion, - JobID: env.JobID, + CallID: env.CallID, Expiry: expiry, }, StreamID: streamID, @@ -1831,7 +1831,7 @@ func encodeFittingResultStreamChunk( func (h *Handler) sendResultStreamEnd( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, expiry uint64, streamID lcp.Hash32, totalLen uint64, @@ -1839,9 +1839,9 @@ func (h *Handler) sendResultStreamEnd( remoteMaxPayload uint32, ) bool { end := lcpwire.StreamEnd{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: env.ProtocolVersion, - JobID: env.JobID, + CallID: env.CallID, MsgID: h.mustNewMsgID(), Expiry: expiry, }, @@ -1881,7 +1881,7 @@ func (h *Handler) sendResultStreamEnd( func (h *Handler) sendTerminalOKResult( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, expiry uint64, streamID lcp.Hash32, totalLen uint64, @@ -1890,44 +1890,44 @@ func (h *Handler) sendTerminalOKResult( resultContentEncoding string, remoteMaxPayload uint32, ) bool { - terminal := lcpwire.Result{ - Envelope: lcpwire.JobEnvelope{ + terminal := lcpwire.Complete{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: env.ProtocolVersion, - JobID: env.JobID, + CallID: env.CallID, MsgID: h.mustNewMsgID(), Expiry: expiry, }, - Status: lcpwire.ResultStatusOK, - OK: &lcpwire.ResultOK{ - ResultStreamID: streamID, - ResultHash: resultHash, - ResultLen: totalLen, - ResultContentType: resultContentType, - ResultContentEncoding: resultContentEncoding, + Status: lcpwire.CompleteStatusOK, + OK: &lcpwire.CompleteOK{ + ResponseStreamID: streamID, + ResponseHash: resultHash, + ResponseLen: totalLen, + ResponseContentType: resultContentType, + ResponseContentEncoding: resultContentEncoding, }, } - terminalPayload, err := lcpwire.EncodeResult(terminal) + terminalPayload, err := lcpwire.EncodeComplete(terminal) if err != nil { h.logger.Warnw( - "encode lcp_result failed", + "encode lcp_complete failed", "peer_pub_key", peerPubKey, - "job_id", env.JobID.String(), + "job_id", env.CallID.String(), "err", err, ) return false } if uint64(len(terminalPayload)) > uint64(remoteMaxPayload) { h.logger.Warnw( - "lcp_result exceeds remote max_payload_bytes", + "lcp_complete exceeds remote max_payload_bytes", "peer_pub_key", peerPubKey, "payload_len", len(terminalPayload), "max_payload_bytes", remoteMaxPayload, ) return false } - if sendErr := h.messenger.SendCustomMessage(ctx, peerPubKey, lcpwire.MessageTypeResult, terminalPayload); sendErr != nil { + if sendErr := h.messenger.SendCustomMessage(ctx, peerPubKey, lcpwire.MessageTypeComplete, terminalPayload); sendErr != nil { h.logger.Warnw( - "send lcp_result failed", + "send lcp_complete failed", "peer_pub_key", peerPubKey, "err", sendErr, ) @@ -1939,8 +1939,8 @@ func (h *Handler) sendTerminalOKResult( func (h *Handler) sendTerminalResultStatus( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, - status lcpwire.ResultStatus, + env lcpwire.CallEnvelope, + status lcpwire.CompleteStatus, message *string, remoteMaxPayload uint32, ) bool { @@ -1948,10 +1948,10 @@ func (h *Handler) sendTerminalResultStatus( return false } - res := lcpwire.Result{ - Envelope: lcpwire.JobEnvelope{ + res := lcpwire.Complete{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: env.ProtocolVersion, - JobID: env.JobID, + CallID: env.CallID, MsgID: h.mustNewMsgID(), Expiry: h.resultExpiry(), }, @@ -1959,10 +1959,10 @@ func (h *Handler) sendTerminalResultStatus( Message: message, } - payload, err := lcpwire.EncodeResult(res) + payload, err := lcpwire.EncodeComplete(res) if err != nil { h.logger.Warnw( - "encode lcp_result failed", + "encode lcp_complete failed", "peer_pub_key", peerPubKey, "err", err, ) @@ -1970,7 +1970,7 @@ func (h *Handler) sendTerminalResultStatus( } if remoteMaxPayload != 0 && uint64(len(payload)) > uint64(remoteMaxPayload) { h.logger.Warnw( - "lcp_result exceeds remote max_payload_bytes", + "lcp_complete exceeds remote max_payload_bytes", "peer_pub_key", peerPubKey, "payload_len", len(payload), "max_payload_bytes", remoteMaxPayload, @@ -1978,11 +1978,11 @@ func (h *Handler) sendTerminalResultStatus( return false } - if sendErr := h.messenger.SendCustomMessage(ctx, peerPubKey, lcpwire.MessageTypeResult, payload); sendErr != nil { + if sendErr := h.messenger.SendCustomMessage(ctx, peerPubKey, lcpwire.MessageTypeComplete, payload); sendErr != nil { h.logger.Warnw( - "send lcp_result failed", + "send lcp_complete failed", "peer_pub_key", peerPubKey, - "job_id", env.JobID.String(), + "job_id", env.CallID.String(), "err", sendErr, ) return false @@ -1994,7 +1994,7 @@ func (h *Handler) sendTerminalResultStatus( func (h *Handler) sendError( ctx context.Context, peerPubKey string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, code lcpwire.ErrorCode, message string, ) { @@ -2007,9 +2007,9 @@ func (h *Handler) sendError( } errPayload, err := lcpwire.EncodeError(lcpwire.Error{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: env.ProtocolVersion, - JobID: env.JobID, + CallID: env.CallID, MsgID: msgID, Expiry: expiry, }, @@ -2020,7 +2020,7 @@ func (h *Handler) sendError( h.logger.Warnw( "encode lcp_error failed", "peer_pub_key", peerPubKey, - "job_id", env.JobID.String(), + "job_id", env.CallID.String(), "code", code, "err", err, ) @@ -2030,7 +2030,7 @@ func (h *Handler) sendError( h.logger.Warnw( "send lcp_error failed", "peer_pub_key", peerPubKey, - "job_id", env.JobID.String(), + "job_id", env.CallID.String(), "code", code, "err", sendErr, ) @@ -2110,8 +2110,8 @@ func remoteMaxPayloadBytes(manifest *lcpwire.Manifest) uint32 { return manifest.MaxPayloadBytes } -func quoteRequestEqual(a lcpwire.QuoteRequest, b lcpwire.QuoteRequest) bool { - if a.TaskKind != b.TaskKind { +func quoteRequestEqual(a lcpwire.Call, b lcpwire.Call) bool { + if a.Method != b.Method { return false } @@ -2187,7 +2187,7 @@ func validateInputStreamBegin(begin lcpwire.StreamBegin) (uint64, lcp.Hash32, *V } totalLen := *begin.TotalLen - if totalLen > defaultMaxStreamBytes || totalLen > defaultMaxJobBytes { + if totalLen > defaultMaxStreamBytes || totalLen > defaultMaxCallBytes { return 0, lcp.Hash32{}, &ValidationError{ Code: lcpwire.ErrorCodePayloadTooLarge, Message: "input stream exceeds local limits", @@ -2199,14 +2199,14 @@ func validateInputStreamBegin(begin lcpwire.StreamBegin) (uint64, lcp.Hash32, *V //nolint:gocognit // Validation intentionally mirrors wire rules and error codes. func validateInputStream( - req lcpwire.QuoteRequest, + req lcpwire.Call, input jobstore.InputStreamState, ) *ValidationError { - switch req.TaskKind { + switch req.Method { case taskKindOpenAIChatCompletionsV1: if req.ParamsBytes == nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.chat_completions.v1 params are required", } } @@ -2214,7 +2214,7 @@ func validateInputStream( params, err := lcpwire.DecodeOpenAIChatCompletionsV1Params(*req.ParamsBytes) if err != nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.chat_completions.v1 params must be a valid TLV stream", } } @@ -2222,25 +2222,25 @@ func validateInputStream( parsed, err := parseOpenAIChatCompletionsRequest(input.Buf) if err != nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.chat_completions.v1 input must be valid json", } } if parsed.Model == "" { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "request_json.model is required", } } if len(parsed.Messages) == 0 { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "request_json.messages is required", } } if parsed.Model != params.Model { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "params.model must match request_json.model", } } @@ -2249,7 +2249,7 @@ func validateInputStream( case taskKindOpenAIResponsesV1: if req.ParamsBytes == nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.responses.v1 params are required", } } @@ -2257,7 +2257,7 @@ func validateInputStream( params, err := lcpwire.DecodeOpenAIResponsesV1Params(*req.ParamsBytes) if err != nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.responses.v1 params must be a valid TLV stream", } } @@ -2265,25 +2265,25 @@ func validateInputStream( parsed, err := parseOpenAIResponsesRequest(input.Buf) if err != nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.responses.v1 input must be valid json", } } if parsed.Model == "" { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "request_json.model is required", } } if len(bytes.TrimSpace(parsed.Input)) == 0 || bytes.Equal(bytes.TrimSpace(parsed.Input), []byte("null")) { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "request_json.input is required", } } if parsed.Model != params.Model { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "params.model must match request_json.model", } } @@ -2322,12 +2322,12 @@ func resultContentTypeForTaskKind(taskKind string, streaming bool) string { } } -func modelFromRequest(req lcpwire.QuoteRequest) string { +func modelFromRequest(req lcpwire.Call) string { if req.ParamsBytes == nil { return "" } - switch req.TaskKind { + switch req.Method { case taskKindOpenAIChatCompletionsV1: params, err := lcpwire.DecodeOpenAIChatCompletionsV1Params(*req.ParamsBytes) if err != nil { @@ -2348,9 +2348,9 @@ func modelFromRequest(req lcpwire.QuoteRequest) string { func computeErrorCode(err error) lcpwire.ErrorCode { switch { case errors.Is(err, computebackend.ErrUnsupportedTaskKind): - return lcpwire.ErrorCodeUnsupportedTask + return lcpwire.ErrorCodeUnsupportedMethod case errors.Is(err, computebackend.ErrInvalidTask): - return lcpwire.ErrorCodeUnsupportedParams + return lcpwire.ErrorCodeUnsupportedMethod case errors.Is(err, computebackend.ErrUnauthenticated): return lcpwire.ErrorCodePaymentInvalid case errors.Is(err, computebackend.ErrBackendUnavailable): @@ -2363,7 +2363,7 @@ func computeErrorCode(err error) lcpwire.ErrorCode { func quoteErrorCode(err error) lcpwire.ErrorCode { switch { case errors.Is(err, llm.ErrUnknownModel): - return lcpwire.ErrorCodeUnsupportedTask + return lcpwire.ErrorCodeUnsupportedMethod case errors.Is(err, llm.ErrPriceOverflow): return lcpwire.ErrorCodeRateLimited default: @@ -2490,10 +2490,10 @@ func (h *Handler) maxOutputTokensForModel(model string) (uint32, error) { } func (h *Handler) planTask( - req lcpwire.QuoteRequest, + req lcpwire.Call, inputBytes []byte, ) (computebackend.Task, llm.ExecutionPolicy, string, bool, error) { - switch req.TaskKind { + switch req.Method { case taskKindOpenAIChatCompletionsV1: return h.planOpenAIChatCompletionsV1Task(req, inputBytes) case taskKindOpenAIResponsesV1: @@ -2502,13 +2502,13 @@ func (h *Handler) planTask( return computebackend.Task{}, llm.ExecutionPolicy{}, "", false, fmt.Errorf( "%w: %q", computebackend.ErrUnsupportedTaskKind, - req.TaskKind, + req.Method, ) } } func (h *Handler) planOpenAIChatCompletionsV1Task( - req lcpwire.QuoteRequest, + req lcpwire.Call, inputBytes []byte, ) (computebackend.Task, llm.ExecutionPolicy, string, bool, error) { if req.ParamsBytes == nil { @@ -2583,7 +2583,7 @@ func (h *Handler) planOpenAIChatCompletionsV1Task( streaming := parsed.Stream != nil && *parsed.Stream task := computebackend.Task{ - TaskKind: req.TaskKind, + TaskKind: req.Method, Model: params.Model, InputBytes: append([]byte(nil), inputBytes...), } @@ -2592,7 +2592,7 @@ func (h *Handler) planOpenAIChatCompletionsV1Task( } func (h *Handler) planOpenAIResponsesV1Task( - req lcpwire.QuoteRequest, + req lcpwire.Call, inputBytes []byte, ) (computebackend.Task, llm.ExecutionPolicy, string, bool, error) { if req.ParamsBytes == nil { @@ -2650,7 +2650,7 @@ func (h *Handler) planOpenAIResponsesV1Task( streaming := parsed.Stream != nil && *parsed.Stream task := computebackend.Task{ - TaskKind: req.TaskKind, + TaskKind: req.Method, Model: params.Model, InputBytes: append([]byte(nil), inputBytes...), } @@ -2673,7 +2673,7 @@ func (h *Handler) priceTable() llm.PriceTable { } func (h *Handler) quotePrice( - req lcpwire.QuoteRequest, + req lcpwire.Call, inputBytes []byte, ) (llm.PriceBreakdown, error) { planned, policy, model, _, err := h.planTask(req, inputBytes) diff --git a/go-lcpd/internal/provider/handler_test.go b/go-lcpd/internal/provider/handler_test.go index 24cf9b8..b45994d 100644 --- a/go-lcpd/internal/provider/handler_test.go +++ b/go-lcpd/internal/provider/handler_test.go @@ -47,7 +47,7 @@ func TestHandler_HandleInputStream_SendsQuoteResponse(t *testing.T) { ProtocolVersion: lcpwire.ProtocolVersionV02, MaxPayloadBytes: 65535, MaxStreamBytes: 4_194_304, - MaxJobBytes: 8_388_608, + MaxCallBytes: 8_388_608, }) handler := NewHandler( @@ -69,14 +69,14 @@ func TestHandler_HandleInputStream_SendsQuoteResponse(t *testing.T) { model := "gpt-5.2" req := newOpenAIChatCompletionsV1QuoteRequest(model) inputBytes := fmt.Appendf(nil, `{"model":%q,"messages":[{"role":"user","content":"prompt"}]}`, model) - payload := mustEncodeQuoteRequest(t, req) + payload := mustEncodeCall(t, req) handler.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer1", - MsgType: uint16(lcpwire.MessageTypeQuoteRequest), + MsgType: uint16(lcpwire.MessageTypeCall), Payload: payload, }) - key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.JobID} + key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.CallID} t.Cleanup(func() { handler.cancelJob(key) }) streamID := mustHash32(0x11) @@ -85,14 +85,14 @@ func TestHandler_HandleInputStream_SendsQuoteResponse(t *testing.T) { inputHash := lcp.Hash32(sum) beginPayload := mustEncodeStreamBegin(t, lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x04), Expiry: req.Envelope.Expiry, }, StreamID: streamID, - Kind: lcpwire.StreamKindInput, + Kind: lcpwire.StreamKindRequest, TotalLen: &totalLen, SHA256: &inputHash, ContentType: contentTypeApplicationJSON, @@ -100,9 +100,9 @@ func TestHandler_HandleInputStream_SendsQuoteResponse(t *testing.T) { }) chunkPayload := mustEncodeStreamChunk(t, lcpwire.StreamChunk{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, Expiry: req.Envelope.Expiry, }, StreamID: streamID, @@ -111,9 +111,9 @@ func TestHandler_HandleInputStream_SendsQuoteResponse(t *testing.T) { }) endPayload := mustEncodeStreamEnd(t, lcpwire.StreamEnd{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x05), Expiry: req.Envelope.Expiry, }, @@ -139,11 +139,11 @@ func TestHandler_HandleInputStream_SendsQuoteResponse(t *testing.T) { }) sent := requireSingleSentMessage(t, messenger.messages()) - if sent.msgType != lcpwire.MessageTypeQuoteResponse { + if sent.msgType != lcpwire.MessageTypeQuote { t.Fatalf("expected quote_response message, got %d", sent.msgType) } - gotResp := mustDecodeQuoteResponse(t, sent.payload) + gotResp := mustDecodeQuote(t, sent.payload) price := mustQuotePriceForPrompt(t, policy, estimator, model, inputBytes) if diff := cmp.Diff(price.PriceMsat, gotResp.PriceMsat); diff != "" { t.Fatalf("price_msat mismatch (-want +got):\n%s", diff) @@ -173,15 +173,15 @@ func TestHandler_HandleInputStream_SendsQuoteResponse(t *testing.T) { } wantTermsHash := mustComputeTermsHash(t, lcp.Terms{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + JobID: req.Envelope.CallID, PriceMsat: price.PriceMsat, QuoteExpiry: wantExpiry, }, protocolcompat.TermsCommit{ - TaskKind: req.TaskKind, - Input: inputBytes, - InputContentType: contentTypeApplicationJSON, - InputContentEncoding: "identity", - Params: paramsBytes, + Method: req.Method, + Request: inputBytes, + RequestContentType: contentTypeApplicationJSON, + RequestContentEncoding: "identity", + Params: paramsBytes, }) if diff := cmp.Diff(wantTermsHash, gotResp.TermsHash); diff != "" { t.Fatalf("terms_hash mismatch (-want +got):\n%s", diff) @@ -203,9 +203,9 @@ func TestHandler_CheckReplay_ClampsEnvelopeExpiryWindow(t *testing.T) { } now := uint64(1000) - env := lcpwire.JobEnvelope{ + env := lcpwire.CallEnvelope{ ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + CallID: jobID, MsgID: msgID, Expiry: now + 1_000_000, } @@ -266,14 +266,14 @@ func TestHandler_HandleQuoteRequest_RejectsUnsupportedModel(t *testing.T) { handler.newMsgIDFn = func() (lcpwire.MsgID, error) { return fixedMsgID(0x03), nil } req := newOpenAIChatCompletionsV1QuoteRequest("x") - payload, err := lcpwire.EncodeQuoteRequest(req) + payload, err := lcpwire.EncodeCall(req) if err != nil { t.Fatalf("encode quote_request: %v", err) } handler.handleQuoteRequest(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer1", - MsgType: uint16(lcpwire.MessageTypeQuoteRequest), + MsgType: uint16(lcpwire.MessageTypeCall), Payload: payload, }) @@ -293,7 +293,7 @@ func TestHandler_HandleQuoteRequest_RejectsUnsupportedModel(t *testing.T) { if err != nil { t.Fatalf("decode lcp_error: %v", err) } - if diff := cmp.Diff(lcpwire.ErrorCodeUnsupportedTask, gotErr.Code); diff != "" { + if diff := cmp.Diff(lcpwire.ErrorCodeUnsupportedMethod, gotErr.Code); diff != "" { t.Fatalf("error code mismatch (-want +got):\n%s", diff) } if gotErr.Message == nil { @@ -337,24 +337,24 @@ func TestHandler_HandleQuoteRequest_ReusesExistingQuoteResponse(t *testing.T) { } termsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + JobID: req.Envelope.CallID, PriceMsat: price, QuoteExpiry: quoteExpiry, }, protocolcompat.TermsCommit{ - TaskKind: req.TaskKind, - Input: inputBytes, - InputContentType: contentTypeApplicationJSON, - InputContentEncoding: "identity", - Params: paramsBytes, + Method: req.Method, + Request: inputBytes, + RequestContentType: contentTypeApplicationJSON, + RequestContentEncoding: "identity", + Params: paramsBytes, }) if err != nil { t.Fatalf("ComputeTermsHash: %v", err) } - quoteResp := lcpwire.QuoteResponse{ - Envelope: lcpwire.JobEnvelope{ + quoteResp := lcpwire.Quote{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x10), Expiry: quoteExpiry, }, @@ -367,13 +367,13 @@ func TestHandler_HandleQuoteRequest_ReusesExistingQuoteResponse(t *testing.T) { addIndex := invoices.result.AddIndex jobs.Upsert(jobstore.Job{ PeerPubKey: "peer1", - JobID: req.Envelope.JobID, + JobID: req.Envelope.CallID, State: jobstore.StateWaitingPayment, QuoteExpiry: quoteExpiry, TermsHash: "eResp.TermsHash, PaymentHash: &paymentHash, InvoiceAddIndex: &addIndex, - QuoteResponse: "eResp, + Quote: "eResp, }) handler := NewHandler( @@ -391,17 +391,17 @@ func TestHandler_HandleQuoteRequest_ReusesExistingQuoteResponse(t *testing.T) { ) handler.clock = clock - payload, err := lcpwire.EncodeQuoteRequest(req) + payload, err := lcpwire.EncodeCall(req) if err != nil { t.Fatalf("encode quote_request: %v", err) } handler.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer1", - MsgType: uint16(lcpwire.MessageTypeQuoteRequest), + MsgType: uint16(lcpwire.MessageTypeCall), Payload: payload, }) - key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.JobID} + key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.CallID} t.Cleanup(func() { handler.cancelJob(key) }) if invoices.createCalls() != 0 { @@ -411,7 +411,7 @@ func TestHandler_HandleQuoteRequest_ReusesExistingQuoteResponse(t *testing.T) { if len(messages) != 1 { t.Fatalf("expected 1 message sent, got %d", len(messages)) } - gotResp, err := lcpwire.DecodeQuoteResponse(messages[0].payload) + gotResp, err := lcpwire.DecodeQuote(messages[0].payload) if err != nil { t.Fatalf("decode quote_response: %v", err) } @@ -456,18 +456,18 @@ func TestHandler_RunJob_OpenAIChatCompletionsV1_SettledSendsJSONResultMetadata(t handler.clock = clock handler.replay = nil - req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.QuoteRequest) { + req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.Call) { r.Envelope.Expiry = uint64(clock.now.Add(30 * time.Second).Unix()) }) inputBytes := []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"hi"}]}`) - payload := mustEncodeQuoteRequest(t, req) + payload := mustEncodeCall(t, req) handler.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer1", - MsgType: uint16(lcpwire.MessageTypeQuoteRequest), + MsgType: uint16(lcpwire.MessageTypeCall), Payload: payload, }) - key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.JobID} + key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.CallID} inputStreamID := mustHash32(0x11) inputTotalLen := uint64(len(inputBytes)) @@ -475,14 +475,14 @@ func TestHandler_RunJob_OpenAIChatCompletionsV1_SettledSendsJSONResultMetadata(t inputHash := lcp.Hash32(inputSum) beginPayload := mustEncodeStreamBegin(t, lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x04), Expiry: req.Envelope.Expiry, }, StreamID: inputStreamID, - Kind: lcpwire.StreamKindInput, + Kind: lcpwire.StreamKindRequest, TotalLen: &inputTotalLen, SHA256: &inputHash, ContentType: contentTypeApplicationJSON, @@ -490,9 +490,9 @@ func TestHandler_RunJob_OpenAIChatCompletionsV1_SettledSendsJSONResultMetadata(t }) chunkPayload := mustEncodeStreamChunk(t, lcpwire.StreamChunk{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, Expiry: req.Envelope.Expiry, }, StreamID: inputStreamID, @@ -501,9 +501,9 @@ func TestHandler_RunJob_OpenAIChatCompletionsV1_SettledSendsJSONResultMetadata(t }) endPayload := mustEncodeStreamEnd(t, lcpwire.StreamEnd{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x05), Expiry: req.Envelope.Expiry, }, @@ -538,26 +538,26 @@ func TestHandler_RunJob_OpenAIChatCompletionsV1_SettledSendsJSONResultMetadata(t messages := messenger.messages() begin := mustDecodeStreamBegin(t, messages[1].payload) - if got, want := begin.Kind, lcpwire.StreamKindResult; got != want { + if got, want := begin.Kind, lcpwire.StreamKindResponse; got != want { t.Fatalf("stream_begin.kind mismatch (-want +got):\n%s", cmp.Diff(want, got)) } if got, want := begin.ContentType, contentTypeApplicationJSON; got != want { t.Fatalf("stream_begin.content_type mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - gotResult := mustDecodeResult(t, messages[4].payload) - if got, want := gotResult.Status, lcpwire.ResultStatusOK; got != want { + gotResult := mustDecodeComplete(t, messages[4].payload) + if got, want := gotResult.Status, lcpwire.CompleteStatusOK; got != want { t.Fatalf("result.status mismatch (-want +got):\n%s", cmp.Diff(want, got)) } if gotResult.OK == nil { t.Fatalf("result ok metadata is nil") } - if got, want := gotResult.OK.ResultContentType, contentTypeApplicationJSON; got != want { - t.Fatalf("result.ok.result_content_type mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if got, want := gotResult.OK.ResponseContentType, contentTypeApplicationJSON; got != want { + t.Fatalf("result.ok.response_content_type mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if got, want := gotResult.OK.ResultContentEncoding, "identity"; got != want { + if got, want := gotResult.OK.ResponseContentEncoding, "identity"; got != want { t.Fatalf( - "result.ok.result_content_encoding mismatch (-want +got):\n%s", + "result.ok.response_content_encoding mismatch (-want +got):\n%s", cmp.Diff(want, got), ) } @@ -613,7 +613,7 @@ func TestHandler_LogsDoNotContainPromptOrResult(t *testing.T) { handler.clock = clock handler.replay = nil - req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.QuoteRequest) { + req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.Call) { r.Envelope.Expiry = uint64(clock.now.Add(30 * time.Second).Unix()) }) inputBytes := fmt.Appendf( @@ -621,14 +621,14 @@ func TestHandler_LogsDoNotContainPromptOrResult(t *testing.T) { `{"model":"gpt-5.2","messages":[{"role":"user","content":%q}]}`, promptText, ) - payload := mustEncodeQuoteRequest(t, req) + payload := mustEncodeCall(t, req) handler.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer1", - MsgType: uint16(lcpwire.MessageTypeQuoteRequest), + MsgType: uint16(lcpwire.MessageTypeCall), Payload: payload, }) - key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.JobID} + key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.CallID} t.Cleanup(func() { handler.cancelJob(key) }) inputStreamID := mustHash32(0x11) @@ -637,14 +637,14 @@ func TestHandler_LogsDoNotContainPromptOrResult(t *testing.T) { inputHash := lcp.Hash32(inputSum) beginPayload := mustEncodeStreamBegin(t, lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x04), Expiry: req.Envelope.Expiry, }, StreamID: inputStreamID, - Kind: lcpwire.StreamKindInput, + Kind: lcpwire.StreamKindRequest, TotalLen: &inputTotalLen, SHA256: &inputHash, ContentType: contentTypeApplicationJSON, @@ -652,9 +652,9 @@ func TestHandler_LogsDoNotContainPromptOrResult(t *testing.T) { }) chunkPayload := mustEncodeStreamChunk(t, lcpwire.StreamChunk{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, Expiry: req.Envelope.Expiry, }, StreamID: inputStreamID, @@ -663,9 +663,9 @@ func TestHandler_LogsDoNotContainPromptOrResult(t *testing.T) { }) endPayload := mustEncodeStreamEnd(t, lcpwire.StreamEnd{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x05), Expiry: req.Envelope.Expiry, }, @@ -764,18 +764,18 @@ func TestHandler_HandleStreamBegin_OpenAIChatCompletionsV1_RejectsWrongContentTy handler.clock = clock handler.replay = nil - req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.QuoteRequest) { + req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.Call) { r.Envelope.Expiry = uint64(clock.now.Add(30 * time.Second).Unix()) }) inputBytes := []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"hi"}]}`) - payload := mustEncodeQuoteRequest(t, req) + payload := mustEncodeCall(t, req) handler.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer1", - MsgType: uint16(lcpwire.MessageTypeQuoteRequest), + MsgType: uint16(lcpwire.MessageTypeCall), Payload: payload, }) - key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.JobID} + key := jobstore.Key{PeerPubKey: "peer1", JobID: req.Envelope.CallID} t.Cleanup(func() { handler.cancelJob(key) }) inputStreamID := mustHash32(0x11) @@ -784,14 +784,14 @@ func TestHandler_HandleStreamBegin_OpenAIChatCompletionsV1_RejectsWrongContentTy inputHash := lcp.Hash32(inputSum) beginPayload := mustEncodeStreamBegin(t, lcpwire.StreamBegin{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: req.Envelope.ProtocolVersion, - JobID: req.Envelope.JobID, + CallID: req.Envelope.CallID, MsgID: fixedMsgID(0x04), Expiry: req.Envelope.Expiry, }, StreamID: inputStreamID, - Kind: lcpwire.StreamKindInput, + Kind: lcpwire.StreamKindRequest, TotalLen: &inputTotalLen, SHA256: &inputHash, ContentType: contentTypeTextPlain, @@ -993,10 +993,10 @@ func mustQuotePriceForPrompt( return price } -func mustEncodeQuoteRequest(t *testing.T, req lcpwire.QuoteRequest) []byte { +func mustEncodeCall(t *testing.T, req lcpwire.Call) []byte { t.Helper() - payload, err := lcpwire.EncodeQuoteRequest(req) + payload, err := lcpwire.EncodeCall(req) if err != nil { t.Fatalf("encode quote_request: %v", err) } @@ -1033,10 +1033,10 @@ func mustEncodeStreamEnd(t *testing.T, end lcpwire.StreamEnd) []byte { return payload } -func mustDecodeQuoteResponse(t *testing.T, payload []byte) lcpwire.QuoteResponse { +func mustDecodeQuote(t *testing.T, payload []byte) lcpwire.Quote { t.Helper() - resp, err := lcpwire.DecodeQuoteResponse(payload) + resp, err := lcpwire.DecodeQuote(payload) if err != nil { t.Fatalf("decode quote_response: %v", err) } @@ -1053,10 +1053,10 @@ func mustDecodeStreamBegin(t *testing.T, payload []byte) lcpwire.StreamBegin { return begin } -func mustDecodeResult(t *testing.T, payload []byte) lcpwire.Result { +func mustDecodeComplete(t *testing.T, payload []byte) lcpwire.Complete { t.Helper() - result, err := lcpwire.DecodeResult(payload) + result, err := lcpwire.DecodeComplete(payload) if err != nil { t.Fatalf("decode lcp_result: %v", err) } diff --git a/go-lcpd/internal/provider/test_helpers_test.go b/go-lcpd/internal/provider/test_helpers_test.go index 8002b38..4c31c68 100644 --- a/go-lcpd/internal/provider/test_helpers_test.go +++ b/go-lcpd/internal/provider/test_helpers_test.go @@ -9,8 +9,8 @@ import ( func newOpenAIChatCompletionsV1QuoteRequest( model string, - opts ...func(*lcpwire.QuoteRequest), -) lcpwire.QuoteRequest { + opts ...func(*lcpwire.Call), +) lcpwire.Call { var jobID lcp.JobID for i := range jobID { jobID[i] = 0x01 @@ -25,14 +25,14 @@ func newOpenAIChatCompletionsV1QuoteRequest( } encoded := mustEncodeOpenAIChatCompletionsV1Params(params) - req := lcpwire.QuoteRequest{ - Envelope: lcpwire.JobEnvelope{ + req := lcpwire.Call{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + CallID: jobID, MsgID: msgID, Expiry: 1234, }, - TaskKind: "openai.chat_completions.v1", + Method: "openai.chat_completions.v1", ParamsBytes: &encoded, } @@ -42,20 +42,20 @@ func newOpenAIChatCompletionsV1QuoteRequest( return req } -func withTaskKind(taskKind string) func(*lcpwire.QuoteRequest) { - return func(req *lcpwire.QuoteRequest) { - req.TaskKind = taskKind +func withTaskKind(taskKind string) func(*lcpwire.Call) { + return func(req *lcpwire.Call) { + req.Method = taskKind req.ParamsBytes = nil } } -func manifestWithTemplates(templates ...lcpwire.TaskTemplate) *lcpwire.Manifest { +func manifestWithTemplates(templates ...lcpwire.MethodDescriptor) *lcpwire.Manifest { m := &lcpwire.Manifest{ - ProtocolVersion: lcpwire.ProtocolVersionV02, - MaxPayloadBytes: 65535, - MaxStreamBytes: 4_194_304, - MaxJobBytes: 8_388_608, - SupportedTasks: templates, + ProtocolVersion: lcpwire.ProtocolVersionV02, + MaxPayloadBytes: 65535, + MaxStreamBytes: 4_194_304, + MaxCallBytes: 8_388_608, + SupportedMethods: templates, } return m } @@ -75,7 +75,7 @@ func manifestPeerDirectory() *peerdirectory.Directory { ProtocolVersion: lcpwire.ProtocolVersionV02, MaxPayloadBytes: 65535, MaxStreamBytes: 4_194_304, - MaxJobBytes: 8_388_608, + MaxCallBytes: 8_388_608, }) return peers } diff --git a/go-lcpd/internal/provider/validation.go b/go-lcpd/internal/provider/validation.go index f44c217..b1a99b6 100644 --- a/go-lcpd/internal/provider/validation.go +++ b/go-lcpd/internal/provider/validation.go @@ -1,7 +1,6 @@ package provider import ( - "bytes" "fmt" "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" @@ -14,15 +13,15 @@ const ( type QuoteRequestValidator struct { SupportedProtocolVersions map[uint16]struct{} - SupportedTaskKinds map[string]struct{} + SupportedMethods map[string]struct{} } func DefaultValidator() QuoteRequestValidator { return QuoteRequestValidator{ SupportedProtocolVersions: map[uint16]struct{}{ - lcpwire.ProtocolVersionV02: {}, + lcpwire.ProtocolVersionV03: {}, }, - SupportedTaskKinds: map[string]struct{}{ + SupportedMethods: map[string]struct{}{ taskKindOpenAIChatCompletionsV1: {}, taskKindOpenAIResponsesV1: {}, }, @@ -42,7 +41,7 @@ func (e *ValidationError) Error() string { } func (v QuoteRequestValidator) ValidateQuoteRequest( - req lcpwire.QuoteRequest, + req lcpwire.Call, remoteManifest *lcpwire.Manifest, ) *ValidationError { if !v.protocolSupported(req.Envelope.ProtocolVersion) { @@ -52,10 +51,10 @@ func (v QuoteRequestValidator) ValidateQuoteRequest( } } - if !v.taskKindSupported(req.TaskKind) { + if !v.methodSupported(req.Method) { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedTask, - Message: fmt.Sprintf("unsupported task_kind: %q", req.TaskKind), + Code: lcpwire.ErrorCodeUnsupportedMethod, + Message: fmt.Sprintf("unsupported method: %q", req.Method), } } @@ -63,11 +62,11 @@ func (v QuoteRequestValidator) ValidateQuoteRequest( return err } - if remoteManifest != nil && len(remoteManifest.SupportedTasks) > 0 { - if !matchesSupportedTemplate(req, remoteManifest.SupportedTasks) { + if remoteManifest != nil && len(remoteManifest.SupportedMethods) > 0 { + if !matchesSupportedMethod(req.Method, remoteManifest.SupportedMethods) { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedTask, - Message: "task does not match any supported_tasks template", + Code: lcpwire.ErrorCodeUnsupportedMethod, + Message: "method does not match any supported_methods descriptor", } } } @@ -77,26 +76,26 @@ func (v QuoteRequestValidator) ValidateQuoteRequest( func (v QuoteRequestValidator) protocolSupported(protocolVersion uint16) bool { if len(v.SupportedProtocolVersions) == 0 { - return protocolVersion == lcpwire.ProtocolVersionV02 + return protocolVersion == lcpwire.ProtocolVersionV03 } _, ok := v.SupportedProtocolVersions[protocolVersion] return ok } -func (v QuoteRequestValidator) taskKindSupported(taskKind string) bool { - if len(v.SupportedTaskKinds) == 0 { - return taskKind == taskKindOpenAIChatCompletionsV1 +func (v QuoteRequestValidator) methodSupported(method string) bool { + if len(v.SupportedMethods) == 0 { + return method == taskKindOpenAIChatCompletionsV1 } - _, ok := v.SupportedTaskKinds[taskKind] + _, ok := v.SupportedMethods[method] return ok } -func validateTaskParams(req lcpwire.QuoteRequest) *ValidationError { - switch req.TaskKind { +func validateTaskParams(req lcpwire.Call) *ValidationError { + switch req.Method { case taskKindOpenAIChatCompletionsV1: if req.ParamsBytes == nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.chat_completions.v1 params are required", } } @@ -104,14 +103,14 @@ func validateTaskParams(req lcpwire.QuoteRequest) *ValidationError { params, err := lcpwire.DecodeOpenAIChatCompletionsV1Params(*req.ParamsBytes) if err != nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.chat_completions.v1 params must be a valid TLV stream", } } if len(params.Unknown) > 0 { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.chat_completions.v1 params contain unknown tlv types", } } @@ -120,7 +119,7 @@ func validateTaskParams(req lcpwire.QuoteRequest) *ValidationError { case taskKindOpenAIResponsesV1: if req.ParamsBytes == nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.responses.v1 params are required", } } @@ -128,14 +127,14 @@ func validateTaskParams(req lcpwire.QuoteRequest) *ValidationError { params, err := lcpwire.DecodeOpenAIResponsesV1Params(*req.ParamsBytes) if err != nil { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.responses.v1 params must be a valid TLV stream", } } if len(params.Unknown) > 0 { return &ValidationError{ - Code: lcpwire.ErrorCodeUnsupportedParams, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: "openai.responses.v1 params contain unknown tlv types", } } @@ -146,29 +145,11 @@ func validateTaskParams(req lcpwire.QuoteRequest) *ValidationError { } } -func matchesSupportedTemplate( - req lcpwire.QuoteRequest, - templates []lcpwire.TaskTemplate, -) bool { - for _, tmpl := range templates { - if matchesTemplate(req, tmpl) { +func matchesSupportedMethod(method string, supported []lcpwire.MethodDescriptor) bool { + for _, desc := range supported { + if desc.Method == method { return true } } return false } - -func matchesTemplate(req lcpwire.QuoteRequest, tmpl lcpwire.TaskTemplate) bool { - if req.TaskKind != tmpl.TaskKind { - return false - } - - if tmpl.ParamsBytes != nil { - if req.ParamsBytes == nil { - return false - } - return bytes.Equal(*req.ParamsBytes, *tmpl.ParamsBytes) - } - - return true -} diff --git a/go-lcpd/internal/provider/validation_test.go b/go-lcpd/internal/provider/validation_test.go index 783db98..5f927e5 100644 --- a/go-lcpd/internal/provider/validation_test.go +++ b/go-lcpd/internal/provider/validation_test.go @@ -41,7 +41,7 @@ func TestQuoteRequestValidator_UnsupportedTaskKind(t *testing.T) { t.Fatalf("expected validation error, got nil") } - want := lcpwire.ErrorCodeUnsupportedTask + want := lcpwire.ErrorCodeUnsupportedMethod if diff := cmp.Diff(want, err.Code); diff != "" { t.Fatalf("error code mismatch (-want +got):\n%s", diff) } @@ -51,12 +51,8 @@ func TestQuoteRequestValidator_SupportedTasksTemplateMismatch(t *testing.T) { t.Parallel() validator := defaultValidator() - encoded := mustEncodeOpenAIChatCompletionsV1Params( - lcpwire.OpenAIChatCompletionsV1Params{Model: "model-template"}, - ) - manifest := manifestWithTemplates(lcpwire.TaskTemplate{ - TaskKind: taskKindOpenAIChatCompletionsV1, - ParamsBytes: &encoded, + manifest := manifestWithTemplates(lcpwire.MethodDescriptor{ + Method: "some.other.method", }) req := newOpenAIChatCompletionsV1QuoteRequest("model-request") @@ -66,7 +62,7 @@ func TestQuoteRequestValidator_SupportedTasksTemplateMismatch(t *testing.T) { t.Fatalf("expected validation error, got nil") } - want := lcpwire.ErrorCodeUnsupportedTask + want := lcpwire.ErrorCodeUnsupportedMethod if diff := cmp.Diff(want, err.Code); diff != "" { t.Fatalf("error code mismatch (-want +got):\n%s", diff) } @@ -76,12 +72,8 @@ func TestQuoteRequestValidator_SupportedTasksTemplateMatch(t *testing.T) { t.Parallel() validator := defaultValidator() - encoded := mustEncodeOpenAIChatCompletionsV1Params( - lcpwire.OpenAIChatCompletionsV1Params{Model: "model-template"}, - ) - manifest := manifestWithTemplates(lcpwire.TaskTemplate{ - TaskKind: taskKindOpenAIChatCompletionsV1, - ParamsBytes: &encoded, + manifest := manifestWithTemplates(lcpwire.MethodDescriptor{ + Method: taskKindOpenAIChatCompletionsV1, }) req := newOpenAIChatCompletionsV1QuoteRequest("model-template") @@ -98,7 +90,7 @@ func TestQuoteRequestValidator_OpenAIChatCompletionsV1_AcceptsValidParams(t *tes SupportedProtocolVersions: map[uint16]struct{}{ lcpwire.ProtocolVersionV02: {}, }, - SupportedTaskKinds: map[string]struct{}{ + SupportedMethods: map[string]struct{}{ taskKindOpenAIChatCompletionsV1: {}, }, } @@ -117,12 +109,12 @@ func TestQuoteRequestValidator_OpenAIChatCompletionsV1_RejectsMissingParams(t *t SupportedProtocolVersions: map[uint16]struct{}{ lcpwire.ProtocolVersionV02: {}, }, - SupportedTaskKinds: map[string]struct{}{ + SupportedMethods: map[string]struct{}{ taskKindOpenAIChatCompletionsV1: {}, }, } - req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.QuoteRequest) { + req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.Call) { r.ParamsBytes = nil }) @@ -131,7 +123,7 @@ func TestQuoteRequestValidator_OpenAIChatCompletionsV1_RejectsMissingParams(t *t t.Fatalf("expected validation error, got nil") } - want := lcpwire.ErrorCodeUnsupportedParams + want := lcpwire.ErrorCodeUnsupportedMethod if diff := cmp.Diff(want, err.Code); diff != "" { t.Fatalf("error code mismatch (-want +got):\n%s", diff) } @@ -144,7 +136,7 @@ func TestQuoteRequestValidator_OpenAIChatCompletionsV1_RejectsUnknownParams(t *t SupportedProtocolVersions: map[uint16]struct{}{ lcpwire.ProtocolVersionV02: {}, }, - SupportedTaskKinds: map[string]struct{}{ + SupportedMethods: map[string]struct{}{ taskKindOpenAIChatCompletionsV1: {}, }, } @@ -165,7 +157,7 @@ func TestQuoteRequestValidator_OpenAIChatCompletionsV1_RejectsUnknownParams(t *t } encoded := buf.Bytes() - req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.QuoteRequest) { + req := newOpenAIChatCompletionsV1QuoteRequest("gpt-5.2", func(r *lcpwire.Call) { r.ParamsBytes = &encoded }) @@ -174,7 +166,7 @@ func TestQuoteRequestValidator_OpenAIChatCompletionsV1_RejectsUnknownParams(t *t t.Fatalf("expected validation error, got nil") } - want := lcpwire.ErrorCodeUnsupportedParams + want := lcpwire.ErrorCodeUnsupportedMethod if diff := cmp.Diff(want, vErr.Code); diff != "" { t.Fatalf("error code mismatch (-want +got):\n%s", diff) } @@ -185,7 +177,7 @@ func defaultValidator() QuoteRequestValidator { SupportedProtocolVersions: map[uint16]struct{}{ lcpwire.ProtocolVersionV02: {}, }, - SupportedTaskKinds: map[string]struct{}{ + SupportedMethods: map[string]struct{}{ taskKindOpenAIChatCompletionsV1: {}, }, } diff --git a/go-lcpd/internal/requesterjobstore/store.go b/go-lcpd/internal/requesterjobstore/store.go index 8c29d25..e504126 100644 --- a/go-lcpd/internal/requesterjobstore/store.go +++ b/go-lcpd/internal/requesterjobstore/store.go @@ -26,9 +26,9 @@ const ( var ( ErrPeerIDRequired = errors.New("peer_id is required") - ErrTaskRequired = errors.New("task is required") - ErrTermsRequired = errors.New("terms are required") - ErrInvalidJobID = errors.New("job_id must be 32 bytes") + ErrCallRequired = errors.New("call is required") + ErrQuoteRequired = errors.New("quote is required") + ErrInvalidCallID = errors.New("call_id must be 32 bytes") ErrQuoteExpiryRequired = errors.New("quote_expiry is required") ErrInvalidState = errors.New("invalid state") ErrNotFound = errors.New("job not found") @@ -43,8 +43,8 @@ type key struct { type jobEntry struct { peerID string jobID lcp.JobID - task *lcpdv1.Task - terms *lcpdv1.Terms + call *lcpdv1.CallSpec + quote *lcpdv1.Quote state State quoteExpiry time.Time createdAt time.Time @@ -54,8 +54,8 @@ type jobEntry struct { type Job struct { PeerID string JobID lcp.JobID - Task *lcpdv1.Task - Terms *lcpdv1.Terms + Call *lcpdv1.CallSpec + Quote *lcpdv1.Quote State State CreatedAt time.Time UpdatedAt time.Time @@ -103,40 +103,40 @@ func (s *Store) SetClock(now func() time.Time) { s.clock = now } -// PutQuote stores a quoted job. Existing entries for the same peer+job_id are replaced. -func (s *Store) PutQuote(peerID string, task *lcpdv1.Task, terms *lcpdv1.Terms) error { +// PutQuote stores a quoted call. Existing entries for the same peer+call_id are replaced. +func (s *Store) PutQuote(peerID string, call *lcpdv1.CallSpec, quote *lcpdv1.Quote) error { if peerID == "" { return ErrPeerIDRequired } - if task == nil { - return ErrTaskRequired + if call == nil { + return ErrCallRequired } - if terms == nil { - return ErrTermsRequired + if quote == nil { + return ErrQuoteRequired } - jobID, err := toJobID(terms.GetJobId()) + jobID, err := toJobID(quote.GetCallId()) if err != nil { return err } - quoteExpiry := terms.GetQuoteExpiry() + quoteExpiry := quote.GetQuoteExpiry() if quoteExpiry == nil { return ErrQuoteExpiryRequired } now := s.now() - taskClone := proto.Clone(task) - taskCopy, ok := taskClone.(*lcpdv1.Task) + callClone := proto.Clone(call) + callCopy, ok := callClone.(*lcpdv1.CallSpec) if !ok { - return fmt.Errorf("clone task: unexpected type %T", taskClone) + return fmt.Errorf("clone call: unexpected type %T", callClone) } - termsClone := proto.Clone(terms) - termsCopy, ok := termsClone.(*lcpdv1.Terms) + quoteClone := proto.Clone(quote) + quoteCopy, ok := quoteClone.(*lcpdv1.Quote) if !ok { - return fmt.Errorf("clone terms: unexpected type %T", termsClone) + return fmt.Errorf("clone quote: unexpected type %T", quoteClone) } k := key{peerID: peerID, jobID: jobID} @@ -157,8 +157,8 @@ func (s *Store) PutQuote(peerID string, task *lcpdv1.Task, terms *lcpdv1.Terms) entry := jobEntry{ peerID: peerID, jobID: jobID, - task: taskCopy, - terms: termsCopy, + call: callCopy, + quote: quoteCopy, state: StateQuoted, quoteExpiry: quoteExpiry.AsTime(), createdAt: createdAt, @@ -170,9 +170,9 @@ func (s *Store) PutQuote(peerID string, task *lcpdv1.Task, terms *lcpdv1.Terms) return nil } -// GetTerms returns a copy of the stored terms for the job. If the quote_expiry +// GetQuote returns a copy of the stored quote for the call. If the quote_expiry // has passed, the job is marked expired and ErrExpired is returned. -func (s *Store) GetTerms(peerID string, jobID lcp.JobID) (*lcpdv1.Terms, error) { +func (s *Store) GetQuote(peerID string, jobID lcp.JobID) (*lcpdv1.Quote, error) { k := key{peerID: peerID, jobID: jobID} s.mu.Lock() @@ -194,12 +194,12 @@ func (s *Store) GetTerms(peerID string, jobID lcp.JobID) (*lcpdv1.Terms, error) return nil, ErrExpired } - termsClone := proto.Clone(entry.terms) - termsCopy, ok := termsClone.(*lcpdv1.Terms) + quoteClone := proto.Clone(entry.quote) + quoteCopy, ok := quoteClone.(*lcpdv1.Quote) if !ok { - return nil, fmt.Errorf("clone terms: unexpected type %T", termsClone) + return nil, fmt.Errorf("clone quote: unexpected type %T", quoteClone) } - return termsCopy, nil + return quoteCopy, nil } // MarkState sets the job state. Returns ErrNotFound if the job does not exist. @@ -329,7 +329,7 @@ func (s *Store) evictOldestLocked(preferExpired bool) bool { func toJobID(b []byte) (lcp.JobID, error) { if len(b) != lcp.Hash32Len { - return lcp.JobID{}, ErrInvalidJobID + return lcp.JobID{}, ErrInvalidCallID } var id lcp.JobID copy(id[:], b) @@ -352,23 +352,23 @@ func isValidState(state State) bool { } func cloneJob(entry jobEntry) Job { - var taskCopy *lcpdv1.Task - if entry.task != nil { - if cloned, ok := proto.Clone(entry.task).(*lcpdv1.Task); ok { - taskCopy = cloned + var callCopy *lcpdv1.CallSpec + if entry.call != nil { + if cloned, ok := proto.Clone(entry.call).(*lcpdv1.CallSpec); ok { + callCopy = cloned } } - var termsCopy *lcpdv1.Terms - if entry.terms != nil { - if cloned, ok := proto.Clone(entry.terms).(*lcpdv1.Terms); ok { - termsCopy = cloned + var quoteCopy *lcpdv1.Quote + if entry.quote != nil { + if cloned, ok := proto.Clone(entry.quote).(*lcpdv1.Quote); ok { + quoteCopy = cloned } } return Job{ PeerID: entry.peerID, JobID: entry.jobID, - Task: taskCopy, - Terms: termsCopy, + Call: callCopy, + Quote: quoteCopy, State: entry.state, CreatedAt: entry.createdAt, UpdatedAt: entry.updatedAt, diff --git a/go-lcpd/internal/requesterjobstore/store_test.go b/go-lcpd/internal/requesterjobstore/store_test.go index 183153f..6514ae8 100644 --- a/go-lcpd/internal/requesterjobstore/store_test.go +++ b/go-lcpd/internal/requesterjobstore/store_test.go @@ -21,27 +21,27 @@ func TestPutQuoteAndGetTerms(t *testing.T) { store := newStoreWithClock(now) jobID := newJobID(0x01) - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobID[:], + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobID[:], PriceMsat: 10, QuoteExpiry: timestamppb.New(now.Add(5 * time.Minute)), } - task := newTestTask() + call := newTestCall() - if err := store.PutQuote("peer-a", task, terms); err != nil { + if err := store.PutQuote("peer-a", call, quote); err != nil { t.Fatalf("PutQuote: %v", err) } - gotTerms, err := store.GetTerms("peer-a", jobID) + gotQuote, err := store.GetQuote("peer-a", jobID) if err != nil { - t.Fatalf("GetTerms: %v", err) + t.Fatalf("GetQuote: %v", err) } - if diff := cmp.Diff(terms, gotTerms, protocmp.Transform()); diff != "" { - t.Fatalf("terms mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(quote, gotQuote, protocmp.Transform()); diff != "" { + t.Fatalf("quote mismatch (-want +got):\n%s", diff) } - if gotTerms == terms { - t.Fatalf("GetTerms returned the same pointer") + if gotQuote == quote { + t.Fatalf("GetQuote returned the same pointer") } job, ok := store.Get("peer-a", jobID) @@ -63,23 +63,23 @@ func TestGetTerms_Expired(t *testing.T) { store := newStoreWithClock(now) jobID := newJobID(0x02) - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobID[:], + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobID[:], PriceMsat: 20, QuoteExpiry: timestamppb.New(now.Add(-time.Second)), } - task := newTestTask() + call := newTestCall() - if err := store.PutQuote("peer-b", task, terms); err != nil { + if err := store.PutQuote("peer-b", call, quote); err != nil { t.Fatalf("PutQuote: %v", err) } - _, err := store.GetTerms("peer-b", jobID) + _, err := store.GetQuote("peer-b", jobID) if !errors.Is(err, requesterjobstore.ErrExpired) { t.Fatalf( - "GetTerms error mismatch (-want +got):\n%s", + "GetQuote error mismatch (-want +got):\n%s", cmp.Diff(requesterjobstore.ErrExpired, err), ) } @@ -107,15 +107,15 @@ func TestMarkState(t *testing.T) { store := newStoreWithClock(now) jobID := newJobID(0x03) - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobID[:], + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobID[:], PriceMsat: 30, QuoteExpiry: timestamppb.New(now.Add(10 * time.Minute)), } - task := newTestTask() + call := newTestCall() - if err := store.PutQuote("peer-c", task, terms); err != nil { + if err := store.PutQuote("peer-c", call, quote); err != nil { t.Fatalf("PutQuote: %v", err) } @@ -144,26 +144,26 @@ func TestGC_ExpiresQuotedJobsOnly(t *testing.T) { store := newStoreWithClock(now) jobQuoted := newJobID(0x04) - quotedTerms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobQuoted[:], + quotedQuote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobQuoted[:], PriceMsat: 40, QuoteExpiry: timestamppb.New(now.Add(-time.Second)), } jobDone := newJobID(0x05) - doneTerms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobDone[:], + doneQuote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobDone[:], PriceMsat: 50, QuoteExpiry: timestamppb.New(now.Add(-time.Hour)), } - task := newTestTask() + call := newTestCall() - if err := store.PutQuote("peer-d", task, quotedTerms); err != nil { + if err := store.PutQuote("peer-d", call, quotedQuote); err != nil { t.Fatalf("PutQuote quoted: %v", err) } - if err := store.PutQuote("peer-d", task, doneTerms); err != nil { + if err := store.PutQuote("peer-d", call, doneQuote); err != nil { t.Fatalf("PutQuote done: %v", err) } if err := store.MarkState("peer-d", jobDone, requesterjobstore.StateDone); err != nil { @@ -190,20 +190,20 @@ func TestPeerIsolation(t *testing.T) { store := newStoreWithClock(now) jobID := newJobID(0x06) - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobID[:], + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobID[:], PriceMsat: 60, QuoteExpiry: timestamppb.New(now.Add(time.Hour)), } - task := newTestTask() + call := newTestCall() - if err := store.PutQuote("peer-e", task, terms); err != nil { + if err := store.PutQuote("peer-e", call, quote); err != nil { t.Fatalf("PutQuote peer-e: %v", err) } // Same job_id but different peer. - if err := store.PutQuote("peer-f", task, terms); err != nil { + if err := store.PutQuote("peer-f", call, quote); err != nil { t.Fatalf("PutQuote peer-f: %v", err) } @@ -229,60 +229,60 @@ func TestPutQuote_Validation(t *testing.T) { store := newStoreWithClock(now) jobID := newJobID(0x07) - validTerms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobID[:], + validQuote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobID[:], PriceMsat: 70, QuoteExpiry: timestamppb.New(now.Add(time.Hour)), } - task := newTestTask() + call := newTestCall() tests := []struct { name string peerID string - task *lcpdv1.Task - terms *lcpdv1.Terms + call *lcpdv1.CallSpec + quote *lcpdv1.Quote wantErr error }{ { name: "missing peer id", peerID: "", - task: task, - terms: validTerms, + call: call, + quote: validQuote, wantErr: requesterjobstore.ErrPeerIDRequired, }, { - name: "nil task", + name: "nil call", peerID: "peer", - task: nil, - terms: validTerms, - wantErr: requesterjobstore.ErrTaskRequired, + call: nil, + quote: validQuote, + wantErr: requesterjobstore.ErrCallRequired, }, { - name: "nil terms", + name: "nil quote", peerID: "peer", - task: task, - terms: nil, - wantErr: requesterjobstore.ErrTermsRequired, + call: call, + quote: nil, + wantErr: requesterjobstore.ErrQuoteRequired, }, { - name: "job_id wrong length", + name: "call_id wrong length", peerID: "peer", - task: task, - terms: &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: []byte{0x01, 0x02}, + call: call, + quote: &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: []byte{0x01, 0x02}, QuoteExpiry: timestamppb.New(now.Add(time.Hour)), }, - wantErr: requesterjobstore.ErrInvalidJobID, + wantErr: requesterjobstore.ErrInvalidCallID, }, { name: "quote expiry nil", peerID: "peer", - task: task, - terms: &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: jobID[:], + call: call, + quote: &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: jobID[:], }, wantErr: requesterjobstore.ErrQuoteExpiryRequired, }, @@ -292,7 +292,7 @@ func TestPutQuote_Validation(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - err := store.PutQuote(tc.peerID, tc.task, tc.terms) + err := store.PutQuote(tc.peerID, tc.call, tc.quote) if !errors.Is(err, tc.wantErr) { t.Fatalf("PutQuote error mismatch (-want +got):\n%s", cmp.Diff(tc.wantErr, err)) } @@ -300,15 +300,13 @@ func TestPutQuote_Validation(t *testing.T) { } } -func newTestTask() *lcpdv1.Task { - const model = "gpt-5.2" - return &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"hi"}]}`), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: model}, - }, - }, +func newTestCall() *lcpdv1.CallSpec { + return &lcpdv1.CallSpec{ + Method: "openai.chat_completions.v1", + Params: []byte{0x01, 0x05, 'm', 'o', 'd', 'e', 'l'}, + RequestBytes: []byte(`{"model":"model","messages":[{"role":"user","content":"hi"}]}`), + RequestContentType: "application/json; charset=utf-8", + RequestContentEncoding: "identity", } } diff --git a/go-lcpd/internal/requesterwait/waiter.go b/go-lcpd/internal/requesterwait/waiter.go index 6354f7d..e631dab 100644 --- a/go-lcpd/internal/requesterwait/waiter.go +++ b/go-lcpd/internal/requesterwait/waiter.go @@ -13,17 +13,17 @@ import ( ) type QuoteOutcome struct { - QuoteResponse *lcpwire.QuoteResponse - Error *lcpwire.Error + Quote *lcpwire.Quote + Error *lcpwire.Error } type ResultOutcome struct { - // Result is the decoded terminal `lcp_result`. - Result *lcpwire.Result + // Complete is the decoded terminal `lcp_complete`. + Complete *lcpwire.Complete - // ResultBytes is the reconstructed decoded bytes from the validated result - // stream. It is set iff Result.Status == ok. - ResultBytes []byte + // ResponseBytes is the reconstructed decoded bytes from the validated response + // stream. It is set iff Complete.Status == ok. + ResponseBytes []byte Error *lcpwire.Error } @@ -82,9 +82,9 @@ type entry struct { resultStreamSub *resultStreamSub - // Result stream reassembly state (v0.2). + // Response stream reassembly state. resultStream *streamState - terminalResult *lcpwire.Result + terminalResult *lcpwire.Complete terminalDelivered bool } @@ -94,12 +94,12 @@ type Waiter struct { logger *zap.SugaredLogger maxStreamBytes uint64 - maxJobBytes uint64 + maxCallBytes uint64 } const ( defaultMaxStreamBytes = uint64(4_194_304) - defaultMaxJobBytes = uint64(8_388_608) + defaultMaxCallBytes = uint64(8_388_608) resultStreamEventBuffer = 16 ) @@ -123,13 +123,13 @@ func New(logger *zap.SugaredLogger, localManifest *lcpwire.Manifest) *Waiter { } maxStreamBytes := defaultMaxStreamBytes - maxJobBytes := defaultMaxJobBytes + maxCallBytes := defaultMaxCallBytes if localManifest != nil { if localManifest.MaxStreamBytes != 0 { maxStreamBytes = localManifest.MaxStreamBytes } - if localManifest.MaxJobBytes != 0 { - maxJobBytes = localManifest.MaxJobBytes + if localManifest.MaxCallBytes != 0 { + maxCallBytes = localManifest.MaxCallBytes } } @@ -137,11 +137,11 @@ func New(logger *zap.SugaredLogger, localManifest *lcpwire.Manifest) *Waiter { entries: make(map[key]*entry), logger: logger.With("component", "requesterwait"), maxStreamBytes: maxStreamBytes, - maxJobBytes: maxJobBytes, + maxCallBytes: maxCallBytes, } } -// WaitQuoteResponse blocks until it receives lcp_quote_response or lcp_error for the job. +// WaitQuoteResponse blocks until it receives lcp_quote or lcp_error for the job. func (w *Waiter) WaitQuoteResponse( ctx context.Context, peerID string, @@ -160,7 +160,7 @@ func (w *Waiter) WaitQuoteResponse( ) } -// WaitResult blocks until it receives lcp_result or lcp_error for the job. +// WaitResult blocks until it receives lcp_complete or lcp_error for the job. func (w *Waiter) WaitResult( ctx context.Context, peerID string, @@ -226,20 +226,20 @@ func (w *Waiter) SubscribeResultStream( return ch, unsubscribe, nil } -// HandleInboundCustomMessage decodes inbound job-scope messages and delivers them +// HandleInboundCustomMessage decodes inbound call-scope messages and delivers them // to the appropriate waiter. Unknown message types are ignored. func (w *Waiter) HandleInboundCustomMessage( _ context.Context, msg lndpeermsg.InboundCustomMessage, ) { switch lcpwire.MessageType(msg.MsgType) { - case lcpwire.MessageTypeQuoteResponse: - resp, err := lcpwire.DecodeQuoteResponse(msg.Payload) + case lcpwire.MessageTypeQuote: + resp, err := lcpwire.DecodeQuote(msg.Payload) if err != nil { - w.logger.Debugw("decode lcp_quote_response failed", "err", err) + w.logger.Debugw("decode lcp_quote failed", "err", err) return } - w.deliverQuoteResponse(msg.PeerPubKey, resp) + w.deliverQuote(msg.PeerPubKey, resp) case lcpwire.MessageTypeStreamBegin: begin, err := lcpwire.DecodeStreamBegin(msg.Payload) if err != nil { @@ -261,13 +261,13 @@ func (w *Waiter) HandleInboundCustomMessage( return } w.handleStreamEnd(msg.PeerPubKey, end) - case lcpwire.MessageTypeResult: - res, err := lcpwire.DecodeResult(msg.Payload) + case lcpwire.MessageTypeComplete: + res, err := lcpwire.DecodeComplete(msg.Payload) if err != nil { - w.logger.Debugw("decode lcp_result failed", "err", err) + w.logger.Debugw("decode lcp_complete failed", "err", err) return } - w.handleTerminalResult(msg.PeerPubKey, res) + w.handleTerminalComplete(msg.PeerPubKey, res) case lcpwire.MessageTypeError: errMsg, err := lcpwire.DecodeError(msg.Payload) if err != nil { @@ -276,7 +276,7 @@ func (w *Waiter) HandleInboundCustomMessage( } w.deliverError(msg.PeerPubKey, errMsg) case lcpwire.MessageTypeManifest, - lcpwire.MessageTypeQuoteRequest, + lcpwire.MessageTypeCall, lcpwire.MessageTypeCancel: return default: @@ -286,7 +286,7 @@ func (w *Waiter) HandleInboundCustomMessage( } func (w *Waiter) handleStreamBegin(peerID string, begin lcpwire.StreamBegin) { - if begin.Kind != lcpwire.StreamKindResult { + if begin.Kind != lcpwire.StreamKindResponse { return } if begin.ContentEncoding != "identity" { @@ -300,18 +300,18 @@ func (w *Waiter) handleStreamBegin(peerID string, begin lcpwire.StreamBegin) { } if begin.TotalLen != nil { - if *begin.TotalLen > w.maxStreamBytes || *begin.TotalLen > w.maxJobBytes { + if *begin.TotalLen > w.maxStreamBytes || *begin.TotalLen > w.maxCallBytes { w.deliverProtocolError( peerID, begin.Envelope, lcpwire.ErrorCodePayloadTooLarge, - "result stream exceeds local limits", + "response stream exceeds local limits", ) return } } - k := key{peerID: peerID, jobID: begin.Envelope.JobID} + k := key{peerID: peerID, jobID: begin.Envelope.CallID} var sub *resultStreamSub @@ -344,7 +344,7 @@ func (w *Waiter) handleStreamBegin(peerID string, begin lcpwire.StreamBegin) { } func (w *Waiter) handleStreamChunk(peerID string, chunk lcpwire.StreamChunk) { - k := key{peerID: peerID, jobID: chunk.Envelope.JobID} + k := key{peerID: peerID, jobID: chunk.Envelope.CallID} var sub *resultStreamSub @@ -374,10 +374,10 @@ func (w *Waiter) handleStreamChunk(peerID string, chunk lcpwire.StreamChunk) { } nextLen := uint64(len(st.buf)) + uint64(len(chunk.Data)) - if nextLen > w.maxStreamBytes || nextLen > w.maxJobBytes { + if nextLen > w.maxStreamBytes || nextLen > w.maxCallBytes { env := chunk.Envelope w.mu.Unlock() - w.deliverProtocolError(peerID, env, lcpwire.ErrorCodePayloadTooLarge, "result stream exceeds local limits") + w.deliverProtocolError(peerID, env, lcpwire.ErrorCodePayloadTooLarge, "response stream exceeds local limits") return } @@ -393,7 +393,7 @@ func (w *Waiter) handleStreamChunk(peerID string, chunk lcpwire.StreamChunk) { } func (w *Waiter) handleStreamEnd(peerID string, end lcpwire.StreamEnd) { - k := key{peerID: peerID, jobID: end.Envelope.JobID} + k := key{peerID: peerID, jobID: end.Envelope.CallID} w.mu.Lock() e := w.ensureEntryLocked(k) @@ -432,12 +432,12 @@ func (w *Waiter) handleStreamEnd(peerID string, end lcpwire.StreamEnd) { w.mu.Unlock() if terminal != nil { - w.tryDeliverResult(peerID, *terminal, bufCopy, contentType, contentEncoding) + w.tryDeliverComplete(peerID, *terminal, bufCopy, contentType, contentEncoding) } } -func (w *Waiter) handleTerminalResult(peerID string, res lcpwire.Result) { - k := key{peerID: peerID, jobID: res.Envelope.JobID} +func (w *Waiter) handleTerminalComplete(peerID string, res lcpwire.Complete) { + k := key{peerID: peerID, jobID: res.Envelope.CallID} w.mu.Lock() e := w.ensureEntryLocked(k) @@ -445,7 +445,7 @@ func (w *Waiter) handleTerminalResult(peerID string, res lcpwire.Result) { w.mu.Unlock() return } - rCopy := cloneTerminalResult(res) + rCopy := cloneTerminalComplete(res) e.terminalResult = rCopy e.terminalDelivered = true @@ -464,8 +464,8 @@ func (w *Waiter) handleTerminalResult(peerID string, res lcpwire.Result) { } w.mu.Unlock() - if res.Status != lcpwire.ResultStatusOK { - w.deliverResult(peerID, res, nil) + if res.Status != lcpwire.CompleteStatusOK { + w.deliverComplete(peerID, res, nil) return } @@ -474,55 +474,55 @@ func (w *Waiter) handleTerminalResult(peerID string, res lcpwire.Result) { return } - w.tryDeliverResult(peerID, res, streamBuf, ct, ce) + w.tryDeliverComplete(peerID, res, streamBuf, ct, ce) } -func (w *Waiter) tryDeliverResult( +func (w *Waiter) tryDeliverComplete( peerID string, - terminal lcpwire.Result, - resultBytes []byte, + terminal lcpwire.Complete, + responseBytes []byte, streamContentType string, streamContentEncoding string, ) { - if terminal.Status != lcpwire.ResultStatusOK || terminal.OK == nil { - w.deliverResult(peerID, terminal, resultBytes) + if terminal.OK == nil { + w.deliverComplete(peerID, terminal, responseBytes) return } - sum := sha256Sum(resultBytes) - if sum != terminal.OK.ResultHash { - w.deliverProtocolError(peerID, terminal.Envelope, lcpwire.ErrorCodeChecksumMismatch, "result_hash mismatch") + sum := sha256Sum(responseBytes) + if sum != terminal.OK.ResponseHash { + w.deliverProtocolError(peerID, terminal.Envelope, lcpwire.ErrorCodeChecksumMismatch, "response_hash mismatch") return } - if uint64(len(resultBytes)) != terminal.OK.ResultLen { - w.deliverProtocolError(peerID, terminal.Envelope, lcpwire.ErrorCodeChecksumMismatch, "result_len mismatch") + if uint64(len(responseBytes)) != terminal.OK.ResponseLen { + w.deliverProtocolError(peerID, terminal.Envelope, lcpwire.ErrorCodeChecksumMismatch, "response_len mismatch") return } - if streamContentType != "" && streamContentType != terminal.OK.ResultContentType { + if streamContentType != "" && streamContentType != terminal.OK.ResponseContentType { w.deliverProtocolError( peerID, terminal.Envelope, lcpwire.ErrorCodeChecksumMismatch, - "result_content_type mismatch", + "response_content_type mismatch", ) return } - if streamContentEncoding != "" && streamContentEncoding != terminal.OK.ResultContentEncoding { + if streamContentEncoding != "" && streamContentEncoding != terminal.OK.ResponseContentEncoding { w.deliverProtocolError( peerID, terminal.Envelope, lcpwire.ErrorCodeChecksumMismatch, - "result_content_encoding mismatch", + "response_content_encoding mismatch", ) return } - w.deliverResult(peerID, terminal, resultBytes) + w.deliverComplete(peerID, terminal, responseBytes) } func (w *Waiter) deliverProtocolError( peerID string, - env lcpwire.JobEnvelope, + env lcpwire.CallEnvelope, code lcpwire.ErrorCode, message string, ) { @@ -602,9 +602,9 @@ func (w *Waiter) ensureEntryLocked(k key) *entry { return e } -func (w *Waiter) deliverQuoteResponse(peerID string, resp lcpwire.QuoteResponse) { - k := key{peerID: peerID, jobID: resp.Envelope.JobID} - out := QuoteOutcome{QuoteResponse: cloneQuoteResponse(resp)} +func (w *Waiter) deliverQuote(peerID string, quote lcpwire.Quote) { + k := key{peerID: peerID, jobID: quote.Envelope.CallID} + out := QuoteOutcome{Quote: cloneQuote(quote)} var ch chan QuoteOutcome @@ -621,12 +621,12 @@ func (w *Waiter) deliverQuoteResponse(peerID string, resp lcpwire.QuoteResponse) } } -func (w *Waiter) deliverResult(peerID string, res lcpwire.Result, resultBytes []byte) { - k := key{peerID: peerID, jobID: res.Envelope.JobID} +func (w *Waiter) deliverComplete(peerID string, res lcpwire.Complete, responseBytes []byte) { + k := key{peerID: peerID, jobID: res.Envelope.CallID} out := ResultOutcome{ - Result: cloneTerminalResult(res), - ResultBytes: append([]byte(nil), resultBytes...), + Complete: cloneTerminalComplete(res), + ResponseBytes: append([]byte(nil), responseBytes...), } var ch chan ResultOutcome @@ -645,7 +645,7 @@ func (w *Waiter) deliverResult(peerID string, res lcpwire.Result, resultBytes [] } func (w *Waiter) deliverError(peerID string, errMsg lcpwire.Error) { - k := key{peerID: peerID, jobID: errMsg.Envelope.JobID} + k := key{peerID: peerID, jobID: errMsg.Envelope.CallID} out := QuoteOutcome{Error: cloneError(errMsg)} outResult := ResultOutcome{Error: cloneError(errMsg)} @@ -690,7 +690,7 @@ func (w *Waiter) maybeCleanupLocked(k key, e *entry) { delete(w.entries, k) } -func cloneQuoteResponse(in lcpwire.QuoteResponse) *lcpwire.QuoteResponse { +func cloneQuote(in lcpwire.Quote) *lcpwire.Quote { c := in return &c } @@ -706,7 +706,7 @@ func cloneError(in lcpwire.Error) *lcpwire.Error { var _ lndpeermsg.InboundMessageHandler = (*Waiter)(nil) -func cloneTerminalResult(in lcpwire.Result) *lcpwire.Result { +func cloneTerminalComplete(in lcpwire.Complete) *lcpwire.Complete { c := in if in.OK != nil { okCopy := *in.OK diff --git a/go-lcpd/internal/requesterwait/waiter_test.go b/go-lcpd/internal/requesterwait/waiter_test.go index de3714b..dc006c8 100644 --- a/go-lcpd/internal/requesterwait/waiter_test.go +++ b/go-lcpd/internal/requesterwait/waiter_test.go @@ -22,10 +22,10 @@ func TestWaitQuoteResponse_DeliversQuote(t *testing.T) { jobID := newJobID(0x11) msgID := newMsgID(0x22) - quote := lcpwire.QuoteResponse{ - Envelope: lcpwire.JobEnvelope{ + quote := lcpwire.Quote{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + CallID: jobID, MsgID: msgID, Expiry: 123, }, @@ -34,7 +34,7 @@ func TestWaitQuoteResponse_DeliversQuote(t *testing.T) { TermsHash: newHash(0xaa), PaymentRequest: "bolt11", } - payload, err := lcpwire.EncodeQuoteResponse(quote) + payload, err := lcpwire.EncodeQuote(quote) if err != nil { t.Fatalf("encode quote response: %v", err) } @@ -51,16 +51,16 @@ func TestWaitQuoteResponse_DeliversQuote(t *testing.T) { waiter.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer-a", - MsgType: uint16(lcpwire.MessageTypeQuoteResponse), + MsgType: uint16(lcpwire.MessageTypeQuote), Payload: payload, }) select { case out := <-done: - if out.QuoteResponse == nil { + if out.Quote == nil { t.Fatalf("expected QuoteResponse, got nil") } - if diff := cmp.Diff(quote, *out.QuoteResponse); diff != "" { + if diff := cmp.Diff(quote, *out.Quote); diff != "" { t.Fatalf("QuoteResponse mismatch (-want +got):\n%s", diff) } case <-ctx.Done(): @@ -79,13 +79,13 @@ func TestWaitQuoteResponse_DeliversError(t *testing.T) { msgID := newMsgID(0x44) message := "oops" errMsg := lcpwire.Error{ - Envelope: lcpwire.JobEnvelope{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + CallID: jobID, MsgID: msgID, Expiry: 456, }, - Code: lcpwire.ErrorCodeUnsupportedTask, + Code: lcpwire.ErrorCodeUnsupportedMethod, Message: &message, } payload, err := lcpwire.EncodeError(errMsg) @@ -130,25 +130,25 @@ func TestWaitResult_ReceivesPendingFailedResult(t *testing.T) { jobID := newJobID(0x55) msgID := newMsgID(0x66) msg := "failed" - result := lcpwire.Result{ - Envelope: lcpwire.JobEnvelope{ + complete := lcpwire.Complete{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + CallID: jobID, MsgID: msgID, Expiry: 777, }, - Status: lcpwire.ResultStatusFailed, + Status: lcpwire.CompleteStatusFailed, Message: &msg, } - payload, err := lcpwire.EncodeResult(result) + payload, err := lcpwire.EncodeComplete(complete) if err != nil { - t.Fatalf("encode result: %v", err) + t.Fatalf("encode complete: %v", err) } - // Deliver result before waiting. + // Deliver complete before waiting. waiter.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer-c", - MsgType: uint16(lcpwire.MessageTypeResult), + MsgType: uint16(lcpwire.MessageTypeComplete), Payload: payload, }) @@ -159,14 +159,14 @@ func TestWaitResult_ReceivesPendingFailedResult(t *testing.T) { if waitErr != nil { t.Fatalf("WaitResult: %v", waitErr) } - if out.Result == nil { - t.Fatalf("expected result, got nil") + if out.Complete == nil { + t.Fatalf("expected complete, got nil") } - if diff := cmp.Diff(result, *out.Result); diff != "" { + if diff := cmp.Diff(complete, *out.Complete); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } - if len(out.ResultBytes) != 0 { - t.Fatalf("expected empty result_bytes for failed status, got %d bytes", len(out.ResultBytes)) + if len(out.ResponseBytes) != 0 { + t.Fatalf("expected empty result_bytes for failed status, got %d bytes", len(out.ResponseBytes)) } } @@ -188,24 +188,24 @@ func TestWaitResult_CancelThenRetry(t *testing.T) { msgID := newMsgID(0x88) msg := "cancelled" - result := lcpwire.Result{ - Envelope: lcpwire.JobEnvelope{ + complete := lcpwire.Complete{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + CallID: jobID, MsgID: msgID, Expiry: 1234, }, - Status: lcpwire.ResultStatusCancelled, + Status: lcpwire.CompleteStatusCancelled, Message: &msg, } - payload, err := lcpwire.EncodeResult(result) + payload, err := lcpwire.EncodeComplete(complete) if err != nil { - t.Fatalf("encode result: %v", err) + t.Fatalf("encode complete: %v", err) } waiter.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer-d", - MsgType: uint16(lcpwire.MessageTypeResult), + MsgType: uint16(lcpwire.MessageTypeComplete), Payload: payload, }) @@ -216,7 +216,7 @@ func TestWaitResult_CancelThenRetry(t *testing.T) { if waitErr != nil { t.Fatalf("WaitResult retry: %v", waitErr) } - if diff := cmp.Diff(result, *out.Result); diff != "" { + if diff := cmp.Diff(complete, *out.Complete); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } } @@ -227,10 +227,10 @@ func TestWaitQuoteResponse_IgnoresDifferentPeer(t *testing.T) { waiter := requesterwait.New(nil, nil) jobID := newJobID(0x99) msgID := newMsgID(0xaa) - quote := lcpwire.QuoteResponse{ - Envelope: lcpwire.JobEnvelope{ + quote := lcpwire.Quote{ + Envelope: lcpwire.CallEnvelope{ ProtocolVersion: lcpwire.ProtocolVersionV02, - JobID: jobID, + CallID: jobID, MsgID: msgID, Expiry: 42, }, @@ -239,14 +239,14 @@ func TestWaitQuoteResponse_IgnoresDifferentPeer(t *testing.T) { TermsHash: newHash(0xbb), PaymentRequest: "bolt", } - payload, err := lcpwire.EncodeQuoteResponse(quote) + payload, err := lcpwire.EncodeQuote(quote) if err != nil { t.Fatalf("encode quote response: %v", err) } waiter.HandleInboundCustomMessage(context.Background(), lndpeermsg.InboundCustomMessage{ PeerPubKey: "peer-other", - MsgType: uint16(lcpwire.MessageTypeQuoteResponse), + MsgType: uint16(lcpwire.MessageTypeQuote), Payload: payload, }) diff --git a/go-lcpd/itest/e2e/regtest_requester_grpc_test.go b/go-lcpd/itest/e2e/regtest_requester_grpc_test.go index 1af12d1..6c2436c 100644 --- a/go-lcpd/itest/e2e/regtest_requester_grpc_test.go +++ b/go-lcpd/itest/e2e/regtest_requester_grpc_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" - "github.com/bruwbird/lcp/go-lcpd/internal/lcptasks" + "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" "github.com/bruwbird/lcp/go-lcpd/internal/lnd/lnrpc" "github.com/bruwbird/lcp/go-lcpd/internal/protocolcompat" "github.com/bruwbird/lcp/go-lcpd/itest/harness/lcpd" @@ -145,19 +145,21 @@ func TestE2E_Regtest_RequesterGRPC(t *testing.T) { quoteReq := &lcpdv1.RequestQuoteRequest{ PeerId: bobPubKey, - Task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte( - `{"model":"gpt-5.2","messages":[{"role":"user","content":"hello from grpc requester"}]}`, - ), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: "gpt-5.2", - }, - }, - }, + Call: &lcpdv1.CallSpec{ + Method: "openai.chat_completions.v1", + RequestBytes: []byte( + `{"model":"gpt-5.2","messages":[{"role":"user","content":"hello from grpc requester"}]}`, + ), + RequestContentType: "application/json; charset=utf-8", + RequestContentEncoding: "identity", }, } + paramsBytes, err := lcpwire.EncodeOpenAIChatCompletionsV1Params(lcpwire.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}) + if err != nil { + t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) + } + quoteReq.Call.Params = paramsBytes + quoteResp, err := aliceClient.RequestQuote(ctx, quoteReq) if err != nil { t.Fatalf("RequestQuote: %v", err) @@ -165,54 +167,52 @@ func TestE2E_Regtest_RequesterGRPC(t *testing.T) { if got, want := quoteResp.GetPeerId(), bobPubKey; got != want { t.Fatalf("quote peer_id mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - terms := quoteResp.GetTerms() - if terms == nil { - t.Fatalf("quote terms is nil") + quote := quoteResp.GetQuote() + if quote == nil { + t.Fatalf("quote is nil") } - if got, want := len(terms.GetJobId()), lcp.Hash32Len; got != want { - t.Fatalf("quote terms job_id length mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if got, want := len(quote.GetCallId()), lcp.Hash32Len; got != want { + t.Fatalf("quote call_id length mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if got, want := len(terms.GetTermsHash()), lcp.Hash32Len; got != want { - t.Fatalf("quote terms terms_hash length mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if got, want := len(quote.GetTermsHash()), lcp.Hash32Len; got != want { + t.Fatalf("quote terms_hash length mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - if terms.GetPaymentRequest() == "" { - t.Fatalf("quote terms payment_request is empty") + if quote.GetPaymentRequest() == "" { + t.Fatalf("quote payment_request is empty") } - if terms.GetQuoteExpiry() == nil { - t.Fatalf("quote terms quote_expiry is nil") + if quote.GetQuoteExpiry() == nil { + t.Fatalf("quote quote_expiry is nil") } - if got, want := terms.GetQuoteExpiry().GetNanos(), int32(0); got != want { - t.Fatalf("quote terms quote_expiry nanos mismatch (-want +got):\n%s", cmp.Diff(want, got)) + if got, want := quote.GetQuoteExpiry().GetNanos(), int32(0); got != want { + t.Fatalf("quote quote_expiry nanos mismatch (-want +got):\n%s", cmp.Diff(want, got)) } - var jobID lcp.JobID - copy(jobID[:], terms.GetJobId()) + var callID lcp.JobID + copy(callID[:], quote.GetCallId()) - wireTask, err := lcptasks.ToWireQuoteRequestTask(quoteReq.GetTask()) - if err != nil { - t.Fatalf("ToWireQuoteRequestTask: %v", err) - } - inputStream, err := lcptasks.ToWireInputStream(quoteReq.GetTask()) - if err != nil { - t.Fatalf("ToWireInputStream: %v", err) + call := quoteReq.GetCall() + if call == nil { + t.Fatalf("quote request call is nil") } wantTermsHash, err := protocolcompat.ComputeTermsHash(lcp.Terms{ - ProtocolVersion: uint16(terms.GetProtocolVersion()), - JobID: jobID, - PriceMsat: terms.GetPriceMsat(), - QuoteExpiry: uint64(terms.GetQuoteExpiry().GetSeconds()), + ProtocolVersion: uint16(quote.GetProtocolVersion()), + JobID: callID, + PriceMsat: quote.GetPriceMsat(), + QuoteExpiry: uint64(quote.GetQuoteExpiry().GetSeconds()), }, protocolcompat.TermsCommit{ - TaskKind: wireTask.TaskKind, - Input: inputStream.DecodedBytes, - InputContentType: inputStream.ContentType, - InputContentEncoding: inputStream.ContentEncoding, - Params: wireTask.ParamsBytes, + Method: call.GetMethod(), + Request: call.GetRequestBytes(), + RequestContentType: call.GetRequestContentType(), + RequestContentEncoding: call.GetRequestContentEncoding(), + Params: call.GetParams(), + ResponseContentType: quote.ResponseContentType, + ResponseContentEncoding: quote.ResponseContentEncoding, }) if err != nil { t.Fatalf("ComputeTermsHash: %v", err) } - if diff := cmp.Diff(wantTermsHash[:], terms.GetTermsHash()); diff != "" { + if diff := cmp.Diff(wantTermsHash[:], quote.GetTermsHash()); diff != "" { t.Fatalf("terms_hash mismatch (-want +got):\n%s", diff) } @@ -221,14 +221,14 @@ func TestE2E_Regtest_RequesterGRPC(t *testing.T) { ctx, t, aliceRPC, - terms.GetPaymentRequest(), + quote.GetPaymentRequest(), bobPubKey, - terms.GetTermsHash(), - terms.GetPriceMsat(), - uint64(terms.GetQuoteExpiry().GetSeconds()), + quote.GetTermsHash(), + quote.GetPriceMsat(), + uint64(quote.GetQuoteExpiry().GetSeconds()), ) - amtSats := int64((terms.GetPriceMsat() + 999) / 1000) + amtSats := int64((quote.GetPriceMsat() + 999) / 1000) if amtSats <= 0 { amtSats = 1 } @@ -237,20 +237,20 @@ func TestE2E_Regtest_RequesterGRPC(t *testing.T) { execResp, err := aliceClient.AcceptAndExecute(ctx, &lcpdv1.AcceptAndExecuteRequest{ PeerId: bobPubKey, - JobId: terms.GetJobId(), + CallId: quote.GetCallId(), PayInvoice: true, }) if err != nil { t.Fatalf("AcceptAndExecute: %v", err) } - if execResp.GetResult() == nil { - t.Fatalf("AcceptAndExecute: result is nil") + if execResp.GetComplete() == nil { + t.Fatalf("AcceptAndExecute: complete is nil") } - if diff := cmp.Diff("application/json; charset=utf-8", execResp.GetResult().GetContentType()); diff != "" { - t.Fatalf("result content_type mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff("application/json; charset=utf-8", execResp.GetComplete().GetResponseContentType()); diff != "" { + t.Fatalf("complete response_content_type mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff([]byte("deterministic-output"), execResp.GetResult().GetResult()); diff != "" { - t.Fatalf("result bytes mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff([]byte("deterministic-output"), execResp.GetComplete().GetResponseBytes()); diff != "" { + t.Fatalf("complete response_bytes mismatch (-want +got):\n%s", diff) } } diff --git a/go-lcpd/tools/lcpd-grpcd/main.go b/go-lcpd/tools/lcpd-grpcd/main.go index bd0141a..a606be3 100644 --- a/go-lcpd/tools/lcpd-grpcd/main.go +++ b/go-lcpd/tools/lcpd-grpcd/main.go @@ -9,7 +9,6 @@ import ( "net" "os" "os/signal" - "slices" "strings" "time" @@ -688,51 +687,16 @@ func localManifestForProvider(providerCfg provider.Config) *lcpwire.Manifest { ProtocolVersion: lcpwire.ProtocolVersionV02, MaxPayloadBytes: defaultMaxPayloadBytes, MaxStreamBytes: defaultMaxStreamBytes, - MaxJobBytes: defaultMaxJobBytes, + MaxCallBytes: defaultMaxJobBytes, } if !providerCfg.Enabled || len(providerCfg.Models) == 0 { return manifest } - models := make([]string, 0, len(providerCfg.Models)) - for model := range providerCfg.Models { - models = append(models, model) - } - slices.Sort(models) - - manifest.SupportedTasks = make( - []lcpwire.TaskTemplate, - 0, - len(models), - ) - for _, modelKey := range models { - model := strings.TrimSpace(modelKey) - if model == "" { - continue - } - - chatParams, err := lcpwire.EncodeOpenAIChatCompletionsV1Params( - lcpwire.OpenAIChatCompletionsV1Params{Model: model}, - ) - if err == nil { - paramsBytes := chatParams - manifest.SupportedTasks = append(manifest.SupportedTasks, lcpwire.TaskTemplate{ - TaskKind: "openai.chat_completions.v1", - ParamsBytes: ¶msBytes, - }) - } - - responsesParams, err := lcpwire.EncodeOpenAIResponsesV1Params( - lcpwire.OpenAIResponsesV1Params{Model: model}, - ) - if err == nil { - paramsBytes := responsesParams - manifest.SupportedTasks = append(manifest.SupportedTasks, lcpwire.TaskTemplate{ - TaskKind: "openai.responses.v1", - ParamsBytes: ¶msBytes, - }) - } + manifest.SupportedMethods = []lcpwire.MethodDescriptor{ + {Method: "openai.chat_completions.v1"}, + {Method: "openai.responses.v1"}, } return manifest } diff --git a/go-lcpd/tools/lcpd-oneshot/chat.go b/go-lcpd/tools/lcpd-oneshot/chat.go index 734f28d..7691c46 100644 --- a/go-lcpd/tools/lcpd-oneshot/chat.go +++ b/go-lcpd/tools/lcpd-oneshot/chat.go @@ -8,6 +8,7 @@ import ( "io" "strings" + "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" lcpdv1 "github.com/bruwbird/lcp/proto-go/lcpd/v1" ) @@ -83,17 +84,22 @@ func (s *chatSession) Turn( return chatTurnOutput{Trimmed: trimmed}, err } + paramsBytes, err := lcpwire.EncodeOpenAIChatCompletionsV1Params( + lcpwire.OpenAIChatCompletionsV1Params{Model: s.baseOpts.Model}, + ) + if err != nil { + s.history = s.history[:len(s.history)-1] + return chatTurnOutput{Trimmed: trimmed}, fmt.Errorf("encode openai.chat_completions.v1 params: %w", err) + } + quoteResp, err := client.RequestQuote(turnCtx, &lcpdv1.RequestQuoteRequest{ PeerId: s.baseOpts.PeerID, - Task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: s.baseOpts.Model, - }, - }, - }, + Call: &lcpdv1.CallSpec{ + Method: "openai.chat_completions.v1", + Params: paramsBytes, + RequestBytes: requestJSON, + RequestContentType: "application/json; charset=utf-8", + RequestContentEncoding: "identity", }, }) if err != nil { @@ -101,15 +107,15 @@ func (s *chatSession) Turn( return chatTurnOutput{Trimmed: trimmed}, fmt.Errorf("request quote: %w", err) } - terms := quoteResp.GetTerms() - if terms == nil { + quote := quoteResp.GetQuote() + if quote == nil { s.history = s.history[:len(s.history)-1] - return chatTurnOutput{Trimmed: trimmed}, errors.New("request quote: response terms is nil") + return chatTurnOutput{Trimmed: trimmed}, errors.New("request quote: response quote is nil") } execResp, err := client.AcceptAndExecute(turnCtx, &lcpdv1.AcceptAndExecuteRequest{ PeerId: s.baseOpts.PeerID, - JobId: terms.GetJobId(), + CallId: quote.GetCallId(), PayInvoice: true, }) if err != nil { @@ -120,12 +126,22 @@ func (s *chatSession) Turn( s.history = s.history[:len(s.history)-1] return chatTurnOutput{Trimmed: trimmed}, errors.New("accept and execute: response is nil") } - if execResp.GetResult() == nil { + if execResp.GetComplete() == nil { s.history = s.history[:len(s.history)-1] - return chatTurnOutput{Trimmed: trimmed}, errors.New("accept and execute: result is nil") + return chatTurnOutput{Trimmed: trimmed}, errors.New("accept and execute: complete is nil") + } + + complete := execResp.GetComplete() + if complete.GetStatus() != lcpdv1.Complete_STATUS_OK { + s.history = s.history[:len(s.history)-1] + return chatTurnOutput{Trimmed: trimmed}, fmt.Errorf( + "accept and execute: status=%s message=%s", + complete.GetStatus().String(), + strings.TrimSpace(complete.GetMessage()), + ) } - replyBytes := execResp.GetResult().GetResult() + replyBytes := complete.GetResponseBytes() reply := strings.TrimSpace(extractAssistantContentFromChatCompletionsResponse(replyBytes)) if reply == "" { reply = normalizeAssistantReply(string(replyBytes)) @@ -134,12 +150,12 @@ func (s *chatSession) Turn( } reply = strings.TrimRight(reply, "\r\n") - s.totalMsat += terms.GetPriceMsat() + s.totalMsat += quote.GetPriceMsat() s.history = append(s.history, chatMessage{Role: chatRoleAssistant, Content: reply}) return chatTurnOutput{ Reply: reply, - PriceMsat: terms.GetPriceMsat(), + PriceMsat: quote.GetPriceMsat(), TotalMsat: s.totalMsat, Trimmed: trimmed, }, nil diff --git a/go-lcpd/tools/lcpd-oneshot/main.go b/go-lcpd/tools/lcpd-oneshot/main.go index c2e502d..a1e58b5 100644 --- a/go-lcpd/tools/lcpd-oneshot/main.go +++ b/go-lcpd/tools/lcpd-oneshot/main.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" lcpdv1 "github.com/bruwbird/lcp/proto-go/lcpd/v1" "golang.org/x/term" "google.golang.org/grpc" @@ -55,9 +56,9 @@ type runOptions struct { } type pipelineResult struct { - PeerID string - Terms *lcpdv1.Terms - Result *lcpdv1.Result + PeerID string + Quote *lcpdv1.Quote + Complete *lcpdv1.Complete } type lcpdClient interface { @@ -356,32 +357,36 @@ func runPipeline( return pipelineResult{}, err } + paramsBytes, err := lcpwire.EncodeOpenAIChatCompletionsV1Params( + lcpwire.OpenAIChatCompletionsV1Params{Model: opts.Model}, + ) + if err != nil { + return pipelineResult{}, fmt.Errorf("encode openai.chat_completions.v1 params: %w", err) + } + quoteResp, err := client.RequestQuote(ctx, &lcpdv1.RequestQuoteRequest{ PeerId: opts.PeerID, - Task: &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: opts.Model, - }, - }, - }, + Call: &lcpdv1.CallSpec{ + Method: "openai.chat_completions.v1", + Params: paramsBytes, + RequestBytes: requestJSON, + RequestContentType: "application/json; charset=utf-8", + RequestContentEncoding: "identity", }, }) if err != nil { return pipelineResult{}, fmt.Errorf("request quote: %w", err) } - terms := quoteResp.GetTerms() - if terms == nil { - return pipelineResult{}, errors.New("request quote: response terms is nil") + quote := quoteResp.GetQuote() + if quote == nil { + return pipelineResult{}, errors.New("request quote: response quote is nil") } res := pipelineResult{ - PeerID: opts.PeerID, - Terms: terms, - Result: nil, + PeerID: opts.PeerID, + Quote: quote, + Complete: nil, } if !opts.PayInvoice { @@ -390,7 +395,7 @@ func runPipeline( execResp, err := client.AcceptAndExecute(ctx, &lcpdv1.AcceptAndExecuteRequest{ PeerId: opts.PeerID, - JobId: terms.GetJobId(), + CallId: quote.GetCallId(), PayInvoice: true, }) if err != nil { @@ -399,11 +404,11 @@ func runPipeline( if execResp == nil { return pipelineResult{}, errors.New("accept and execute: response is nil") } - if execResp.GetResult() == nil { - return pipelineResult{}, errors.New("accept and execute: result is nil") + if execResp.GetComplete() == nil { + return pipelineResult{}, errors.New("accept and execute: complete is nil") } - res.Result = execResp.GetResult() + res.Complete = execResp.GetComplete() return res, nil } @@ -457,30 +462,40 @@ func buildOpenAIChatCompletionsV1RequestJSON( func formatHumanSummary(res pipelineResult) string { var b strings.Builder - fmt.Fprintf(&b, "peer_id=%s protocol_version=%d\n", res.PeerID, res.Terms.GetProtocolVersion()) + if res.Quote == nil { + fmt.Fprintf(&b, "peer_id=%s\n", res.PeerID) + fmt.Fprintf(&b, "quote:\n(none)\n") + return b.String() + } + + fmt.Fprintf(&b, "peer_id=%s protocol_version=%d\n", res.PeerID, res.Quote.GetProtocolVersion()) fmt.Fprintf( &b, "price_sat=%s price_msat=%d\n", - formatMsatAsSat(res.Terms.GetPriceMsat()), - res.Terms.GetPriceMsat(), + formatMsatAsSat(res.Quote.GetPriceMsat()), + res.Quote.GetPriceMsat(), ) fmt.Fprintf( &b, - "terms_hash=%s job_id=%s\n", - hex.EncodeToString(res.Terms.GetTermsHash()), - hex.EncodeToString(res.Terms.GetJobId()), + "terms_hash=%s call_id=%s\n", + hex.EncodeToString(res.Quote.GetTermsHash()), + hex.EncodeToString(res.Quote.GetCallId()), ) - fmt.Fprintf(&b, "payment_request:\n%s\n", res.Terms.GetPaymentRequest()) + fmt.Fprintf(&b, "payment_request:\n%s\n", res.Quote.GetPaymentRequest()) - if res.Result == nil { - fmt.Fprintf(&b, "result:\n(none)\n") + if res.Complete == nil { + fmt.Fprintf(&b, "complete:\n(none)\n") return b.String() } - if ct := strings.TrimSpace(res.Result.GetContentType()); ct != "" { + fmt.Fprintf(&b, "status=%s\n", res.Complete.GetStatus().String()) + if msg := strings.TrimSpace(res.Complete.GetMessage()); msg != "" { + fmt.Fprintf(&b, "message=%s\n", msg) + } + if ct := strings.TrimSpace(res.Complete.GetResponseContentType()); ct != "" { fmt.Fprintf(&b, "content_type=%s\n", ct) } - fmt.Fprintf(&b, "result:\n%s\n", string(res.Result.GetResult())) + fmt.Fprintf(&b, "response:\n%s\n", string(res.Complete.GetResponseBytes())) return b.String() } @@ -492,24 +507,34 @@ func writeJSONSummary(res pipelineResult, w io.Writer) error { PriceMsat uint64 `json:"price_msat"` PriceSat string `json:"price_sat"` TermsHashHex string `json:"terms_hash_hex"` - JobIDHex string `json:"job_id_hex"` + CallIDHex string `json:"call_id_hex"` PaymentRequest string `json:"payment_request"` - ResultText string `json:"result_text"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + ResponseText string `json:"response_text"` ContentType string `json:"content_type"` }{ - PeerID: res.PeerID, - ProtocolVersion: res.Terms.GetProtocolVersion(), - PriceMsat: res.Terms.GetPriceMsat(), - PriceSat: formatMsatAsSat(res.Terms.GetPriceMsat()), - TermsHashHex: hex.EncodeToString(res.Terms.GetTermsHash()), - JobIDHex: hex.EncodeToString(res.Terms.GetJobId()), - PaymentRequest: res.Terms.GetPaymentRequest(), - ResultText: "", - ContentType: "", - } - if res.Result != nil { - payload.ResultText = string(res.Result.GetResult()) - payload.ContentType = res.Result.GetContentType() + PeerID: res.PeerID, + Status: "", + Message: "", + ResponseText: "", + ContentType: "", + } + + if res.Quote != nil { + payload.ProtocolVersion = res.Quote.GetProtocolVersion() + payload.PriceMsat = res.Quote.GetPriceMsat() + payload.PriceSat = formatMsatAsSat(res.Quote.GetPriceMsat()) + payload.TermsHashHex = hex.EncodeToString(res.Quote.GetTermsHash()) + payload.CallIDHex = hex.EncodeToString(res.Quote.GetCallId()) + payload.PaymentRequest = res.Quote.GetPaymentRequest() + } + + if res.Complete != nil { + payload.Status = res.Complete.GetStatus().String() + payload.Message = res.Complete.GetMessage() + payload.ResponseText = string(res.Complete.GetResponseBytes()) + payload.ContentType = res.Complete.GetResponseContentType() } enc := json.NewEncoder(w) diff --git a/go-lcpd/tools/lcpd-oneshot/main_test.go b/go-lcpd/tools/lcpd-oneshot/main_test.go index 8470a23..238f2c9 100644 --- a/go-lcpd/tools/lcpd-oneshot/main_test.go +++ b/go-lcpd/tools/lcpd-oneshot/main_test.go @@ -47,16 +47,19 @@ func TestRunPipeline_RequestQuoteOnly(t *testing.T) { if diff := cmp.Diff(opts.PeerID, fake.requestQuoteReq.GetPeerId()); diff != "" { t.Fatalf("peer_id mismatch (-want +got):\n%s", diff) } - openaiTask := fake.requestQuoteReq.GetTask().GetOpenaiChatCompletionsV1() - if openaiTask == nil { - t.Fatalf("task.openai_chat_completions_v1 is nil") + call := fake.requestQuoteReq.GetCall() + if call == nil { + t.Fatalf("call is nil") } - params := openaiTask.GetParams() - if params == nil { - t.Fatalf("task.openai_chat_completions_v1.params is nil") + if diff := cmp.Diff("openai.chat_completions.v1", call.GetMethod()); diff != "" { + t.Fatalf("call.method mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(opts.Model, params.GetModel()); diff != "" { + decodedParams, err := lcpwire.DecodeOpenAIChatCompletionsV1Params(call.GetParams()) + if err != nil { + t.Fatalf("DecodeOpenAIChatCompletionsV1Params: %v", err) + } + if diff := cmp.Diff(opts.Model, decodedParams.Model); diff != "" { t.Fatalf("params.model mismatch (-want +got):\n%s", diff) } @@ -69,7 +72,7 @@ func TestRunPipeline_RequestQuoteOnly(t *testing.T) { Temperature *float64 `json:"temperature,omitempty"` MaxTokens *uint32 `json:"max_tokens,omitempty"` } - if unmarshalErr := json.Unmarshal(openaiTask.GetRequestJson(), &req); unmarshalErr != nil { + if unmarshalErr := json.Unmarshal(call.GetRequestBytes(), &req); unmarshalErr != nil { t.Fatalf("unmarshal request_json: %v", unmarshalErr) } if diff := cmp.Diff(opts.Model, req.Model); diff != "" { @@ -99,11 +102,11 @@ func TestRunPipeline_RequestQuoteOnly(t *testing.T) { t.Fatalf("request_json.max_tokens mismatch (-want +got):\n%s", diff) } - if res.Terms == nil { - t.Fatalf("Terms is nil") + if res.Quote == nil { + t.Fatalf("Quote is nil") } - if res.Result != nil { - t.Fatalf("Result must be nil when pay-invoice=false") + if res.Complete != nil { + t.Fatalf("Complete must be nil when pay-invoice=false") } } @@ -139,18 +142,18 @@ func TestRunPipeline_RequestQuoteAndAcceptAndExecute(t *testing.T) { if diff := cmp.Diff(opts.PeerID, fake.acceptReq.GetPeerId()); diff != "" { t.Fatalf("accept peer_id mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(fake.terms.GetJobId(), fake.acceptReq.GetJobId()); diff != "" { - t.Fatalf("accept job_id mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(fake.quote.GetCallId(), fake.acceptReq.GetCallId()); diff != "" { + t.Fatalf("accept call_id mismatch (-want +got):\n%s", diff) } if got := fake.acceptReq.GetPayInvoice(); got != true { t.Fatalf("accept pay_invoice: got %v want true", got) } - if res.Result == nil { - t.Fatalf("Result is nil") + if res.Complete == nil { + t.Fatalf("Complete is nil") } - if diff := cmp.Diff([]byte(fake.resultText), res.Result.GetResult()); diff != "" { - t.Fatalf("result mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff([]byte(fake.resultText), res.Complete.GetResponseBytes()); diff != "" { + t.Fatalf("response mismatch (-want +got):\n%s", diff) } } @@ -215,15 +218,19 @@ func TestFormatHumanSummary(t *testing.T) { res := pipelineResult{ PeerID: strings.Repeat("a", 66), - Terms: &lcpdv1.Terms{ - JobId: bytes.Repeat([]byte{0xbb}, 32), + Quote: &lcpdv1.Quote{ + CallId: bytes.Repeat([]byte{0xbb}, 32), TermsHash: bytes.Repeat([]byte{0xaa}, 32), PriceMsat: 1500, PaymentRequest: "lnbc1testinvoice", }, - Result: &lcpdv1.Result{ - Result: []byte("hello"), - ContentType: "text/plain; charset=utf-8", + Complete: &lcpdv1.Complete{ + Status: lcpdv1.Complete_STATUS_OK, + ResponseBytes: []byte("hello"), + ResponseContentType: func() *string { + ct := "text/plain; charset=utf-8" + return &ct + }(), }, } @@ -233,10 +240,11 @@ func TestFormatHumanSummary(t *testing.T) { "peer_id=" + strings.Repeat("a", 66), "price_msat=1500", "terms_hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "job_id=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "call_id=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "payment_request:\nlnbc1testinvoice", + "status=STATUS_OK", "content_type=text/plain; charset=utf-8", - "result:\nhello", + "response:\nhello", } { if !strings.Contains(out, substr) { t.Fatalf("formatHumanSummary missing substring %q\noutput:\n%s", substr, out) @@ -252,7 +260,7 @@ type fakeClient struct { requestQuoteReq *lcpdv1.RequestQuoteRequest acceptReq *lcpdv1.AcceptAndExecuteRequest - terms *lcpdv1.Terms + quote *lcpdv1.Quote acceptResp *lcpdv1.AcceptAndExecuteResponse } @@ -263,18 +271,18 @@ func (f *fakeClient) RequestQuote( ) (*lcpdv1.RequestQuoteResponse, error) { f.requestQuoteReq = req - terms := &lcpdv1.Terms{ - ProtocolVersion: uint32(lcpwire.ProtocolVersionV02), - JobId: bytes.Repeat([]byte{0x01}, 32), + quote := &lcpdv1.Quote{ + ProtocolVersion: uint32(lcpwire.ProtocolVersionV03), + CallId: bytes.Repeat([]byte{0x01}, 32), PriceMsat: 1500, TermsHash: bytes.Repeat([]byte{0x02}, 32), PaymentRequest: "lnbc1fakeinvoice", } - f.terms = terms + f.quote = quote return &lcpdv1.RequestQuoteResponse{ PeerId: req.GetPeerId(), - Terms: terms, + Quote: quote, }, nil } @@ -289,9 +297,13 @@ func (f *fakeClient) AcceptAndExecute( } f.acceptResp = &lcpdv1.AcceptAndExecuteResponse{ - Result: &lcpdv1.Result{ - Result: []byte(f.resultText), - ContentType: f.contentType, + Complete: &lcpdv1.Complete{ + Status: lcpdv1.Complete_STATUS_OK, + ResponseBytes: []byte(f.resultText), + ResponseContentType: func() *string { + ct := f.contentType + return &ct + }(), }, } return f.acceptResp, nil From 90e7c8cd8b011fe163355a12b339cf884ba588e4 Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 9 Jan 2026 16:27:21 +0900 Subject: [PATCH 3/4] apps/openai-serve: Update for LCP v0.3 Switch the gateway to the v0.3 call/quote/complete gRPC API. - Build CallSpec with TLV params (model) and raw request bytes. - Route by peer supported_methods; v0.3 manifests do not advertise models. - Rename response header X-Lcp-Job-Id -> X-Lcp-Call-Id. - Update tests and go-lcpd openai-serve regtest assertions. --- apps/openai-serve/README.md | 15 +- .../internal/httpapi/constants.go | 9 +- .../internal/httpapi/handler_chat.go | 183 +++++++------ .../internal/httpapi/handler_models.go | 11 +- .../internal/httpapi/handler_responses.go | 35 +-- .../internal/httpapi/handler_stream.go | 30 +-- .../internal/httpapi/lcp_params.go | 77 ++++++ apps/openai-serve/internal/httpapi/routing.go | 133 +++------ .../internal/httpapi/server_test.go | 253 +++++++++++------- .../itest/e2e/regtest_openai_serve_test.go | 50 ++-- 10 files changed, 454 insertions(+), 342 deletions(-) create mode 100644 apps/openai-serve/internal/httpapi/lcp_params.go diff --git a/apps/openai-serve/README.md b/apps/openai-serve/README.md index ac1aabd..5d4a802 100644 --- a/apps/openai-serve/README.md +++ b/apps/openai-serve/README.md @@ -87,8 +87,8 @@ High-level data flow: 1. Your client calls the OpenAI-compatible HTTP endpoint (`/v1/chat/completions` or `/v1/responses`). 2. `openai-serve` forwards the raw request body bytes as an LCP `openai.chat_completions.v1` or - `openai.responses.v1` task. -3. `openai-serve` forwards the task to a local `lcpd-grpcd` (Requester) over gRPC. + `openai.responses.v1` method call. +3. `openai-serve` forwards the call to a local `lcpd-grpcd` (Requester) over gRPC. 4. The Requester talks to an LCP Provider over Lightning custom messages, requests a quote, pays, and receives the result. 5. `openai-serve` returns the raw Provider response bytes (no re-encoding) and includes LCP metadata in response headers. @@ -125,16 +125,15 @@ For a given `model`, the peer is chosen in this order: 1. `OPENAI_SERVE_MODEL_MAP` (`model=peer_id;...`) if the peer is connected/LCP-ready. 2. `OPENAI_SERVE_DEFAULT_PEER_ID` if set and connected/LCP-ready. -3. A peer that advertises the model in `supported_tasks` (if any). -4. Fallback to the first connected peer. +3. A peer that advertises the required method in `supported_methods` (if any). +4. Fallback to any connected peer. If there are no connected peers, the request fails. ### Model validation - If `OPENAI_SERVE_MODEL_ALLOWLIST` is set: `model` must be in the allowlist (unless `OPENAI_SERVE_ALLOW_UNLISTED_MODELS=true`). -- Otherwise: if any connected peers advertise `supported_tasks`, the model must be advertised by at least one peer. - If no peers advertise `supported_tasks`, validation is skipped to keep the gateway usable. +- Otherwise: validation is skipped (LCP v0.3 manifests do not advertise models; Providers enforce their own model policies). ## Safety knobs @@ -147,7 +146,7 @@ If there are no connected peers, the request fails. This service treats logs as sensitive. - Logs MUST NOT contain raw prompts (`messages[].content`) or raw model outputs. -- Logs include only operational metadata (e.g., peer id, job id, price, durations, and byte/token counts). +- Logs include only operational metadata (e.g., peer id, call id, price, durations, and byte/token counts). - `OPENAI_SERVE_LOG_LEVEL=debug` enables more verbose request logging; keep `info` (default) for production unless needed. ## Response metadata headers @@ -155,7 +154,7 @@ This service treats logs as sensitive. `POST /v1/chat/completions` and `POST /v1/responses` include LCP metadata headers: - `X-Lcp-Peer-Id`: the chosen Provider peer id -- `X-Lcp-Job-Id`: the job id (hex) +- `X-Lcp-Call-Id`: the call id (hex) - `X-Lcp-Price-Msat`: the accepted quote price - `X-Lcp-Terms-Hash`: the accepted quote terms hash (hex) diff --git a/apps/openai-serve/internal/httpapi/constants.go b/apps/openai-serve/internal/httpapi/constants.go index cc7b430..5b526b5 100644 --- a/apps/openai-serve/internal/httpapi/constants.go +++ b/apps/openai-serve/internal/httpapi/constants.go @@ -1,3 +1,10 @@ package httpapi -const contentEncodingIdentity = "identity" +const ( + contentEncodingIdentity = "identity" + + requestContentTypeJSONUTF8 = "application/json; charset=utf-8" + + lcpMethodOpenAIChatCompletionsV1 = "openai.chat_completions.v1" + lcpMethodOpenAIResponsesV1 = "openai.responses.v1" +) diff --git a/apps/openai-serve/internal/httpapi/handler_chat.go b/apps/openai-serve/internal/httpapi/handler_chat.go index fb5c503..6ffb05b 100644 --- a/apps/openai-serve/internal/httpapi/handler_chat.go +++ b/apps/openai-serve/internal/httpapi/handler_chat.go @@ -25,7 +25,7 @@ const ( providerReturnedNonOKStatusMessage = "provider returned non-ok status" ) -type buildTaskFunc func(c *gin.Context, model string, reqBytes []byte) (*lcpdv1.Task, bool) +type buildCallFunc func(c *gin.Context, model string, reqBytes []byte) (*lcpdv1.CallSpec, bool) func (s *Server) handleChatCompletions(c *gin.Context) { if c.Request.Method != http.MethodPost { @@ -45,11 +45,11 @@ func (s *Server) handleChatCompletions(c *gin.Context) { s.handleOpenAIPassthrough( c, started, - lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1, + lcpMethodOpenAIChatCompletionsV1, model, reqBytes, streaming, - s.buildLCPOpenAIChatCompletionsV1Task, + s.buildLCPOpenAIChatCompletionsV1Call, "chat completion", ) } @@ -57,34 +57,34 @@ func (s *Server) handleChatCompletions(c *gin.Context) { func (s *Server) handleOpenAIPassthrough( c *gin.Context, started time.Time, - taskKind lcpdv1.LCPTaskKind, + method string, model string, reqBytes []byte, streaming bool, - buildTask buildTaskFunc, + buildCall buildCallFunc, operation string, ) { requestBytes := len(reqBytes) requestTokens := openai.ApproxTokensFromBytes(requestBytes) - peerID, ok := s.resolvePeerForTaskKindAndModel(c, taskKind, model) + peerID, ok := s.resolvePeerForMethodAndModel(c, method, model) if !ok { return } - task, ok := buildTask(c, model, reqBytes) + call, ok := buildCall(c, model, reqBytes) if !ok { return } quoteStart := time.Now() - quote, ok := s.requestQuoteAndValidatePrice(c, peerID, task) + quote, ok := s.requestQuoteAndValidatePrice(c, peerID, call) if !ok { return } quoteLatency := time.Since(quoteStart) - jobID := copyBytes(quote.GetTerms().GetJobId()) + callID := copyBytes(quote.GetQuote().GetCallId()) if streaming { s.handleOpenAIStreaming( @@ -93,7 +93,7 @@ func (s *Server) handleOpenAIPassthrough( operation, model, peerID, - jobID, + callID, quote, requestBytes, requestTokens, @@ -108,7 +108,7 @@ func (s *Server) handleOpenAIPassthrough( operation, model, peerID, - jobID, + callID, quote, requestBytes, requestTokens, @@ -122,29 +122,29 @@ func (s *Server) handleOpenAIStreaming( operation string, model string, peerID string, - jobID []byte, + callID []byte, quote *lcpdv1.RequestQuoteResponse, requestBytes int, requestTokens int, quoteLatency time.Duration, ) { execStart := time.Now() - grpcStream, cancel, err := s.acceptAndExecuteStream(c, peerID, jobID) + grpcStream, cancel, err := s.acceptAndExecuteStream(c, peerID, callID) if err != nil { cancel() - s.cancelJobIfRequestCanceled(c, peerID, jobID, err) + s.cancelJobIfRequestCanceled(c, peerID, callID, err) writeOpenAIError(c, httpStatusFromGRPC(err), "server_error", grpcErrMessage(err)) return } defer cancel() - writeLCPHeaders(c, peerID, jobID, quote) + writeLCPHeaders(c, peerID, callID, quote) streamed, streamErr := s.writeLCPStreamToHTTP(c, grpcStream) execLatency := time.Since(execStart) if streamErr != nil { - s.cancelJobIfRequestCanceled(c, peerID, jobID, streamErr) + s.cancelJobIfRequestCanceled(c, peerID, callID, streamErr) if !streamed.wroteBody { writeOpenAIError(c, httpStatusFromGRPC(streamErr), "server_error", grpcErrMessage(streamErr)) } @@ -161,7 +161,7 @@ func (s *Server) handleOpenAIStreaming( started, model, peerID, - jobID, + callID, quote, requestBytes, requestTokens, @@ -177,14 +177,14 @@ func (s *Server) handleOpenAINonStreaming( operation string, model string, peerID string, - jobID []byte, + callID []byte, quote *lcpdv1.RequestQuoteResponse, requestBytes int, requestTokens int, quoteLatency time.Duration, ) { execStart := time.Now() - execResp, ok := s.acceptAndExecuteWithCancelOnFailure(c, peerID, jobID) + execResp, ok := s.acceptAndExecuteWithCancelOnFailure(c, peerID, callID) if !ok { return } @@ -201,7 +201,7 @@ func (s *Server) handleOpenAINonStreaming( started, model, peerID, - jobID, + callID, quote, requestBytes, requestTokens, @@ -217,20 +217,20 @@ func (s *Server) cancelJobIfRequestCanceled(c *gin.Context, peerID string, jobID } } -func validateStreamedTerminalResult(c *gin.Context, streamed streamWriteResult) (*lcpdv1.Result, bool) { - if streamed.terminalResult == nil { +func validateStreamedTerminalResult(c *gin.Context, streamed streamWriteResult) (*lcpdv1.Complete, bool) { + if streamed.terminalComplete == nil { writeOpenAIErrorIfNoBody(c, streamed.wroteBody, http.StatusBadGateway, providerReturnedNoResultMessage) return nil, false } - if streamed.terminalResult.GetStatus() != lcpdv1.Result_STATUS_OK { - msg := strings.TrimSpace(streamed.terminalResult.GetMessage()) + if streamed.terminalComplete.GetStatus() != lcpdv1.Complete_STATUS_OK { + msg := strings.TrimSpace(streamed.terminalComplete.GetMessage()) if msg == "" { msg = providerReturnedNonOKStatusMessage } writeOpenAIErrorIfNoBody(c, streamed.wroteBody, http.StatusBadGateway, msg) return nil, false } - return streamed.terminalResult, true + return streamed.terminalComplete, true } func writeOpenAIErrorIfNoBody(c *gin.Context, wroteBody bool, status int, message string) { @@ -240,12 +240,16 @@ func writeOpenAIErrorIfNoBody(c *gin.Context, wroteBody bool, status int, messag writeOpenAIError(c, status, "server_error", message) } -func writeLCPHeaders(c *gin.Context, peerID string, jobID []byte, quote *lcpdv1.RequestQuoteResponse) { - price := quote.GetTerms().GetPriceMsat() - jobIDHex := hex.EncodeToString(jobID) - termsHashHex := hex.EncodeToString(quote.GetTerms().GetTermsHash()) +func writeLCPHeaders(c *gin.Context, peerID string, callID []byte, quote *lcpdv1.RequestQuoteResponse) { + q := quote.GetQuote() + if q == nil { + return + } + price := q.GetPriceMsat() + callIDHex := hex.EncodeToString(callID) + termsHashHex := hex.EncodeToString(q.GetTermsHash()) c.Header("X-Lcp-Peer-Id", peerID) - c.Header("X-Lcp-Job-Id", jobIDHex) + c.Header("X-Lcp-Call-Id", callIDHex) c.Header("X-Lcp-Price-Msat", strconv.FormatUint(price, 10)) c.Header("X-Lcp-Terms-Hash", termsHashHex) } @@ -256,7 +260,7 @@ func (s *Server) logStreamResult( started time.Time, model string, peerID string, - jobID []byte, + callID []byte, quote *lcpdv1.RequestQuoteResponse, requestBytes int, requestTokens int, @@ -266,21 +270,25 @@ func (s *Server) logStreamResult( ) { ctx := c.Request.Context() - price := quote.GetTerms().GetPriceMsat() + q := quote.GetQuote() + if q == nil { + return + } + price := q.GetPriceMsat() totalLatency := time.Since(started) streamedTokens := openai.ApproxTokensFromBytes(streamedBytes) totalTokens := requestTokens + streamedTokens - jobIDHex := hex.EncodeToString(jobID) - termsHashHex := hex.EncodeToString(quote.GetTerms().GetTermsHash()) + callIDHex := hex.EncodeToString(callID) + termsHashHex := hex.EncodeToString(q.GetTermsHash()) s.log.InfoContext( ctx, operation, "model", model, "peer_id", peerID, - "job_id", jobIDHex, + "call_id", callIDHex, "price_msat", price, "terms_hash", termsHashHex, "request_bytes", requestBytes, @@ -311,9 +319,9 @@ func decodeAndValidateChatRequest(c *gin.Context) ([]byte, openai.ChatCompletion return body, parsed, true } -func (s *Server) resolvePeerForTaskKindAndModel( +func (s *Server) resolvePeerForMethodAndModel( c *gin.Context, - taskKind lcpdv1.LCPTaskKind, + method string, model string, ) (string, bool) { peersResp, err := s.listPeers(c) @@ -321,11 +329,11 @@ func (s *Server) resolvePeerForTaskKindAndModel( writeOpenAIError(c, httpStatusFromGRPC(err), "server_error", grpcErrMessage(err)) return "", false } - if modelErr := s.validateModelForTaskKind(model, taskKind, peersResp); modelErr != nil { + if modelErr := s.validateModelForMethod(model, method, peersResp); modelErr != nil { writeOpenAIError(c, http.StatusBadRequest, "invalid_request_error", modelErr.Error()) return "", false } - peerID, err := s.resolvePeerIDForTaskKind(model, taskKind, peersResp) + peerID, err := s.resolvePeerIDForMethod(model, method, peersResp) if err != nil { writeOpenAIError(c, http.StatusServiceUnavailable, "server_error", err.Error()) return "", false @@ -333,31 +341,36 @@ func (s *Server) resolvePeerForTaskKindAndModel( return peerID, true } -func (s *Server) buildLCPOpenAIChatCompletionsV1Task( +func (s *Server) buildLCPOpenAIChatCompletionsV1Call( c *gin.Context, model string, reqBytes []byte, -) (*lcpdv1.Task, bool) { - task, err := buildLCPOpenAIChatCompletionsV1Task(model, reqBytes) +) (*lcpdv1.CallSpec, bool) { + call, err := buildLCPOpenAIChatCompletionsV1Call(model, reqBytes) if err != nil { writeOpenAIError(c, http.StatusBadRequest, "invalid_request_error", err.Error()) return nil, false } - return task, true + return call, true } func (s *Server) requestQuoteAndValidatePrice( c *gin.Context, peerID string, - task *lcpdv1.Task, + call *lcpdv1.CallSpec, ) (*lcpdv1.RequestQuoteResponse, bool) { - quote, err := s.requestQuote(c, peerID, task) + quote, err := s.requestQuote(c, peerID, call) if err != nil { writeOpenAIError(c, httpStatusFromGRPC(err), "server_error", grpcErrMessage(err)) return nil, false } - price := quote.GetTerms().GetPriceMsat() + q := quote.GetQuote() + if q == nil { + writeOpenAIError(c, http.StatusBadGateway, "server_error", "provider returned no quote") + return nil, false + } + price := q.GetPriceMsat() if s.cfg.MaxPriceMsat > 0 && price > s.cfg.MaxPriceMsat { writeOpenAIError( c, @@ -373,12 +386,12 @@ func (s *Server) requestQuoteAndValidatePrice( func (s *Server) acceptAndExecuteWithCancelOnFailure( c *gin.Context, peerID string, - jobID []byte, + callID []byte, ) (*lcpdv1.AcceptAndExecuteResponse, bool) { - execResp, err := s.acceptAndExecute(c, peerID, jobID) + execResp, err := s.acceptAndExecute(c, peerID, callID) if err != nil { if shouldCancelAfterExecuteError(c, err) { - s.cancelJobAsync(peerID, jobID) + s.cancelJobAsync(peerID, callID) } writeOpenAIError(c, httpStatusFromGRPC(err), "server_error", grpcErrMessage(err)) return nil, false @@ -389,13 +402,13 @@ func (s *Server) acceptAndExecuteWithCancelOnFailure( func validateResult( c *gin.Context, execResp *lcpdv1.AcceptAndExecuteResponse, -) (*lcpdv1.Result, bool) { - result := execResp.GetResult() +) (*lcpdv1.Complete, bool) { + result := execResp.GetComplete() if result == nil { writeOpenAIError(c, http.StatusBadGateway, "server_error", providerReturnedNoResultMessage) return nil, false } - if result.GetStatus() != lcpdv1.Result_STATUS_OK { + if result.GetStatus() != lcpdv1.Complete_STATUS_OK { msg := strings.TrimSpace(result.GetMessage()) if msg == "" { msg = providerReturnedNonOKStatusMessage @@ -412,33 +425,38 @@ func (s *Server) logAndWriteResult( started time.Time, model string, peerID string, - jobID []byte, + callID []byte, quote *lcpdv1.RequestQuoteResponse, requestBytes int, requestTokens int, quoteLatency time.Duration, execLatency time.Duration, - result *lcpdv1.Result, + result *lcpdv1.Complete, ) bool { ctx := c.Request.Context() - price := quote.GetTerms().GetPriceMsat() + q := quote.GetQuote() + if q == nil { + writeOpenAIError(c, http.StatusBadGateway, "server_error", "provider returned no quote") + return false + } + price := q.GetPriceMsat() totalLatency := time.Since(started) - resultBytes := result.GetResult() + resultBytes := result.GetResponseBytes() completionBytes := len(resultBytes) completionTokens := openai.ApproxTokensFromBytes(completionBytes) totalTokens := requestTokens + completionTokens - jobIDHex := hex.EncodeToString(jobID) - termsHashHex := hex.EncodeToString(quote.GetTerms().GetTermsHash()) + callIDHex := hex.EncodeToString(callID) + termsHashHex := hex.EncodeToString(q.GetTermsHash()) s.log.InfoContext( ctx, operation, "model", model, "peer_id", peerID, - "job_id", jobIDHex, + "call_id", callIDHex, "price_msat", price, "terms_hash", termsHashHex, "request_bytes", requestBytes, @@ -451,14 +469,14 @@ func (s *Server) logAndWriteResult( "total_ms", totalLatency.Milliseconds(), ) - writeLCPHeaders(c, peerID, jobID, quote) + writeLCPHeaders(c, peerID, callID, quote) - if enc := strings.TrimSpace(result.GetContentEncoding()); enc != "" && enc != contentEncodingIdentity { + if enc := strings.TrimSpace(result.GetResponseContentEncoding()); enc != "" && enc != contentEncodingIdentity { writeOpenAIError(c, http.StatusBadGateway, "server_error", fmt.Sprintf("unsupported content encoding: %q", enc)) return false } - contentType := strings.TrimSpace(result.GetContentType()) + contentType := strings.TrimSpace(result.GetResponseContentType()) if contentType == "" { contentType = "application/json; charset=utf-8" } @@ -472,37 +490,41 @@ func (s *Server) listPeers(c *gin.Context) (*lcpdv1.ListLCPPeersResponse, error) return s.lcpd.ListLCPPeers(ctx, &lcpdv1.ListLCPPeersRequest{}) } -func (s *Server) requestQuote(c *gin.Context, peerID string, task *lcpdv1.Task) (*lcpdv1.RequestQuoteResponse, error) { +func (s *Server) requestQuote( + c *gin.Context, + peerID string, + call *lcpdv1.CallSpec, +) (*lcpdv1.RequestQuoteResponse, error) { ctx, cancel := context.WithTimeout(c.Request.Context(), s.cfg.TimeoutQuote) defer cancel() return s.lcpd.RequestQuote(ctx, &lcpdv1.RequestQuoteRequest{ PeerId: peerID, - Task: task, + Call: call, }) } func (s *Server) acceptAndExecute( c *gin.Context, peerID string, - jobID []byte, + callID []byte, ) (*lcpdv1.AcceptAndExecuteResponse, error) { ctx, cancel := context.WithTimeout(c.Request.Context(), s.cfg.TimeoutExecute) defer cancel() return s.lcpd.AcceptAndExecute(ctx, &lcpdv1.AcceptAndExecuteRequest{ PeerId: peerID, - JobId: jobID, + CallId: callID, PayInvoice: true, }) } -func (s *Server) cancelJobAsync(peerID string, jobID []byte) { - jobIDCopy := copyBytes(jobID) +func (s *Server) cancelJobAsync(peerID string, callID []byte) { + callIDCopy := copyBytes(callID) go func() { ctx, cancel := context.WithTimeout(context.Background(), cancelJobTimeout) defer cancel() _, err := s.lcpd.CancelJob(ctx, &lcpdv1.CancelJobRequest{ PeerId: peerID, - JobId: jobIDCopy, + CallId: callIDCopy, Reason: cancelReasonRequestCanceled, }) if err != nil { @@ -524,22 +546,25 @@ func shouldCancelAfterExecuteError(c *gin.Context, err error) bool { return false } -func buildLCPOpenAIChatCompletionsV1Task(model string, requestJSON []byte) (*lcpdv1.Task, error) { +func buildLCPOpenAIChatCompletionsV1Call(model string, requestJSON []byte) (*lcpdv1.CallSpec, error) { if strings.TrimSpace(model) == "" { return nil, errors.New("model is required") } if len(requestJSON) == 0 { return nil, errors.New("request body is required") } - return &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: model, - }, - }, - }, + + paramsBytes, err := encodeOpenAIModelParamsTLV(model) + if err != nil { + return nil, err + } + + return &lcpdv1.CallSpec{ + Method: lcpMethodOpenAIChatCompletionsV1, + Params: paramsBytes, + RequestBytes: requestJSON, + RequestContentType: requestContentTypeJSONUTF8, + RequestContentEncoding: contentEncodingIdentity, }, nil } diff --git a/apps/openai-serve/internal/httpapi/handler_models.go b/apps/openai-serve/internal/httpapi/handler_models.go index 77811ab..c3f2ae2 100644 --- a/apps/openai-serve/internal/httpapi/handler_models.go +++ b/apps/openai-serve/internal/httpapi/handler_models.go @@ -1,7 +1,6 @@ package httpapi import ( - "context" "net/http" "sort" "time" @@ -16,15 +15,7 @@ func (s *Server) handleModels(c *gin.Context) { return } - ctx, cancel := context.WithTimeout(c.Request.Context(), s.cfg.TimeoutQuote) - defer cancel() - - models, err := s.discoverModels(ctx) - if err != nil { - s.log.ErrorContext(ctx, "models discovery failed", "err", err) - writeOpenAIError(c, httpStatusFromGRPC(err), "server_error", "failed to list models") - return - } + models := s.discoverModels() ids := make([]string, 0, len(models)) for id := range models { diff --git a/apps/openai-serve/internal/httpapi/handler_responses.go b/apps/openai-serve/internal/httpapi/handler_responses.go index e80e6a9..ffd52ad 100644 --- a/apps/openai-serve/internal/httpapi/handler_responses.go +++ b/apps/openai-serve/internal/httpapi/handler_responses.go @@ -30,11 +30,11 @@ func (s *Server) handleResponses(c *gin.Context) { s.handleOpenAIPassthrough( c, started, - lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1, + lcpMethodOpenAIResponsesV1, model, reqBytes, streaming, - s.buildLCPOpenAIResponsesV1Task, + s.buildLCPOpenAIResponsesV1Call, "response", ) } @@ -58,34 +58,37 @@ func decodeAndValidateResponsesRequest(c *gin.Context) ([]byte, openai.Responses return body, parsed, true } -func (s *Server) buildLCPOpenAIResponsesV1Task( +func (s *Server) buildLCPOpenAIResponsesV1Call( c *gin.Context, model string, reqBytes []byte, -) (*lcpdv1.Task, bool) { - task, err := buildLCPOpenAIResponsesV1Task(model, reqBytes) +) (*lcpdv1.CallSpec, bool) { + call, err := buildLCPOpenAIResponsesV1Call(model, reqBytes) if err != nil { writeOpenAIError(c, http.StatusBadRequest, "invalid_request_error", err.Error()) return nil, false } - return task, true + return call, true } -func buildLCPOpenAIResponsesV1Task(model string, requestJSON []byte) (*lcpdv1.Task, error) { +func buildLCPOpenAIResponsesV1Call(model string, requestJSON []byte) (*lcpdv1.CallSpec, error) { if strings.TrimSpace(model) == "" { return nil, errors.New("model is required") } if len(requestJSON) == 0 { return nil, errors.New("request body is required") } - return &lcpdv1.Task{ - Spec: &lcpdv1.Task_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1TaskSpec{ - RequestJson: requestJSON, - Params: &lcpdv1.OpenAIResponsesV1Params{ - Model: model, - }, - }, - }, + + paramsBytes, err := encodeOpenAIModelParamsTLV(model) + if err != nil { + return nil, err + } + + return &lcpdv1.CallSpec{ + Method: lcpMethodOpenAIResponsesV1, + Params: paramsBytes, + RequestBytes: requestJSON, + RequestContentType: requestContentTypeJSONUTF8, + RequestContentEncoding: contentEncodingIdentity, }, nil } diff --git a/apps/openai-serve/internal/httpapi/handler_stream.go b/apps/openai-serve/internal/httpapi/handler_stream.go index 650937e..6e4c7b9 100644 --- a/apps/openai-serve/internal/httpapi/handler_stream.go +++ b/apps/openai-serve/internal/httpapi/handler_stream.go @@ -21,18 +21,18 @@ type streamWriteResult struct { contentType string contentEncoding string - terminalResult *lcpdv1.Result + terminalComplete *lcpdv1.Complete } func (s *Server) acceptAndExecuteStream( c *gin.Context, peerID string, - jobID []byte, + callID []byte, ) (lcpdv1.LCPDService_AcceptAndExecuteStreamClient, context.CancelFunc, error) { ctx, cancel := context.WithTimeout(c.Request.Context(), s.cfg.TimeoutExecute) stream, err := s.lcpd.AcceptAndExecuteStream(ctx, &lcpdv1.AcceptAndExecuteStreamRequest{ PeerId: peerID, - JobId: jobID, + CallId: callID, PayInvoice: true, }) if err != nil { @@ -83,8 +83,8 @@ func newStreamWriteResult() streamWriteResult { func finishStreamWrite(out streamWriteResult, err error) (streamWriteResult, error) { if errors.Is(err, io.EOF) { - if out.terminalResult == nil { - return out, errors.New("stream ended without a terminal result") + if out.terminalComplete == nil { + return out, errors.New("stream ended without a terminal complete") } return out, nil } @@ -97,23 +97,23 @@ func (s *Server) handleStreamMessage( msg *lcpdv1.AcceptAndExecuteStreamResponse, ) (bool, error) { switch ev := msg.GetEvent().(type) { - case *lcpdv1.AcceptAndExecuteStreamResponse_ResultBegin: - applyStreamBegin(out, ev.ResultBegin) + case *lcpdv1.AcceptAndExecuteStreamResponse_ResponseBegin: + applyStreamBegin(out, ev.ResponseBegin) return false, nil - case *lcpdv1.AcceptAndExecuteStreamResponse_ResultChunk: - return false, writeStreamChunk(c, out, ev.ResultChunk) - case *lcpdv1.AcceptAndExecuteStreamResponse_Result: - out.terminalResult = ev.Result - return true, nil - case *lcpdv1.AcceptAndExecuteStreamResponse_ResultEnd: + case *lcpdv1.AcceptAndExecuteStreamResponse_ResponseChunk: + return false, writeStreamChunk(c, out, ev.ResponseChunk) + case *lcpdv1.AcceptAndExecuteStreamResponse_ResponseEnd: // Metadata only (hash/len). Ignored by the HTTP passthrough. return false, nil + case *lcpdv1.AcceptAndExecuteStreamResponse_Complete: + out.terminalComplete = ev.Complete + return true, nil default: return false, nil } } -func applyStreamBegin(out *streamWriteResult, begin *lcpdv1.ResultStreamBegin) { +func applyStreamBegin(out *streamWriteResult, begin *lcpdv1.ResponseStreamBegin) { if begin == nil { return } @@ -125,7 +125,7 @@ func applyStreamBegin(out *streamWriteResult, begin *lcpdv1.ResultStreamBegin) { } } -func writeStreamChunk(c *gin.Context, out *streamWriteResult, chunk *lcpdv1.ResultStreamChunk) error { +func writeStreamChunk(c *gin.Context, out *streamWriteResult, chunk *lcpdv1.ResponseStreamChunk) error { if chunk == nil { return nil } diff --git a/apps/openai-serve/internal/httpapi/lcp_params.go b/apps/openai-serve/internal/httpapi/lcp_params.go new file mode 100644 index 0000000..9d0f193 --- /dev/null +++ b/apps/openai-serve/internal/httpapi/lcp_params.go @@ -0,0 +1,77 @@ +package httpapi + +import ( + "encoding/binary" + "errors" + "strings" + "unicode/utf8" +) + +// encodeOpenAIModelParamsTLV encodes an OpenAI method params TLV stream that +// contains only: +// - type=1: model (UTF-8 string) +// +// This format matches the provider-side expectations for: +// - openai.chat_completions.v1 +// - openai.responses.v1. +func encodeOpenAIModelParamsTLV(model string) ([]byte, error) { + model = strings.TrimSpace(model) + if model == "" { + return nil, errors.New("model is required") + } + if !utf8.ValidString(model) { + return nil, errors.New("model must be valid UTF-8") + } + + modelBytes := []byte(model) + return encodeTLVBigSize(1, modelBytes), nil +} + +func encodeTLVBigSize(t uint64, v []byte) []byte { + tt := encodeBigSize(t) + ll := encodeBigSize(uint64(len(v))) + + out := make([]byte, 0, len(tt)+len(ll)+len(v)) + out = append(out, tt...) + out = append(out, ll...) + out = append(out, v...) + return out +} + +// encodeBigSize encodes an integer using the BOLT bigsize encoding: +// - 0x00..0xfc: 1 byte +// - 0xfd..0xffff: 0xfd + 2 bytes +// - 0x10000..0xffffffff: 0xfe + 4 bytes +// - else: 0xff + 8 bytes. +func encodeBigSize(v uint64) []byte { + const ( + prefix16 = 0xfd + prefix32 = 0xfe + prefix64 = 0xff + + max8 = 0xfc + max16 = 0xffff + max32 = 0xffffffff + + u16Len = 2 + u32Len = 4 + u64Len = 8 + ) + + switch { + case v <= max8: + return []byte{byte(v)} + case v <= max16: + var b [u16Len]byte + binary.BigEndian.PutUint16(b[:], uint16(v)) + return append([]byte{prefix16}, b[:]...) + case v <= max32: + var b [u32Len]byte + binary.BigEndian.PutUint32(b[:], uint32(v)) + return append([]byte{prefix32}, b[:]...) + default: + var b [u64Len]byte + binary.BigEndian.PutUint64(b[:], v) + return append([]byte{prefix64}, b[:]...) + } +} diff --git a/apps/openai-serve/internal/httpapi/routing.go b/apps/openai-serve/internal/httpapi/routing.go index a7c5e0a..dd161c2 100644 --- a/apps/openai-serve/internal/httpapi/routing.go +++ b/apps/openai-serve/internal/httpapi/routing.go @@ -1,7 +1,6 @@ package httpapi import ( - "context" "errors" "fmt" "strings" @@ -9,28 +8,28 @@ import ( lcpdv1 "github.com/bruwbird/lcp/proto-go/lcpd/v1" ) -func (s *Server) discoverModels(ctx context.Context) (map[string]struct{}, error) { +func (s *Server) discoverModels() map[string]struct{} { if len(s.cfg.ModelAllowlist) > 0 { - return copySet(s.cfg.ModelAllowlist), nil + return copySet(s.cfg.ModelAllowlist) } - resp, err := s.lcpd.ListLCPPeers(ctx, &lcpdv1.ListLCPPeersRequest{}) - if err != nil { - return nil, err + if len(s.cfg.ModelMap) != 0 { + out := make(map[string]struct{}, len(s.cfg.ModelMap)) + for model := range s.cfg.ModelMap { + out[model] = struct{}{} + } + return out } - return collectModelsFromPeersForKinds( - resp, - []lcpdv1.LCPTaskKind{ - lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1, - lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1, - }, - ), nil + + // LCP v0.3 manifests do not advertise available models. If no allowlist or + // explicit model map is configured, return an empty list. + return nil } -func (s *Server) validateModelForTaskKind( +func (s *Server) validateModelForMethod( model string, - taskKind lcpdv1.LCPTaskKind, - peersResp *lcpdv1.ListLCPPeersResponse, + _ string, + _ *lcpdv1.ListLCPPeersResponse, ) error { model = strings.TrimSpace(model) if model == "" { @@ -47,25 +46,22 @@ func (s *Server) validateModelForTaskKind( ) } - discovered := collectModelsFromPeersForKind(peersResp, taskKind) - if len(discovered) == 0 { - // Some providers do not advertise supported_tasks. If we have no allowlist - // and no discovery signal, skip validation to keep the gateway usable. - return nil - } - - if _, ok := discovered[model]; ok || s.cfg.AllowUnlistedModels { - return nil - } - return fmt.Errorf("model is not advertised by any connected LCP peer: %q", model) + // LCP v0.3 manifests do not advertise models. Without an explicit allowlist, + // skip validation to keep the gateway usable (providers enforce their own + // model policies). + return nil } -func (s *Server) resolvePeerIDForTaskKind( +func (s *Server) resolvePeerIDForMethod( model string, - taskKind lcpdv1.LCPTaskKind, + method string, peersResp *lcpdv1.ListLCPPeersResponse, ) (string, error) { model = strings.TrimSpace(model) + method = strings.TrimSpace(method) + if method == "" { + return "", errors.New("method is required") + } if peerID, ok, err := s.resolvePeerIDFromModelMap(model, peersResp); ok || err != nil { return peerID, err @@ -75,7 +71,7 @@ func (s *Server) resolvePeerIDForTaskKind( return peerID, err } - if peerID, ok := resolvePeerIDFromSupportedTasks(peersResp, taskKind, model); ok { + if peerID, ok := resolvePeerIDFromSupportedMethods(peersResp, method); ok { return peerID, nil } @@ -118,19 +114,20 @@ func (s *Server) resolvePeerIDFromDefaultPeer( return defaultPeerID, true, nil } -func resolvePeerIDFromSupportedTasks( +func resolvePeerIDFromSupportedMethods( peersResp *lcpdv1.ListLCPPeersResponse, - taskKind lcpdv1.LCPTaskKind, - model string, + method string, ) (string, bool) { for _, p := range peersResp.GetPeers() { - for _, tmpl := range p.GetRemoteManifest().GetSupportedTasks() { - if tmpl.GetKind() != taskKind { + m := p.GetRemoteManifest() + if m == nil { + continue + } + for _, desc := range m.GetSupportedMethods() { + if strings.TrimSpace(desc.GetMethod()) != method { continue } - if advertisedModel := modelFromTaskTemplate(taskKind, tmpl); strings.TrimSpace(advertisedModel) == model { - return p.GetPeerId(), true - } + return p.GetPeerId(), true } } return "", false @@ -143,66 +140,6 @@ func resolvePeerIDFromAnyPeer(peersResp *lcpdv1.ListLCPPeersResponse) (string, b return "", false } -func modelFromTaskTemplate(kind lcpdv1.LCPTaskKind, tmpl *lcpdv1.LCPTaskTemplate) string { - if tmpl == nil { - return "" - } - - switch kind { - case lcpdv1.LCPTaskKind_LCP_TASK_KIND_UNSPECIFIED: - return "" - case lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1: - return tmpl.GetOpenaiChatCompletionsV1().GetModel() - case lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1: - return tmpl.GetOpenaiResponsesV1().GetModel() - } - return "" -} - -func collectModelsFromPeersForKinds(resp *lcpdv1.ListLCPPeersResponse, kinds []lcpdv1.LCPTaskKind) map[string]struct{} { - if resp == nil { - return nil - } - - out := make(map[string]struct{}) - for _, kind := range kinds { - for model := range collectModelsFromPeersForKind(resp, kind) { - out[model] = struct{}{} - } - } - - if len(out) == 0 { - return nil - } - return out -} - -func collectModelsFromPeersForKind(resp *lcpdv1.ListLCPPeersResponse, kind lcpdv1.LCPTaskKind) map[string]struct{} { - if resp == nil { - return nil - } - - out := make(map[string]struct{}) - for _, p := range resp.GetPeers() { - for _, tmpl := range p.GetRemoteManifest().GetSupportedTasks() { - if tmpl.GetKind() != kind { - continue - } - - model := strings.TrimSpace(modelFromTaskTemplate(kind, tmpl)) - if model == "" { - continue - } - out[model] = struct{}{} - } - } - - if len(out) == 0 { - return nil - } - return out -} - func peerInList(resp *lcpdv1.ListLCPPeersResponse, peerID string) bool { peerID = strings.TrimSpace(peerID) if peerID == "" || resp == nil { diff --git a/apps/openai-serve/internal/httpapi/server_test.go b/apps/openai-serve/internal/httpapi/server_test.go index 55564d5..17c44d7 100644 --- a/apps/openai-serve/internal/httpapi/server_test.go +++ b/apps/openai-serve/internal/httpapi/server_test.go @@ -3,8 +3,10 @@ package httpapi_test import ( "bytes" "context" + "encoding/binary" "encoding/hex" "encoding/json" + "errors" "io" "log/slog" "maps" @@ -199,6 +201,63 @@ func mustDiff[T any](t *testing.T, want, got T, opts ...cmp.Option) { } } +func mustDecodeOpenAIModelParamsTLV(t *testing.T, payload []byte) string { + t.Helper() + + typ, n, err := decodeBigSize(payload) + if err != nil { + t.Fatalf("decode params type: %v", err) + } + if typ != 1 { + t.Fatalf("unexpected params tlv type: got %d want 1", typ) + } + + length, m, err := decodeBigSize(payload[n:]) + if err != nil { + t.Fatalf("decode params length: %v", err) + } + + offset := n + m + if offset > len(payload) { + t.Fatalf("params payload is truncated") + } + if length > uint64(len(payload)-offset) { + t.Fatalf("params length exceeds payload: length=%d remaining=%d", length, len(payload)-offset) + } + + modelBytes := payload[offset : offset+int(length)] + return string(modelBytes) +} + +func decodeBigSize(b []byte) (uint64, int, error) { + if len(b) == 0 { + return 0, 0, io.ErrUnexpectedEOF + } + + switch b[0] { + case 0xfd: + if len(b) < 3 { + return 0, 0, io.ErrUnexpectedEOF + } + return uint64(binary.BigEndian.Uint16(b[1:3])), 3, nil + case 0xfe: + if len(b) < 5 { + return 0, 0, io.ErrUnexpectedEOF + } + return uint64(binary.BigEndian.Uint32(b[1:5])), 5, nil + case 0xff: + if len(b) < 9 { + return 0, 0, io.ErrUnexpectedEOF + } + return binary.BigEndian.Uint64(b[1:9]), 9, nil + default: + if b[0] > 0xfc { + return 0, 0, errors.New("invalid bigsize prefix") + } + return uint64(b[0]), 1, nil + } +} + func TestChatCompletions_Success(t *testing.T) { const ( model = "gpt-5.2" @@ -223,12 +282,9 @@ func TestChatCompletions_Success(t *testing.T) { { PeerId: peerID, RemoteManifest: &lcpdv1.LCPManifest{ - SupportedTasks: []*lcpdv1.LCPTaskTemplate{ + SupportedMethods: []*lcpdv1.MethodDescriptor{ { - Kind: lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1, - ParamsTemplate: &lcpdv1.LCPTaskTemplate_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1Params{Model: model}, - }, + Method: "openai.chat_completions.v1", }, }, }, @@ -237,18 +293,24 @@ func TestChatCompletions_Success(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte(jobIDStr), + Quote: &lcpdv1.Quote{ + CallId: []byte(jobIDStr), PriceMsat: priceMsat123, TermsHash: []byte(termsStr), }, }, acceptResp: &lcpdv1.AcceptAndExecuteResponse{ - Result: &lcpdv1.Result{ - Status: lcpdv1.Result_STATUS_OK, - Result: wantRespBytes, - ContentType: "application/json; charset=utf-8", - ContentEncoding: "identity", + Complete: &lcpdv1.Complete{ + Status: lcpdv1.Complete_STATUS_OK, + ResponseBytes: wantRespBytes, + ResponseContentType: func() *string { + ct := "application/json; charset=utf-8" + return &ct + }(), + ResponseContentEncoding: func() *string { + ce := "identity" + return &ce + }(), }, }, } @@ -276,7 +338,7 @@ func TestChatCompletions_Success(t *testing.T) { mustDiff(t, wantRespBytes, rec.Body.Bytes()) mustDiff(t, peerID, rec.Header().Get("X-Lcp-Peer-Id")) - mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Job-Id")) + mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Call-Id")) mustDiff(t, "123", rec.Header().Get("X-Lcp-Price-Msat")) mustDiff(t, hex.EncodeToString([]byte(termsStr)), rec.Header().Get("X-Lcp-Terms-Hash")) @@ -289,15 +351,18 @@ func TestChatCompletions_Success(t *testing.T) { mustDiff(t, peerID, client.requestQuoteReq.GetPeerId()) mustDiff(t, peerID, client.acceptReq.GetPeerId()) - mustDiff(t, []byte(jobIDStr), client.acceptReq.GetJobId()) + mustDiff(t, []byte(jobIDStr), client.acceptReq.GetCallId()) mustDiff(t, true, client.acceptReq.GetPayInvoice()) - openaiTask := client.requestQuoteReq.GetTask().GetOpenaiChatCompletionsV1() - if openaiTask == nil { - t.Fatalf("expected openai_chat_completions_v1 task") + call := client.requestQuoteReq.GetCall() + if call == nil { + t.Fatalf("expected call") } - mustDiff(t, model, openaiTask.GetParams().GetModel()) - mustDiff(t, reqBody, openaiTask.GetRequestJson()) + mustDiff(t, "openai.chat_completions.v1", call.GetMethod()) + mustDiff(t, model, mustDecodeOpenAIModelParamsTLV(t, call.GetParams())) + mustDiff(t, reqBody, call.GetRequestBytes()) + mustDiff(t, "application/json; charset=utf-8", call.GetRequestContentType()) + mustDiff(t, "identity", call.GetRequestContentEncoding()) } func TestChatCompletions_Passthrough_AllowsExtraFields(t *testing.T) { @@ -343,14 +408,9 @@ func TestChatCompletions_Passthrough_AllowsExtraFields(t *testing.T) { { PeerId: peerID, RemoteManifest: &lcpdv1.LCPManifest{ - SupportedTasks: []*lcpdv1.LCPTaskTemplate{ + SupportedMethods: []*lcpdv1.MethodDescriptor{ { - Kind: lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1, - ParamsTemplate: &lcpdv1.LCPTaskTemplate_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1Params{ - Model: model, - }, - }, + Method: "openai.chat_completions.v1", }, }, }, @@ -359,18 +419,24 @@ func TestChatCompletions_Passthrough_AllowsExtraFields(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte("job-1"), + Quote: &lcpdv1.Quote{ + CallId: []byte("job-1"), PriceMsat: priceMsat123, TermsHash: []byte("terms-1"), }, }, acceptResp: &lcpdv1.AcceptAndExecuteResponse{ - Result: &lcpdv1.Result{ - Status: lcpdv1.Result_STATUS_OK, - Result: wantRespBytes, - ContentType: "application/json; charset=utf-8", - ContentEncoding: "identity", + Complete: &lcpdv1.Complete{ + Status: lcpdv1.Complete_STATUS_OK, + ResponseBytes: wantRespBytes, + ResponseContentType: func() *string { + ct := "application/json; charset=utf-8" + return &ct + }(), + ResponseContentEncoding: func() *string { + ce := "identity" + return &ce + }(), }, }, } @@ -398,11 +464,11 @@ func TestChatCompletions_Passthrough_AllowsExtraFields(t *testing.T) { mustDiff(t, http.StatusOK, rec.Code) mustDiff(t, wantRespBytes, rec.Body.Bytes()) - openaiTask := client.requestQuoteReq.GetTask().GetOpenaiChatCompletionsV1() - if openaiTask == nil { - t.Fatalf("expected openai_chat_completions_v1 task") + call := client.requestQuoteReq.GetCall() + if call == nil { + t.Fatalf("expected call") } - mustDiff(t, reqBody, openaiTask.GetRequestJson()) + mustDiff(t, reqBody, call.GetRequestBytes()) }) } } @@ -424,12 +490,9 @@ func TestChatCompletions_StreamSuccess(t *testing.T) { { PeerId: peerID, RemoteManifest: &lcpdv1.LCPManifest{ - SupportedTasks: []*lcpdv1.LCPTaskTemplate{ + SupportedMethods: []*lcpdv1.MethodDescriptor{ { - Kind: lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1, - ParamsTemplate: &lcpdv1.LCPTaskTemplate_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1Params{Model: model}, - }, + Method: "openai.chat_completions.v1", }, }, }, @@ -438,8 +501,8 @@ func TestChatCompletions_StreamSuccess(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte(jobIDStr), + Quote: &lcpdv1.Quote{ + CallId: []byte(jobIDStr), PriceMsat: priceMsat123, TermsHash: []byte(termsStr), }, @@ -447,26 +510,26 @@ func TestChatCompletions_StreamSuccess(t *testing.T) { acceptStreamClient: &staticStreamClient{ responses: []*lcpdv1.AcceptAndExecuteStreamResponse{ { - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultBegin{ - ResultBegin: &lcpdv1.ResultStreamBegin{ + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseBegin{ + ResponseBegin: &lcpdv1.ResponseStreamBegin{ ContentType: "text/event-stream; charset=utf-8", ContentEncoding: "identity", }, }, }, { - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultChunk{ - ResultChunk: &lcpdv1.ResultStreamChunk{Data: chunk1}, + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseChunk{ + ResponseChunk: &lcpdv1.ResponseStreamChunk{Data: chunk1}, }, }, { - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultChunk{ - ResultChunk: &lcpdv1.ResultStreamChunk{Data: chunk2}, + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseChunk{ + ResponseChunk: &lcpdv1.ResponseStreamChunk{Data: chunk2}, }, }, { - Event: &lcpdv1.AcceptAndExecuteStreamResponse_Result{ - Result: &lcpdv1.Result{Status: lcpdv1.Result_STATUS_OK}, + Event: &lcpdv1.AcceptAndExecuteStreamResponse_Complete{ + Complete: &lcpdv1.Complete{Status: lcpdv1.Complete_STATUS_OK}, }, }, }, @@ -497,7 +560,7 @@ func TestChatCompletions_StreamSuccess(t *testing.T) { mustDiff(t, append(chunk1, chunk2...), rec.Body.Bytes()) mustDiff(t, peerID, rec.Header().Get("X-Lcp-Peer-Id")) - mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Job-Id")) + mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Call-Id")) mustDiff(t, "123", rec.Header().Get("X-Lcp-Price-Msat")) mustDiff(t, hex.EncodeToString([]byte(termsStr)), rec.Header().Get("X-Lcp-Terms-Hash")) @@ -531,12 +594,9 @@ func TestResponses_Success(t *testing.T) { { PeerId: peerID, RemoteManifest: &lcpdv1.LCPManifest{ - SupportedTasks: []*lcpdv1.LCPTaskTemplate{ + SupportedMethods: []*lcpdv1.MethodDescriptor{ { - Kind: lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1, - ParamsTemplate: &lcpdv1.LCPTaskTemplate_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1Params{Model: model}, - }, + Method: "openai.responses.v1", }, }, }, @@ -545,18 +605,24 @@ func TestResponses_Success(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte(jobIDStr), + Quote: &lcpdv1.Quote{ + CallId: []byte(jobIDStr), PriceMsat: priceMsat123, TermsHash: []byte(termsStr), }, }, acceptResp: &lcpdv1.AcceptAndExecuteResponse{ - Result: &lcpdv1.Result{ - Status: lcpdv1.Result_STATUS_OK, - Result: wantRespBytes, - ContentType: "application/json; charset=utf-8", - ContentEncoding: "identity", + Complete: &lcpdv1.Complete{ + Status: lcpdv1.Complete_STATUS_OK, + ResponseBytes: wantRespBytes, + ResponseContentType: func() *string { + ct := "application/json; charset=utf-8" + return &ct + }(), + ResponseContentEncoding: func() *string { + ce := "identity" + return &ce + }(), }, }, } @@ -582,15 +648,17 @@ func TestResponses_Success(t *testing.T) { mustDiff(t, wantRespBytes, rec.Body.Bytes()) mustDiff(t, peerID, rec.Header().Get("X-Lcp-Peer-Id")) - mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Job-Id")) + mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Call-Id")) mustDiff(t, "123", rec.Header().Get("X-Lcp-Price-Msat")) mustDiff(t, hex.EncodeToString([]byte(termsStr)), rec.Header().Get("X-Lcp-Terms-Hash")) - openaiTask := client.requestQuoteReq.GetTask().GetOpenaiResponsesV1() - if openaiTask == nil { - t.Fatalf("expected openai_responses_v1 task") + call := client.requestQuoteReq.GetCall() + if call == nil { + t.Fatalf("expected call") } - mustDiff(t, reqBody, openaiTask.GetRequestJson()) + mustDiff(t, "openai.responses.v1", call.GetMethod()) + mustDiff(t, model, mustDecodeOpenAIModelParamsTLV(t, call.GetParams())) + mustDiff(t, reqBody, call.GetRequestBytes()) } func TestResponses_StreamSuccess(t *testing.T) { @@ -609,12 +677,9 @@ func TestResponses_StreamSuccess(t *testing.T) { { PeerId: peerID, RemoteManifest: &lcpdv1.LCPManifest{ - SupportedTasks: []*lcpdv1.LCPTaskTemplate{ + SupportedMethods: []*lcpdv1.MethodDescriptor{ { - Kind: lcpdv1.LCPTaskKind_LCP_TASK_KIND_OPENAI_RESPONSES_V1, - ParamsTemplate: &lcpdv1.LCPTaskTemplate_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1Params{Model: model}, - }, + Method: "openai.responses.v1", }, }, }, @@ -623,8 +688,8 @@ func TestResponses_StreamSuccess(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte(jobIDStr), + Quote: &lcpdv1.Quote{ + CallId: []byte(jobIDStr), PriceMsat: priceMsat123, TermsHash: []byte(termsStr), }, @@ -632,21 +697,21 @@ func TestResponses_StreamSuccess(t *testing.T) { acceptStreamClient: &staticStreamClient{ responses: []*lcpdv1.AcceptAndExecuteStreamResponse{ { - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultBegin{ - ResultBegin: &lcpdv1.ResultStreamBegin{ + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseBegin{ + ResponseBegin: &lcpdv1.ResponseStreamBegin{ ContentType: "text/event-stream; charset=utf-8", ContentEncoding: "identity", }, }, }, { - Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResultChunk{ - ResultChunk: &lcpdv1.ResultStreamChunk{Data: streamData}, + Event: &lcpdv1.AcceptAndExecuteStreamResponse_ResponseChunk{ + ResponseChunk: &lcpdv1.ResponseStreamChunk{Data: streamData}, }, }, { - Event: &lcpdv1.AcceptAndExecuteStreamResponse_Result{ - Result: &lcpdv1.Result{Status: lcpdv1.Result_STATUS_OK}, + Event: &lcpdv1.AcceptAndExecuteStreamResponse_Complete{ + Complete: &lcpdv1.Complete{Status: lcpdv1.Complete_STATUS_OK}, }, }, }, @@ -675,7 +740,7 @@ func TestResponses_StreamSuccess(t *testing.T) { mustDiff(t, streamData, rec.Body.Bytes()) mustDiff(t, peerID, rec.Header().Get("X-Lcp-Peer-Id")) - mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Job-Id")) + mustDiff(t, hex.EncodeToString([]byte(jobIDStr)), rec.Header().Get("X-Lcp-Call-Id")) mustDiff(t, "123", rec.Header().Get("X-Lcp-Price-Msat")) mustDiff(t, hex.EncodeToString([]byte(termsStr)), rec.Header().Get("X-Lcp-Terms-Hash")) @@ -846,8 +911,8 @@ func TestChatCompletions_RejectsQuotePriceOverLimit(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte("job"), + Quote: &lcpdv1.Quote{ + CallId: []byte("job"), PriceMsat: priceMsatOverLimit, TermsHash: []byte("terms"), }, @@ -897,8 +962,8 @@ func TestChatCompletions_CancelsJobOnCanceledExecute(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte(jobID), + Quote: &lcpdv1.Quote{ + CallId: []byte(jobID), PriceMsat: uint64(len("x")), TermsHash: []byte("terms"), }, @@ -936,7 +1001,7 @@ func TestChatCompletions_CancelsJobOnCanceledExecute(t *testing.T) { select { case cancelReq := <-cancelCh: mustDiff(t, peerID, cancelReq.GetPeerId()) - mustDiff(t, []byte(jobID), cancelReq.GetJobId()) + mustDiff(t, []byte(jobID), cancelReq.GetCallId()) case <-time.After(cancelWaitTimeout): t.Fatalf("expected CancelJob to be called") } @@ -958,16 +1023,16 @@ func TestChatCompletions_DoesNotLogPromptOrCompletion(t *testing.T) { }, requestQuoteResp: &lcpdv1.RequestQuoteResponse{ PeerId: peerID, - Terms: &lcpdv1.Terms{ - JobId: []byte(jobIDStr), + Quote: &lcpdv1.Quote{ + CallId: []byte(jobIDStr), PriceMsat: priceMsat123, TermsHash: []byte(termsStr), }, }, acceptResp: &lcpdv1.AcceptAndExecuteResponse{ - Result: &lcpdv1.Result{ - Status: lcpdv1.Result_STATUS_OK, - Result: []byte(outputText), + Complete: &lcpdv1.Complete{ + Status: lcpdv1.Complete_STATUS_OK, + ResponseBytes: []byte(outputText), }, }, } diff --git a/go-lcpd/itest/e2e/regtest_openai_serve_test.go b/go-lcpd/itest/e2e/regtest_openai_serve_test.go index 5a3dbf7..6034824 100644 --- a/go-lcpd/itest/e2e/regtest_openai_serve_test.go +++ b/go-lcpd/itest/e2e/regtest_openai_serve_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/bruwbird/lcp/go-lcpd/internal/domain/lcp" + "github.com/bruwbird/lcp/go-lcpd/internal/lcpwire" "github.com/bruwbird/lcp/go-lcpd/internal/lnd/lnrpc" "github.com/bruwbird/lcp/go-lcpd/itest/harness/lcpd" "github.com/bruwbird/lcp/go-lcpd/itest/harness/openaiserve" @@ -134,35 +135,42 @@ func TestE2E_Regtest_OpenAIServe_StreamAndResponses(t *testing.T) { _ = regtest.WaitForLCPPeers(ctx, t, bobClient) maxPriceMsat := uint64(0) - for _, task := range []*lcpdv1.Task{ + chatParams, err := lcpwire.EncodeOpenAIChatCompletionsV1Params(lcpwire.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}) + if err != nil { + t.Fatalf("EncodeOpenAIChatCompletionsV1Params: %v", err) + } + respParams, err := lcpwire.EncodeOpenAIResponsesV1Params(lcpwire.OpenAIResponsesV1Params{Model: "gpt-5.2"}) + if err != nil { + t.Fatalf("EncodeOpenAIResponsesV1Params: %v", err) + } + + for _, call := range []*lcpdv1.CallSpec{ { - Spec: &lcpdv1.Task_OpenaiChatCompletionsV1{ - OpenaiChatCompletionsV1: &lcpdv1.OpenAIChatCompletionsV1TaskSpec{ - RequestJson: []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"preflight"}]}`), - Params: &lcpdv1.OpenAIChatCompletionsV1Params{Model: "gpt-5.2"}, - }, - }, + Method: "openai.chat_completions.v1", + Params: chatParams, + RequestBytes: []byte(`{"model":"gpt-5.2","messages":[{"role":"user","content":"preflight"}]}`), + RequestContentType: "application/json; charset=utf-8", + RequestContentEncoding: "identity", }, { - Spec: &lcpdv1.Task_OpenaiResponsesV1{ - OpenaiResponsesV1: &lcpdv1.OpenAIResponsesV1TaskSpec{ - RequestJson: []byte(`{"model":"gpt-5.2","input":"preflight"}`), - Params: &lcpdv1.OpenAIResponsesV1Params{Model: "gpt-5.2"}, - }, - }, + Method: "openai.responses.v1", + Params: respParams, + RequestBytes: []byte(`{"model":"gpt-5.2","input":"preflight"}`), + RequestContentType: "application/json; charset=utf-8", + RequestContentEncoding: "identity", }, } { quoteResp, quoteErr := aliceClient.RequestQuote(ctx, &lcpdv1.RequestQuoteRequest{ PeerId: bobPubKey, - Task: task, + Call: call, }) if quoteErr != nil { t.Fatalf("RequestQuote (preflight): %v", quoteErr) } - if quoteResp.GetTerms() == nil { - t.Fatalf("RequestQuote (preflight): terms is nil") + if quoteResp.GetQuote() == nil { + t.Fatalf("RequestQuote (preflight): quote is nil") } - price := quoteResp.GetTerms().GetPriceMsat() + price := quoteResp.GetQuote().GetPriceMsat() if price > maxPriceMsat { maxPriceMsat = price } @@ -209,11 +217,11 @@ func TestE2E_Regtest_OpenAIServe_StreamAndResponses(t *testing.T) { t.Fatalf("peer-id header mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(lcp.Hash32Len*2, len(resp.Header.Get("X-Lcp-Job-Id"))); diff != "" { - t.Fatalf("job-id header length mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(lcp.Hash32Len*2, len(resp.Header.Get("X-Lcp-Call-Id"))); diff != "" { + t.Fatalf("call-id header length mismatch (-want +got):\n%s", diff) } - if _, decodeErr := hex.DecodeString(resp.Header.Get("X-Lcp-Job-Id")); decodeErr != nil { - t.Fatalf("job-id header is not hex: %v", decodeErr) + if _, decodeErr := hex.DecodeString(resp.Header.Get("X-Lcp-Call-Id")); decodeErr != nil { + t.Fatalf("call-id header is not hex: %v", decodeErr) } if diff := cmp.Diff(lcp.Hash32Len*2, len(resp.Header.Get("X-Lcp-Terms-Hash"))); diff != "" { t.Fatalf("terms-hash header length mismatch (-want +got):\n%s", diff) From 25b172d829b8529dfd0b64db1897790cc6450d8e Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 9 Jan 2026 16:27:41 +0900 Subject: [PATCH 4/4] docs: Refresh for LCP v0.3 Update user-facing docs to match the breaking v0.3 terminology and APIs. - Align go-lcpd and openai-serve docs with call/quote/complete. - Replace supported_tasks with supported_methods and remove model discovery. - Document X-Lcp-Call-Id and other updated headers. --- CONTRIBUTING.md | 2 +- README.md | 14 +- docs/go-lcpd/docs/background-ja.md | 2 +- docs/go-lcpd/docs/background.md | 2 +- docs/go-lcpd/docs/cli-ja.md | 8 +- docs/go-lcpd/docs/cli.md | 34 +- docs/go-lcpd/docs/configuration-ja.md | 17 +- docs/go-lcpd/docs/configuration.md | 13 +- docs/go-lcpd/docs/devnet-ja.md | 12 +- docs/go-lcpd/docs/devnet.md | 8 +- docs/go-lcpd/docs/logging-ja.md | 18 +- docs/go-lcpd/docs/logging.md | 18 +- docs/go-lcpd/docs/quickstart-mainnet-ja.md | 13 +- docs/go-lcpd/docs/quickstart-mainnet.md | 11 +- docs/go-lcpd/spec-ja.md | 10 +- docs/go-lcpd/spec.md | 865 +++++++-------------- docs/openai-serve/configuration-ja.mdx | 9 +- docs/openai-serve/configuration.mdx | 9 +- docs/openai-serve/overview-ja.mdx | 4 +- docs/openai-serve/overview.mdx | 4 +- 20 files changed, 386 insertions(+), 687 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a4528b..82ec0e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ If you are new to Lightning, Go, or protocol work, open an issue or a draft PR a ## Quick links -- Protocol draft (LCP v0.2): `docs/protocol/protocol.md` +- Protocol draft (LCP v0.3): `docs/protocol/protocol.md` - Reference implementation (daemon): `go-lcpd/` - go-lcpd developer docs: `docs/go-lcpd/docs/` diff --git a/README.md b/README.md index e92e441..b821cfb 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,18 @@ LCP (Lightning Compute Protocol) is an application-layer protocol for paying for small compute jobs over Lightning. It uses BOLT #1 custom messages on a direct Lightning peer connection. -LCP v0.2 defines a quote → pay → stream flow: +LCP v0.3 defines a manifest → call → quote → pay → stream → complete flow: - Both sides exchange `lcp_manifest` (mandatory) to advertise limits. -- The Requester sends `lcp_quote_request`, then streams the input (`lcp_stream_begin/chunk/end` with `stream_kind=input`). -- The Provider replies with `lcp_quote_response` containing `price_msat`, `terms_hash`, and a BOLT11 invoice. -- After settlement, the Provider streams the result (`stream_kind=result`) and then sends `lcp_result` (terminal completion + metadata). +- The Requester sends `lcp_call`, then streams the request payload (`lcp_stream_begin/chunk/end` with `stream_kind=request`). +- The Provider replies with `lcp_quote` containing `price_msat`, `terms_hash`, and a BOLT11 invoice. +- After settlement, the Provider streams the response (`stream_kind=response`) and then sends `lcp_complete` (terminal completion + metadata). LCP binds payment to job terms by setting the invoice `description_hash` to `terms_hash`. Limitations: -- Payloads are limited by the BOLT #1 custom message size (about 65 KB), but v0.2 supports large inputs/results via chunked streams bounded by peer-declared limits (`max_payload_bytes`, `max_stream_bytes`, `max_job_bytes`). +- Payloads are limited by the BOLT #1 custom message size (about 65 KB), but v0.3 supports large request/response bodies via chunked streams bounded by peer-declared limits (`max_payload_bytes`, `max_stream_bytes`, `max_call_bytes`). - The Requester and Provider must be directly peered. This leaks metadata compared to onion messages or blinded paths. -- Payment happens before execution. This is not an atomic swap (and v0.2 also requires sending the full input stream before quoting). +- Payment happens before execution. This is not an atomic swap (and v0.3 also requires sending the full request stream before quoting). Reference: BOLT #1 messaging (`https://github.com/lightning/bolts/blob/master/01-messaging.md`). @@ -44,7 +44,7 @@ This project is unaudited. Running it against real funds and real peers can lead - Configuration: [docs/go-lcpd/docs/configuration.md](docs/go-lcpd/docs/configuration.md) - Background run + logging: [docs/go-lcpd/docs/background.md](docs/go-lcpd/docs/background.md) - regtest walkthrough: [docs/go-lcpd/docs/regtest.md](docs/go-lcpd/docs/regtest.md) -- Protocol spec (LCP v0.2): [docs/protocol/protocol.md](docs/protocol/protocol.md) +- Protocol spec (LCP v0.3): [docs/protocol/protocol.md](docs/protocol/protocol.md) - One-shot client (demo): [go-lcpd/tools/lcpd-oneshot](go-lcpd/tools/lcpd-oneshot) ## Docs site (Mintlify) diff --git a/docs/go-lcpd/docs/background-ja.md b/docs/go-lcpd/docs/background-ja.md index 3d44fbe..f9d9724 100644 --- a/docs/go-lcpd/docs/background-ja.md +++ b/docs/go-lcpd/docs/background-ja.md @@ -5,7 +5,7 @@ - `lcpd-grpcd` をバックグラウンドで動かす - ログをディスクに残す(簡易ローテーション付き) -注意: ログはセンシティブです。`lcpd-grpcd` は生のプロンプト/出力をログに残さない設計ですが、ログにはメタデータ(peer id / job id / 価格 / 時間など)が残ります。詳細は [ログとプライバシー](/go-lcpd/docs/logging-ja) を参照してください。 +注意: ログはセンシティブです。`lcpd-grpcd` は生のプロンプト/出力をログに残さない設計ですが、ログにはメタデータ(peer id / call id / 価格 / 時間など)が残ります。詳細は [ログとプライバシー](/go-lcpd/docs/logging-ja) を参照してください。 長期運用のインフラでは service manager(systemd/launchd)を使ってください。 「ターミナルを閉じても動き続けてほしい」程度なら、このリポジトリのスクリプトで十分な場合が多いです。 diff --git a/docs/go-lcpd/docs/background.md b/docs/go-lcpd/docs/background.md index 4509cbe..80f6950 100644 --- a/docs/go-lcpd/docs/background.md +++ b/docs/go-lcpd/docs/background.md @@ -6,7 +6,7 @@ This doc describes practical ways to: - keep logs on disk (with basic rotation) Note: logs are sensitive. `lcpd-grpcd` is designed to avoid logging raw prompts/outputs, but logs still contain metadata -(peer ids, job ids, prices, timings). See [Logging & privacy](/go-lcpd/docs/logging). +(peer ids, call ids, prices, timings). See [Logging & privacy](/go-lcpd/docs/logging). For long-running infrastructure, use a service manager (systemd/launchd). For "keep running after I close my terminal", the repo script is usually enough. diff --git a/docs/go-lcpd/docs/cli-ja.md b/docs/go-lcpd/docs/cli-ja.md index 9f38535..82d6611 100644 --- a/docs/go-lcpd/docs/cli-ja.md +++ b/docs/go-lcpd/docs/cli-ja.md @@ -32,10 +32,10 @@ GOBIN="$PWD/bin" go install ./tools/lcpd-grpcd ./tools/lcpdctl ./tools/lcpd-ones - `lcpd-grpcd` は plaintext gRPC(TLS なし / 認証なし)を提供します。`127.0.0.1` にバインドするか、遠隔アクセスが必要なら SSH/VPN/リバースプロキシ等で保護してください。 - `AcceptAndExecute` は `lnd` を使って BOLT11 invoice を支払います。mainnet では実際に資金を消費します。regtest から始め、少額で試してください。 - LCP メッセージングには Provider への Lightning ピア接続が必要であり、Provider 側が LCP(manifest 観測)をサポートしている必要があります。 -- サポートされるタスク種別は `openai.chat_completions.v1` のみです(OpenAI 互換の request/response JSON bytes をそのまま運ぶ passthrough)。 -- Quote の実行は `terms.quoteExpiry` によって時間制限されます。expiry 後の呼び出しは失敗するのが想定挙動です。 -- LCP ピアメッセージングには payload 制限(manifest の `max_payload_bytes`)があります。`go-lcpd` のデフォルトは `16384` バイトです。 -- `job_id` の CLI フラグは base64(proto の `bytes`)で、protojson のエンコーディングに合わせています。 +- 主な対応 method は `openai.chat_completions.v1` と `openai.responses.v1` です(OpenAI 互換の request/response JSON bytes をそのまま運ぶ passthrough)。 +- Quote の実行は `quote.quoteExpiry` によって時間制限されます。expiry 後の呼び出しは失敗するのが想定挙動です。 +- LCP ピアメッセージングには payload/stream 制限(manifest の `max_payload_bytes` / `max_stream_bytes` / `max_call_bytes`)があります。`go-lcpd` のデフォルトは payload `16384` バイト、stream `4 MiB`、call `8 MiB` です。 +- `call_id` の CLI フラグは base64(proto の `bytes`)で、protojson のエンコーディングに合わせています。 - `lcpdctl --timeout` は dial timeout(接続確立までの上限)であり RPC の deadline ではありません。長い RPC は Ctrl-C でキャンセルするか、全体の deadline が必要なら `lcpd-oneshot -timeout ...` を使ってください。 古いドキュメントについて: diff --git a/docs/go-lcpd/docs/cli.md b/docs/go-lcpd/docs/cli.md index 3e741b7..83eff93 100644 --- a/docs/go-lcpd/docs/cli.md +++ b/docs/go-lcpd/docs/cli.md @@ -30,10 +30,10 @@ All command examples below use: - `lcpd-grpcd` serves plaintext gRPC (no TLS, no auth). Bind to `127.0.0.1` or protect it via SSH/VPN/reverse-proxy if you must access it remotely. - `AcceptAndExecute` pays a BOLT11 invoice via your `lnd`. On mainnet this spends real funds. Start with regtest and use small amounts. - LCP messaging requires an active Lightning peer connection to the Provider, and the Provider must support LCP (manifest observed). -- The only supported task type is `openai.chat_completions.v1` (raw OpenAI-compatible request/response JSON bytes passthrough). -- Quote execution is time-bounded by `terms.quoteExpiry`; calls after expiry are expected to fail. -- LCP peer messaging enforces payload/stream limits (`max_payload_bytes`, `max_stream_bytes`, `max_job_bytes` in the manifest). `go-lcpd` defaults to `16384` bytes payload, `4 MiB` stream, `8 MiB` job. -- `job_id` CLI flags are base64 (proto `bytes`), matching protojson encoding. +- The primary supported methods are `openai.chat_completions.v1` and `openai.responses.v1` (raw OpenAI-compatible request/response JSON bytes passthrough). +- Quote execution is time-bounded by `quote.quoteExpiry`; calls after expiry are expected to fail. +- LCP peer messaging enforces payload/stream limits (`max_payload_bytes`, `max_stream_bytes`, `max_call_bytes` in the manifest). `go-lcpd` defaults to `16384` bytes payload, `4 MiB` stream, `8 MiB` call. +- `call_id` CLI flags are base64 (proto `bytes`), matching protojson encoding. - `lcpdctl --timeout` is a dial timeout (not an RPC deadline). Cancel long-running RPCs with Ctrl-C, or use `lcpd-oneshot -timeout ...` for an overall deadline. Note about older docs: @@ -190,7 +190,7 @@ Tip: ## Quickstart (RequestQuote → AcceptAndExecute) -This section shows the full flow using only `lcpdctl lcpd ...` (quote → pay → stream(result) → `lcp_result`). +This section shows the full flow using only `lcpdctl lcpd ...` (call → quote → pay → stream(response) → `lcp_complete`). - The examples do not write files (stdout only). - No Python is needed (we use `jq` to format JSON). @@ -210,6 +210,7 @@ cd go-lcpd SERVER_ADDR="127.0.0.1:50051" PEER_ID="" MODEL="gpt-5.2" +METHOD="openai.chat_completions.v1" PROMPT="Say hello in one word." @@ -217,12 +218,19 @@ request_json="$(jq -nc --arg model "$MODEL" --arg prompt "$PROMPT" \ '{model:$model, messages:[{role:"user", content:$prompt}]}' )" request_json_b64="$(printf '%s' "$request_json" | base64 | tr -d '\n')" +# For openai.* methods, call.params is a method-defined TLV stream (BOLT-style TLVs). +# For openai.chat_completions.v1, params currently include the model as TLV type=1. +# This value is base64 for: 0x01 0x07 "gpt-5.2" +call_params_b64="AQdncHQtNS4y" + quote_json="$(./bin/lcpdctl lcpd request-quote \ -s "$SERVER_ADDR" \ --peer-id "$PEER_ID" \ - --task-openai-chat-completions-v1 \ - --task-openai-chat-completions-v1-params-model "$MODEL" \ - --task-openai-chat-completions-v1-request-json "$request_json_b64" \ + --call-method "$METHOD" \ + --call-params "$call_params_b64" \ + --call-request-bytes "$request_json_b64" \ + --call-request-content-type "application/json; charset=utf-8" \ + --call-request-content-encoding "identity" \ -o json)" echo "$quote_json" | jq -C . # If you don't have `jq` installed, use: echo "$quote_json" @@ -230,28 +238,28 @@ echo "$quote_json" | jq -C . #### 2) AcceptAndExecute (pay invoice and wait for result) -`jobId` is base64 in protojson (because `job_id` is `bytes`). +`callId` is base64 in protojson (because `call_id` is `bytes`). ```sh cd go-lcpd -job_id_b64="$(echo "$quote_json" | jq -r '.terms.jobId')" +call_id_b64="$(echo "$quote_json" | jq -r '.quote.callId')" exec_json="$(./bin/lcpdctl lcpd accept-and-execute \ -s "$SERVER_ADDR" \ --peer-id "$PEER_ID" \ - --job-id "$job_id_b64" \ + --call-id "$call_id_b64" \ --pay-invoice \ -o json)" echo "$exec_json" | jq -C . # If you don't have `jq` installed, use: echo "$exec_json" ``` -Note: `AcceptAndExecuteResponse.result.result` is base64-encoded bytes in JSON output. +Note: `AcceptAndExecuteResponse.complete.responseBytes` is base64-encoded bytes in JSON output. To decode and pretty-print the raw OpenAI response JSON: ```sh -result_b64="$(echo "$exec_json" | jq -r '.result.result')" +result_b64="$(echo "$exec_json" | jq -r '.complete.responseBytes')" # GNU coreutils: # echo "$result_b64" | base64 -d | jq -C . diff --git a/docs/go-lcpd/docs/configuration-ja.md b/docs/go-lcpd/docs/configuration-ja.md index f670721..754f604 100644 --- a/docs/go-lcpd/docs/configuration-ja.md +++ b/docs/go-lcpd/docs/configuration-ja.md @@ -42,7 +42,7 @@ export LCPD_LND_ADMIN_MACAROON_PATH="$HOME/.lnd/data/chain/bitcoin/mainnet/admin - `LCPD_LOG_LEVEL` で詳細度を制御します(`debug` / `info` / `warn` / `error`。デフォルトは `info`)。 - ログには、生のプロンプト / 生のモデル出力 / API key / macaroon / BOLT11 invoice を残してはいけません。 -- 生データを残さなくても、ログにはメタデータ(peer id / job id / 価格 / 時間など)が残ります。 +- 生データを残さなくても、ログにはメタデータ(peer id / call id / 価格 / 時間など)が残ります。 詳細: [ログとプライバシー](/go-lcpd/docs/logging-ja)。 @@ -116,9 +116,8 @@ llm: - `model` は OpenAI の model ID です。 - 出現箇所: - - wire の `openai_chat_completions_v1_params_tlvs.model`(`params_bytes`) - - input stream bytes に入る OpenAI request JSON の `request_json.model` - - gRPC の `LCPManifest.supported_tasks[].openai_chat_completions_v1.model`(広告) + - wire の `openai_chat_completions_v1_params_tlvs.model`(`lcp_call.params` 内) + - request stream bytes に入る OpenAI request JSON の `request_json.model` - Provider は `model` を allowlist / pricing / backend routing に使います。 ### フィールドリファレンス @@ -130,7 +129,7 @@ llm: - `per_job_bps`: `threshold` を超えた 1 job あたりの加算倍率(bps。10,000 = 1.0x)。`0` の場合は無効。 - `max_multiplier_bps`: 総倍率の上限(bps)。`0` の場合は安全なデフォルト上限が使われます。 - `llm.max_output_tokens`: Provider 全体の max output tokens 上限。quote 時の推定と request validation に使います。デフォルト 4096。 -- `llm.models`: 許可/広告する `openai.chat_completions.v1` model ID のマップ。空の場合、任意の `model` を受け付けますが manifest では広告しません。 +- `llm.models`: 許可する `openai.*` model ID のマップ。空の場合、任意の `model` を受け付けます(ただし Provider 側のポリシーにより拒否される場合があります)。 - `max_output_tokens`: 任意の model ごとの上書き(0 より大きいこと)。 - `price`: model ごとの価格(msat / 100 万トークン)。`input_msat_per_mtok` と `output_msat_per_mtok` は必須、`cached_input_msat_per_mtok` は任意。 @@ -142,20 +141,20 @@ YAML がない場合、内蔵の価格表(msat / 100 万トークン)を使 ### Quote → Execute フロー(`openai.chat_completions.v1`) 1. QuoteRequest を検証し、model が許可されているか確認します。 -2. input stream bytes を OpenAI request JSON として受信・検証します(`request_json.model` / `request_json.messages` / `request_json.stream=false`)。 +2. request stream bytes を OpenAI request JSON として受信・検証します(`request_json.model` / `request_json.messages` / optional `request_json.stream`)。 3. quote 時の推定に使う `max_output_tokens` を決定します: - `llm.max_output_tokens`(任意で model ごとの上書き)を上限とする - request が output token 上限(`max_completion_tokens` / `max_tokens` / `max_output_tokens`)を指定している場合、Provider 上限以下であることを検証し、その値で推定する 4. `UsageEstimator`(`approx.v1`: `ceil(len(bytes)/4)`)でトークン使用量を推定します。 5. `QuotePrice(model, estimate, cached=0, price_table)` で msat 価格を計算し、任意で `pricing.in_flight_surge` を適用してから TermsHash / invoice binding に埋め込みます。 -6. 支払いが確定したら、passthrough request を backend で実行し、result stream を返して `lcp_result` で完了します。 +6. 支払いが確定したら、passthrough request を backend で実行し、response stream を返して `lcp_complete` で完了します。 ## backend に関する補足 - `openai`: 外部 API を呼び出します(課金 / レート制限 / ネットワーク依存)。 - OpenAI 互換 Chat Completions API(`POST /v1/chat/completions`)を使用します。 - - LCP input stream の raw request body bytes をそのまま送信します(non-streaming)。 - - OpenAI 互換の response body bytes をそのまま返します(non-streaming JSON)。 + - LCP request stream の raw request body bytes をそのまま送信します(upstream streaming の有無は request JSON に依存)。 + - OpenAI 互換の response body bytes をそのまま返します(streaming の場合は `text/event-stream`)。 - `deterministic`: 開発用の固定出力 backend(外部 API なし)。 - `disabled`: 実行しません(Requester のみ運用で便利)。 diff --git a/docs/go-lcpd/docs/configuration.md b/docs/go-lcpd/docs/configuration.md index a60ce10..7fc80ca 100644 --- a/docs/go-lcpd/docs/configuration.md +++ b/docs/go-lcpd/docs/configuration.md @@ -42,7 +42,7 @@ export LCPD_LND_ADMIN_MACAROON_PATH="$HOME/.lnd/data/chain/bitcoin/mainnet/admin - `LCPD_LOG_LEVEL` controls verbosity (`debug`, `info`, `warn`, `error`; default is `info`). - Logs MUST NOT contain raw prompts, raw model outputs, API keys, macaroons, or BOLT11 invoices. -- Even with content redaction, logs still contain metadata (peer ids, job ids, prices, timings). +- Even with content redaction, logs still contain metadata (peer ids, call ids, prices, timings). Details: [Logging & privacy](/go-lcpd/docs/logging). @@ -116,9 +116,8 @@ llm: - `model` is the OpenAI model ID. - It appears in: - - `openai_chat_completions_v1_params_tlvs.model` on the wire (in `params_bytes`) - - the OpenAI request JSON (`request_json.model`) carried in the input stream bytes - - gRPC `LCPManifest.supported_tasks[].openai_chat_completions_v1.model` (model advertising) + - `openai_chat_completions_v1_params_tlvs.model` on the wire (in `lcp_call.params`) + - the OpenAI request JSON (`request_json.model`) carried in the request stream bytes - The Provider uses `model` for allowlisting, pricing, and routing to the compute backend. ### Field reference @@ -130,7 +129,7 @@ llm: - `per_job_bps`: Additive multiplier per job above `threshold` (basis points; 10,000 = 1.0x). If `0`, surge is disabled. - `max_multiplier_bps`: Caps the total multiplier in basis points. If `0`, a safe default cap is used. - `llm.max_output_tokens`: Provider-wide default for output token limits, used for quote-time estimation and request validation. Default is 4096. -- `llm.models`: Map of allowed/advertised `openai.chat_completions.v1` model IDs. If empty, accepts any `model` but does not advertise `supported_tasks`. +- `llm.models`: Map of allowed `openai.*` model IDs. If empty, accepts any `model` (providers may still apply their own policy). - `max_output_tokens`: Optional per-model override (must be > 0). - `price`: Required per-model pricing (msat per 1M tokens). `input_msat_per_mtok` and `output_msat_per_mtok` are required; `cached_input_msat_per_mtok` is optional. @@ -142,13 +141,13 @@ If YAML is not provided, a built-in price table is used (msat per 1M tokens): ### Quote → Execute flow (`openai.chat_completions.v1`) 1. Validate the QuoteRequest and check the model is allowed. -2. Receive and validate the input stream as OpenAI request JSON bytes (`request_json.model`, `request_json.messages`, `request_json.stream=false`). +2. Receive and validate the request stream as OpenAI request JSON bytes (`request_json.model`, `request_json.messages`, optional `request_json.stream`). 3. Determine `max_output_tokens` for quote-time estimation: - Start from `llm.max_output_tokens` (and optional per-model override). - If the request sets an output-token cap (`max_completion_tokens` / `max_tokens` / `max_output_tokens`), it must be `<=` the Provider max, and that value is used for estimation. 4. Estimate token usage via `UsageEstimator` (`approx.v1`: `ceil(len(bytes)/4)`). 5. Compute price in msat via `QuotePrice(model, estimate, cached=0, price_table)` and apply optional `pricing.in_flight_surge`, then embed it into TermsHash / invoice binding. -6. After payment settles, execute the passthrough request in the backend, stream the result (`lcp_stream_*`), and finalize with `lcp_result`. +6. After payment settles, execute the passthrough request in the backend, stream the response (`lcp_stream_*`), and finalize with `lcp_complete`. ## Backend notes diff --git a/docs/go-lcpd/docs/devnet-ja.md b/docs/go-lcpd/docs/devnet-ja.md index 2c29773..3567852 100644 --- a/docs/go-lcpd/docs/devnet-ja.md +++ b/docs/go-lcpd/docs/devnet-ja.md @@ -9,7 +9,7 @@ 手順が失敗したら、`./.data/devnet/` 配下のログ/状態を確認してください。 必要に応じて `LCPD_LOG_LEVEL=debug` で再実行してください。 -このドキュメントでは、2 つの役割で最小の Quote → Pay → Result を実行します: +このドキュメントでは、2 つの役割で最小の Quote → Pay → Stream を実行します: - Alice: Provider。Provider モードは Alice のみ有効化します。外部 API を避けるため `LCPD_BACKEND=deterministic` を使います。 - Bob: Requester。 @@ -125,13 +125,13 @@ PAY_REQ="$(./scripts/devnet lncli alice addinvoice --amt 1000 | jq -r .payment_r ./scripts/devnet lncli bob payinvoice "$PAY_REQ" ``` -## go-lcpd を試す(カスタムメッセージ / Quote → Pay → Result) +## go-lcpd を試す(カスタムメッセージ / Quote → Pay → Stream) 2 つの `lnd` ノードがピアとして接続できたら、両側で `go-lcpd` を起動します。 これにより BOLT #1 のカスタムメッセージ上で `lcp_manifest` が交換されます。 `ListLCPPeers` で観測できます。 -この手順では Alice を Provider として起動し、外部依存なしで `lcp_result` を返します。 +この手順では Alice を Provider として起動し、外部依存なしで `lcp_complete` を返します。 `LCPD_BACKEND=deterministic` を使います。 Provider 設定は YAML 優先(`LCPD_PROVIDER_CONFIG_PATH`)です。 @@ -193,9 +193,11 @@ cd go-lcpd ./bin/lcpdctl lcpd list-lcp-peers -s 127.0.0.1:23052 -o prettyjson ``` -`peers[0].remoteManifest.supportedTasks[].openaiChatCompletionsV1.model` に `gpt-5.2` が見えれば、Provider が model を広告できています。 +`peers[0].remoteManifest.supportedMethods[].method` に `openai.chat_completions.v1` が見えれば、Provider が method を広告できています。 -### 4) Bob から Alice にジョブを 1 つ送る(Quote → Pay → Result) +注: LCP v0.3 の manifest は model を広告しません。 + +### 4) Bob から Alice にコールを 1 つ送る(Quote → Pay → Stream) ```sh cd go-lcpd diff --git a/docs/go-lcpd/docs/devnet.md b/docs/go-lcpd/docs/devnet.md index 8f14e34..ff5a924 100644 --- a/docs/go-lcpd/docs/devnet.md +++ b/docs/go-lcpd/docs/devnet.md @@ -131,7 +131,7 @@ Once the two `lnd` nodes are connected as peers, start `go-lcpd` on both sides. This triggers `lcp_manifest` exchange over BOLT #1 custom messages. You can observe it via `ListLCPPeers`. -In this walkthrough, Alice runs as a Provider, streams the result, and sends `lcp_result` without external dependencies. +In this walkthrough, Alice runs as a Provider, streams the response, and sends `lcp_complete` without external dependencies. It uses `LCPD_BACKEND=deterministic`. Provider configuration is YAML-first (`LCPD_PROVIDER_CONFIG_PATH`). @@ -193,9 +193,11 @@ cd go-lcpd ./bin/lcpdctl lcpd list-lcp-peers -s 127.0.0.1:23052 -o prettyjson ``` -If you can see `gpt-5.2` under `peers[0].remoteManifest.supportedTasks[].openaiChatCompletionsV1.model`, it means the Provider successfully advertised the model. +If you can see `openai.chat_completions.v1` under `peers[0].remoteManifest.supportedMethods[].method`, it means the Provider successfully advertised the method. -### 4) Send one job from Bob to Alice (Quote → Pay → Stream) +Note: LCP v0.3 manifests do not advertise models. + +### 4) Send one call from Bob to Alice (Quote → Pay → Stream) ```sh cd go-lcpd diff --git a/docs/go-lcpd/docs/logging-ja.md b/docs/go-lcpd/docs/logging-ja.md index 1abb5ad..69e0dd3 100644 --- a/docs/go-lcpd/docs/logging-ja.md +++ b/docs/go-lcpd/docs/logging-ja.md @@ -1,12 +1,12 @@ # ログとプライバシー(lcpd-grpcd / go-lcpd) このプロジェクトではログを **センシティブ** と扱います。 -ログは「何が起きたか(quote → pay → result)」を追えるようにしつつ、ユーザーの生データ(プロンプト等)を永続化しないためのものです。 +ログは「何が起きたか(call → quote → pay → complete)」を追えるようにしつつ、ユーザーの生データ(プロンプト等)を永続化しないためのものです。 ## 絶対にログに残してはいけないもの(MUST NOT) -- 生のリクエスト JSON(`openai_chat_completions_v1.request_json` / wire の `input` stream bytes) -- 生のモデル出力(result stream bytes / gRPC `Result.result`) +- 生のリクエスト bytes(wire の request stream bytes / gRPC `CallSpec.request_bytes`) +- 生のモデル出力(wire の response stream bytes / gRPC `Complete.response_bytes`) - 秘密情報(API key / macaroon / access token など) - BOLT11 の `payment_request`(invoice 文字列) - Lightning カスタムメッセージの生 payload や、gRPC のリクエスト/レスポンス全体ダンプ @@ -15,23 +15,23 @@ ログは **メタデータ** に寄せます: -- 相関: `job_id`, `peer_id` / `peer_pub_key` -- タスク情報: `task_kind`, `model`, `input_bytes` +- 相関: `call_id`, `peer_id` / `peer_pub_key` +- 呼び出し情報: `method`, `model`, `request_bytes` - 見積もり/支払い: `price_msat`, `quote_expiry_unix` - 時間: `quote_ms`, `pay_ms`, `wait_ms`, `execute_ms`, `total_ms` -- 出力メタデータ: `output_bytes`, `content_type`, `usage_*`(可能な場合の token unit) +- 出力メタデータ: `response_bytes`, `content_type`, `usage_*`(可能な場合の token unit) ## ログレベル `LCPD_LOG_LEVEL` で出力を制御します(`debug` / `info` / `warn` / `error`)。 - `error`: サービスとして致命的・継続不能な失敗。 -- `warn`: ジョブ単位の失敗や異常(ただしプロンプト/出力はログに残さない)。 -- `info`: 計測と相関ができるように、ライフサイクルの要点を記録(quote 発行/受領、result 受領、job 完了など)。 +- `warn`: call 単位の失敗や異常(ただしプロンプト/出力はログに残さない)。 +- `info`: 計測と相関ができるように、ライフサイクルの要点を記録(quote 発行/受領、response 受領、call 完了など)。 - `debug`: 診断向けの追加情報(drop/resend/replay など)。それでも秘密は残さない。 ## 運用上の注意 -- 生データを残さなくても、ログには **メタデータ**(peer id / job id / 価格 / 時間など)が残ります。保存先・保持期間はセキュリティ上の判断です。 +- 生データを残さなくても、ログには **メタデータ**(peer id / call id / 価格 / 時間など)が残ります。保存先・保持期間はセキュリティ上の判断です。 - ログをディスクに保存する場合は、権限の制限とローテーションを推奨します。 [バックグラウンド実行とログ](/go-lcpd/docs/background-ja) も参照してください。 diff --git a/docs/go-lcpd/docs/logging.md b/docs/go-lcpd/docs/logging.md index ae8eb28..afa8e10 100644 --- a/docs/go-lcpd/docs/logging.md +++ b/docs/go-lcpd/docs/logging.md @@ -1,12 +1,12 @@ # Logging & privacy (lcpd-grpcd / go-lcpd) -This project treats logs as **sensitive**. Logs are meant to let you reconstruct what happened (quote → pay → result) +This project treats logs as **sensitive**. Logs are meant to let you reconstruct what happened (call → quote → pay → complete) without ever persisting raw user content. ## Hard rules (MUST NOT) -- MUST NOT log raw request JSON (`openai_chat_completions_v1.request_json` / wire `input` stream bytes). -- MUST NOT log raw model outputs (raw result stream bytes / gRPC `Result.result`). +- MUST NOT log raw request bytes (wire request stream bytes / gRPC `CallSpec.request_bytes`). +- MUST NOT log raw model outputs (wire response stream bytes / gRPC `Complete.response_bytes`). - MUST NOT log secrets: API keys, macaroons, access tokens. - MUST NOT log BOLT11 `payment_request` strings (invoices). - MUST NOT log raw Lightning custom-message payloads or full gRPC request/response objects. @@ -15,24 +15,24 @@ without ever persisting raw user content. The code favors logging **metadata** only: -- Correlation: `job_id`, `peer_id` / `peer_pub_key` -- Task metadata: `task_kind`, `model`, `input_bytes` +- Correlation: `call_id`, `peer_id` / `peer_pub_key` +- Call metadata: `method`, `model`, `request_bytes` - Quote/payment: `price_msat`, `quote_expiry_unix` - Timing: `quote_ms`, `pay_ms`, `wait_ms`, `execute_ms`, `total_ms` -- Output metadata: `output_bytes`, `content_type`, `usage_*` (token units when available) +- Output metadata: `response_bytes`, `content_type`, `usage_*` (token units when available) ## Log levels `LCPD_LOG_LEVEL` controls verbosity (`debug`, `info`, `warn`, `error`). - `error`: service-level failures (unexpected / cannot proceed). -- `warn`: per-job failures or anomalous events (still no prompt/output). -- `info`: lifecycle summaries that allow measurement and correlation (quote issued/received, result received, job completed). +- `warn`: per-call failures or anomalous events (still no prompt/output). +- `info`: lifecycle summaries that allow measurement and correlation (quote issued/received, response received, call completed). - `debug`: additional details for diagnosis (drops, resends, replay handling), still no secrets. ## Operational notes -- Even with content redaction, logs still contain **metadata** (peer ids, job ids, prices, timings). Treat log storage and +- Even with content redaction, logs still contain **metadata** (peer ids, call ids, prices, timings). Treat log storage and retention as a security decision. - If you write logs to disk, use restrictive permissions and rotation. See [Background run + logging](/go-lcpd/docs/background). diff --git a/docs/go-lcpd/docs/quickstart-mainnet-ja.md b/docs/go-lcpd/docs/quickstart-mainnet-ja.md index e95d84c..a4f08e3 100644 --- a/docs/go-lcpd/docs/quickstart-mainnet-ja.md +++ b/docs/go-lcpd/docs/quickstart-mainnet-ja.md @@ -99,7 +99,7 @@ export LCPD_LND_ADMIN_MACAROON_PATH="$HOME/.lnd/data/chain/bitcoin/mainnet/admin ./bin/lcpd-grpcd -grpc_addr=127.0.0.1:50051 ``` -## 6) Provider が対応する model を確認(任意) +## 6) Provider が対応する method を確認(任意) 別ターミナルで: @@ -108,14 +108,15 @@ cd go-lcpd ./bin/lcpdctl lcpd list-lcp-peers -s 127.0.0.1:50051 -o prettyjson ``` -`peers[].remoteManifest.supportedTasks[].openaiChatCompletionsV1.model` に Provider の model(広告されている場合)が入っています。 +`peers[].remoteManifest.supportedMethods[].method` に Provider の method(広告されている場合)が入っています。 注意: -- `supportedTasks` は `lcp_manifest` の任意フィールドです。Provider が `llm.models` を設定していない(または Provider モードが disabled)場合、`supported_tasks` を広告しません。 -- `-o prettyjson` は空フィールドを省略するため、仕様上フィールドが absent であっても `supportedTasks` が見えない場合があります。 +- `supportedMethods` は Provider が対応する LCP method を列挙します(例: `openai.chat_completions.v1`)。 +- LCP v0.3 の manifest は model を広告しません。送信する `model` は Provider のドキュメント/ポリシー(または自分の allowlist)で判断してください。 +- `-o prettyjson` は空フィールドを省略するため、peer が descriptor を広告していない場合は `supportedMethods` が表示されないことがあります。 -## 7) ジョブを 1 回実行(Quote → Pay → Result) +## 7) コールを 1 回実行(Quote → Pay → Stream) 別ターミナルで: @@ -153,5 +154,5 @@ PROVIDER_PUBKEY="03737b4a2e44b45f786a18e43c3cf462ab97891e9f8992a0d493394691ac0db ## トラブルシューティング - `peer is not ready for lcp`: `lnd` が Provider に接続できているか(`lncli listpeers`)確認し、`lcpdctl list-lcp-peers` でも見えることを確認してください。 -- `lcp_error code=2: unsupported model ...`: `-model` を Provider がサポートする値にしてください(手順 6 を参照)。 +- `unsupported model` / `unsupported method`: `-model` / method を Provider がサポートする値にしてください(Provider 側でポリシー検証されます)。 - `payment failed`: チャネル / ルート / 流動性が不足している可能性があります。`lncli walletbalance` と `lncli listchannels` を確認してください。 diff --git a/docs/go-lcpd/docs/quickstart-mainnet.md b/docs/go-lcpd/docs/quickstart-mainnet.md index 3f88dc5..7c5216d 100644 --- a/docs/go-lcpd/docs/quickstart-mainnet.md +++ b/docs/go-lcpd/docs/quickstart-mainnet.md @@ -99,7 +99,7 @@ export LCPD_LND_ADMIN_MACAROON_PATH="$HOME/.lnd/data/chain/bitcoin/mainnet/admin ./bin/lcpd-grpcd -grpc_addr=127.0.0.1:50051 ``` -## 6) Inspect Provider supported models (optional) +## 6) Inspect Provider supported methods (optional) In another terminal: @@ -108,12 +108,13 @@ cd go-lcpd ./bin/lcpdctl lcpd list-lcp-peers -s 127.0.0.1:50051 -o prettyjson ``` -`peers[].remoteManifest.supportedTasks[].openaiChatCompletionsV1.model` contains the Provider models (if advertised). +`peers[].remoteManifest.supportedMethods[].method` contains the Provider methods (if advertised). Notes: -- `supportedTasks` is an optional field in `lcp_manifest`. If the Provider does not configure `llm.models` (or Provider mode is disabled), it will not advertise `supported_tasks`. -- `-o prettyjson` omits empty fields, so it may not show `supportedTasks` even if the field is absent by design. +- `supportedMethods` lists the Provider's supported LCP methods (for example `openai.chat_completions.v1`). +- LCP v0.3 manifests do **not** advertise available models. Use Provider documentation/policy (or your own allowlist) to decide which `model` values to send. +- `-o prettyjson` omits empty fields, so it may not show `supportedMethods` if the peer doesn't advertise any descriptors. ## 7) Run one job (Quote → Pay → Stream) @@ -153,5 +154,5 @@ PROVIDER_PUBKEY="03737b4a2e44b45f786a18e43c3cf462ab97891e9f8992a0d493394691ac0db ## Troubleshooting - `peer is not ready for lcp`: check that `lnd` is connected to the Provider (`lncli listpeers`), and confirm `lcpdctl list-lcp-peers` sees it. -- `lcp_error code=2: unsupported model ...`: set `-model` to a value the Provider supports (see step 6). +- `unsupported model` / `unsupported method`: set `-model` / method to a value the Provider supports (providers enforce their own policy). - `payment failed`: you may have no channel / no route / insufficient liquidity. Check `lncli walletbalance` and `lncli listchannels`. diff --git a/docs/go-lcpd/spec-ja.md b/docs/go-lcpd/spec-ja.md index dda6cd1..1a75720 100644 --- a/docs/go-lcpd/spec-ja.md +++ b/docs/go-lcpd/spec-ja.md @@ -6,20 +6,24 @@ ## これは何? `go-lcpd` は LCP(Lightning Compute Protocol)の参照実装(daemon)です。 -Lightning の BOLT #1 カスタムメッセージを `lnd` のピア接続上でやり取りし、Requester 側のフロー(quote → pay → result)をローカル gRPC API で操作できるようにします。 +Lightning の BOLT #1 カスタムメッセージを `lnd` のピア接続上でやり取りし、 +Requester 側のフロー(call → quote → pay → stream(response) → complete)をローカル gRPC API で操作できるようにします。 + +同じバイナリは Provider モードとしても動作でき、`lcp_call` と request stream を受け取り、見積もり(`lcp_quote`)と +支払い確認のあとに response stream と `lcp_complete` を返します。 ## 主要ポイント(抜粋) - 単一の真実(SSOT): - gRPC API: `go-lcpd/proto/lcpd/v1/lcpd.proto` - - wire protocol: `protocol/protocol.md`(LCP v0.1, `protocol_version=1`) + - wire protocol: `protocol/protocol.md`(LCP v0.3, `protocol_version=3`) - transport: - 独自 TCP/UDP transport は実装しません(peer messaging は `lnd` に委譲) - 互換性: - unknown odd message を無視、unknown even を切断(BOLT #1 parity) - unknown TLV は無視(forward compatibility) - セキュリティ: - - 支払い前に invoice の `description_hash == terms_hash` 等を検証 + - 支払い前に invoice の `description_hash == terms_hash` 等を検証(invoice swapping 防止) - quote expiry / envelope expiry の制限と重複排除で replay/DoS を軽減 - ログには生のプロンプト/出力や秘密情報(API key / macaroon / invoice 文字列など)を残さない - テスト: diff --git a/docs/go-lcpd/spec.md b/docs/go-lcpd/spec.md index c69406f..678599e 100644 --- a/docs/go-lcpd/spec.md +++ b/docs/go-lcpd/spec.md @@ -1,4 +1,4 @@ -# spec.md — go-lcpd (Lightning Compute Protocol Daemon) +# go-lcpd spec (WYSIWID) This document is the implementation specification (single source of truth) for `go-lcpd/` in this repository. It is written in WYSIWID format (Concepts + Synchronizations). @@ -6,630 +6,315 @@ It is written in WYSIWID format (Concepts + Synchronizations). Sources of truth: - gRPC API: `go-lcpd/proto/lcpd/v1/lcpd.proto` -- LCP wire protocol: `protocol/protocol.md` (LCP v0.2, `protocol_version=2`) +- LCP wire protocol: `protocol/protocol.md` (LCP v0.3, `protocol_version=3`) -go-lcpd exchanges LCP messages (TLV streams) with peers over `lnd` BOLT #1 custom messages as the transport. -It exposes a gRPC API (`lcpd.v1.LCPDService`) for local clients to drive the requester-side LCP flow (quote → pay → stream(result) → complete). -The same process can also run as a provider-side handler (when configured) to process inbound `lcp_quote_request`, receive the input stream, return an invoice, and then stream the result and send `lcp_result`. +`go-lcpd` exchanges LCP messages (TLV streams) with peers over `lnd` BOLT #1 custom messages as the transport. +It exposes a gRPC API (`lcpd.v1.LCPDService`) for local clients to drive the requester-side LCP flow: + +**call → quote → pay → stream(response) → complete** + +The same process can also run as a provider-side handler (when configured) to process inbound `lcp_call`, receive the +request stream, return a quote + invoice (`lcp_quote`), and then stream the response and send `lcp_complete`. It does not implement its own P2P (TCP) transport. Protobuf is managed via `buf`. The goal is to keep the following integration tests reproducible within this project: - gRPC: smoke check `GetLocalInfo` / `ListLCPPeers` (no external processes) - Lightning (custom messages, regtest): two nodes exchange `lcp_manifest` and `ListLCPPeers` can enumerate LCP-capable peers (opt-in) -- Lightning (LCP provider, regtest): complete inbound `lcp_quote_request` → input stream → invoice → result stream → `lcp_result` over custom messages (opt-in) -- Lightning (LCP requester, regtest): receive result stream + `lcp_result` via `RequestQuote` → `AcceptAndExecute` (opt-in) +- Lightning (LCP provider, regtest): complete inbound `lcp_call` → request stream → invoice → response stream → `lcp_complete` over custom messages (opt-in) +- Lightning (LCP requester, regtest): receive response stream + `lcp_complete` via `RequestQuote` → `AcceptAndExecute` (opt-in) ## Security & Architectural Constraints These constraints are system invariants. -- Do not modify BOLTs: Do not propose or require changes to Lightning BOLT specs (L2). Rationale: preserve interoperability. -- Do not implement a custom P2P (TCP) transport: go-lcpd MUST NOT implement peer transport over TCP/UDP. Delegate peer messaging to `lnd` peer connections. Rationale: avoid re-implementation and reduce operational risk. -- LCP message types MUST match `protocol/protocol.md`: Type numbers for `lcp_manifest`, etc. MUST follow the odd assignments in `protocol/protocol.md`. Rationale: coexistence with non-LCP peers. -- BOLT #1 unknown message rule: Ignore unknown odd messages. Disconnect on unknown even messages via `lnrpc.Lightning.DisconnectPeer`. Rationale: match BOLT #1 parity behavior. - -- Enforce `protocol_version=2`: LCP v0.2 uses `protocol_version=2` (encoding: `major*100 + minor`). Implementations MUST send and accept only version 2. Rationale: interoperability. -- Manifest-gated job messages: go-lcpd MUST NOT send any job-scope messages until it has (1) sent `lcp_manifest` and (2) received the peer's `lcp_manifest`. Rationale: required by `protocol/protocol.md` and needed to learn peer limits (`max_payload_bytes`, etc.). -- Preserve TLV stream canonical form: Encode TLVs as `bigsize(type)` → `bigsize(length)` → `value`. Sort by type. Do not duplicate types. Rationale: byte-exact `terms_hash` and forward compatibility. -- Do not disconnect on unknown TLVs: Unknown TLVs MUST NOT cause disconnection. Continue decoding. Rationale: forward compatibility. -- `openai_chat_completions_v1_params_tlvs` is strict: Providers MUST reject unknown param types with `lcp_error(code=unsupported_params)`. Rationale: fixed interpretation of typed params. - -- Always include job-scope envelope TLVs: All LCP messages except `lcp_manifest` MUST include `job_id`, `msg_id`, and `expiry` per `protocol/protocol.md`. Rationale: replay safety and idempotency. -- Deterministic chunk `msg_id`: For `lcp_stream_chunk`, `msg_id` MUST be `SHA256(stream_id || u32be(seq))` per `protocol/protocol.md`. Rationale: bounded replay state for large streams. -- Ignore expired messages (Provider inbound): Providers MUST ignore inbound job-scope messages where `expiry < now`. Rationale: replay resistance. -- Clamp envelope expiry window (Provider inbound): Providers MUST use `effective_expiry = min(expiry, now + MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS)` per `protocol/protocol.md`. Rationale: prevent state-holding DoS via far-future `expiry`. -- Ignore duplicate messages (Provider inbound): Providers MUST de-duplicate inbound job-scope messages by (`peer_pubkey`, `job_id`, `msg_id`) until `effective_expiry`. Rationale: idempotency. - * For `lcp_stream_chunk`, go-lcpd MUST treat `seq < expected_seq` as a duplicate and MUST ignore it (without storing per-chunk replay entries). - -- LCP v0.2 `terms_hash`: Compute `terms_hash` byte-exactly as defined in `protocol/protocol.md` (including canonicalization of `params` and input stream metadata). Rationale: invoice binding depends on byte-exactness. -- Provider execution rule: Providers MUST NOT execute or deliver results for `job_id` before invoice settlement. Rationale: `protocol/protocol.md` execution rule. -- Requester invoice checks: Before paying, verify `description_hash == terms_hash`, `payee_pubkey == peer_id`, `invoice_amount_msat == price_msat`, and `invoice_expiry_unix <= quote_expiry` (with `ALLOWED_CLOCK_SKEW_SECONDS`). Rationale: invoice swapping defense and amount/expiry integrity. -- Do not pay past quote expiry: Reject Terms where `quote_expiry <= now`. Rationale: avoid unnecessary payments. - -- Default constants: Use these defaults unless configured: - - `MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS = 600` - - `ALLOWED_CLOCK_SKEW_SECONDS = 5` - - `DEFAULT_MAX_STORE_ENTRIES = 1024` (for `ReplayStore`, `ProviderJobStore`, and `RequesterJobStore`) - - `DEFAULT_MAX_PAYLOAD_BYTES = 16384` (for `lcp_manifest.max_payload_bytes`) - - `DEFAULT_MAX_STREAM_BYTES = 4_194_304` (for `lcp_manifest.max_stream_bytes`, 4 MiB) - - `DEFAULT_MAX_JOB_BYTES = 8_388_608` (for `lcp_manifest.max_job_bytes`, 8 MiB) -- Defaults override env vars: - - `LCP_MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS` - - `LCP_ALLOWED_CLOCK_SKEW_SECONDS` - - `LCP_DEFAULT_MAX_STORE_ENTRIES` - - `LCP_INVOICE_EXPIRY_SLACK_SECONDS` (optional; defaults to `LCP_ALLOWED_CLOCK_SKEW_SECONDS`) - -- Payload/stream limit checks: Do not send any LCP message whose payload exceeds `remote_manifest.max_payload_bytes`. In particular, input/result streaming MUST chunk into `lcp_stream_chunk` messages that fit under the peer limit. gRPC returns `RESOURCE_EXHAUSTED`. Provider returns `lcp_error(code=payload_too_large)`. Rationale: DoS resistance and compatibility. -- Enforce declared stream limits: go-lcpd MUST enforce `max_stream_bytes` and `max_job_bytes` from the local manifest for inbound streams. Rationale: protocol-defined DoS bounds. - -- Prefer regtest for Lightning integration: Development/integration tests MUST use regtest. They MUST NOT depend on mainnet/testnet. Rationale: safety and reproducibility. -- Do not implicitly require external processes: `go test ./...` MUST NOT require `bitcoind` or `lnd`. Lightning integration tests MUST be opt-in. Rationale: developer experience and safety. -- Integration tests MUST NOT depend on external networks: CI/dev MUST not require external APIs. Keep smoke tests opt-in. Rationale: reproducibility. - -- Do not log secrets: Logs MUST NOT contain API keys, access tokens, macaroons, BOLT11 invoices (`payment_request`), raw prompts, or raw model outputs at any log level. Rationale: logs leak. -- Primary interface is gRPC: go-lcpd MUST be operable via gRPC. Do not make CLI the primary interface. Rationale: stabilize an API shape for integrations. -- Do not provide `cmd/`: go-lcpd MUST NOT provide CLI entry points under `go-lcpd/cmd/*`. Rationale: keep operations centered on gRPC. -- Manage protobuf with buf: API protobufs MUST be generated via `buf`. Rationale: change detection, compatibility, reproducible generation. -- DI and lifecycle: Go components MUST be wired with `go.uber.org/fx`. Rationale: explicit composition and testability. -- Do not assume Nix: Core workflows SHOULD work with a standard Go toolchain and externally installed binaries. `flake.nix` MAY exist as an optional pinned devShell. Rationale: onboarding. +- Do not modify BOLTs: `go-lcpd` MUST NOT propose or require changes to Lightning BOLT specs (L2). Rationale: preserve interoperability. +- Do not implement a custom P2P (TCP) transport: `go-lcpd` MUST NOT implement peer transport over TCP/UDP. Delegate peer messaging to `lnd` peer connections. Rationale: avoid re-implementation and reduce operational risk. +- LCP message types MUST match `protocol/protocol.md`: type numbers for `lcp_manifest`, etc. MUST follow the odd assignments in `protocol/protocol.md`. Rationale: coexistence with non-LCP peers. +- BOLT #1 unknown message rule: ignore unknown odd messages. Disconnect on unknown even messages via `lnrpc.Lightning.DisconnectPeer`. Rationale: match BOLT #1 parity behavior. + +- Enforce LCP v0.3 wire rules: `protocol_version=3` MUST be used for all LCP messages. Implementations MUST send and accept only version 3. Rationale: interoperability. +- Manifest-gated call messages: `go-lcpd` MUST NOT send any call-scope messages until it has (1) sent `lcp_manifest` and (2) received the peer's `lcp_manifest`. Rationale: required by `protocol/protocol.md` and needed to learn peer limits (`max_payload_bytes`, etc.). + +- Preserve TLV stream canonical form: encode TLVs as `bigsize(type)` → `bigsize(length)` → `value`. Sort by type. Do not duplicate types. Rationale: byte-exact `terms_hash` and forward compatibility. +- Do not disconnect on unknown TLVs: unknown TLVs MUST NOT cause disconnection. Continue decoding. Rationale: forward compatibility. + +- Always include call-scope envelope TLVs: all LCP messages except `lcp_manifest` MUST include `call_id`, `msg_id`, and `expiry` per `protocol/protocol.md`. Rationale: replay safety and idempotency. +- Deterministic chunk `msg_id`: for `lcp_stream_chunk`, `msg_id` MUST be `SHA256(stream_id || u32be(seq))` per `protocol/protocol.md`. Rationale: bounded replay state for large streams. +- Ignore expired messages: receivers MUST ignore call-scope messages where `expiry < now`. Rationale: replay resistance. +- Clamp envelope expiry window: receivers MUST use `effective_expiry = min(expiry, now + MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS)` per `protocol/protocol.md`. Rationale: prevent state-holding DoS via far-future `expiry`. +- Ignore duplicate messages: receivers MUST de-duplicate by (`call_id`, `msg_id`) until `effective_expiry`. Rationale: idempotency. + - For `lcp_stream_chunk`, implementations MUST treat `seq < expected_seq` as a duplicate and MUST ignore it (without storing per-chunk replay entries). + +- LCP v0.3 `terms_hash`: compute `terms_hash` byte-exactly as defined in `protocol/protocol.md` (including canonicalization of method `params` and request/response stream metadata). Rationale: invoice binding depends on byte-exactness. +- Provider execution rule: Providers MUST NOT execute or deliver responses for `call_id` before invoice settlement. Rationale: `protocol/protocol.md` execution rule. +- Requester invoice checks: before paying, verify at least: + - `description_hash == terms_hash` + - invoice payee pubkey matches the Provider peer pubkey + - invoice amount matches `price_msat` (amount-less invoices are rejected) + - invoice expiry does not exceed `quote_expiry` (allow small clock skew) + +- `openai.*` params TLVs are strict: Providers MUST reject unknown param types (and invalid TLV encoding). Rationale: fixed interpretation of typed params. ## Concepts -### ProtocolCompatibility -Purpose: Provide LCP-compatible core primitives that remain stable across protocol versions. -Domain Model: -* `ProtocolVersion`: uint16 (const 2) -* `JobID`: 32 bytes -* `TermsCore`: `protocol_version`, `job_id`, `price_msat`, `quote_expiry_unix_seconds`, `task_kind`, `input_hash32`, `input_len`, `input_content_type`, `input_content_encoding`, `params_hash32` -* `TermsHash`: 32 bytes (SHA256) -Actions: -* `new_job_id() -> job_id` -* `compute_params_hash(params_bytes_opt) -> params_hash32 | error` -* `compute_terms_hash(terms_core) -> terms_hash` -Operational Principle: -* `terms_hash` MUST be computed exactly as `protocol/protocol.md` defines (canonical TLV encoding + SHA256). -* `terms_hash` MUST be computed only from the canonical `terms_tlvs` fields in `protocol/protocol.md` (do not include `payment_request`, etc.). -* `compute_params_hash` MUST follow `protocol/protocol.md`'s `canonical_tlv(params)` rules: - * If `params` is absent: `params_hash = SHA256(empty)`. - * If `params` is present: decode TLVs (reject invalid), re-encode canonically (ascending type, no duplicates), then `SHA256(canonical_bytes)`. -* The gRPC API represents `quote_expiry` as `google.protobuf.Timestamp`. - `terms_hash` uses only `quote_expiry.seconds` (Unix epoch seconds). Nanos are always 0. - -### LCPTasks -Purpose: Provide strict task definitions from `go-lcpd/proto/lcpd/v1/lcpd.proto` and pin down the mapping to the wire format. -Domain Model: -* `LCPTaskKind`: `LCP_TASK_KIND_UNSPECIFIED` | `LCP_TASK_KIND_OPENAI_CHAT_COMPLETIONS_V1` -* `Task`: `oneof spec` (currently only `OpenAIChatCompletionsV1TaskSpec`) -* `OpenAIChatCompletionsV1TaskSpec`: request_json([]byte), params(OpenAIChatCompletionsV1Params) -* `OpenAIChatCompletionsV1Params`: model(string) -Actions: -* `validate_task(task) -> ok | error` -* `to_wire_quote_request_task(task) -> task_kind(string), params_bytes(*[]byte) | error` -* `to_wire_input_stream(task) -> decoded_bytes([]byte), content_type(string), content_encoding(string) | error` -Operational Principle: -* This implementation supports only `openai.chat_completions.v1` and MUST reject everything else. -* `OpenAIChatCompletionsV1TaskSpec.request_json` MUST be non-empty valid JSON. -* `OpenAIChatCompletionsV1Params.model` MUST NOT be empty. -* `request_json.model` MUST be present and MUST match `params.model`. -* `request_json.messages` MUST be present and MUST be non-empty. -* `request_json.stream` MUST be omitted or false (streaming is not supported yet). -* Wire mapping (`protocol/protocol.md`): - * `task_kind = "openai.chat_completions.v1"` - * input stream decoded bytes = raw `request_json` bytes (UTF-8 JSON) - * input stream metadata: `content_type = "application/json; charset=utf-8"`, `content_encoding = "identity"` - * `params = openai_chat_completions_v1_params_tlvs` (must include at least `model`) - -### LCPWire -Purpose: Encode/decode the LCP wire messages (TLV streams) defined in `protocol/protocol.md`. -Domain Model: -* `MessageType`: `lcp_manifest(42081)` / `lcp_quote_request(42083)` / `lcp_quote_response(42085)` / `lcp_result(42087)` / `lcp_stream_begin(42089)` / `lcp_stream_chunk(42091)` / `lcp_stream_end(42093)` / `lcp_cancel(42095)` / `lcp_error(42097)` -* `JobEnvelope`: protocol_version(uint16), job_id(32 bytes), msg_id(32 bytes), expiry(uint64) -* `Manifest`: protocol_version, max_payload_bytes(uint32), max_stream_bytes(uint64), max_job_bytes(uint64), max_inflight_jobs(*uint16), supported_tasks([]TaskTemplate) -* `TaskTemplate`: task_kind(string), params_bytes(*[]byte) -* `QuoteRequest`: envelope, task_kind(string), params_bytes(*[]byte) -* `QuoteResponse`: envelope, price_msat(uint64), quote_expiry(uint64), terms_hash(32 bytes), payment_request(string) -* `StreamKind`: `input(1)` | `result(2)` -* `StreamBegin`: envelope, stream_id(32 bytes), stream_kind(uint16), total_len(*uint64), sha256(*32 bytes), content_type(string), content_encoding(string) -* `StreamChunk`: envelope, stream_id(32 bytes), seq(uint32), data([]byte) -* `StreamEnd`: envelope, stream_id(32 bytes), total_len(uint64), sha256(32 bytes) -* `Result`: envelope, status(uint16), ok(*ResultOK), message(*string) -* `ResultOK`: result_stream_id(32 bytes), result_hash(32 bytes), result_len(uint64), result_content_type(string), result_content_encoding(string) -* `Cancel`: envelope, reason(*string) -* `Error`: envelope, code(uint16), message(*string) -* `OpenAIChatCompletionsV1Params`: model(string), unknown(map[uint64][]byte) +### LCPWireCodec + +Purpose: Encode/decode LCP v0.3 custom message payloads (TLV streams) used on the wire. + +Domain Model (concept-local): + +- `Manifest`: protocol_version, max_payload_bytes, max_stream_bytes, max_call_bytes, max_inflight_calls?, supported_methods[] +- `CallEnvelope`: protocol_version, call_id(32 bytes), msg_id(32 bytes), expiry(tu64) +- `Call`: envelope, method(string), params_bytes?(bytes), params_content_type?(string) +- `Quote`: envelope, price_msat, quote_expiry, terms_hash(32 bytes), payment_request(string), response_content_type?, response_content_encoding? +- `Complete`: envelope, status(ok|failed|cancelled), message?, response_stream_id?, response_hash?, response_len?, response_content_type?, response_content_encoding? +- `StreamBegin`: envelope, stream_id(32 bytes), stream_kind(request|response), content_type, content_encoding, total_len?, sha256? +- `StreamChunk`: envelope, stream_id, seq(u32), data(bytes) +- `StreamEnd`: envelope, stream_id, total_len(tu64), sha256(32 bytes) +- `Cancel`: envelope, reason? +- `Error`: envelope, code(u16), message? + Actions: -* `encode_manifest(m) -> payload_bytes | error` -* `decode_manifest(payload_bytes) -> manifest | error` -* `encode_quote_request(q) -> payload_bytes | error` -* `decode_quote_request(payload_bytes) -> quote_request | error` -* `encode_quote_response(q) -> payload_bytes | error` -* `decode_quote_response(payload_bytes) -> quote_response | error` -* `encode_stream_begin(b) -> payload_bytes | error` -* `decode_stream_begin(payload_bytes) -> stream_begin | error` -* `encode_stream_chunk(c) -> payload_bytes, msg_id | error` -* `decode_stream_chunk(payload_bytes) -> stream_chunk | error` -* `encode_stream_end(e) -> payload_bytes | error` -* `decode_stream_end(payload_bytes) -> stream_end | error` -* `encode_result(r) -> payload_bytes | error` (encodes `lcp_result`) -* `decode_result(payload_bytes) -> result | error` (decodes `lcp_result`) -* `encode_cancel(c) -> payload_bytes | error` -* `decode_cancel(payload_bytes) -> cancel | error` -* `encode_error(e) -> payload_bytes | error` -* `decode_error(payload_bytes) -> lcp_error | error` -* `encode_openai_chat_completions_v1_params(p) -> params_bytes | error` -* `decode_openai_chat_completions_v1_params(params_bytes) -> params | error` + +- `encode_* (message) -> payload_bytes | error` +- `decode_* (payload_bytes) -> message | error` + Operational Principle: -* Treat `lcp_manifest` as connection-scoped (do not include job envelope TLVs). -* Job-scope messages (everything except `lcp_manifest`) MUST include `JobEnvelope` (`protocol_version`, `job_id`, `msg_id`, `expiry`). -* `lcp_stream_chunk.msg_id` MUST be deterministic (`SHA256(stream_id || u32be(seq))`). `encode_stream_chunk` MUST compute it; `decode_stream_chunk` MUST verify it. -* `openai_chat_completions_v1_params_tlvs` MUST preserve unknown TLVs in `unknown` and MUST NOT fail decoding solely due to unknown TLVs (forward compatibility). + +- All payloads are TLV streams and MUST follow canonical TLV rules. +- Encoding/decoding MUST be round-trip safe for all known fields, and MUST ignore unknown TLVs. ### LCPMessageRouter + Purpose: Classify inbound custom messages into known LCP message types and apply the BOLT #1 parity rule for unknown message types. -Domain Model: -* `CustomMessage`: peer_pubkey, msg_type(uint16), payload_bytes -* `RouteDecision`: action(dispatch_manifest|dispatch_quote_request|dispatch_quote_response|dispatch_stream_begin|dispatch_stream_chunk|dispatch_stream_end|dispatch_result|dispatch_cancel|dispatch_error|ignore|disconnect), reason + +Domain Model (concept-local): + +- `CustomMessage`: peer_pubkey, msg_type(uint16), payload_bytes +- `RouteDecision`: action(dispatch_manifest|dispatch_call|dispatch_quote|dispatch_stream_begin|dispatch_stream_chunk|dispatch_stream_end|dispatch_complete|dispatch_cancel|dispatch_error|ignore|disconnect), reason + Actions: -* `route(custom_message) -> decision` + +- `route(custom_message) -> decision` + Operational Principle: -* LCP v0.2 message types follow the assignments in `protocol/protocol.md` (all odd). -* Unknown odd is ignored; unknown even triggers disconnect (BOLT #1 parity rule). -* `route` does not decode the payload (classification only). + +- LCP v0.3 message types follow the assignments in `protocol/protocol.md` (all odd). +- Unknown odd is ignored; unknown even triggers disconnect (BOLT #1 parity rule). +- `route` does not decode the payload (classification only). ### ReplayStore -Purpose: Provide idempotency (de-duplication) and replay safety for job-scope messages. -Domain Model: -* `ReplayKey`: peer_pubkey, job_id(32 bytes), msg_id(32 bytes) -* `ReplayEntry`: key, expiry(uint64) -Actions: -* `check_and_remember(key, effective_expiry, now) -> ok | duplicate | expired` -* `prune(now) -> void` -Operational Principle: -* Messages with `expiry < now` MUST be treated as `expired` and ignored (`protocol/protocol.md`). -* `duplicate` entries MUST be retained until `effective_expiry` (de-dup at least until `effective_expiry`). -* The store MUST be pruned by TTL and MUST have bounds (e.g., max entries) to cap memory usage. -* Default bounds MUST be `DEFAULT_MAX_STORE_ENTRIES = 1024`. - -### ProviderJobStore -Purpose: Hold Provider-side job state (input stream / quote / payment / execute / result streaming) to support idempotency and cancellation. -Domain Model: -* `JobKey`: peer_pubkey, job_id -* `JobState`: awaiting_input | waiting_payment | paid | executing | streaming_result | done | canceled | failed -* `InputStreamState`: stream_id(32 bytes), content_type(string), content_encoding(string), expected_seq(uint32), buf([]byte), total_len(uint64), sha256(32 bytes), validated(bool) -* `Job`: peer_pubkey, job_id, state, quote_request, input_stream(*InputStreamState), quote_expiry, params_hash32, input_hash32, input_len, input_content_type, input_content_encoding, terms_hash, payment_hash, invoice_add_index, quote_response, created_at -Actions: -* `get(job_key) -> job | not_found` -* `upsert(job) -> void` -* `update_state(job_key, state) -> void` -* `set_quote_response(job_key, quote_response) -> void` -* `set_payment_hash(job_key, payment_hash) -> void` -Operational Principle: -* Since `job_id` can collide across peers, jobs MUST be keyed by `peer_pubkey` as well. -* The store MUST have bounds (e.g., max entries) and MUST NOT retain pending state past `quote_expiry` (evict/GC). Rationale: prevent state-holding DoS via unbounded job creation. -* For jobs in `awaiting_input`, implementations SHOULD evict/GC state once `expiry` (from the job envelope) is past the retention window (`effective_expiry`). Rationale: prevent state-holding DoS via incomplete streams. -* Default bounds MUST be `DEFAULT_MAX_STORE_ENTRIES = 1024`. -### ComputeBackend -Purpose: Execute `openai.chat_completions.v1` on the Provider side and produce output bytes. -Domain Model: -* `Task`: task_kind(string), model(string), input_bytes([]byte), params_bytes([]byte) -* `ExecutionResult`: output_bytes([]byte), usage -Actions: -* `execute(ctx, task) -> execution_result | error` -Operational Principle: -* In integration tests, `execute` MUST be a deterministic stub/dummy implementation (connectivity and reproducibility come first). -* For `task_kind="openai.chat_completions.v1"`, `input_bytes` is treated as raw UTF-8 JSON request bytes for `POST /v1/chat/completions`. -* For `task_kind="openai.chat_completions.v1"`, `task.model` is the upstream model ID (from `request_json.model` / `params.model`). -* For `task_kind="openai.chat_completions.v1"`, `params_bytes` MUST be empty (request configuration is carried in `input_bytes`). -* `output_bytes` MUST be returned as raw OpenAI-compatible non-streaming response body bytes (JSON). - -### LLMExecutionPolicy -Purpose: Control `openai.chat_completions.v1` max output tokens for pricing, DoS resistance, and determinism. -Domain Model: -* `ExecutionPolicy`: max_output_tokens(uint32) -* `ExecutionPolicyProvider`: `policy() -> ExecutionPolicy` -Actions: -* `policy() -> ExecutionPolicy` -Operational Principle: -* `policy.max_output_tokens` MUST be > 0. -* If the request specifies an output-token cap (`max_completion_tokens` / `max_tokens` / `max_output_tokens`), the Provider MUST reject values that exceed `policy.max_output_tokens`. -* If the request does not specify an output-token cap, the Provider MUST use `policy.max_output_tokens` for quote-time estimation. - -### LLMUsageEstimator -Purpose: Estimate `openai.chat_completions.v1` token usage during quoting to drive pricing decisions. -Domain Model: -* `UsageEstimate`: input_tokens(uint64), max_output_tokens(uint64), total_tokens(uint64) -* `Estimation`: usage, resources([]ResourceEstimate), estimator_id(string) -* `ResourceEstimate`: name(string), amount(uint64) -Actions: -* `estimate(task, policy) -> estimation | error` -Operational Principle: -* Token estimation uses `approx.v1`: `input_tokens = ceil(len(input_bytes)/4)`, `max_output_tokens = policy.max_output_tokens`. -* `task_kind!="openai.chat_completions.v1"` MUST be rejected. - -### LLMPricing -Purpose: Compute `price_msat` from estimated token usage on the Provider side. -Domain Model: -* `PriceTableEntry`: model(string), input_msat_per_mtok(uint64), cached_input_msat_per_mtok(*uint64), output_msat_per_mtok(uint64) -* `PriceTable`: map[model]PriceTableEntry -* `PriceBreakdown`: input_tokens, cached_input_tokens, output_tokens, price_msat(uint64) -Actions: -* `quote_price(model, usage_estimate, cached_input_tokens, price_table) -> price_breakdown | error` -Operational Principle: -* Prices are computed from a table in units of "msat per 1M tokens", rounding up (ceil). -* Unknown models MUST be rejected. -* Overflows MAY be treated as equivalent to `rate_limited`. - -### LCPProvider -Purpose: Act as an LCP v0.2 Provider: process job-scope messages and complete quote → payment → stream(result) → `lcp_result`. -Domain Model: -* `ProviderConfig`: enabled(bool), quote_ttl_seconds(uint64), pricing(InFlightSurge), models(map[model]ModelConfig) -* `ProviderRuntime`: execution_policy(ExecutionPolicyProvider), usage_estimator(LLMUsageEstimator) -* `InFlightSurge`: threshold(uint32), per_job_bps(uint32), max_multiplier_bps(uint32) -* `ModelConfig`: max_output_tokens(*uint32), price(PriceTableEntry) -Actions: -* `handle_quote_request(peer_pubkey, quote_request, now) -> void` -* `handle_stream_begin(peer_pubkey, stream_begin, now) -> void` -* `handle_stream_chunk(peer_pubkey, stream_chunk, now) -> void` -* `handle_stream_end(peer_pubkey, stream_end, now) -> void` -* `handle_cancel(peer_pubkey, cancel, now) -> void` -Operational Principle: -* Provider mode MUST be explicitly enabled (`lcpd-grpcd` requires `enabled: true` in the YAML at `LCPD_PROVIDER_CONFIG_PATH`). -* If Provider is disabled (or the compute backend is disabled), inbound `lcp_quote_request` MUST be rejected as `lcp_error(code=unsupported_task)` (do not create invoices or execute). -* The Provider MUST decode job-scope messages and validate `protocol_version` and the task (unsupported cases return `lcp_error`). -* `openai_chat_completions_v1_params_tlvs` are strict: if unknown TLV types are present, return `lcp_error(code=unsupported_params)`. -* `openai.chat_completions.v1` inputs MUST be valid JSON and MUST satisfy: - * `request_json.model` is required. - * `request_json.messages` is required and must be non-empty. - * `request_json.stream` must be omitted or false. - * `params.model` is required and must match `request_json.model`. -* If `models` is non-empty, requests whose `params.model` is not in the set MUST be rejected as `lcp_error(code=unsupported_task)`. -* If `models` is empty, any `params.model` MAY be accepted, but the manifest MUST NOT advertise `supported_tasks`. -* The Provider MUST de-duplicate inbound job-scope messages via `ReplayStore`, and drop messages where `expiry < now`. - * For `lcp_stream_chunk`, de-duplication is performed by stream state (`seq < expected_seq` is ignored as a duplicate). -* For multiple `lcp_quote_request` messages with the same `job_id`, the Provider SHOULD return the same `lcp_quote_response` within TTL once it has been issued (idempotency). -* If an existing job's `terms_hash` does not match the recomputed value, the Provider SHOULD return `lcp_error(code=payment_invalid)`. -* Input stream gating (v0.2): - * After accepting `lcp_quote_request`, the Provider MUST require exactly one validated input stream (`stream_kind=input`) before issuing a quote. - * The Provider MUST NOT send `lcp_quote_response` until it has validated the input stream and derived (`input_hash`, `input_len`, `input_content_type`, `input_content_encoding`) as required by `protocol/protocol.md`. -* Stream receive rules (v0.2): - * For input streams, `lcp_stream_begin` MUST include `total_len` and `sha256`. - * `content_encoding="identity"` MUST be supported; unknown encodings MUST be rejected with `lcp_error(code=unsupported_encoding)`. - * `seq` MUST start at 0 and increase by exactly 1; `seq < expected_seq` is ignored as a duplicate; `seq > expected_seq` MUST return `lcp_error(code=chunk_out_of_order)`. - * The Provider MUST enforce local limits (`max_payload_bytes`, `max_stream_bytes`, `max_job_bytes`) for inbound streams. - * Stream end validation failure MUST return `lcp_error(code=checksum_mismatch)`. -* `price_msat` is computed after input stream validation via max-output-token planning + `LLMUsageEstimator.estimate` + `LLMPricing.quote_price` (fixed at quote-time; do not recompute on settlement). -* The Provider MUST compute `terms_hash` and MUST set the BOLT11 invoice `description_hash` to exactly equal `terms_hash` (invoice swapping defense). -* The Provider MUST NOT start execution before invoice settlement. -* When executing, the Provider MUST pass `params.model` to the compute backend as `task.model`. -* Result delivery (v0.2): - * For `task_kind="openai.chat_completions.v1"`, output bytes MUST be treated as raw OpenAI-compatible response body bytes (JSON). - * The Provider SHOULD set `result_content_type = "application/json; charset=utf-8"` and MUST use `result_content_encoding = "identity"`. - * The Provider MUST send exactly one result stream (`stream_kind=result`) after invoice settlement and before `lcp_result(status=ok)`. - * The Provider MUST include in `lcp_result(status=ok)` the metadata matching the validated result stream (`result_stream_id`, `result_hash`, `result_len`, `result_content_type`, `result_content_encoding`). -* Outbound sizing: - * When sending any message, the Provider MUST respect the peer's declared receive limits in `remote_manifest` (`max_payload_bytes`, `max_stream_bytes`, `max_job_bytes`). - * Result streaming MUST be chunked so each message payload fits within `remote_manifest.max_payload_bytes`. -* Failure handling: - * If compute fails after payment, the Provider SHOULD send `lcp_result(status=failed, message=...)`. - * On receiving `lcp_cancel`, the Provider SHOULD stop work best-effort and SHOULD send `lcp_result(status=cancelled)`. - -### RequesterJobStore -Purpose: Hold job state between gRPC `RequestQuote` and `AcceptAndExecute`. -Domain Model: -* `Job`: peer_id, job_id, task, terms, state, created_at, updated_at -* `JobState`: quoted | paying | awaiting_result | done | cancelled | failed | expired -Actions: -* `put_quote(peer_id, task, terms) -> void` -* `get_terms(peer_id, job_id) -> terms | error` -* `mark_state(peer_id, job_id, state) -> void` -* `gc() -> void` -Operational Principle: -* Jobs past `quote_expiry` MUST be treated as `expired`. -* The store MUST have bounds (e.g., max entries) and MUST evict/GC expired jobs. Rationale: prevent local state growth under repeated `RequestQuote`. -* `JobStore` MAY be an in-process memory implementation. -* Default bounds MUST be `DEFAULT_MAX_STORE_ENTRIES = 1024`. - -### RequesterWaiter -Purpose: Deliver inbound `lcp_quote_response` / result stream (`lcp_stream_*`) / `lcp_result` / `lcp_error` to the corresponding waiting gRPC calls. -Domain Model: -* `Key`: peer_id, job_id -* `QuoteOutcome`: quote_response(*QuoteResponse) | error(*Error) -* `ResultStreamState`: stream_id(32 bytes), expected_seq(uint32), buf([]byte), content_type(string), content_encoding(string), validated(bool), total_len(uint64), sha256(32 bytes) -* `ResultOutcome`: ok(*RequesterResult) | error(*Error) -* `RequesterResult`: output_bytes([]byte), content_type(string) -Actions: -* `wait_quote_response(ctx, peer_id, job_id) -> QuoteOutcome | error` -* `wait_result(ctx, peer_id, job_id) -> ResultOutcome | error` -* `handle_inbound_custom_message(peer_pubkey, msg_type, payload_bytes) -> void` -Operational Principle: -* Inbound payloads are decoded via `LCPWire.decode_quote_response` / `decode_stream_begin` / `decode_stream_chunk` / `decode_stream_end` / `decode_result` / `decode_error`. -* The waiter MUST reconstruct exactly one validated result stream (`stream_kind=result`) per job before completing a successful `wait_result`. -* On `lcp_result(status=ok)`, the waiter MUST verify the `lcp_result` metadata matches the validated result stream (`stream_id`, `sha256`, `total_len`, `content_type`, `content_encoding`). -* For the same (peer_id, job_id), allow at most one concurrent waiter for quote/result; additional waiters must fail. -* `lcp_error` is delivered to both the quote and result waiters. -* The requester-side waiter does not implement expiry/replay filtering. - -### PeerDirectory -Purpose: Track connected peers and their LCP readiness (via `lcp_manifest`), and provide `list_lcp_peers()`. -Domain Model: -* `PeerID`: string (Lightning pubkey hex, `lnrpc.Peer.pub_key`) -* `Peer`: peer_id, address, connected, custom_msg_enabled, manifest_sent, remote_manifest, lcp_ready +Purpose: Provide idempotency (de-duplication) and replay safety for call-scope messages. + +Domain Model (concept-local): + +- `ReplayKey`: peer_pubkey, call_id(32 bytes), msg_id(32 bytes) +- `ReplayEntry`: key, expiry(uint64) + Actions: -* `upsert_peer(peer_id, address) -> void` -* `mark_connected(peer_id) -> void` -* `mark_disconnected(peer_id) -> void` -* `mark_custom_msg_enabled(peer_id, enabled) -> void` -* `mark_manifest_sent(peer_id) -> void` -* `mark_lcp_ready(peer_id, remote_manifest) -> void` (records `remote_manifest`) -* `list_lcp_peers() -> []Peer` -* `remote_manifest_for(peer_id) -> manifest | not_found` + +- `check_and_remember(key, effective_expiry, now) -> ok | duplicate | expired` +- `prune(now) -> void` + Operational Principle: -* `lcp_ready` means: connected + `custom_msg_enabled=true` + `manifest_sent=true` + `remote_manifest` present. -* `list_lcp_peers()` returns only peers that are `lcp_ready`. - -### LNDPeerMessaging -Purpose: Send/receive BOLT #1 custom messages over lnd peer connections, and observe peer online/offline and inbound messages. -Domain Model: -* `LNDConnection`: rpc_addr, tls_cert_path, admin_macaroon_path -* `InboundCustomMessage`: peer_pubkey, msg_type(uint16), payload_bytes + +- Messages with `expiry < now` MUST be treated as `expired` and ignored. +- `duplicate` entries MUST be retained until `effective_expiry`. +- The store MUST be pruned by TTL and MUST have bounds (e.g., max entries) to cap memory usage. + +### RequesterCallStore + +Purpose: Hold requester-side call state (call spec, quote, payment state, completion) to support retries, cancellation, and gRPC workflows. + +Domain Model (concept-local): + +- `CallKey`: peer_id, call_id +- `CallState`: quoted | paid | completed | cancelled | failed +- `CallSpec`: method, params_bytes, request_bytes, request_content_type, request_content_encoding +- `QuoteRecord`: quote, received_at +- `CompletionRecord`: complete, received_at +- `CallRecord`: key, spec, quote_record?, completion_record?, state, created_at, updated_at + Actions: -* `dial(conn) -> grpc_conn | error` -* `list_peers(grpc_conn) -> []PeerSnapshot | error` -* `subscribe_peer_events(grpc_conn) -> stream | error` -* `subscribe_custom_messages(grpc_conn) -> stream | error` -* `send_custom_message(grpc_conn, peer_pubkey, msg_type, payload_bytes) -> ok | error` -* `disconnect_peer(grpc_conn, peer_pubkey) -> ok | error` + +- `put_spec(key, spec) -> void` +- `put_quote(key, quote) -> void` +- `put_complete(key, complete) -> void` +- `get(key) -> record | not_found` +- `mark_state(key, state) -> void` + Operational Principle: -* If `LCPD_LND_MANIFEST_RESEND_INTERVAL` is unset or `<= 0`, `lcp_manifest` MUST be sent at most once per connection (to avoid infinite loops). -* If `LCPD_LND_MANIFEST_RESEND_INTERVAL` is set to a positive duration, go-lcpd SHOULD periodically re-send `lcp_manifest` to connected peers on that interval. -* If `lcp_manifest` is received and we have not sent ours yet, reply once (SHOULD). If we have already sent ours, do not reply. -* Inbound messages are classified via `LCPMessageRouter`; manifests are decoded internally and applied to `PeerDirectory`. - -### LightningRPC -Purpose: Provide minimal invoice operations via the lnd gRPC API (create/verify/pay). -Domain Model: -* `PaymentRequest`: string (BOLT11) -* `PayeePubKey`: string (invoice destination pubkey hex; `DecodePayReq.destination`) -* `DescriptionHash`: 32 bytes -* `PaymentHash`: 32 bytes (`AddInvoiceResponse.r_hash`) -* `InvoiceAddIndex`: uint64 (`AddInvoiceResponse.add_index`) -* `PaymentPreimage`: 32 bytes + +- Calls MUST be keyed by `peer_id` as well as `call_id` to avoid cross-peer collisions. +- Records MUST be bounded and SHOULD be GC'd after completion (and/or TTL). + +### ProviderCallStore + +Purpose: Hold provider-side call state (request stream, quote, payment tracking, execution state, response streaming) to support idempotency and cancellation. + +Domain Model (concept-local): + +- `CallKey`: peer_id, call_id +- `CallState`: awaiting_request | quoted | waiting_payment | paid | executing | streaming_response | done | cancelled | failed +- `RequestStreamState`: stream_id, content_type, content_encoding, expected_seq, buf, total_len, sha256, validated +- `QuoteRecord`: quote, payment_request, quote_expiry, terms_hash, created_at +- `CallRecord`: key, method, params_bytes, request_stream_state?, quote_record?, state, created_at, updated_at + Actions: -* `get_info() -> info | error` -* `create_invoice(description_hash, price_msat, expiry_seconds) -> payment_request, payment_hash, add_index | error` -* `wait_for_settlement(payment_hash, add_index) -> ok | error` -* `decode_payment_request(payment_request) -> description_hash, payee_pubkey | error` -* `pay_invoice(payment_request) -> payment_preimage | error` + +- `get(key) -> record | not_found` +- `upsert(record) -> void` +- `update_state(key, state) -> void` +- `set_quote(key, quote_record) -> void` +- `set_request_stream(key, request_stream_state) -> void` + Operational Principle: -* Providers MUST create invoices with `description_hash=terms_hash`. -* Before paying, the Requester MUST verify `description_hash==terms_hash` and `payee_pubkey==peer_id`. - -### GRPCService -Purpose: Provide the gRPC API for operating go-lcpd. -Domain Model: -* `Proto`: `go-lcpd/proto/lcpd/v1/lcpd.proto` (managed by buf) -* `Service`: `lcpd.v1.LCPDService` -* `RPC`: `ListLCPPeers` / `GetLocalInfo` / `RequestQuote` / `AcceptAndExecute` / `CancelJob` + +- Store MUST have bounds and MUST NOT retain pending state past quote expiry. +- For calls awaiting request stream completion, implementations SHOULD evict state once the envelope expiry retention window passes. + +### StreamAssembler + +Purpose: Assemble `lcp_stream_*` messages into decoded bytes with validation (seq ordering + sha256/len at end). + +Domain Model (concept-local): + +- `StreamKey`: peer_id, call_id, stream_id +- `ExpectedSeq`: next expected chunk sequence number +- `StreamBuffer`: bytes, expected_seq +- `StreamMeta`: content_type, content_encoding, total_len, sha256 + Actions: -* `ListLCPPeers(request) -> response` -* `GetLocalInfo(request) -> response` -* `RequestQuote(request) -> response` -* `AcceptAndExecute(request) -> response` -* `CancelJob(request) -> response` + +- `accept_begin(begin) -> ok | error` +- `accept_chunk(chunk) -> ok | duplicate | error` +- `accept_end(end) -> decoded_bytes | error` + Operational Principle: -* The protobuf schema is the API contract; do not expose Go internal domain types directly. -* Invalid input returns a gRPC status (e.g., `InvalidArgument`). -* `AcceptAndExecute` is a blocking call; clients SHOULD set a deadline. - -### RegtestDevnet -Purpose: Provide a repo-local regtest devnet (Bitcoin Core + lnd) to reproduce Lightning integration locally (invoice binding / payments). -Domain Model: -* `Devnet`: data_dir, bitcoind_rpc_addr, lnd_nodes -* `LNDNode`: node_name, rpc_addr, tls_cert_path, admin_macaroon_path + +- `seq` MUST start at 0 and increase by exactly 1. +- For `seq < expected_seq`, treat as duplicate and ignore. +- At end, MUST validate `total_len` and `sha256` against decoded bytes. + +### ComputeBackend + +Purpose: Execute supported `openai.*` methods on the Provider side and produce response bytes/streams. + +Domain Model (concept-local): + +- `Method`: string (e.g. `openai.chat_completions.v1`, `openai.responses.v1`) +- `Model`: string +- `RequestBytes`: bytes (raw OpenAI-compatible request body) +- `ResponseBody`: io stream (raw OpenAI-compatible response body; may be `text/event-stream` for streaming requests) + Actions: -* `up() -> ok | error` -* `down() -> ok | error` -* `paths(node_name) -> LNDNode` -* `lncli(node_name, args...) -> stdout | error` -* `bitcoin_cli(args...) -> stdout | error` + +- `execute(ctx, method, model, request_bytes) -> response_body_stream | error` + Operational Principle: -* The devnet is started via `./scripts/devnet` and stores all state under `./.data/devnet/`. -* RPC must bind to localhost only. + +- `model` is taken from method params TLVs (not from request JSON). +- `request_bytes` is passed through byte-for-byte to the upstream OpenAI-compatible endpoint. +- Implementations MUST bound maximum response bytes and MUST fail safely on oversized responses. ## Synchronizations -### sync job_state_machine_requester -Summary: Define an intuitive state machine for a Requester job (via gRPC). -Diagrams: -```mermaid -stateDiagram-v2 - [*] --> Quoted: RequestQuote OK - Quoted --> Expired: quote_expiry elapsed - - Quoted --> Paying: AcceptAndExecute started - Paying --> AwaitingResult: payment settled - AwaitingResult --> Done: result stream + lcp_result received - - Quoted --> Cancelled: CancelJob sent - Paying --> Cancelled: CancelJob sent - AwaitingResult --> Cancelled: CancelJob sent - - Quoted --> Failed: lcp_error received - Paying --> Failed: payment failed / lcp_error - AwaitingResult --> Failed: lcp_error -``` - -### sync job_state_machine_provider -Summary: Define an intuitive state machine for a Provider job. -Diagrams: -```mermaid - stateDiagram-v2 - [*] --> AwaitingInput: lcp_quote_request received - AwaitingInput --> WaitingPayment: input stream validated + invoice created + quote_response sent - WaitingPayment --> Paid: invoice settled - Paid --> Executing: compute started - Executing --> StreamingResult: result stream sent - StreamingResult --> Done: lcp_result sent - - AwaitingInput --> Canceled: lcp_cancel received - WaitingPayment --> Canceled: lcp_cancel received - Executing --> Canceled: best-effort cancel - - AwaitingInput --> Failed: stream/protocol error - WaitingPayment --> Failed: invoice error - Executing --> Failed: execution error - ``` - -### sync lnd_peer_messaging_startup -Summary: On startup, connect to lnd, load an initial peer snapshot, and start subscription loops. -When: on go-lcpd startup. -Then: -1. Connect to lnd via `LNDPeerMessaging.dial` (if not configured, disable integration and do nothing). -2. Fetch currently connected peers via `LNDPeerMessaging.list_peers` and apply them to `PeerDirectory` (connected + custom_msg_enabled). -3. For each peer, send `lcp_manifest` once if it has not been sent yet and call `PeerDirectory.mark_manifest_sent`. -4. If `LCPD_LND_MANIFEST_RESEND_INTERVAL` is set to a positive duration, start a periodic loop that re-sends `lcp_manifest` to connected peers on that interval. -5. Start the `subscribe_peer_events` and `subscribe_custom_messages` loops. - -### sync lnd_inbound_custom_message_dispatch -Summary: Classify inbound custom messages; apply manifests to `PeerDirectory`; dispatch the rest to handlers. -When: `LNDPeerMessaging` receives a `CustomMessage`. -Then: -1. Classify via `LCPMessageRouter.route`. -2. For `dispatch_manifest`, decode via `LCPWire.decode_manifest`, call `PeerDirectory.mark_lcp_ready` (recording `remote_manifest`), and if we have not sent ours yet, reply with `lcp_manifest` once and call `PeerDirectory.mark_manifest_sent`. -3. For `dispatch_quote_request` / `dispatch_cancel`, dispatch to the Provider handler (replay/expiry handling lives on the Provider handler side). -4. For `dispatch_quote_response` / `dispatch_result` / `dispatch_error`, dispatch to the requester-side waiter (bound to the waiting gRPC call). -5. For `dispatch_stream_begin` / `dispatch_stream_chunk` / `dispatch_stream_end`, dispatch to both the Provider handler and the requester-side waiter. Each side MUST ignore streams that do not match its expected state (input vs result). -6. For `disconnect`, call `DisconnectPeer`. -7. For `ignore`, do nothing. - -### sync grpc_get_local_info -Summary: Return local `node_id` and `manifest` via `GetLocalInfo`. -When: `GRPCService.GetLocalInfo` is called. -Then: -1. Call `LightningRPC.get_info` and use `identity_pubkey` as `node_id`. -2. Build `manifest` from local config (`protocol_version=2`, `max_payload_bytes=DEFAULT_MAX_PAYLOAD_BYTES`, `max_stream_bytes=DEFAULT_MAX_STREAM_BYTES`, `max_job_bytes=DEFAULT_MAX_JOB_BYTES`). Include `supported_tasks` only when Provider is enabled and `models` is non-empty. -3. Return `GetLocalInfoResponse{node_id, manifest}`. - -### sync grpc_list_lcp_peers -Summary: Enumerate connected LCP-capable peers (those that have responded with `lcp_manifest`) via `ListLCPPeers`. -When: `GRPCService.ListLCPPeers` is called. -Then: -1. Call `PeerDirectory.list_lcp_peers()`. -2. Pack each peer's `peer_id` / `address` / `remote_manifest` into the gRPC response and return it. - -### sync grpc_request_quote -Summary: Send `lcp_quote_request` via `RequestQuote` and return `lcp_quote_response` as `Terms`. -When: `GRPCService.RequestQuote` is called. -Where: -1. Gather and validate inputs (`peer_id`, `task`). -2. Precondition: the target peer is `lcp_ready` in `PeerDirectory` (otherwise return FAILED_PRECONDITION). -Then: -1. Call `LCPTasks.validate_task`. -2. Generate `job_id`, a `msg_id` for `lcp_quote_request`, and `expiry` (`expiry = now + 300 seconds`). -3. Build a `QuoteRequest` by calling `LCPTasks.to_wire_quote_request_task` to get `task_kind/params_bytes`. -4. Encode `lcp_quote_request` via `LCPWire.encode_quote_request`. If it would exceed the peer's `remote_manifest.max_payload_bytes`, return RESOURCE_EXHAUSTED (do not send). -5. Send `lcp_quote_request` via `SendCustomMessage(type=42083)`. -6. Build the input stream via `LCPTasks.to_wire_input_stream` to get `decoded_bytes/content_type/content_encoding`. -7. Verify the input fits the peer's declared stream limits (`len(decoded_bytes) <= remote_manifest.max_stream_bytes` and `<= remote_manifest.max_job_bytes`), otherwise return RESOURCE_EXHAUSTED (do not send). -8. Send the input stream: - 1. Generate `stream_id` and a `msg_id` for `lcp_stream_begin`. - 2. Send `lcp_stream_begin(stream_kind=input, total_len=len(decoded_bytes), sha256=SHA256(decoded_bytes), content_type, content_encoding)`. - 3. Send `lcp_stream_chunk` × N with `seq=0..N-1`, choosing chunk sizes so each encoded message payload fits within `remote_manifest.max_payload_bytes`. - 4. Send `lcp_stream_end(total_len, sha256)` (with a fresh random `msg_id`). -9. Wait until deadline for `lcp_quote_response` or `lcp_error` with the same `job_id`. -10. On `lcp_quote_response`, recompute and verify `terms_hash` (including input stream metadata and canonicalized `params_hash`), call `RequesterJobStore.put_quote`, and return `RequestQuoteResponse{terms}`. - -### sync grpc_accept_and_execute -Summary: Pay the invoice in `AcceptAndExecute`, wait for the result stream + `lcp_result`, and return `Result`. -When: `GRPCService.AcceptAndExecute` is called. -Where: -1. Gather and validate inputs (`peer_id`, `job_id`, `pay_invoice`). -2. Precondition: `RequesterJobStore` has a quote for `job_id` (otherwise NOT_FOUND). -Then: -1. Verify `pay_invoice=true` (false is INVALID_ARGUMENT). -2. Verify `quote_expiry` has not passed (if it has, FAILED_PRECONDITION). -3. Call `LightningRPC.decode_payment_request(terms.payment_request)` and verify invoice binding (failure is FAILED_PRECONDITION): - - `description_hash == terms.terms_hash` - - `payee_pubkey == peer_id` - - `invoice_amount_msat == price_msat` - - `invoice_expiry_unix <= quote_expiry (+ ALLOWED_CLOCK_SKEW_SECONDS)` -4. Call `LightningRPC.pay_invoice`, then wait until deadline for either: - - a validated result stream + matching `lcp_result(status=ok)`, or - - `lcp_error`. -5. On success, return `AcceptAndExecuteResponse{result}` where `result.result` is the reconstructed result bytes and `result.content_type` matches the result stream metadata. - -### sync grpc_cancel_job -Summary: Send `lcp_cancel` via `CancelJob` (best-effort). -When: `GRPCService.CancelJob` is called. -Then: -1. Generate `msg_id` and `expiry` (`expiry = now + 300 seconds`). -2. Build payload via `LCPWire.encode_cancel` and call `SendCustomMessage(type=42095)`. -3. On success, return `CancelJobResponse{success=true}`. - -### sync lnd_lcp_provider_quote_pay_result -Summary: As a Provider, receive `lcp_quote_request`, receive the input stream, issue an invoice-bound quote, wait for settlement, stream the result, and send `lcp_result`. -When: the Provider handler receives `lcp_quote_request`. -Then: -1. If Provider is disabled or the compute backend is disabled, send `lcp_error(code=unsupported_task)` (do not create invoices or execute). -2. Validate `protocol_version`; if unsupported, send `lcp_error(code=unsupported_version)`. -3. Validate `task_kind` and `params`; if unsupported, send `lcp_error(code=unsupported_task|unsupported_params)`. -4. If `expiry < now`, drop; otherwise compute `effective_expiry = min(expiry, now + MAX_ENVELOPE_EXPIRY_WINDOW_SECONDS)` and de-duplicate via `ReplayStore` until `effective_expiry` (drop duplicates). -5. Compute `params_hash` via `ProtocolCompatibility.compute_params_hash` and persist a job record in `ProviderJobStore` as `awaiting_input`. -6. Receive and validate exactly one input stream (`stream_kind=input`) for this `job_id` via the Provider stream handlers: - - On `lcp_stream_begin`: validate required fields (`total_len`, `sha256`), enforce `content_encoding`, enforce local `max_stream_bytes/max_job_bytes`, and initialize stream state. - - On `lcp_stream_chunk`: enforce `seq` ordering (ignore duplicates; reject out-of-order), append bytes, and enforce local limits. - - On `lcp_stream_end`: validate length/hash; on failure send `lcp_error(code=checksum_mismatch)` and fail the job. -7. Set `quote_expiry = now + quote_ttl_seconds`, compute `price_msat` via `LLMExecutionPolicy.apply` / `LLMUsageEstimator.estimate` / `LLMPricing.quote_price`, then compute `terms_hash` (including input metadata and `params_hash`). -8. Satisfy idempotency for the same `job_id`: - - If `ProviderJobStore` already has a valid quote, re-send the same `lcp_quote_response`. - - If it exists but is expired, send `lcp_error(code=quote_expired)`. - - If the existing job's `terms_hash` mismatches, send `lcp_error(code=payment_invalid)`. -9. Call `create_invoice(description_hash=terms_hash, price_msat, expiry_seconds)` and obtain `payment_request`. - - `expiry_seconds = max(1, quote_ttl_seconds - ALLOWED_CLOCK_SKEW_SECONDS)` - - Rationale: ensure `invoice_expiry_unix <= quote_expiry (+ ALLOWED_CLOCK_SKEW_SECONDS)` on the requester side. -10. Encode and send `lcp_quote_response`, and persist it in `ProviderJobStore` (state `waiting_payment`). -11. Wait for settlement via `wait_for_settlement` (MUST NOT start execution before settlement). -12. After settlement, call `ComputeBackend.execute` and obtain output. -13. Send the result: - - Stream `output` via `lcp_stream_begin(stream_kind=result)` / `lcp_stream_chunk` × N / `lcp_stream_end`. - - Send `lcp_result(status=ok)` with metadata matching the validated result stream. - - If execution fails, send `lcp_result(status=failed, message=...)` instead of `status=ok`. - -### sync lnd_lcp_provider_cancel -Summary: On receiving `lcp_cancel`, stop the job as much as possible. -When: the Provider handler receives `lcp_cancel`. -Then: -1. If `expiry < now`, drop; if duplicate by `ReplayStore`, drop. -2. If the job is `awaiting_input` or `waiting_payment`, mark `ProviderJobStore` as `canceled`, stop further work, and SHOULD send `lcp_result(status=cancelled)`. -3. If the job is executing, attempt context cancellation and stop as much as possible (best-effort) and SHOULD send `lcp_result(status=cancelled)`. - -### sync integration_grpc_smoke -Summary: Ensure gRPC request/response works via an integration test (no external processes). -When: the integration test starts. -Then: -1. Server startup succeeds. -2. Verify `ListLCPPeers` returns a response (typically an empty list). -3. Verify `GetLocalInfo` returns UNAVAILABLE when `lnd` is not configured. - -### sync integration_regtest_lnd_lcp_manifest_roundtrip -Summary: On regtest, show via an integration test (opt-in) that two nodes can exchange `lcp_manifest` over lnd custom messages. -When: the integration test starts with `LCP_ITEST_REGTEST=1`. -Then: -1. Start `bitcoind` and two `lnd` nodes and initialize wallets. -2. Connect the two nodes via `ConnectPeer` and ensure they are online (no channel required). -3. Start go-lcpd on both sides and begin `lnd_inbound_custom_message_dispatch`. -4. Wait (short timeout) until `ListLCPPeers` returns one peer. -5. Verify `peer_id` equals the remote pubkey and required fields like `remote_manifest.protocol_version==2` and stream limit fields match expectations. - -### sync integration_regtest_lnd_lcp_quote_pay_result_custom_messages -Summary: On regtest, show via an integration test (opt-in) that LCP v0.2 quote → payment → result streaming completes over custom messages. -When: the integration test starts with `LCP_ITEST_REGTEST=1`. -Then: -1. Start `bitcoind` and two `lnd` nodes (Alice=Requester, Bob=Provider) and initialize wallets. -2. Open a channel from Alice → Bob (required to settle invoices). -3. Start go-lcpd on Bob and enable Provider flows `lnd_inbound_custom_message_dispatch` and `lnd_lcp_provider_quote_pay_result` (compute backend is a deterministic stub). -4. On Alice (the test Requester), send `lcp_quote_request` and the input stream (`lcp_stream_begin/chunk/end`) via `SendCustomMessage`. -5. Receive and decode `lcp_quote_response` via `SubscribeCustomMessages`, verify `description_hash == terms_hash` and `destination(payee) == bob_pubkey` via `DecodePayReq`, then pay. -6. Receive and decode the result stream (`lcp_stream_begin/chunk/end`) and `lcp_result`, verify stream validation + metadata consistency, and verify the reconstructed output matches expectations (use `cmp.Diff`). +### sync ManifestExchange + +Summary: Exchange `lcp_manifest` with a peer and track the peer's limits/capabilities. + +Flow: + +1. When: `go-lcpd` observes a peer connection event (lnd peer). +2. Then: send local `lcp_manifest` (v0.3). +3. When: `lcp_manifest` is received from the peer. +4. Then: decode it, validate `protocol_version=3`, and record it for the connection. +5. Then: mark the peer as LCP-ready only after both local and remote manifests are known. + +Error branches: + +- If the peer's manifest has unsupported `protocol_version`, mark the peer as not ready. +- If a manifest fails TLV decoding, ignore it and keep the peer not ready. + +### sync RequestQuote (Requester) + +Summary: Send `lcp_call` + request stream to a Provider and receive `lcp_quote`. + +Flow: + +1. When: gRPC `RequestQuote(peer_id, call_spec)` is called. +2. Then: ensure the peer is connected and LCP-ready (manifest exchange completed). +3. Then: generate a fresh `call_id` and store `call_spec` in `RequesterCallStore`. +4. Then: send `lcp_call` with (`call_id`, `method`, optional `params`). +5. Then: send the request stream (`stream_kind=request`) using `lcp_stream_begin/chunk/end`: + - content_type/content_encoding come from `call_spec`. + - enforce remote limits (`max_payload_bytes`, `max_stream_bytes`, `max_call_bytes`). +6. Then: wait for either: + - `lcp_quote` for that `call_id` (success), OR + - `lcp_error` for that `call_id` (failure), OR + - deadline exceeded (unknown outcome). +7. Then: on `lcp_quote`, store quote in `RequesterCallStore` and return it to the client. + +### sync AcceptAndExecute (Requester) + +Summary: Pay the invoice from a previously received quote and wait for `lcp_complete` and the validated response stream. + +Flow: + +1. When: gRPC `AcceptAndExecute(peer_id, call_id, pay_invoice=true)` is called. +2. Then: load the stored quote for (`peer_id`, `call_id`) from `RequesterCallStore` (NOT_FOUND if absent). +3. Then: verify quote expiry and validate the invoice binding preconditions. +4. Then: pay the BOLT11 invoice via `lnd` (block until settled or deadline). +5. Then: wait for the response stream (`stream_kind=response`) and assemble it via `StreamAssembler`. +6. Then: wait for terminal `lcp_complete` and validate it against the stream metadata (hash/len/content-type/encoding). +7. Then: store completion in `RequesterCallStore` and return `Complete` to the client. + +Streaming variant: + +- `AcceptAndExecuteStream` emits: + - `response_begin` (content-type/encoding), + - `response_chunk` (decoded bytes), + - `response_end` (hash/len), + - `complete` (terminal status). + +### sync ProviderHandleCall (Provider) + +Summary: As a Provider, receive `lcp_call`, receive the request stream, issue an invoice-bound quote, wait for settlement, stream the response, and send `lcp_complete`. + +Flow: + +1. When: Provider handler receives `lcp_call` for `call_id`. +2. Then: validate: + - `protocol_version=3`, + - `method` is supported, + - `params` are present and valid for the method (strict TLV). +3. Then: receive and validate exactly one request stream (`stream_kind=request`) for this `call_id`: + - enforce chunk ordering, + - validate sha256/len at end. +4. Then: estimate usage / price and build `terms_hash` per wire spec. +5. Then: create a BOLT11 invoice with `description_hash=terms_hash` and amount=`price_msat`. +6. Then: send `lcp_quote` containing `price_msat`, `terms_hash`, `quote_expiry`, and `payment_request`. +7. Then: wait for invoice settlement (best-effort with TTL). +8. Then: execute the method via `ComputeBackend`, streaming the response: + - send `lcp_stream_begin/chunk/end` with `stream_kind=response`. +9. Then: send terminal `lcp_complete(status=ok)` including response metadata (hash/len/content-type/encoding). + +Error branches: + +- If validation fails before quoting, send `lcp_error(code=unsupported_method|invalid_state|...)`. +- If cancelled (`lcp_cancel`) before settlement, stop and send `lcp_complete(status=cancelled)`. +- If execution fails after settlement, send `lcp_complete(status=failed, message=...)`. + +### sync CancelJob (Requester) + +Summary: Send `lcp_cancel` to the provider for an in-flight call. + +Flow: + +1. When: gRPC `CancelJob(peer_id, call_id)` is called. +2. Then: ensure the peer is connected. +3. Then: send `lcp_cancel` for that `call_id` (best-effort). +4. Then: mark the call as cancelled locally (best-effort; the provider may still complete). + diff --git a/docs/openai-serve/configuration-ja.mdx b/docs/openai-serve/configuration-ja.mdx index 6b5fe9b..88eda6f 100644 --- a/docs/openai-serve/configuration-ja.mdx +++ b/docs/openai-serve/configuration-ja.mdx @@ -28,7 +28,7 @@ description: 環境変数、ルーティング、モデル検証、上限(Safe `openai-serve` のログは、生のユーザー入力を永続化しなくても運用できるように設計しています。 - ログには、生のプロンプト(`messages[].content`)や生のモデル出力を残してはいけません。 -- `info` は、運用メタデータ(model/peer/job id、価格、時間、byte/token カウント)を記録します。 +- `info` は、運用メタデータ(model/peer/call id、価格、時間、byte/token カウント)を記録します。 - `debug` は HTTP リクエストログがより詳細になり(`client_ip` を含むことがあります)、よりセンシティブとして扱ってください。 ## ルーティング(peer の選び方) @@ -37,8 +37,8 @@ description: 環境変数、ルーティング、モデル検証、上限(Safe 1. `OPENAI_SERVE_MODEL_MAP`(マップ先の peer が connected/LCP-ready の場合) 2. `OPENAI_SERVE_DEFAULT_PEER_ID`(設定されており connected/LCP-ready の場合) -3. manifest の `supported_tasks` で model を広告している peer(存在する場合) -4. フォールバックとして、接続されている peer の先頭 +3. manifest の `supported_methods` で必要な method を広告している peer(存在する場合) +4. フォールバックとして、接続されている peer のいずれか 接続中の LCP peers が 0 の場合、リクエストは失敗します。 @@ -48,8 +48,7 @@ description: 環境変数、ルーティング、モデル検証、上限(Safe - `OPENAI_SERVE_MODEL_ALLOWLIST` が設定されている場合: allowlist に含まれる `model` のみ許可します (ただし `OPENAI_SERVE_ALLOW_UNLISTED_MODELS=true` の場合は例外)。 -- それ以外の場合: 接続 peer が `supported_tasks` を広告しているなら、少なくとも 1 peer が広告している `model` のみ許可します。 - もし全 peer が `supported_tasks` を広告していない場合は、ゲートウェイを使える状態に保つため検証をスキップします。 +- それ以外の場合: model の検証はスキップします(LCP v0.3 の manifest では model を広告しないため。Provider 側で model ポリシーを検証します)。 ## Safety limits diff --git a/docs/openai-serve/configuration.mdx b/docs/openai-serve/configuration.mdx index 3a64d16..c4882f3 100644 --- a/docs/openai-serve/configuration.mdx +++ b/docs/openai-serve/configuration.mdx @@ -28,7 +28,7 @@ You can use `apps/openai-serve/.envrc.sample` (direnv) for local development. `openai-serve` logs are intentionally designed to avoid persisting raw user content. - Logs MUST NOT contain raw prompts (`messages[].content`) or raw model outputs. -- `info` logs include request/job metadata (model, peer/job ids, price, timings, byte/token counts). +- `info` logs include request metadata (model, peer/call ids, price, timings, byte/token counts). - `debug` may include HTTP request logs (including `client_ip`); treat debug logs as more sensitive. ## Routing and peer selection @@ -37,8 +37,8 @@ For a given `model`, the Provider peer is selected in this order: 1. `OPENAI_SERVE_MODEL_MAP` (if the mapped peer is connected/LCP-ready) 2. `OPENAI_SERVE_DEFAULT_PEER_ID` (if set and connected/LCP-ready) -3. A peer that advertises the model in its manifest `supported_tasks` (if any) -4. Fallback to the first connected peer +3. A peer that advertises the required method in its manifest `supported_methods` (if any) +4. Fallback to any connected peer If there are no connected LCP peers, the request fails. @@ -48,8 +48,7 @@ Model validation is designed to prevent accidental misrouting or unexpected char - If `OPENAI_SERVE_MODEL_ALLOWLIST` is set: `model` must be in the allowlist unless `OPENAI_SERVE_ALLOW_UNLISTED_MODELS=true`. -- Otherwise: if any connected peers advertise `supported_tasks`, the model must be advertised by at least one peer. - If **no** peers advertise `supported_tasks`, validation is skipped to keep the gateway usable. +- Otherwise: model validation is skipped (LCP v0.3 manifests do not advertise models; providers enforce their own model policies). ## Safety limits diff --git a/docs/openai-serve/overview-ja.mdx b/docs/openai-serve/overview-ja.mdx index 66c55ef..0fe2127 100644 --- a/docs/openai-serve/overview-ja.mdx +++ b/docs/openai-serve/overview-ja.mdx @@ -34,7 +34,7 @@ lcpd-grpcd(Requester) --- Lightning --- Provider peer(lcpd-grpcd Provider ログはセンシティブとして扱います。`openai-serve` は、生のユーザー入力を永続化しなくても診断できるように設計しています。 - ログには、生のプロンプト(`messages[].content`)や生のモデル出力を残してはいけません。 -- ログは運用メタデータ(model/peer/job id、価格、時間、byte/token カウント)のみに寄せます。 +- ログは運用メタデータ(model/peer/call id、価格、時間、byte/token カウント)のみに寄せます。 - `OPENAI_SERVE_LOG_LEVEL=debug` はリクエストログがより詳細になります。運用では(必要になるまで)デフォルトの `info` を推奨します。 ## 対応エンドポイント @@ -169,6 +169,6 @@ llm -m lcp-gpt-5.2 "Say hello in Japanese." `POST /v1/chat/completions` と `POST /v1/responses` のレスポンスには以下が含まれます: - `X-Lcp-Peer-Id`: 選択された Provider の peer id -- `X-Lcp-Job-Id`: job id(hex) +- `X-Lcp-Call-Id`: call id(hex) - `X-Lcp-Price-Msat`: 受諾した quote の価格 - `X-Lcp-Terms-Hash`: 受諾した quote の terms hash(hex) diff --git a/docs/openai-serve/overview.mdx b/docs/openai-serve/overview.mdx index 9d7567e..ee481c2 100644 --- a/docs/openai-serve/overview.mdx +++ b/docs/openai-serve/overview.mdx @@ -33,7 +33,7 @@ Notes: Logs are treated as sensitive. `openai-serve` is designed to be diagnosable without persisting raw user content. - Logs MUST NOT contain raw prompts (`messages[].content`) or raw model outputs. -- Logs include only operational metadata (model/peer/job ids, price, timings, and byte/token counts). +- Logs include only operational metadata (model/peer/call ids, price, timings, and byte/token counts). - `OPENAI_SERVE_LOG_LEVEL=debug` enables more verbose request logging; keep `info` (default) for production unless needed. ## Supported endpoints @@ -166,6 +166,6 @@ For configuration, routing rules, and safety knobs (max price, timeouts, model a `POST /v1/chat/completions` and `POST /v1/responses` responses include: - `X-Lcp-Peer-Id`: chosen Provider peer id -- `X-Lcp-Job-Id`: job id (hex) +- `X-Lcp-Call-Id`: call id (hex) - `X-Lcp-Price-Msat`: accepted quote price - `X-Lcp-Terms-Hash`: accepted quote terms hash (hex)