feat(signer): charge BYOC live payments at the per-capability price#3967
feat(signer): charge BYOC live payments at the per-capability price#3967seanhanca wants to merge 3 commits into
Conversation
The remote signer hardcoded the create_signed_ticket `pipeline` to the lv2v constant whenever the gateway sent `type:"lv2v"` (which BYOC jobs always do for fee/pixel routing), and derived `model_id` only from the LiveVideoToVideo capability constraints. For BYOC capabilities (e.g. nano-banana) this emitted pipeline=live-video-to-video and an empty model_id, which the OpenMeter collector recorded as live-video-to-video / unknown. Add two additive, backward-compatible fields to RemotePaymentRequest: `capability` and `model_id`. When set by the gateway, they override the metered pipeline + model_id labels, decoupling usage attribution from `Type` (which still drives fee/pixel routing). When empty, behavior is byte-identical to before (lv2v constant + capabilities-derived model id; collector defaults empties to "unknown"). Label resolution is extracted into a pure resolveUsageLabels helper with a hermetic table test (TestResolveUsageLabels) that runs without the CGO ffmpeg toolchain.
The remote signer's GenerateLivePayment charged every BYOC generation a flat, model-independent fee: it read only oInfo.PriceInfo (the base price) and synthesized lv2v pixels (1280*720*30 * billableSecs), so nano-banana, recraft-v4, ltx-*, etc. all cost the same regardless of the per-capability prices the orchestrator already advertises in oInfo.CapabilitiesPrices. Resolve the fee per capability instead, keyed on req.Capability (added for metering by the stacked #3966): scan oInfo.CapabilitiesPrices for the Capability_BYOC entry whose Constraint matches the capability and charge that USD->wei/sec rate over compute-seconds (pixels = ceil(billableSecs)). This aligns the gateway's paid amount with the orchestrator's existing per-capability per-second accounting (byoc.JobPriceInfo * seconds). The resolved price is written back to oInfo.PriceInfo so it is the single source for state init, initialPrice, the max-price ceiling, the payment's ExpectedPrice (which the orch uses to set its fixed per-session price), and the price-doubling guard in validatePrice (which reads sess.OrchestratorInfo.PriceInfo) -- keeping all of them cap-vs-cap consistent and preventing a false "price more than doubled" rejection when base and cap diverge. Gated behind a new default-OFF flag (-byocPerCapPricing / LivepeerNode.ByocPerCapPricing). When OFF, or when req.Capability is empty, or when no usable cap price matches, behavior is byte-identical to the base-price path (zero regression). resolveByocPrice is a pure, hermetically testable function; ticket params (faceValue/winProb) are passed through unchanged so EV/ticket math stays valid. Tests: TestResolveByocPrice (resolution, fallback, non-BYOC/zero-rate ignored) and TestGenerateLivePayment_ByocPerCapPricing (flag-off zero-regression, per-cap seconds fee, 2:1 tariff ratio, unknown-cap fallback, doubling-guard not tripped), both CGO-free. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 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 |
There was a problem hiding this comment.
Pull request overview
Adds an optional (default-OFF) remote-signer path to charge BYOC live payments using the orchestrator-advertised per-capability price (keyed by request capability) and to bill on compute-seconds instead of LV2V synthetic pixels, aligning payment with orchestrator-side per-second accounting.
Changes:
- Add per-capability BYOC price resolution and flag-gated use in
GenerateLivePayment, with compute-second billing when enabled. - Plumb a
ByocPerCapPricingboolean from CLI config intocore.LivepeerNode. - Add hermetic unit tests for BYOC price resolution and end-to-end
GenerateLivePaymentbehavior under the flag.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
server/remote_signer.go |
Resolve per-cap BYOC price from CapabilitiesPrices, optionally override PriceInfo, and bill on compute-seconds when enabled. |
server/remote_signer_test.go |
Add tests for per-cap price resolution and flag ON/OFF fee behavior + ratios. |
core/livepeernode.go |
Add ByocPerCapPricing toggle to node config. |
cmd/livepeer/starter/starter.go |
Add config wiring for ByocPerCapPricing default + node assignment. |
cmd/livepeer/starter/flags.go |
Add -byocPerCapPricing CLI flag. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| priceInfo := oInfo.PriceInfo | ||
| useByocPricing := false | ||
| if ls.LivepeerNode.ByocPerCapPricing && req.Capability != "" { | ||
| if capPrice := resolveByocPrice(&req, &oInfo); capPrice != nil { | ||
| priceInfo = capPrice | ||
| oInfo.PriceInfo = capPrice | ||
| useByocPricing = true | ||
| } | ||
| } |
There was a problem hiding this comment.
Addressed the first part in 84c706a: BYOC pricing is now additionally gated on req.Type == RemoteType_LiveVideoToVideo, so it only applies to the lv2v job type BYOC jobs always use and can't change the billing basis for a non-lv2v request that happens to carry a capability.
On persisting the pricing mode in signed state: I left that out intentionally for now. The price is re-derived per request from oInfo.CapabilitiesPrices + req.Capability and written back to oInfo.PriceInfo, which is the single source for ExpectedPrice and the validatePrice doubling-guard, so a given request is internally consistent. -byocPerCapPricing is an operator/node-level flag (not per-session), so it can't flip mid-session; the only way to fall back to lv2v synthetic pixels within a session is if a later request omits Capability, which the gateway never does for BYOC. Threading the mode through the signed state blob is a heavier change (state is signed/validated) that I'd rather do as a dedicated follow-up if we want to defend against a misbehaving caller while the flag is on — happy to file that. Flagging for @eliteprox's call given it's default-OFF today.
Addresses Copilot review feedback on PR #3967: - resolveByocPrice now skips a matched-but-invalid (non-positive rate) entry and continues scanning instead of returning nil, so a later valid duplicate entry for the same constraint (e.g. from a misconfiguration) is still honored. Added a regression test case. (Copilot, remote_signer.go:431) - BYOC per-capability pricing is now additionally gated on Type==RemoteType_LiveVideoToVideo. Enabling it changes the billing basis (overrides req.InPixels and bypasses lv2v pixel synthesis), so it must only apply to the lv2v job type that BYOC jobs always use, never to a non-lv2v request that happens to carry a capability. (Copilot, remote_signer.go:488)
Stacked on #3966 (review/merge that first)
This PR is stacked on #3966 (
fix/byoc-usage-attribution, which addsRemotePaymentRequest.Capability/.ModelIDfor metering). Because #3966's head lives on a fork, this PR is opened against the shared basefeat/add-model-id-signer-kafka, so the diff currently shows two commits:023d25e9(= #3966) and597dbc62(this change). Review only597dbc62here (or merge #3966 first and rebase — the first commit then collapses). Gateway side: livepeer/livepeer-python-gateway#33 (no further gateway change needed). Config side: livepeer/simple-infra per-cap USD tariff PR.Summary
BYOC generations are charged a flat, model-independent fee today: the remote signer's
GenerateLivePaymentreads onlyoInfo.PriceInfo(the base price) and synthesizes lv2v pixels (1280*720*30 * billableSecs), so nano-banana, recraft-v4, ltx-*, etc. all cost the same — even though the orchestrator already advertises per-capability prices inoInfo.CapabilitiesPricesand already debits per-capability per-second on its side (byoc.JobPriceInfo * seconds). The advertised per-cap prices are dead on the charged path.This PR makes the signer resolve the fee per capability and charge it on compute-seconds, aligning the gateway's paid amount with the orchestrator's accounted amount.
Root cause:
priceInfo := oInfo.PriceInfo+ the lv2v synthetic-pixel basis ignoreoInfo.CapabilitiesPrices.The per-cap price chain this completes (a capability registers a price → it is actually paid + metered):
price_per_unit/currencyper capability → orch stores it → advertises it inoInfo.CapabilitiesPricesas{Capability: Capability_BYOC, Constraint: <name>}(already works);capabilityon/generate-live-payment(Node Status Reporting #33);oInfo.CapabilitiesPricesand chargescapPrice * billableSecs, minting the ticket at that price;computed_feeon thecreate_signed_ticketevent becomes the per-cap value → OpenMeter bills correctly with no collector/pymthouse change.Changes (file:line)
server/remote_signer.goresolveByocPrice(req, oInfo)— scansoInfo.CapabilitiesPricesfor theCapability_BYOCentry whoseConstraint == req.Capability; returns nil (→ base fallback) for no-match / empty capability / non-positive rate. Per-capability only (model_idis not used for price selection).GenerateLivePayment: whenByocPerCapPricingis on and a cap price resolves, use it and write it back tooInfo.PriceInfoso it is the single source for state init,initialPrice, the max-price ceiling, the payment'sExpectedPrice(orch's fixed per-session price viaProcessPayment), and the doubling guardvalidatePrice(which readssess.OrchestratorInfo.PriceInfo). Charge on seconds:pixels = ceil(billableSecs)with the resolved wei/sec rational (nocalculateFeechange).core/livepeernode.go:ByocPerCapPricing bool.cmd/livepeer/starter/{flags,starter}.go:-byocPerCapPricingCLI flag.Flag default-OFF / zero-regression
-byocPerCapPricingdefaults OFF. When OFF — orreq.Capability == ""— or no usable cap price matches, the code is byte-identical to today (base price + lv2v synthetic pixels;oInfountouched). Unconfigured/unknown capabilities always fall back to the base price. Ticket params (faceValue/winProb/recipientRandHash) come fromoInfo.TicketParamsunchanged; only the ticket count scales with the fee, so EV/ticket math stays valid. Writing the resolved price back tooInfo.PriceInfokeeps the doubling guard cap-vs-cap (prevents a false "price more than doubled" rejection when base and cap diverge).Test plan
Hermetic, CGO-free tests in
server/remote_signer_test.go:TestResolveByocPrice: resolves per cap; falls back (nil) on unknown/empty/zero-rate; ignores non-BYOC entries with the same constraint.TestGenerateLivePayment_ByocPerCapPricing: flag-OFF byte-identical base fee (zero-regression); flag-ONfee == capPrice * 60s; 2:1 tariff →fee(recraft-v4) == 2 * fee(nano-banana); unknown-cap fallback; base ≫ 2× cap returns 200 (doubling guard not tripped).TestResolveUsageLabelsandTestGenerateLivePayment_LV2V_Succeedsstill pass.Verified locally in a Linux container (CGO ffmpeg via
install_ffmpeg.sh):go build ./server/...+ the four tests above → PASS (theserverpackage needs CGO and does not build on macOS; relying on CI for the full suite).E2E recipe (staging, owner-gated)
feat-add-model-id-signer-kafka+ fix(signer): meter real BYOC capability + model_id for usage events #3966 + this) with-byocPerCapPricing=false.CapabilitiesPricesadvertise the expected rates.-byocPerCapPricing=truein staging; drivenano-bananavsrecraft-v4; assertcomputed_fee(recraft-v4) ≈ 2 × computed_fee(nano-banana)and OpenMeternetwork_fee_usd_microsdiffer by pipeline+model.Deployment note
The signer image (
livepeer/go-livepeer:feat-add-model-id-signer-kafka) must be rebuilt to include this change, and the orchestrator must have a USD price feed for the USD→wei conversion, before the flag is enabled. Do not merge or enable the flag until #3966 lands and rollout is signed off.Made with Cursor