Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions src/livepeer_gateway/byoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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).
Expand Down
10 changes: 10 additions & 0 deletions src/livepeer_gateway/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")

Expand Down