Skip to content

feat(runner): report each signed payment session from call_runner - #3

Closed
eliteprox wants to merge 4 commits into
releases/finalfrom
ja/payment-callback
Closed

eliteprox wants to merge 4 commits into
releases/finalfrom
ja/payment-callback

Conversation

@eliteprox

@eliteprox eliteprox commented Oct 20, 2026 •

Copy link
Copy Markdown

Summary

This PR brings upstream livepeer/livepeer-python-gateway#81 onto releases/final and names/exports LivePaymentSessionCallback once. call_runner takes an optional on_payment_session callback, and LivePaymentSession.manifest_id exposes the manifest id from the 402 challenge so a gateway can match the session to a Clearinghouse usage record.

#4 removed the in-session callback from #1. Callers that don't pass on_payment_session see no change in results, errors, or streams.

Why

livepeer-builder-core collects session.manifest_id from on_payment_session in SdkRunnerTransport and treats a reported session as payment sent. Keeping our fork branch aligned with upstream livepeer#81 plus the LivePaymentSessionCallback type alias avoids carrying custom cancellation/shielding logic in the fork.

What callers see

manifest_ids: list[str] = []
result = await call_runner(
    ...,
    on_payment_session=lambda s: manifest_ids.append(s.manifest_id),
)
  • When it runs. Once for each new payment session, after the signer returns a payment string and before the paid request goes to the runner. The callback can be sync or async.
  • What it reports. Each session the signer signs, including one the body then rejects, one followed by a runner failure, and each new session after another 402. session.manifest_id is the challenge's manifest id. A blank segCreds (None or "") is reported, then call_runner raises LivepeerGatewayError (Live runner payment response missing segCreds). A non-string segCreds or a missing state is reported, then raises PaymentError. A blank segment still stores a dict state, so the next metered renewal sends that cursor.
  • What it doesn't report. Signer refusals and responses with no payment string, which raise before the callback, and metered renewals of a session already reported.
  • If the callback raises, the paid request isn't sent, and the caller gets the callback's error unchanged without retrying as a new challenge.
  • If the call is cancelled, cancellation waits for the in-progress signer request and the callback. Callback errors still propagate.

Known limits

  • livepeer-builder-core still pins e6d243b. After this merges, that pin should move to the new releases/final head. The on_payment_session and manifest_id contract the adapter uses is unchanged.

Test plan

  • uv run pytest at 91cefc0: 246 passed. The new tests cover a blank segCreds renewal sending the new cursor, and call_runner raising LivepeerGatewayError for that body with and without a callback.
  • uv run ruff check on the touched files: the new lines are clean. remote_signer.py still has four pre-existing UP045 Optional annotations that this change does not touch.
  • SonarQube local scan (pymthouse_livepeer-python-gateway) of remote_signer.py and tests/test_live_payment_session.py: 0 issues.
  • Snyk code scan of remote_signer.py and live_runner.py: 0 issues. The tests directory still has pre-existing low-severity hardcoded secrets in older tests.

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

The split between a signed payment and a usable one is right: _signed is set only after a payment field, unusable segCreds / state still reach the callback, and a signer refusal does not. The cancel path does not keep the same promise once the callback awaits.

_drain_runner_payment finishes the signer request across repeated cancellations, then the callback runs unprotected. A second cancel() while an async callback is awaiting ends the task immediately, so the manifest id is never recorded, and contextlib.suppress(Exception) does not catch that CancelledError. Callback exceptions on this path are discarded as well. The docstring says those exceptions propagate.

That matters for the gateway this is for. SdkRunnerTransport only copies manifest_ids onto RunnerCallError. CancelledError skips that handler, and the attempt stays in_flight with no sessions. An async callback that is cancelled mid-write never gets a second chance to persist the id.

Invalid segCreds now raises PaymentError from get_payment instead of the old LivepeerGatewayError ("missing segCreds") after a successful return. PaymentError is a subclass, but the builder adapter maps PaymentError to failure kind payment and a bare LivepeerGatewayError to http. Re-pinning changes that kind. payment_sent still follows the callback, so failover stays blocked.

Open in Web View Automation 

Sent by Cursor Automation: build-track code reviews

Comment thread src/livepeer_gateway/live_runner.py Outdated
Comment thread src/livepeer_gateway/live_runner.py Outdated
@eliteprox
eliteprox changed the base branch from releases/final to ja/revert-call-runner-manifest-id October 20, 2026 13:59
Support sync and async on_payment_session callbacks in call_runner and
expose LivePaymentSession.manifest_id for payment attribution. Invoke the
callback for each new session.

Validate segCreds when parsing signer responses and remove redundant
caller checks. Cover callback ordering, failures, and cancellation.
@eliteprox
eliteprox force-pushed the ja/payment-callback branch from f56d858 to 784249d Compare October 20, 2026 14:00
@eliteprox
eliteprox changed the base branch from ja/revert-call-runner-manifest-id to releases/final October 20, 2026 14:01
Use LivePaymentSessionCallback for every on_payment_session parameter instead of repeating the callable type, and export it from the package.
@eliteprox
eliteprox force-pushed the ja/payment-callback branch from 784249d to 30fa1ef Compare October 20, 2026 15:36

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

A signer response that already contains a payment can still fail the call without ever reporting the session. That drops the manifest id SdkRunnerTransport stores, so the job is recorded as unpaid.

on_payment_session runs only after get_payment() returns. _payment_request now raises PaymentError for a missing, empty, or non-string segCreds, and it already raised for a missing state, before that return. The pinned contract (e6d243b, upstream livepeer#77) reports the session as soon as payment is a non-empty string, then rejects an unusable body. The builder treats that callback as proof the signer charged the allocation and copies manifest_id onto the attempt. With this order, a body such as {"payment": "...", "segCreds": ""} or a missing state becomes PaymentError with payment_sent=False and no sessions. Kind payment is not a failover outcome, so this attempt does not pay a second runner, but the charge is invisible to cost matching and a later retry can pay again.

Cancellation has the same hole once the signer has produced that payment. test_cancellation_remains_prompt cancels the callback on its first await and only checks that the task ends. The callback can return without recording manifest_id. The livepeer#77 path kept the payment task running, including the callback, and then raised CancelledError. Signer refusals and a missing payment field should still skip the callback.

Callback exceptions do propagate, and a runner failure after a successful sign still reports the session. Those parts match the tests.

Open in Web View Automation 

Sent by Cursor Automation: build-track code reviews

Comment thread src/livepeer_gateway/live_runner.py Outdated
Comment thread tests/test_live_runner.py Outdated
Comment thread tests/test_live_runner.py Outdated

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

The cancellation and attribution gaps from the last pass are fixed: a payment string is reported before an unusable segCreds or state fails the call, and cancellation waits until that callback finishes.

One regression is still in the shared signer parse. Blank segCreds used to be rejected only after a dict state was stored. It is now rejected first, so a metered renewal retries with the previous cursor after the signer has already advanced.

The PR description still says invalid signer bodies skip the callback and that cancellation stays prompt. The docstring matches the new behavior; the description does not.

Open in Web View Automation 

Sent by Cursor Automation: build-track code reviews

Comment thread src/livepeer_gateway/remote_signer.py
eliteprox added a commit to NE-Build-Track/livepeer-builder-core that referenced this pull request Oct 20, 2026
Update the livepeer-gateway pin to NE-Build-Track/livepeer-python-gateway#3 (upstream #81, 9c084b6) and adjust documentation to reflect changes in the SDK's payment session reporting contract.
A metered renewal was resubmitting the previous cursor because a blank
segCreds failed before the new state was saved. call_runner again raises
LivepeerGatewayError for that body, after the payment callback.
@sonarqubecloud

Copy link
Copy Markdown

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

The blank-segment cursor bug from the last pass is fixed. A dict state is stored before a missing or empty segCreds is rejected, and the next metered sign sends that cursor.

send_payment still raises PaymentError (Signer returned a payment with no segCreds) after that store, so run_payments retries the new cursor instead of the previous one. call_runner still reports the session, skips the paid request, and raises a bare LivepeerGatewayError (Live runner payment response missing segCreds), including when no callback is passed. A non-string segCreds or a missing state still raises _SignedPaymentError before self._state is replaced, then the callback runs. Signer refusals and a body with no payment string still skip the callback.

Cancellation still waits until that callback finishes, and a callback failure still propagates. The renewal test covers None and "".

Open in Web View Automation 

Sent by Cursor Automation: build-track code reviews

@eliteprox eliteprox closed this Oct 21, 2026
@eliteprox
eliteprox deleted the ja/payment-callback branch October 21, 2026 18:10
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.

2 participants