feat(runner): minimal Pipeline SDK + BYOC hello-world E2E#7
Draft
feat(runner): minimal Pipeline SDK + BYOC hello-world E2E#7
Conversation
Adds livepeer_gateway.runner — a Pipeline ABC and a thin aiohttp serve
layer — plus a hello-world example that runs end-to-end against an
unmodified go-livepeer BYOC stack.
Surface:
- livepeer_gateway.runner.Pipeline — ABC with predict()
- livepeer_gateway.runner.serve(pipeline) → aiohttp app:
- POST /predict — body JSON kwargs to predict();
TypeError → 400, other exception → 500
- GET /health — {"status": "ready"}
- examples/runner/hello_world/ — Pipeline subclass + Dockerfile +
docker-compose + capability registration + e2e curl test
The container's /predict path matches the existing go-livepeer BYOC
contract — no go-livepeer changes required.
./examples/runner/hello_world/test.sh printing PASS proves the round-trip:
curl → gateway → orchestrator → SDK container → response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipeline.setup() is a non-abstract no-op called once before serve() accepts requests. Subclasses override to load model weights. Adds examples/runner/sentiment/ — a Pipeline subclass that classifies text via Hugging Face transformers. setup() loads the distilbert model from the local HF cache populated at build time by prepare_models.py. Surface: - Pipeline.setup() no-op default - make_app() invokes pipeline.setup() before binding routes - examples/runner/sentiment/ — pipeline + prepare_models + Dockerfile + docker-compose + register + test.sh + README Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracks operational items not suited to code comments — examples extraction trigger, BYOC offchain compose cleanup pending #3906, SDK feature gaps mapped to planned commits, related upstream PRs. Working surface, drained as items land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the aiohttp serve layer with FastAPI + uvicorn. Pipeline API
unchanged — Pipeline.predict() and Pipeline.setup() behave identically.
Free additions from FastAPI:
- GET /docs (Swagger UI)
- GET /redoc
- GET /openapi.json (minimal until Input/Output land)
Handler dispatch:
- /predict and /health are sync def, so pipeline.predict() (CPU/GPU
bound) runs in FastAPI's threadpool and never blocks the event loop.
- Request body parsed via Body(...) — framework handles JSON parse
errors and dict-type validation, returning HTTP 422.
Notes:
- Error response shape changes from {"error": ...} to {"detail": ...}.
Body validation errors return 422 (was 400 in aiohttp). Other status
codes unchanged: TypeError on wrong predict() kwargs → 400; pipeline
exceptions → 500.
- aiohttp stays in deps; livepeer_gateway.transport's trickle client
uses aiohttp.ClientSession. FastAPI server + aiohttp client coexist.
Refs #8 (C3)
Switches expose: to ports: so /docs, /redoc, and /openapi.json are browsable on http://localhost:5000 during dev. Example READMEs updated.
…tion predict()'s signature drives FastAPI's body type and response model. Two paths: - Explicit BaseModel param: pass body to predict() directly - Bare typed params: auto-derive a Pydantic model via create_model and unpack as kwargs OpenAPI now reflects real types — /docs shows declared fields with descriptions, examples, constraints, and typed responses when the return annotation is a BaseModel. Refs #8 (C4)
…se64Bytes Swin2SR x2 super-resolution as a BYOC capability. Input image is a base64-encoded JPEG/PNG; output is a base64-encoded PNG. Pydantic's Base64Bytes auto-decodes the request body to bytes, so the pipeline gets bytes directly and the SDK ships zero binary-handling code. Refs #8 (C5)
Pipeline tracks state across setup() and exposes it via /health, matching go-livepeer's HealthCheck wire format (ai/worker/runner.gen.go). Re-raises on setup() failure so the container still exits fail-fast. Refs #8 (C6)
When predict() is a generator, the SDK wraps the response with StreamingResponse(text/event-stream) and frames each yielded value as an OpenAI-style SSE event terminated by [DONE]. Both go-livepeer's BYOC gateway and the Python caller-side gateway watch for [DONE] to end the stream. Co-authored-by: John | Elite Encoder <john@eliteencoder.net>
pricePerUnit=0 means no orchestrator charges, no ticket settlement, empty wallet stays unused. Replaces the previous pricePerUnit=1 workaround that relied on tickets rarely firing.
f2a0b1c to
9c920ff
Compare
Adds LivePipeline base class with setup/on_stream_start/process_video/ process_audio/on_params_update/on_stream_stop hooks (all default-passthrough) plus emit_event/emit_data stubs. Splits make_app dispatch into _make_pipeline_app (Pipeline → /predict) and _make_live_pipeline_app (LivePipeline → /stream/start|stop|params), sharing _run_setup and _add_health_route. Routes accept and validate the orchestrator's wire contract; streaming coordinator (subscribe/publish loops, lifecycle dispatch) lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds _run_passthrough coroutine that bridges subscribe → publish trickle channels segment-by-segment using the existing TrickleSubscriber and TricklePublisher. /stream/start spawns it as a background task on the LivePipeline; /stream/stop cancels and waits up to 5s for graceful cleanup before returning. Single-session for now (409 on double-start); data-only / event-only streams (no subscribe_url + publish_url) return 400 — both extensions land in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Pipeline SDK — both halves: a request/response
Pipelinefor batch HTTP/SSE and aLivePipelinefor real-time bidirectional video/audio over BYOC trickle. Developers write a single Python class and get a containerised, BYOC-compatible, schema-described capability with/health,/docs,/openapi.json, SSE streaming, and (when complete)/stream/*real-time endpoints. No go-livepeer changes required.Each commit on this branch ships a strictly more capable SDK than the last;
test.shstays green at every step.Authoring surfaces
Batch / streaming HTTP —
Pipeline:For binary I/O, swap
text: strforimage: Base64Bytes. For streaming, return a generator frompredict()— the runtime auto-detects and frames each yielded value as SSE.Real-time A/V —
LivePipeline(in progress in this PR):Subclasses override any of
setup,on_stream_start,process_video,process_audio,on_params_update,on_stream_stop(all default to no-op / passthrough). A subclass that overrides nothing is a valid passthrough relay.What's in this PR
Commit-by-commit progression (each shippable on its own):
PipelineABC withpredict()+ aiohttpserve()+ first BYOC docker-composesetup()lifecycle + sentiment example/docs,/openapi.json,/redocfor freepredict()params with type hints become a Pydantic model automatically; explicitBaseModelparameter also supported;/docsshows real fieldsBase64Bytes)Base64Bytesproves the SDK handles binary I/O cleanly/healthstate machineLOADING / OK / ERROR / IDLEmatching go-livepeer'sHealthCheckwire formatpredict()→text/event-streamwith[DONE]terminator; Qwen2.5-0.5B example/stream/*HTTP skeleton (landed)LivePipelinebase class +_make_live_pipeline_appdispatch path; routes accept the orchestrator's wire contract (validated via Pydantic); streaming logic lands in subsequent commits/stream/start|stop(landed)_run_passthroughbridges subscribe → publish via existingTrickleSubscriber/TricklePublisher, segment-aligned and unmodified./stream/startspawns the bridge as a background task on the pipeline;/stream/stopcancels and waits up to 5s for cleanup. Single-session for now (409 on double-start).MediaOutput/MediaPublish; introducelivepeer_gateway.runner.framesnamespace (cleanVideoFrame/AudioFramealiases)on_stream_start/on_params_update/on_stream_stopdispatch;emit_event/emit_dataover events / data trickle channels; introduce_LiveSessionto encapsulate per-session stateexamples/runner/live_grayscale/end-to-end (planned)register_capability,test.shexercising real stream lifecycleSurface
livepeer_gateway.runner.Pipelinesetup()and abstractpredict(**kwargs) -> Anylivepeer_gateway.runner.LivePipelineprocess_video/process_audiolivepeer_gateway.runner.PipelineStateLOADING / OK / ERROR / IDLEenum; matches go-livepeerHealthCheckformatlivepeer_gateway.runner.serve(pipeline, *, host, port)PipelinevsLivePipelinelivepeer_gateway.runner.make_app(pipeline)POST /predict(Pipeline)predict()'s signature; returns JSON ortext/event-streamifpredict()is a generatorPOST /stream/start | stop | params(LivePipeline)GET /healthHealthResponse { status: PipelineState }— orchestrator-alignedGET /docs,GET /openapi.json,GET /redocexamples/runner/hello_world/register_capability+ curltest.shexamples/runner/sentiment/setup()lifecycle + HF sentiment classifierexamples/runner/image_upscale/Base64Bytes— Swin2SR ~2x super-resolutionexamples/runner/llm/TextIteratorStreamer— Qwen2.5-0.5B-Instructexamples/runner/live_grayscale/(planned)LivePipeline+ go-livepeer trickleThe container's
/predictand/stream/*paths match the existing go-livepeer BYOC contract verified againstbyoc/stream_orchestrator.go. No go-livepeer changes required.Authoring patterns (Pipeline)
Test plan
Each example ships its own
test.shthat printsPASSon success.uv run python examples/runner/hello_world/pipeline.py→curl localhost:5000/{health,predict}returns expected JSON.cd examples/runner/hello_world && docker compose up -d --wait && ./test.sh→PASS. Round-trip: curl → gateway → orchestrator → SDK container.setup()loads HF model once;test.shexercisesPOSITIVE/NEGATIVEcases viaEXPECTED_LABEL.Base64Bytesround-trip; output asserted to be at least 2x input dimensions.curl -N; assertion validates token framing +[DONE]terminator./docsand/openapi.jsonrender for every example with the actual field names (noadditionalProp1)./healthreturnsLOADINGduringsetup(),OKafter,ERRORon setup failure./stream/start|stop|paramsaccept the orchestrator's wire contract; missing required fields → 422;/docsand/openapi.jsonshow the new routes; batchPipelineregression-checked.finallyblocks.live_grayscaleexample, full stream lifecycle (start → frames → stop) — lands with C8 Step 5.Compose details
Each example's
docker-compose.ymlmirrors go-livepeer/doc/byoc.md:livepeer/go-livepeer:masterfor orchestrator + gateway (no local build prerequisite)pricePerUnit 0→ no real chain interaction, no funded wallet-network offchainonce livepeer/go-livepeer#3906 ships in:master— TODO comments in each compose track the cleanupprepare_models.pysosetup()loads from local cache in millisecondsWhat's next (separate PRs, after this one merges)
Tracked in #8 — Pipeline SDK roadmap:
livepeer pushCLI +livepeer.yamlmanifestorg.livepeer.pipeline.schema)AGENTS.md, expanded docstrings,examples/runner/_template/)livepeer.runner.*,livepeer.client.*,livepeer.trickle.*)Related work
test.sh's curl-with-base64-Livepeer-header for a Pythonlivepeer_gatewaybatch caller built on PR feat(byoc): BYOC streaming and payments with examples #6's signing primitives — at that point thegatewayservice can be dropped from compose. TODO intest.shtracks.🤖 Generated with Claude Code