Skip to content

Reconcile M3 with upstream and add payment-intent hooks - #7

Closed
mikezupper wants to merge 8 commits into
releases/m3from
tasks/mz/payment-intent
Closed

mikezupper wants to merge 8 commits into
releases/m3from
tasks/mz/payment-intent

Conversation

@mikezupper

@mikezupper mikezupper commented Oct 27, 2026 •

Copy link
Copy Markdown

A signer can charge before its response is lost, cancelled or rejected by validation. Recording payment only in the success callback leaves consumers unable to attribute that charge or safely decide whether to fail over.

This brings releases/m3 up to the published releases/final baseline (2017a80) and adds optional hooks for durable intent:

  • Await on_payment_attempt(session) before the first signing request of each new session. A hook failure prevents signing.
  • Call on_payment_error(session, error) for signing or validation exceptions before a challenge-refresh retry. Consumers receive the original exception and manifest ID.
  • Retain a valid new signer state when rejecting missing or blank segment credentials, so the next metered renewal does not reuse a stale cursor. Blank segments are never posted; invalid types, missing state and invalid payment leave the prior state intact.
  • Keep upstream Add payment session callback for runner calls livepeer/livepeer-python-gateway#81's validated-success callback and prompt cancellation. Cancellation does not wait for an error hook or drain the signer request; previously recorded intent remains unresolved. Metered renewals use the existing session.

The old fork's early callback and cancellation drain are deliberately superseded. Builder-core's companion change records unknown intent before signing, prevents failover on an ambiguous outcome, and reconciles it against unique allocation/manifest usage evidence. These hooks provide the SDK boundary; persistence and the paid/unpaid/unknown policy belong to the consumer.

The history merge preserves the M3 ancestry and makes this PR directly mergeable. The implementation combines upstream 2017a80, the intent/error hooks and preservation of the signer cursor when blank segment credentials are rejected.

Validation: 343 tests passed; Ruff passed; wheel and source distribution built. Companion builder-core validation passed 975 tests with SQLite and PostgreSQL, including lost replies, malformed responses, cancellation, failed intent writes, restart recovery and later usage reconciliation.

j0sh and others added 7 commits October 21, 2026 16:11
Accept dicts, text, bytes, binary files, and async byte streams with
an optional content type. Send omitted and None payloads as empty
bodies.

Add StreamPayload(source=...) for streaming data sources. To accommodate
402 (payment) and other error response codes without consuming the payload,
the client sends an `Expect: 100-continue` and waits for the 100 before
transmitting the body. This is something that most HTTP servers do by
default already, including golang's. Bounded (non-streaming) inputs do
not wait for the 100.

In the case that there is still an error after payment, streaming input
is buffered for replay during runner failover (optional, but enabled by
default). Seekable files are rewound instead of buffered.

This is API-compatible with existing callers, with a few tweaks:

* `None` payloads now don't send a body, instead of `{}`
* `stream=True` (response streaming) now honors `timeout` instead of
  using a fixed 10s

Also remove an unused `Label` field from runner registration.
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.
Store token from `reserved` events and make them available to session
events.

Add LiveRunnerRegistration.stop_session() so apps can stop sessions
before receiving application traffic.
Updates `aiohttp` from 3.13.3 to 3.14.4
Updates `idna` from 3.11 to 3.15

@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 new hooks match the added tests. on_payment_attempt is awaited before the first signing request of a new session, and a failure there does not sign. on_payment_error receives the original signing or validation exception, including a blank segCreds, and a failure there aborts a challenge refresh. Cancelling while the signer wait is in progress does not call the error hook.

Requesting changes because the merge drops the releases/m3 rule that a blank segment still keeps the signer's new cursor. Metered renewals then resubmit the previous cursor, and these hooks never see that attempt.

Open in Web View Automation 

Sent by Cursor Automation: build-track code reviews

Comment thread src/livepeer_gateway/remote_signer.py
@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 gap from the last review is fixed. _payment_request stores a dict state and then raises PaymentError when segCreds is missing or "", so send_payment never POSTs that segment. A later get_payment or send_payment on the same session sends the new cursor. A non-string segCreds, a missing state, and a non-string payment still fail without replacing _state.

The payment-intent hooks are unchanged and match their tests: on_payment_attempt runs before the first sign and a failure there does not sign; on_payment_error receives the original exception and can abort a challenge refresh; cancellation during the signer wait does not call the error hook. Those hooks still wrap only the upfront get_payment. A metered renewal that gets a blank segment keeps the new cursor, logs the PaymentError, and retries on the next cycle.

tests/test_live_payment_session.py and TestPaymentSessionCallback passed (40).

Open in Web View Automation 

Sent by Cursor Automation: build-track code reviews

@eliteprox

eliteprox commented Oct 27, 2026 •

Copy link
Copy Markdown

I noticed your PR is based on https://github.com/NE-Build-Track/livepeer-python-gateway/tree/releases/m3
however, that branch is 5 commits behind and is missing the signer error handling that our cost attribution fixes were based on.

The current state of upstream for livepeer-python-gateway is following main branch. The commits previously made to releases/m3 can be dropped and synced with upstream. This is likely the root cause of testing errors. The current livepeer-gateway pin in livepeer-builder-core is livepeer@76a1a1c. See https://github.com/NE-Build-Track/livepeer-builder-core/blob/dec3e4b160bfc660b1040c39798ff9070afbddee/pyproject.toml#L25

@eliteprox

eliteprox commented Oct 27, 2026 •

Copy link
Copy Markdown

Superseded by NE-Build-Track/livepeer-builder-core#61. That PR now includes NE-Build-Track/livepeer-builder-core#62 (decision D53), which was merged into its branch.

builder-core no longer needs SDK hooks to record payment intent. It now owns the paid runner call (adapters/runner_call.py, adapted from call_runner at 2017a80, MIT) and still signs through the SDK's signer client (get_signer_info, LivePaymentSession, run_payments). The call reports each manifest at three points:

When Reported as
Before the signer is asked unknown. If recording it fails, nothing is signed
The signer explicitly refuses unpaid. This uses the same refusal set as here: 400, 401, 402, 403, 481, a 480 refresh, a 482 skip
The payment validates signed, before the paid request goes out

A lost or malformed reply, a signer 5xx, or a cancellation leaves the manifest unknown. The job then pays no other orchestrator. The SDK stays on upstream 2017a80 with no fork pin, and test_sdk_contract.py fails if a pin bump changes any of the copied call_runner code.

Reference: NE-Build-Track/livepeer-builder-core#56, rebased onto that work, is on https://github.com/NE-Build-Track/livepeer-builder-core/tree/rebase/payment-intent-on-62. On that branch:

  • SdkRunnerTransport implements PaymentIntentTransport and PaymentIntentStreamingTransport over the engine's own call.
  • The fork pin is dropped.
  • The decision is renumbered D45 → D54, because D45 is JWT bearer on releases/m3.
  • The intent tests run against a loopback runner and signer instead of patching SDK internals.

It passes 1141 tests (27 PostgreSQL cases skipped locally). tasks/mz/payment-intent is unchanged.

Not covered by builder-core: this PR's last commit, 202721c (keep the signer's state when rejecting blank segCreds). Metered renewals still run through the SDK's run_payments(), so that fix is still worth sending upstream on its own.

I'm leaving this PR open for you to close.

@mikezupper

Copy link
Copy Markdown
Author

Closed. Builder-core no longer needs this change in the SDK. Each part of it now lives in builder-core, which pins upstream livepeer-python-gateway 2017a80, with no fork commit:

  • Bringing the fork up to 2017a80: builder-core pins upstream 2017a80 directly (builder-core#61, merged).
  • The success callback before the paid request, and the error hook: the engine now makes the paid call itself (adapters/runner_call.py, D53): on_signed and note_failure (builder-core#61, merged).
  • Storing the intent before signing (on_payment_attempt), failing closed: builder-core#64 (D54, in review). The manifest is stored as an unknown session before the signer is asked, and nothing is signed if that write fails.
  • Keeping the signer state when segCreds is blank: not needed. livepeer/node 406b248, the signer we run, always returns segCreds with a successful payment. Tech-debt T20 (builder-core#66, merged) records this, to be rechecked on every node pin bump.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

superseded Replaced by other work; kept for reference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants