Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/livepeer/starter/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ func NewLivepeerConfig(fs *flag.FlagSet) LivepeerConfig {
cfg.RemoteSignerWebhookURL = fs.String("remoteSignerWebhookUrl", *cfg.RemoteSignerWebhookURL, "Authentication webhook URL called by remote signer during GenerateLivePayment")
cfg.RemoteSignerWebhookHeaders = fs.String("remoteSignerWebhookHeaders", *cfg.RemoteSignerWebhookHeaders, "Map of headers to use for remote signer webhook requests. e.g. 'header:val,header2:val2'")
cfg.RemoteDiscovery = fs.Bool("remoteDiscovery", *cfg.RemoteDiscovery, "Enable orchestrator discovery on remote signers")
cfg.ByocPerCapPricing = fs.Bool("byocPerCapPricing", *cfg.ByocPerCapPricing, "Remote signer: resolve BYOC live-payment fee from the orchestrator's per-capability CapabilitiesPrices (keyed on the request capability) instead of the flat base price. Default OFF (byte-identical to base-price behavior).")

// Gateway metrics
cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers")
Expand Down
6 changes: 6 additions & 0 deletions cmd/livepeer/starter/starter.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ type LivepeerConfig struct {
RemoteSignerWebhookURL *string
RemoteSignerWebhookHeaders *string
RemoteDiscovery *bool
ByocPerCapPricing *bool
AIRunnerImage *string
AIRunnerImageOverrides *string
AIVerboseLogs *bool
Expand Down Expand Up @@ -324,6 +325,7 @@ func DefaultLivepeerConfig() LivepeerConfig {
defaultRemoteSignerWebhookURL := ""
defaultRemoteSignerWebhookHeaders := ""
defaultRemoteDiscovery := false
defaultByocPerCapPricing := false

// Gateway logs
defaultKafkaBootstrapServers := ""
Expand Down Expand Up @@ -455,6 +457,7 @@ func DefaultLivepeerConfig() LivepeerConfig {
RemoteSignerWebhookURL: &defaultRemoteSignerWebhookURL,
RemoteSignerWebhookHeaders: &defaultRemoteSignerWebhookHeaders,
RemoteDiscovery: &defaultRemoteDiscovery,
ByocPerCapPricing: &defaultByocPerCapPricing,

// Gateway logs
KafkaBootstrapServers: &defaultKafkaBootstrapServers,
Expand Down Expand Up @@ -1878,6 +1881,9 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) {
if cfg.RemoteDiscovery != nil {
n.RemoteDiscovery = *cfg.RemoteDiscovery
}
if cfg.ByocPerCapPricing != nil {
n.ByocPerCapPricing = *cfg.ByocPerCapPricing
}
if cfg.LiveAIHeartbeatHeaders != nil {
n.LiveAIHeartbeatHeaders = parseHeaderMap(*cfg.LiveAIHeartbeatHeaders)
}
Expand Down
1 change: 1 addition & 0 deletions core/livepeernode.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ type LivepeerNode struct {
RemoteEthAddr ethcommon.Address // eth address of the remote signer
InfoSig []byte // sig over eth address for the OrchestratorInfo request
RemoteDiscovery bool // expose remote discovery endpoint when enabled
ByocPerCapPricing bool // resolve BYOC fee from per-capability CapabilitiesPrices instead of base price (remote signer; default OFF)

// Thread safety for config fields
mu sync.RWMutex
Expand Down
115 changes: 108 additions & 7 deletions server/remote_signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
"math"
"math/big"
"net/http"
"net/url"
Expand Down Expand Up @@ -247,6 +248,20 @@ type RemotePaymentRequest struct {

// Capabilities to include in the ticket. Optional; may be set for the lv2v job type.
Capabilities []byte `json:"capabilities"`

// Capability is the real BYOC capability name (e.g. "nano-banana"). When
// set, it overrides the metered `pipeline` label for the emitted
// create_signed_ticket usage event, decoupling usage attribution from
// `Type` (which stays "lv2v" for fee/pixel routing). Optional; empty
// preserves the previous behavior (pipeline falls back to the lv2v
// constant when Type=="lv2v").
Capability string `json:"capability,omitempty"`

// ModelID is the specific provider model from the request
// payload/parameters. When set, it overrides the metered `model_id`
// label. Optional; empty preserves the previous behavior (the
// capabilities-derived model id, which defaults to "unknown" downstream).
ModelID string `json:"model_id,omitempty"`
}

// Returned by the remote signer and includes a new payment plus updated state.
Expand Down Expand Up @@ -355,6 +370,69 @@ func (ls *LivepeerServer) authLivePayment(r *http.Request, state *RemotePaymentS
return *webhookResp.Status, &webhookResp, errors.New(webhookResp.Reason)
}

// resolveUsageLabels derives the metering `pipeline` and `model_id` labels for
// the emitted create_signed_ticket usage event.
//
// The real BYOC capability/model supplied by the gateway (req.Capability /
// req.ModelID) take precedence so BYOC jobs are attributed to their true
// pipeline + model. When those additive fields are empty, the labels fall back
// to the previous behavior: for lv2v the pipeline is the live-video-to-video
// constant and the model id is derived from the request capabilities; for any
// other request both remain empty (the collector then defaults them to
// "unknown"). This makes non-BYOC (lv2v) callers byte-identical to before and
// keeps usage attribution decoupled from `Type`, which still drives fee/pixel
// routing.
func resolveUsageLabels(req *RemotePaymentRequest, caps *core.Capabilities) (pipeline, modelID string) {
if req.Type == RemoteType_LiveVideoToVideo {
pipeline = PipelineLiveVideoToVideo
modelID = caps.ModelIDForCapability(core.Capability_LiveVideoToVideo)
}
if req.Capability != "" {
pipeline = req.Capability
}
if req.ModelID != "" {
modelID = req.ModelID
}
return pipeline, modelID
}

// resolveByocPrice resolves the per-capability BYOC price advertised in the
// orchestrator's OrchestratorInfo.CapabilitiesPrices, keyed on the request
// capability name (req.Capability). The orchestrator appends external BYOC
// capability prices to CapabilitiesPrices as
// {Capability: Capability_BYOC, Constraint: <capability name>} (see
// core/orchestrator.go GetCapabilitiesPrices), so a match is a BYOC entry whose
// Constraint equals req.Capability.
//
// Granularity is per-capability only: req.ModelID is intentionally NOT used for
// price selection (model_id still flows through for metering attribution).
//
// Returns nil when there is no usable per-capability price (no capability set,
// no matching entry, or a non-positive rate). Callers fall back to the base
// oInfo.PriceInfo in that case, preserving today's behavior for unconfigured or
// misconfigured capabilities. This is a pure function (no flag, no IO) so it is
// hermetically testable without the CGO/ffmpeg toolchain.
func resolveByocPrice(req *RemotePaymentRequest, oInfo *net.OrchestratorInfo) *net.PriceInfo {
if req == nil || oInfo == nil || req.Capability == "" {
return nil
}
for _, p := range oInfo.CapabilitiesPrices {
if p == nil {
continue
}
if p.Capability != uint32(core.Capability_BYOC) || p.Constraint != req.Capability {
continue
}
// Only honor a valid positive rate; otherwise fall back to base so a
// zero/invalid advertised cap price never zeros out or breaks the fee.
if p.PricePerUnit <= 0 || p.PixelsPerUnit <= 0 {
return nil
}
return &net.PriceInfo{PricePerUnit: p.PricePerUnit, PixelsPerUnit: p.PixelsPerUnit}
Comment thread
Copilot marked this conversation as resolved.
}
return nil
}

// GenerateLivePayment handles remote generation of a payment for live streams.
func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Request) {
requestID := string(core.RandomManifestID())
Expand Down Expand Up @@ -389,7 +467,25 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req
respondJsonError(ctx, w, err, http.StatusBadRequest)
return
}
// BYOC per-capability pricing (flag-gated, default OFF). When enabled and the
// gateway supplied a real capability, resolve the per-capability price from
// the orchestrator's advertised CapabilitiesPrices and use it instead of the
// flat base price. 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 orchestrator 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. When the flag is OFF or no usable cap price matches, behavior is
// byte-identical to the base-price path.
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
}
}
Comment on lines 484 to +497

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

if priceInfo == nil || priceInfo.PricePerUnit == 0 || priceInfo.PixelsPerUnit == 0 {
err := fmt.Errorf("missing or zero priceInfo")
respondJsonError(ctx, w, err, http.StatusBadRequest)
Expand Down Expand Up @@ -528,7 +624,17 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req
lastUpdate = now
}
billableSecs := now.Sub(lastUpdate).Seconds()
if req.Type == RemoteType_LiveVideoToVideo {
if useByocPricing {
// BYOC per-capability tariff is denominated per compute-second (wei/sec),
// so charge on seconds directly rather than synthesizing lv2v pixels. With
// pixels = ceil(billableSecs) and the resolved per-cap price kept as its
// reduced wei/sec rational, calculateFee yields capPriceWeiPerSec * seconds.
if billableSecs <= 0 {
// preload with 60 seconds, mirroring the lv2v first-call behavior
billableSecs = (60 * time.Second).Seconds()
}
pixels = int64(math.Ceil(billableSecs))
} else if req.Type == RemoteType_LiveVideoToVideo {
info := defaultSegInfo
if billableSecs <= 0 {
// preload with 60 seconds of data for LV2V
Expand Down Expand Up @@ -680,12 +786,7 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req
if state.SequenceNumber == 0 {
sessionStatus = "new"
}
pipeline := ""
modelID := ""
if req.Type == RemoteType_LiveVideoToVideo {
pipeline = PipelineLiveVideoToVideo
modelID = streamParams.Capabilities.ModelIDForCapability(core.Capability_LiveVideoToVideo)
}
pipeline, modelID := resolveUsageLabels(&req, streamParams.Capabilities)
// NB: This could could drop events if tha Kafka queue is full!
monitor.SendQueueEventAsync("create_signed_ticket", map[string]interface{}{
"session_id": state.StateID,
Expand Down
Loading
Loading