Skip to content

Live Runner + Scope support - #20

Open
j0sh wants to merge 68 commits into
mainfrom
ja/live-runner
Open

Live Runner + Scope support#20
j0sh wants to merge 68 commits into
mainfrom
ja/live-runner

Conversation

@j0sh

@j0sh j0sh commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added runnable demos for video echo, websocket ping/pong, and text/SSE streaming.
    • Added runner discovery, selection, session reservation, streaming calls, and improved payment handling.
    • Added media frame, packet, and byte callbacks with lifecycle management.
    • Expanded public gateway exports and error reporting.
  • Bug Fixes
    • Improved media segment rollover, shutdown handling, signer validation, and HTTP error handling.
  • Documentation
    • Added setup and usage instructions for all new examples and test workflows.
  • Tests
    • Added broad coverage for discovery, runners, streaming, media processing, payments, and shutdown behavior.

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds gateway HTTP, discovery, runner selection, payment, media callback, Scope, and media rollover functionality. It adds echo, ping-pong, and text runner examples, plus tests, coverage settings, CI execution, and test documentation.

Changes

Gateway core

Layer / File(s) Summary
HTTP, discovery, errors, and signer handling
src/livepeer_gateway/http.py, src/livepeer_gateway/discovery.py, src/livepeer_gateway/errors.py, src/livepeer_gateway/remote_signer.py, src/livepeer_gateway/__init__.py, src/livepeer_gateway/orchestrator.py
Adds shared HTTP helpers, discovery APIs, typed errors, async signer payment support, compatibility aliases, and public exports.
Runner lifecycle, payment, and selection
src/livepeer_gateway/live_runner.py, src/livepeer_gateway/selection.py
Adds runner registration, trickle and proxy operations, streaming calls, payment retries, GPU detection, runner selection, and session reservation.
Scope and media callback integration
src/livepeer_gateway/scope.py, src/livepeer_gateway/channel_reader.py, src/livepeer_gateway/media_output.py, src/livepeer_gateway/lv2v.py
Moves Scope startup to asynchronous runner selection and adds callback consumers with lifecycle and error handling.
Media publish rollover
src/livepeer_gateway/media_publish.py
Separates segment draining from EOF closure and updates idle rollover and write-failure handling.

Runner examples

Layer / File(s) Summary
Echo video runner flow
examples/echo/*
Adds a video echo runner and client with frame transformations, runtime updates, trickle channels, and streamed output.
Ping-pong websocket runner
examples/ping-pong/*
Adds runner registration, websocket ping validation, timing responses, client discovery, and usage documentation.
Text streaming runner
examples/text/*
Adds text, SSE, and health endpoints with static runner configuration, story content, and run instructions.

Validation and tooling

Layer / File(s) Summary
Gateway behavior tests
tests/test_channel_reader.py, tests/test_discovery.py, tests/test_live_payment_session.py, tests/test_live_runner.py, tests/test_selection.py, tests/test_start_scope.py, tests/test_token.py, tests/test_websocket_example.py, tests/test_control_keepalive.py
Adds coverage for gateway APIs, runner lifecycle, payment flows, selection, Scope startup, token parsing, keepalive behavior, and the websocket example.
Media pipeline and shutdown tests
tests/test_media_publish.py, tests/test_stats_pull.py, tests/test_decoder_queue_metrics.py, tests/test_trickle_shutdown_races.py
Adds coverage for media rollover, statistics, callbacks, decoder metrics, queue behavior, and shutdown races.
CI and test configuration
pyproject.toml, .github/workflows/tests.yml, README
Raises the Python requirement to 3.12, adds test and coverage configuration, runs tests in CI, and documents test commands.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant EchoClient
  participant RunnerSelector
  participant EchoRunner
  participant MediaPublish
  EchoClient->>RunnerSelector: reserve_session
  RunnerSelector->>EchoRunner: POST /echo
  EchoRunner->>MediaPublish: publish transformed frames
  EchoClient->>EchoRunner: POST /update blur mode
Loading
sequenceDiagram
  participant PingClient
  participant RunnerSelector
  participant PingRunner
  PingClient->>RunnerSelector: discover runner
  PingClient->>PingRunner: websocket ping timestamp
  PingRunner-->>PingClient: pong and delta_ms
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: adding Live Runner and Scope support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ja/live-runner

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/livepeer_gateway/channel_reader.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/media_output.py Fixed
Comment thread src/livepeer_gateway/channel_reader.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/media_output.py Fixed
@j0sh
j0sh force-pushed the ja/live-runner branch from d0d3453 to 2bc02a7 Compare May 27, 2026 16:29
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Outdated
runner_url: str,
app: str,
price_per_unit: int = 0,
pixels_per_unit: int = 1,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@j0sh yea no blocker from my side I see this as a nice follow up improvement for consistency. I am tracking this in #27.

Comment thread src/livepeer_gateway/live_runner.py Outdated
secret: str,
runner_url: str,
app: str,
price_per_unit: int = 0,

@rickstaa rickstaa Jun 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, the runner itself reports the price because the orchestrator is intended to control the runner.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/livepeer_gateway/channel_reader.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/media_output.py Fixed
Comment thread src/livepeer_gateway/channel_reader.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/media_output.py Fixed
seanhanca added a commit that referenced this pull request Jul 18, 2026
…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>
@j0sh
j0sh marked this pull request as ready for review July 27, 2026 18:50
coderabbitai[bot]

This comment was marked as spam.

Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
coderabbitai[bot]

This comment was marked as spam.

Comment thread src/livepeer_gateway/http.py
Comment thread src/livepeer_gateway/http.py
Comment thread src/livepeer_gateway/http.py
Comment thread tests/test_media_publish.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
tests/test_media_publish.py (1)

730-730: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused unpacked segment. Rename to _segment to silence Ruff RUF059.

♻️ Proposed tweak
-        media, segment = self._build_drain_media(fail_after=1)
+        media, _segment = self._build_drain_media(fail_after=1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_media_publish.py` at line 730, In the test setup using
_build_drain_media(fail_after=1), rename the unused unpacked segment variable
from segment to _segment to satisfy Ruff RUF059 while preserving the media value
and test behavior.

Source: Linters/SAST tools

tests/test_stats_pull.py (2)

216-226: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Class-level instances registries leak across tests. Only two tests clear them (Lines 564, 894) before asserting len(instances) == 1; any future test that instantiates these fakes without clearing will break those assertions. An autouse fixture or pytest.fixture that clears both lists would make this robust. Ruff also flags RUF012 here.

Also applies to: 262-272

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_stats_pull.py` around lines 216 - 226, Update the
_TrackingPacketDemuxer and corresponding fake class registries to avoid
class-level mutable defaults flagged by RUF012, and add an autouse pytest
fixture that clears both instances lists before each test. Remove reliance on
individual test cleanup while preserving the existing length assertions.

Source: Linters/SAST tools


467-474: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace hand-rolled module-attribute swaps with mock.patch.object. The original_x = mod.X; mod.X = fake; try/finally: mod.X = original_x pattern is repeated ~10 times; with mock.patch.object(media_output_mod, "MpegTsDecoder", lambda: _FakeDecoder([...])): (or pytest's monkeypatch) removes the boilerplate and the # type: ignore noise, and is restore-safe by construction.

Also applies to: 508-515, 532-540, 548-556, 565-572, 619-626, 661-668, 720-727, 895-902, 958-970, 1177-1192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_stats_pull.py` around lines 467 - 474, Replace the manual
save/assign/try-finally restoration pattern around MpegTsDecoder and the other
repeated module-attribute swaps in the affected tests with mock.patch.object
context managers (or pytest monkeypatch). Keep each test’s existing fake
implementation and assertions unchanged while removing the original-value
variables, explicit restoration, and associated type-ignore comments.
tests/test_live_payment_session.py (1)

16-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clear the signer-info cache before the test too. Currently only teardown clears it, so a cached entry populated by another module (get_signer_info is process-global) can make test_get_signer_info_caches_result observe zero calls and fail.

♻️ Proposed tweak
     `@pytest.fixture`(autouse=True)
     def clear_signer_info_cache(self):
+        get_signer_info.cache_clear()
         yield
         get_signer_info.cache_clear()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_live_payment_session.py` around lines 16 - 19, Update the
clear_signer_info_cache fixture to call get_signer_info.cache_clear() before
yielding as well as during teardown, ensuring each test starts with an empty
process-global signer-info cache while preserving cleanup afterward.
tests/test_websocket_example.py (1)

13-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Load the example module lazily so missing example deps skip instead of breaking collection. spec.loader.exec_module runs at import time, so any optional dependency (e.g. a websocket client) or side effect in examples/ping-pong/runner.py turns into a collection error for the whole session rather than a skipped test.

♻️ Proposed refactor
-spec = importlib.util.spec_from_file_location("websocket_runner_example", RUNNER_PATH)
-assert spec is not None
-runner = importlib.util.module_from_spec(spec)
-assert spec.loader is not None
-spec.loader.exec_module(runner)
+@pytest.fixture(scope="module")
+def runner():
+    spec = importlib.util.spec_from_file_location(
+        "websocket_runner_example", RUNNER_PATH
+    )
+    assert spec is not None and spec.loader is not None
+    module = importlib.util.module_from_spec(spec)
+    try:
+        spec.loader.exec_module(module)
+    except ImportError as exc:
+        pytest.skip(f"ping-pong example unavailable: {exc}")
+    return module

Then take runner as a test argument in both test methods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_websocket_example.py` around lines 13 - 17, Move the
example-module loading currently performed at test module scope into a pytest
fixture, importing it lazily via spec.loader.exec_module and skipping when
optional dependencies are unavailable. Update both test methods to accept the
fixture-provided runner instead of relying on the global runner object, while
preserving their existing assertions.
tests/test_control_keepalive.py (1)

72-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Patching control_mod.asyncio.sleep mutates the global asyncio module. control_mod.asyncio is the stdlib module object, so this replaces asyncio.sleep process-wide for the duration of the block; any other coroutine that sleeps during that window will block on permits. It works today because these tests are serial, but it is fragile under -p xdist/parallel loops. Prefer injecting the sleep function into Control (or exposing a module-level _sleep alias in control.py) and patching that instead. Same pattern at Lines 106 and 131.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_control_keepalive.py` around lines 72 - 78, Stop patching
control_mod.asyncio.sleep in the keepalive tests because it mutates the shared
stdlib asyncio module; introduce a Control-level sleep dependency or
module-level _sleep alias in control.py, use it from the keepalive
implementation, and update the patches at the referenced test cases (including
the occurrences around lines 106 and 131) to target that isolated symbol
instead.
pyproject.toml (1)

39-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Set asyncio_default_fixture_loop_scope explicitly. With pytest-asyncio ≥0.24 in auto mode, leaving this unset emits a deprecation warning when async fixtures use loop-scoped execution; setting it now keeps test output clean and future-proofs the event-loop scope.

♻️ Proposed tweak
 asyncio_mode = "auto"
+asyncio_default_fixture_loop_scope = "function"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 39 - 46, Update the [tool.pytest.ini_options]
configuration to set asyncio_default_fixture_loop_scope explicitly, choosing the
intended fixture event-loop scope and keeping it consistent with the existing
asyncio_mode = "auto" behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/tests.yml:
- Line 16: Update the actions/checkout step in the tests workflow to set
persist-credentials to false, ensuring the checkout token is unavailable to
subsequent pytest and test steps.

In `@tests/test_decode_metrics_sim.py`:
- Around line 1-8: Add the missing livepeer_gateway.decode_metrics_sim module
exposing _actual_decoder_snapshot and simulate_decoder_metric_drift so
tests/test_decode_metrics_sim.py imports successfully; implement the expected
simulation behavior used by the test, and make its timing or drift calculation
deterministic enough to preserve the max_abs_drift bounds without relying on
wall-clock scheduling.

In `@tests/test_multi_track_verify.py`:
- Line 6: Provide the missing livepeer_gateway.multi_track_verify module or
update the test import to the correct existing module. Ensure the module exposes
_goertzel_power, _verify_audio_track, _match_video_tracks, default_audio_specs,
default_video_specs, ObservedAudioTrack, ObservedVideoTrack, and
VideoFrameObservation so tests can collect without ImportError.

In `@tests/test_stats_pull.py`:
- Around line 1094-1109: Update
test_decoder_output_wait_metrics_accumulate_blocked_get_time to remove the tight
output_wait_s upper-bound assertion, retaining the >= 0.02 lower bound that
verifies blocked wait time accumulation.

In `@tests/test_trickle_shutdown_races.py`:
- Around line 154-155: Update the pytest.raises call around publisher.next() to
use a raw string for the regular-expression match pattern, preserving the
existing “closed|closing” alternatives and exception assertion.

---

Nitpick comments:
In `@pyproject.toml`:
- Around line 39-46: Update the [tool.pytest.ini_options] configuration to set
asyncio_default_fixture_loop_scope explicitly, choosing the intended fixture
event-loop scope and keeping it consistent with the existing asyncio_mode =
"auto" behavior.

In `@tests/test_control_keepalive.py`:
- Around line 72-78: Stop patching control_mod.asyncio.sleep in the keepalive
tests because it mutates the shared stdlib asyncio module; introduce a
Control-level sleep dependency or module-level _sleep alias in control.py, use
it from the keepalive implementation, and update the patches at the referenced
test cases (including the occurrences around lines 106 and 131) to target that
isolated symbol instead.

In `@tests/test_live_payment_session.py`:
- Around line 16-19: Update the clear_signer_info_cache fixture to call
get_signer_info.cache_clear() before yielding as well as during teardown,
ensuring each test starts with an empty process-global signer-info cache while
preserving cleanup afterward.

In `@tests/test_media_publish.py`:
- Line 730: In the test setup using _build_drain_media(fail_after=1), rename the
unused unpacked segment variable from segment to _segment to satisfy Ruff RUF059
while preserving the media value and test behavior.

In `@tests/test_stats_pull.py`:
- Around line 216-226: Update the _TrackingPacketDemuxer and corresponding fake
class registries to avoid class-level mutable defaults flagged by RUF012, and
add an autouse pytest fixture that clears both instances lists before each test.
Remove reliance on individual test cleanup while preserving the existing length
assertions.
- Around line 467-474: Replace the manual save/assign/try-finally restoration
pattern around MpegTsDecoder and the other repeated module-attribute swaps in
the affected tests with mock.patch.object context managers (or pytest
monkeypatch). Keep each test’s existing fake implementation and assertions
unchanged while removing the original-value variables, explicit restoration, and
associated type-ignore comments.

In `@tests/test_websocket_example.py`:
- Around line 13-17: Move the example-module loading currently performed at test
module scope into a pytest fixture, importing it lazily via
spec.loader.exec_module and skipping when optional dependencies are unavailable.
Update both test methods to accept the fixture-provided runner instead of
relying on the global runner object, while preserving their existing assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 73d9f912-6504-4b40-9eb7-3b78e6919415

📥 Commits

Reviewing files that changed from the base of the PR and between 9f2bc20 and 533a2ad.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .github/workflows/tests.yml
  • README
  • pyproject.toml
  • src/livepeer_gateway/media_publish.py
  • tests/test_byoc_training.py
  • tests/test_channel_reader.py
  • tests/test_control_keepalive.py
  • tests/test_decode_metrics_sim.py
  • tests/test_discovery.py
  • tests/test_live_payment_session.py
  • tests/test_live_runner.py
  • tests/test_media_publish.py
  • tests/test_multi_track_verify.py
  • tests/test_selection.py
  • tests/test_start_scope.py
  • tests/test_stats_pull.py
  • tests/test_token.py
  • tests/test_trickle_shutdown_races.py
  • tests/test_websocket_example.py
💤 Files with no reviewable changes (1)
  • tests/test_byoc_training.py

pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpt =="
if [ -f .github/workflows/tests.yml ]; then
  cat -n .github/workflows/tests.yml
else
  echo ".github/workflows/tests.yml not found"
fi

echo
echo "== checkout usages =="
rg -n "actions/checkout|persist-credentials|pytest|pull_request|permissions:" .github/workflows || true

echo
echo "== workflow files list =="
git ls-files .github/workflows

Repository: livepeer/livepeer-python-gateway

Length of output: 1465


🌐 Web query:

actions/checkout default persist-credentials true README

💡 Result:

In the actions/checkout GitHub Action, the persist-credentials input controls whether the authentication token or SSH key is configured in the local git configuration [1][2][3]. When set to true (which remains the default), the credentials are made available to subsequent steps in the job to allow authenticated git operations [1][2]. The action automatically removes these credentials during the post-job cleanup phase [1][2]. Starting with version 6.0.0, the action improved security by storing these credentials in a separate file under the $RUNNER_TEMP directory instead of writing them directly into the.git/config file [2][4]. While there has been community discussion and proposals to change the default value of persist-credentials to false to minimize security risks [5][6], the default value remains true in the current official documentation and action configuration [2][3]. Users who wish to disable this behavior and prevent credentials from being available to other steps in their workflow can explicitly set persist-credentials: false [1][2].

Citations:


Disable checkout credential persistence

This workflow runs pull-request code via pytest and does not need authenticated Git operations after checkout. Set persist-credentials: false so the checkout token is not made available to later test steps.

Proposed fix
-      - uses: actions/checkout@v7
+      - uses: actions/checkout@v7
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v7
- uses: actions/checkout@v7
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/tests.yml at line 16, Update the actions/checkout step in
the tests workflow to set persist-credentials to false, ensuring the checkout
token is unavailable to subsequent pytest and test steps.

Source: Linters/SAST tools

Comment thread tests/test_decode_metrics_sim.py Outdated
Comment thread tests/test_multi_track_verify.py Outdated
import math
from array import array

from livepeer_gateway import multi_track_verify as verify_mod

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'multi_track_verify*' .
rg -n 'multi_track_verify' --glob '!tests/test_multi_track_verify.py'
rg -n '_goertzel_power|default_audio_specs|ObservedAudioTrack|_match_video_tracks' -g '!tests/**'

Repository: livepeer/livepeer-python-gateway

Length of output: 205


🏁 Script executed:

#!/bin/bash
set -e

echo "== file listing =="
git ls-files | sed -n '1,200p'

echo "== test import and referenced symbols =="
cat -n tests/test_multi_track_verify.py

echo "== package files and livepeer_gateway references =="
fd -i 'livepeer_gateway' . || true
rg -n 'livepeer_gateway|multi_track_verify|package|packages' pyproject.toml setup.py setup.cfg poetry.lock uv.lock Pipfile requirements*.txt tests/test_multi_track_verify.py || true

Repository: livepeer/livepeer-python-gateway

Length of output: 50389


Add the missing multi_track_verify package module. tests/test_multi_track_verify.py imports livepeer_gateway.multi_track_verify, but only the test file exists; no matching module or referenced helper symbols are present, so collection fails with ImportError. Add src/livepeer_gateway/multi_track_verify.py with _goertzel_power, _verify_audio_track, _match_video_tracks, default_audio_specs, default_video_specs, ObservedAudioTrack, ObservedVideoTrack, and VideoFrameObservation, or point the import at the correct package module.

🧰 Tools
🪛 GitHub Actions: Tests / 0_pytest.txt

[error] 6-6: Pytest import error during test collection: ImportError: cannot import name 'multi_track_verify' from 'livepeer_gateway' ('/home/runner/work/livepeer-python-gateway/livepeer-python-gateway/src/livepeer_gateway/init.py').

🪛 GitHub Actions: Tests / pytest

[error] 6-6: Pytest failed during test collection due to import error: ImportError: cannot import name 'multi_track_verify' from 'livepeer_gateway' (/home/runner/work/livepeer-python-gateway/livepeer-python-gateway/src/livepeer_gateway/init.py).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_multi_track_verify.py` at line 6, Provide the missing
livepeer_gateway.multi_track_verify module or update the test import to the
correct existing module. Ensure the module exposes _goertzel_power,
_verify_audio_track, _match_video_tracks, default_audio_specs,
default_video_specs, ObservedAudioTrack, ObservedVideoTrack, and
VideoFrameObservation so tests can collect without ImportError.

Source: Pipeline failures

Comment thread tests/test_stats_pull.py
Comment on lines +1094 to +1109
def test_decoder_output_wait_metrics_accumulate_blocked_get_time(self) -> None:
decoder = MpegTsDecoder()

def _put_later() -> None:
time.sleep(0.03)
decoder._put_output_item(object())

producer = threading.Thread(target=_put_later, daemon=True)
producer.start()
got = decoder.get()
producer.join()

assert got is not None
stats = decoder.get_stats()
assert stats.output_wait_s >= 0.02
assert stats.output_wait_s < 0.25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wall-clock assertion may be flaky under load. output_wait_s < 0.25 after a 0.03s sleep is tight for a loaded CI runner where the producer thread can be descheduled. Consider dropping the upper bound (or widening it substantially) and keeping only the >= 0.02 lower bound, which is what the metric actually needs to prove.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_stats_pull.py` around lines 1094 - 1109, Update
test_decoder_output_wait_metrics_accumulate_blocked_get_time to remove the tight
output_wait_s upper-bound assertion, retaining the >= 0.02 lower bound that
verifies blocked wait time accumulation.

Comment on lines +154 to +155
with pytest.raises(RuntimeError, match="closed|closing"):
await publisher.next()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a raw string for the regex. Ruff (RUF043) flags the unescaped alternation; the intent is a regex, so mark it raw.

🩹 Proposed fix
-        with pytest.raises(RuntimeError, match="closed|closing"):
+        with pytest.raises(RuntimeError, match=r"closed|closing"):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with pytest.raises(RuntimeError, match="closed|closing"):
await publisher.next()
with pytest.raises(RuntimeError, match=r"closed|closing"):
await publisher.next()
🧰 Tools
🪛 Ruff (0.16.0)

[warning] 154-154: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_trickle_shutdown_races.py` around lines 154 - 155, Update the
pytest.raises call around publisher.next() to use a raw string for the
regular-expression match pattern, preserving the existing “closed|closing”
alternatives and exception assertion.

Source: Linters/SAST tools

Comment thread tests/test_decoder_queue_metrics.py Fixed
Comment thread tests/test_decoder_queue_metrics.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_decoder_queue_metrics.py (1)

149-167: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace getattr with direct attribute access.

getattr(decoder, "_reader"), getattr(reader, "_queue"), getattr(decoder, "_output"), and getattr(reader, "_buffer") use a constant attribute name. Direct attribute access is equivalent and clearer. Ruff flags all four calls (B009).

♻️ Proposed refactor
 def _actual_decoder_snapshot(decoder: object) -> tuple[int, int, int, int]:
-    reader = getattr(decoder, "_reader")
-    input_queue = getattr(reader, "_queue")
+    reader = decoder._reader
+    input_queue = reader._queue
     with input_queue.mutex:
         input_items = list(input_queue.queue)
     queued_payloads = [
         item
         for item in input_items
         if isinstance(item, (bytes, bytearray, memoryview))
     ]
-    output_queue = getattr(decoder, "_output")
+    output_queue = decoder._output
     with output_queue.mutex:
         output_items_queued = len(output_queue.queue)
     return (
         len(queued_payloads),
         sum(len(item) for item in queued_payloads),
-        len(getattr(reader, "_buffer")),
+        len(reader._buffer),
         output_items_queued,
     )

As per static analysis hints, Ruff flags getattr with a constant attribute value at Lines 150, 151, 159, and 165 (B009).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_decoder_queue_metrics.py` around lines 149 - 167, Update
_actual_decoder_snapshot to replace the four constant-name getattr calls with
direct attribute access on decoder and reader: _reader, _queue, _output, and
_buffer. Preserve the existing queue locking, filtering, and snapshot
calculations.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_decoder_queue_metrics.py`:
- Around line 149-167: Update _actual_decoder_snapshot to replace the four
constant-name getattr calls with direct attribute access on decoder and reader:
_reader, _queue, _output, and _buffer. Preserve the existing queue locking,
filtering, and snapshot calculations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c640a1e8-2c98-446e-b181-efa7cdea3c5d

📥 Commits

Reviewing files that changed from the base of the PR and between 533a2ad and dfee3ce.

📒 Files selected for processing (3)
  • tests/test_decoder_queue_metrics.py
  • tests/test_live_payment_session.py
  • tests/test_media_publish.py

Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
Comment thread src/livepeer_gateway/channel_reader.py Fixed
Comment thread src/livepeer_gateway/live_runner.py Fixed
j0sh and others added 22 commits July 31, 2026 14:08
…prod image into ja/live-runner (#46)"

This reverts commit 4e46379.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Content-type detection was a substring test (`"json" in content_type`),
which is right for `+json` types only by accident and wrong for
multi-document formats: `application/jsonl` (this repo's own trickle
channel default), `application/x-ndjson`, and `application/json-seq`
all matched and then failed in json.loads, so a working runner fronting
a streaming API got "did not return valid JSON" instead of its bytes —
the same bug class this branch set out to fix.

Match on the media subtype instead, via aiohttp's own mimetype parser:
`application/json` or the RFC 6839 `+json` structured suffix. Vendor
types (`application/vnd.acme.v1+json`) keep parsing without being
listed; multi-document bodies fall through to `raw`. Only four
classifications change, all previously raising.

Also fold the raw early return into the single existing return, so
`payment_session=None if payment_type == "fixed" else payment_session`
stays in one place — payment-type handling here has been reverted twice
and there is no paid-binary test to catch the two sites drifting. The
`session_id` expression reduces to the early return's behavior when
`data` is empty, so this is equivalent.

Add `content_type` to the JSON parse error: it is the fact that routed
the response into parsing, and without it a misclassification is
undebuggable from the message alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`content_type` is self-documenting from the field name; the invariant
worth stating is that a populated `raw` means an empty `data`. The
subtype helper matches its bare neighbors in this module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`raw` already means "the original JSON dict, un-normalized" on three
dataclasses in this SDK — LiveRunnerSessionEvent.raw, LiveRunnerInstance.raw,
LiveVideoToVideo.raw — and examples dump it with json.dumps(x.raw). A
`raw: Optional[bytes]` on LiveRunnerCallResult overloads the name to mean the
opposite, sharply so on one expression chain: `result.raw` (bytes) sitting
one dot from `result.runner.raw` (dict).

`content` matches its sibling `content_type` and the ecosystem convention for
a response body as bytes (requests/httpx `.content`) — where `.raw` instead
means an unread stream object, so the old name actively misled. Nothing
consumes the field yet (runner-app-examples#45 and api-proxy both use the
streaming path), so this is free now and permanent once released.

Semantics unchanged: still `Optional[bytes] = None`, since `b""` is a valid
empty body and `None` is the only unambiguous "this was JSON" sentinel. Also
renames the local `raw` to `body` — it holds bytes, while `raw` in this
codebase reads as a dict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One helper builds every SDK request and hardcodes Accept:
application/json. That is right for the control plane, where the
orchestrator is a JSON API, but call_runner is the one function whose
destination is arbitrary app code. An upstream that content negotiates
then answers in JSON because the SDK asked: the api-proxy example
advertises raw JPEG bytes and Hugging Face returned a base64 PNG in a
JSON string, so the client wrote base64 text to a .jpg.

App calls now state no preference and let the app pick.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@j0sh
j0sh force-pushed the ja/live-runner branch from d2d84f0 to 0fa8963 Compare July 31, 2026 21:08
@j0sh

j0sh commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

apologies for the force-push, I didn't realize updating GitHub stacked PRs would also change the base branch ... ugh

if not task.done():
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants