From a83aabcc7de036204886e3d60da5600ce171c6f9 Mon Sep 17 00:00:00 2001 From: qianghan Date: Fri, 17 Jul 2026 19:46:05 -0700 Subject: [PATCH] =?UTF-8?q?fix(byoc):=20per-signer=20payment=20type=20(dua?= =?UTF-8?q?l-path)=20=E2=80=94=20rescue=20from=20prod=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The production SDK build (sdk-service:byoc-dual-path-1bf13cd) carries a load-bearing byoc-payment fix that exists in NO branch — only in the running container. This ports it onto ja/live-runner so the unified gateway keeps BYOC payment working (and stops the fix living only as a deployed image). - `_payment_type_for_signer(signer_url)`: legacy Daydream signer (signer.daydream.live) → type:"lv2v" + string `capability`; modern signers (pymthouse DMZ, …) → type:"byoc" + BYOC capabilities protobuf. - `_create_byoc_payment`: build the payment payload + orch-discovery capabilities per the resolved type. - `capabilities.py`: `CapabilityId.BYOC` + `byoc_capabilities_from_app()`. Why this over #41: #41 sends type:"byoc" unconditionally and depends on an undeployed go-livepeer signer+orch change — against signer.daydream.live (which only accepts lv2v today) that reproduces the 2026-07-13 "invalid job type" outage. This per-signer switch is the superset that keeps the legacy signer working while allowing modern signers. It also generalizes to the live-runner payment path (removing the lr-gateway lv2v workaround). Verified: byte-identical to the code running in prod today. Co-Authored-By: Claude Opus 4.8 --- src/livepeer_gateway/byoc.py | 51 +++++++++++++++++++++++----- src/livepeer_gateway/capabilities.py | 10 ++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/livepeer_gateway/byoc.py b/src/livepeer_gateway/byoc.py index 4d27781..970d2b1 100644 --- a/src/livepeer_gateway/byoc.py +++ b/src/livepeer_gateway/byoc.py @@ -36,11 +36,12 @@ import ssl import uuid from dataclasses import dataclass, field -from typing import Any, Optional, Sequence +from typing import Any, Literal, Optional, Sequence from urllib.error import HTTPError, URLError from urllib.parse import urlparse from urllib.request import Request, urlopen +from .capabilities import byoc_capabilities_from_app from .orchestrator import _http_origin, _parse_http_url, discover_orchestrators from .errors import LivepeerGatewayError, NoOrchestratorAvailableError, OrchestratorRejection @@ -144,6 +145,22 @@ def audio_url(self) -> Optional[str]: # Header building # --------------------------------------------------------------------------- +_LEGACY_DAYDREAM_SIGNER_HOST = "signer.daydream.live" + + +def _payment_type_for_signer(signer_url: str) -> Literal["byoc", "lv2v"]: + """ + Select payment payload shape for the target signer. + + Legacy Daydream signer expects type:lv2v + string capability. + pymthouse DMZ (and other modern signers) expect type:byoc + capabilities proto. + """ + hostname = (urlparse(signer_url).hostname or "").lower() + if hostname == _LEGACY_DAYDREAM_SIGNER_HOST: + return "lv2v" + return "byoc" + + def _create_byoc_payment( *, orch_origin: str, @@ -173,10 +190,20 @@ def _create_byoc_payment( parsed = urlparse(orch_origin) grpc_url = f"https://{parsed.hostname}:8935" + payment_type = _payment_type_for_signer(signer_url) + byoc_caps = ( + byoc_capabilities_from_app(capability) + if payment_type == "byoc" + else None + ) + + # Capabilities on orch discovery are required for type:byoc so TicketParams + # use PriceInfoForCaps (per-cap wei/sec) when ByocPerCapPricing is enabled. info = get_orch_info( grpc_url, signer_url=signer_url, signer_headers=signer_headers, + capabilities=byoc_caps, ) # Check if orch has a price set — if price is 0, skip payment @@ -189,16 +216,22 @@ def _create_byoc_payment( _LOG.info("BYOC orch has no ticket_params, skipping payment") return {} - # Step 2: Generate payment via signer + # Step 2: Generate payment via signer — shape depends on signer host. orch_info_b64 = base64.b64encode(info.SerializeToString()).decode("ascii") + payment_payload: dict[str, Any] = { + "orchestrator": orch_info_b64, + "type": payment_type, + } + if payment_type == "byoc" and byoc_caps is not None: + payment_payload["capabilities"] = base64.b64encode( + byoc_caps.SerializeToString() + ).decode("ascii") + elif payment_type == "lv2v": + payment_payload["capability"] = capability signer_origin = _http_origin(signer_url) payment_url = f"{signer_origin}/generate-live-payment" - payment_body = json.dumps({ - "orchestrator": orch_info_b64, - "type": "lv2v", - "capability": capability, - }).encode("utf-8") + payment_body = json.dumps(payment_payload).encode("utf-8") payment_headers = { "Content-Type": "application/json", "Livepeer-Capability": capability, @@ -759,8 +792,8 @@ def refresh_training_payment( Args: job_id: training job_id assigned by orch on submit. orch_url: orchestrator URL accepting the job. - capability: capability name (needed by signer to pick correct - ticket_params). + capability: capability/app name encoded into the BYOC capabilities + proto sent to the signer (not a separate string field). signer_url: remote signer with /generate-live-payment. signer_headers: pass-through headers (notably Authorization Bearer of the user). diff --git a/src/livepeer_gateway/capabilities.py b/src/livepeer_gateway/capabilities.py index 27e5688..a400dc0 100644 --- a/src/livepeer_gateway/capabilities.py +++ b/src/livepeer_gateway/capabilities.py @@ -45,6 +45,7 @@ class CapabilityId(IntEnum): IMAGE_TO_TEXT = 34 LIVE_VIDEO_TO_VIDEO = 35 TEXT_TO_SPEECH = 36 + BYOC = 37 CAPABILITY_ID_TO_NAME: dict[int, str] = { -2: "Invalid", @@ -85,9 +86,18 @@ class CapabilityId(IntEnum): 34: "Image to text", 35: "Live video to video", 36: "Text to speech", + 37: "byoc", } +def byoc_capabilities_from_app(app: str) -> Optional[lp_rpc_pb2.Capabilities]: + """Build BYOC capability constraints from a capability/app name.""" + app = app.strip() + if not app: + return None + return build_capabilities(CapabilityId.BYOC, app) + + def capability_name(cap_id: int) -> str: return CAPABILITY_ID_TO_NAME.get(cap_id, "Unknown capability")