Skip to content

feat: serve a generated OpenAPI spec from every Python runner - #69

Closed
rickstaa wants to merge 2 commits into
mainfrom
rs/hello-world-fastapi
Closed

feat: serve a generated OpenAPI spec from every Python runner#69
rickstaa wants to merge 2 commits into
mainfrom
rs/hello-world-fastapi

Conversation

@rickstaa

@rickstaa rickstaa commented Aug 12, 2026

Copy link
Copy Markdown
Member

Exploratory, for #33. Converts the four examples that run a server of ours, to size the change before deciding whether the convention is worth adopting. Not proposing to merge as is.

Cost

hello-world/runner.py            +35 / -30   (net  +5)
tiles/runner.py                  +43 / -41   (net  +2)
echo/runner.py                  +120 / -118  (net  +2)
realtime-transcription/runner.py +42 / -32   (net +10)

12 files, +254 / -222              net +32 lines

Line count is essentially a wash. The Pydantic models cost lines and delete more: await request.json(), the isinstance(payload, dict) guards, the HTTPBadRequest raises and the web.json_response wrapping all disappear because validation becomes declarative. echo even loses its hand-rolled _parse_mode validator and the MODES frozenset, replaced by a Literal on the model.

Dependencies: fastapi + uvicorn everywhere, plus websockets for realtime-transcription.

vllm and api-proxy cannot participate: one runs the stock vllm/vllm-openai image, the other is nginx. Neither has code of ours to generate a spec from.

Verified end to end

Each app was rebuilt and exercised on the offchain stack:

app check
hello-world {'message': 'Hello, Rick!'} through the orchestrator
tiles 9 tiles in 6.3 s, output image written
echo --mode blur and --mode robot both complete, robot returns h264 + opus
realtime-transcription full transcript with timestamps, ending on [FINAL]

The spec is reachable from the discovered url with a plain GET, no protocol change and no metadata entry:

$ curl -sk https://127.0.0.1:8935/apps/runner_7e7faxra/app/openapi.json
http=200 application/json
{"openapi":"3.1.0","info":{"title":"livepeer-example/hello-world",...

Two things worth knowing before deciding

WebSockets get no spec. OpenAPI 3.1 has no representation for them, so realtime-transcription serves /openapi.json with an empty paths. The one example whose interface is least guessable is the one this does not document. Its wire protocol stays in the README.

Declarative validation is not a drop-in. echo's blur sweep sends radius=0, which the aiohttp version silently clamped via max(1, min(99, ...)). Expressing that as Field(ge=1) turned it into a 422 and broke the sweep. Fixed by clamping in the handler as before, but it is a live example of behaviour changing under a mechanical-looking conversion.

The trade

Size is not the argument against this, which is what I expected to find and did not. The trade is that the examples currently demonstrate "your app is a plain service, being on the network does not change how you write it", and this makes them demonstrate a framework choice as well. Every runner gains build_app(), a lifespan contextmanager and a model layer that exist for the spec rather than for Livepeer.

Also worth noting for the paid path: fetching /openapi.json is a real call, so on a single-shot runner it reserves a session and is billable, the same as vllm's /v1/models.

rickstaa and others added 2 commits August 12, 2026 18:38
Discovery gives a caller an app id and a URL and nothing about the
interface, so the only way to learn what an app accepts is to read its
source. FastAPI derives the schema from the request and response models
and serves it at /openapi.json, which the orchestrator proxies like any
other endpoint, so a caller can fetch it from the discovered url.

Exploratory: one example only, to size the change before deciding whether
the convention is worth adopting across the repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Discovery gives a caller an app id and a URL and nothing about the
interface, so the only way to learn what an app accepts is to read its
source. FastAPI derives the schema from the request and response models
and serves it at /openapi.json, which the orchestrator proxies like any
other endpoint.

Converts the four examples that run a server of ours. vllm and api-proxy
are static runners with no code of ours, so they cannot participate.

Exploratory, to size the change rather than to merge as is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rickstaa rickstaa changed the title feat(hello-world): serve a generated OpenAPI spec via FastAPI feat: serve a generated OpenAPI spec from every Python runner Aug 12, 2026
@rickstaa

Copy link
Copy Markdown
Member Author

Closing. Not because of cost, which is what I set out to measure and was wrong about: the conversion is net +32 lines across 12 files, essentially a wash, because declarative validation deletes more than the models add.

Closing because it is too opinionated for this repo. Four of the six examples would use one framework, which reads as a requirement, and it directly contradicts what the examples exist to say: your app is a plain service and being on the network does not change how you write it. It also hides a real feature, that the SDK is framework-agnostic (LiveRunnerSessionRequest is a Protocol needing only .headers.get()), and it cannot reach vllm or api-proxy at all, so the result is four-of-six uniformity that looks like an abandoned standardisation.

Branch stays up for reference. Findings are summarised on #33.

@rickstaa rickstaa closed this Aug 12, 2026
@rickstaa rickstaa mentioned this pull request Aug 12, 2026
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.

1 participant