Live Runner + Scope support#20
Conversation
The new runner uses `address` as an opaque blob
Registers the app as a runner with an orchestrator.
Also make price info optional
None of these have anything to do with orchestrators. Keep only small shims for backwards compatibility.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| runner_url: str, | ||
| app: str, | ||
| price_per_unit: int = 0, | ||
| pixels_per_unit: int = 1, |
There was a problem hiding this comment.
@j0sh can we use something like unit_scale since this runner also allows different pricing schemes. Also see my pull request in go-liveper livepeer/go-livepeer#3942.
There was a problem hiding this comment.
I'll look into that idea (and the general move from away from pixels to purely timing) but don't want to block this merge for that
| secret: str, | ||
| runner_url: str, | ||
| app: str, | ||
| price_per_unit: int = 0, |
There was a problem hiding this comment.
@j0sh How do you intend pricing to work for dynamic runners? Right now the app self-asserts price_info via register_runner(price_per_unit=…) and go-livepeer trusts it (only > 0 is checked in normalizeHeartbeat). That's fine for operator-deployed/trusted containers — the operator sets the price via env and the app forwards it — but an untrusted image could ignore that and under-report its price. Static sidesteps this (operator sets price_info in runners.json); only dynamic trusts the app.
You can see how I'm currently using this in the hello_world example, which feels a bit strange since it relies on the app to create the argument and forward it.
Two thoughts:
- If the orchestrator is still meant to set the price in dynamic mode (to make gaming harder), the SDK could auto-read it from env (e.g. PRICE_PER_UNIT) instead of the app passing it to register_runner, keeping pricing a deployment/operator concern, out of app code.
- I think we'll eventually move to GPU-based pricing (orchestrator states a price per GPU type, workloads auto-run at that rate), so go-livepeer would override the reported price at registration anyway.
There was a problem hiding this comment.
That's correct, the runner itself reports the price because the orchestrator is intended to control the runner.
There was a problem hiding this comment.
This way runner provisioning (including pricing) can be configured separately from go-livepeer without having to introduce a mutual dependency on one other. Setting the price on go-livepeer itself introduces a tension with the orchestrator needing to be configured separately with details of the runner / workload, hardware, etc, as opposed to runners being able to simply connect and go.
If you want to keep the configuration / pricing within go-livepeer then static configuration is the way to go.
| if wait_callback and (timeout is None or timeout > 0): | ||
| try: | ||
| await self.wait_callback(timeout=timeout) | ||
| except asyncio.TimeoutError: |
| task.cancel() | ||
| try: | ||
| await task | ||
| except asyncio.CancelledError: |
| def _release_session_id(self, session_id: str) -> None: | ||
| try: | ||
| self._active_session_ids.remove(session_id) | ||
| except ValueError: |
| if wait_callbacks and (timeout is None or timeout > 0): | ||
| try: | ||
| await self.wait_callbacks(timeout=timeout) | ||
| except asyncio.TimeoutError: |
|
|
||
| async def _maybe_await(value: object) -> None: | ||
| if inspect.isawaitable(value): | ||
| await value |
|
|
||
|
|
||
| class LiveRunnerSessionHeaders(Protocol): | ||
| def get(self, key: str, default: str = "") -> str: ... |
|
|
||
| async def _maybe_await(value: None | Awaitable[None]) -> None: | ||
| if inspect.isawaitable(value): | ||
| await value |
…ge into ja/live-runner (#46) ## Why The production SDK (`sdk-service:byoc-dual-path-1bf13cd`) carries a **load-bearing byoc-payment fix that exists in no branch** — only in the running container. That's an operational liability and it blocks a unified gateway. This ports it onto `ja/live-runner` (the Live Runner + Scope branch, PR #20 → main) so the consolidated gateway keeps BYOC payment working while gaining LR. ## What - `_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` — assemble the payment payload + orch-discovery capabilities per the resolved type. - `capabilities.py` — `CapabilityId.BYOC` + `byoc_capabilities_from_app()`. Two files, +52/−9. Byte-identical to what runs in prod today. ## Relationship to #41 PR #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 PR is the **superset**: per-signer switching keeps the legacy signer working *and* enables modern signers. Recommend closing #41 in favor of this. ## Follow-on The same per-signer type logic generalizes to the **live-runner** payment path, which will let us drop the `lr-gateway` `lv2v` workaround once this lands. ## Test - [ ] `python -m py_compile` (passes locally) - [ ] `submit_byoc_job` against `signer.daydream.live` → `type:lv2v` → 200 (no regression) - [ ] `submit_byoc_job` against a modern signer → `type:byoc` + caps proto → 200 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ibling to #46) (#47) Sibling to #46. #46 gives the **BYOC** payment path a per-signer dual-path; this gives the **Live Runner** payment path the same, so selecting an LR runner doesn't break payment on the default (Daydream) signer. ## The bug `_get_runner_payment` (live_runner.py) hardcoded `type="live"` for every non-scope runner. `signer.daydream.live` only accepts `lv2v` → **`400 invalid job type`** → LR payment fails. The `lr-gateway` `lv2v` patch only fixed the isolated sidecar; the gateway itself was still broken for LR + Daydream. Without this, the transparent-routing plan's **Scenario 1 (Daydream + LR) breaks**. ## The fix - Canonical `_payment_type_for_signer` moved to **`remote_signer.py`** (the module that owns `LivePaymentSession`) so **both** payment paths share it and LR does **not** import the soon-deprecated `byoc.py`. Legacy Daydream → `lv2v`; modern signers → `byoc`. - `_get_runner_payment`: Scope/LV2V stays `lv2v`; every other runner uses the per-signer switch. Two files, +28/−2. Both compile. ## Verified Generalizes the exact fix proven on-chain via the lr-gateway `lv2v` patch: LR single-shot + Daydream signer → `Payment tickets processed, totalTickets=1` on Arbitrum. ## Follow-up Once #46 merges, `byoc.py`'s local `_payment_type_for_signer` copy should import this canonical one (trivial dedup) — the two PRs touch disjoint files so they merge without conflict. ## Test - [ ] `py_compile` (passes locally) - [ ] LR + Daydream signer → `type:lv2v` → ticket redeems (the Scenario-1 gate) - [ ] LR + modern signer → `type:byoc` (gated on the pymthouse upstream fix + orch byoc-single-shot verification) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
No description provided.