feat(plugin): document streaming function frames in OpenAPI [ASTD-350] - #1024
Merged
marcusds merged 3 commits intoAug 4, 2026
Merged
Conversation
Contributor
|
Streaming function routes registered with `response_model=None`, so FastAPI
emitted an empty 200 schema under `application/json` — untyped, and the wrong
media type for a route that returns `application/x-ndjson`. Generated clients
got back `unknown`, leaving every consumer to re-declare the frame union and
its type guards by hand.
A `NemoFunction` can now declare `frame_schema`, the frame type it yields, and
the route factory turns that into an `application/x-ndjson` response. The
anonymizer `preview` function declares its union, so the SDK now generates
`LogFrame`, `PreviewDatasetFrame`, `TraceDatasetFrame`, `FailedRecordsFrame`,
`Heartbeat`, `Done` and `Error`, and the client returns their discriminated
union instead of `unknown`.
Two details worth knowing for the next streaming function:
`NdjsonFrameResponse` subclasses `JSONResponse` despite never being
instantiated. FastAPI hardcodes the 200 schema to `{"type": "string"}` for any
response class that isn't a `JSONResponse` subclass, which silently discards
the declared model — subclassing it is what gets the union documented while
`media_type` still files it under `application/x-ndjson`.
The schema rides on `response_model` rather than `responses[200]["model"]`.
The latter deep-merges onto that same `{"type": "string"}` default, leaving a
schema that claims to be both a string and a union. Runtime is unchanged
either way: FastAPI skips response validation when a handler returns a
`Response`, which the streaming branch always does.
Functions that declare no `frame_schema` keep their existing registration, so
this is inert for every other function today.
Signed-off-by: mschwab <mschwab@nvidia.com>
marcusds
force-pushed
the
astd-350-streaming-function-response-schema/mschwab
branch
from
August 4, 2026 18:02
7ba1c2b to
f67ff47
Compare
marcusds
marked this pull request as ready for review
August 4, 2026 18:03
maxdubrinsky
approved these changes
Aug 4, 2026
marcusds
enabled auto-merge
August 4, 2026 18:05
Contributor
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughChangesNDJSON frame schema support
Sequence Diagram(s)sequenceDiagram
participant PreviewFunction
participant RouteRegistration
participant OpenAPI
PreviewFunction->>RouteRegistration: expose PreviewFrame as frame_schema
RouteRegistration->>OpenAPI: register NDJSON response schema
OpenAPI-->>RouteRegistration: publish discriminated frame variants
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Contributor
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plugins/nemo-anonymizer/openapi/openapi.yaml`:
- Around line 439-457: Make the kind field required in all seven frame source
models referenced by the oneOf schema, removing constructor defaults and
updating every constructor call to pass the appropriate discriminator value
explicitly. Then regenerate the OpenAPI document with
script/generate-openapi-spec.sh so each schema requires kind and
generated-client narrowing remains unambiguous.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 7c243464-48f7-43d8-9c92-2ea1a224acf6
📒 Files selected for processing (5)
packages/nemo_platform_plugin/src/nemo_platform_plugin/function.pypackages/nemo_platform_plugin/src/nemo_platform_plugin/functions/routes.pypackages/nemo_platform_plugin/tests/test_functions_routes.pyplugins/nemo-anonymizer/openapi/openapi.yamlplugins/nemo-anonymizer/src/nemo_anonymizer_plugin/functions/preview.py
…[ASTD-350] `kind` carries a default on every frame, so it was optional in the generated schema — and an optional discriminator does not discriminate. Clients got `kind?: 'log'`, which misrepresents the wire (the server always emits it) and undercuts the narrowing this whole change exists to provide. A `FrameModel` base promotes `kind` to required via `__get_pydantic_json_schema__`, leaving constructors untouched: `Done()` still works. Deliberately narrower than pydantic's `json_schema_serialization_defaults_required`, which promotes every defaulted field. That would catch `Error.details` and `TraceDatasetFrame.original_text_column` too — and since the spec pipeline collapses their `anyOf: [string, null]` down to `string`, they would end up documented as required *and* non-nullable while the server still sends null. Verified: FastAPI emits the nullable union correctly, so that collapsing is a pre-existing pipeline issue worth its own ticket rather than one to trip over here. Signed-off-by: mschwab <mschwab@nvidia.com>
marcusds
deleted the
astd-350-streaming-function-response-schema/mschwab
branch
August 4, 2026 20:11
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.
Streaming function routes were registered with
response_model=None, so FastAPI emitted an empty 200 schema under the wrong media type:Generated clients therefore returned
unknown, leaving every consumer to re-declare the frame union and its type guards by hand. ASTD-332 hand-rolled ~60 lines of exactly that. Same class of bug as ASTD-349 — the spec not describing what the service actually returns.Closes ASTD-350.
What changed
A
NemoFunctioncan now declareframe_schema— the frame type it yields — and the route factory turns that into anapplication/x-ndjsonresponse. The anonymizerpreviewfunction declares its union:The spec's 200 becomes the full
kind-discriminated union, and the generated TypeScript client's return type goes fromunknownto:with all seven emitted as SDK schemas.
Two FastAPI details worth knowing
Both cost me an experiment, so they're documented in the code for the next streaming function.
NdjsonFrameResponsesubclassesJSONResponsedespite never being instantiated.fastapi/openapi/utils.py:393hardcodes the 200 schema to{"type": "string"}for any response class that isn't aJSONResponsesubclass, silently discarding the declared model. Subclassing it is what gets the union documented, while the overriddenmedia_typestill files it underapplication/x-ndjson. Nothing is ever constructed from it — streaming handlers build their ownStreamingResponse, and FastAPI returns aResponseuntouched.The schema rides on
response_model, notresponses[200]["model"]. The latter deep-merges onto that same{"type": "string"}default, yielding a schema that claims to be both a string and a union. Runtime is unaffected either way: FastAPI skips response validation when a handler returns aResponse, which the streaming branch always does.Blast radius
Opt-in. Functions that declare no
frame_schemakeep their exact previous registration. Regenerating all nine plugin specs changed only the anonymizer's — data designer's streaming preview is untouched until it opts in.web/packages/sdk/generated/is gitignored, so the SDK changes here are reproduced bypnpm --filter @nemo/sdk gen:all-forcerather than committed. The Python SDK regen via Stainless is a separate step and isn't included.Testing
components, functions without aframe_schemabeing left alone, and streamed output being unchangednemo_platform_plugin, anonymizer and data designertyandruffclean; Studio typechecks against the regenerated SDKFollow-on
Once this lands, #1005 can drop its hand-written
PreviewFrameunion and guards frompreviewApi.tsand import the generated types. The streaming reader stays hand-written either way — orval doesn't generate incremental readers.Summary by CodeRabbit
New Features
Bug Fixes