Skip to content

feat(python): replace runtime SSE reflection with compile-time discriminant context#14183

Closed
jsklan wants to merge 6 commits intomainfrom
jsklan/python-sdk-sse-no-reflection-v2
Closed

feat(python): replace runtime SSE reflection with compile-time discriminant context#14183
jsklan wants to merge 6 commits intomainfrom
jsklan/python-sdk-sse-no-reflection-v2

Conversation

@jsklan
Copy link
Copy Markdown
Contributor

@jsklan jsklan commented Mar 27, 2026

Description

Replace runtime reflection-based SSE discriminant detection in the Python SDK generator with compile-time knowledge from the IR's discriminatorContext field. This eliminates per-event type introspection overhead that is critical for high-throughput SSE workloads like LLM inference.

The previous implementation used _get_discriminator_and_variants, _find_variant_by_discriminator, _get_field_annotation, and _is_string_type to reflect on Pydantic union types at runtime on every SSE event. The TypeScript and Go generators already solve this correctly by reading discriminatorContext from the IR at generation time.

Changes Made

  • Core utilities (pydantic_utilities.py x4 variants): Added parse_sse_data (data-level) and parse_sse_protocol (protocol-level) functions. Deleted parse_sse_obj and all reflection helpers (_get_discriminator_and_variants, _find_variant_by_discriminator, _get_field_annotation, _is_string_type)
  • Generator layer (endpoint_response_code_writer.py): Resolves SSE payload's UnionTypeDeclaration.discriminator_context from IR at generation time. Emits parse_sse_protocol for protocol-context endpoints and parse_sse_data for data-context endpoints
  • CoreUtilities class (core_utilities.py): Updated get_construct_sse to accept is_protocol parameter and reference the new functions
  • parse_sse_protocol string-variant fallback: Attempts json.loads first; if Pydantic validation fails (e.g. variant expects str but data was valid JSON like '{"status": "processing"}'), falls back to the raw string so string-typed variants work correctly
  • Sentinel for JSON parse failures: Uses _PARSE_FAILED = object() instead of None so that json.loads("null") returning Python None is correctly distinguished from actual parse failures
  • Updated README.md generator (if applicable) — N/A

Updates since last revision

  • Fixed json.loads("null") sentinel bug flagged by Devin Review: all 4 pydantic_utilities.py variants now use _PARSE_FAILED = object() instead of None as the parse-failure sentinel
  • Updated server-sent-event-examples seed output and hand-written test assertions to match new log messages

Testing

  • pnpm seed test --generator python-sdk --fixture server-sent-events-openapi --skip-scripts — 1/1 passed
  • pnpm seed test --generator python-sdk --fixture exhaustive --skip-scripts — 28/28 passed
  • pnpm seed:local test --generator python-sdk --fixture server-sent-event-examples — 1/1 passed (build + test scripts)
  • Verified protocol-context endpoints (streamProtocolNoCollision, streamProtocolCollision, streamProtocolWithFlatSchema) call parse_sse_protocol
  • Verified data-context endpoints (streamDataContext, streamNoContext, streamDataContextWithEnvelopeSchema, streamOasSpecNative) call parse_sse_data
  • Verified old reflection helpers are absent from generated pydantic_utilities.py

Human Review Checklist

  • Verify all 4 pydantic_utilities.py variants consistently use _PARSE_FAILED = object() sentinel (the cumulative diff truncation makes it hard to confirm the with_pydantic_v1_on_v2/with_aliases copy was updated)
  • Review the broad except Exception fallback in parse_sse_protocol — it intentionally catches validation errors to fall back to raw strings for string-typed variants, but could mask other issues
  • Confirm no other SSE-using seed fixtures need regeneration beyond server-sent-event-examples and server-sent-events-openapi

Link to Devin session: https://app.devin.ai/sessions/cf621d7834184fbab7e24ace259f8c77


Open with Devin

…minant context

Use the IR's discriminatorContext field to determine at code generation time
whether SSE endpoints use protocol-level or data-level discrimination, instead
of reflecting on Pydantic union types at runtime. Protocol-level endpoints now
call parse_sse_protocol and data-level endpoints call parse_sse_data, eliminating
type introspection overhead critical for LLM inference workflows.
@github-actions
Copy link
Copy Markdown
Contributor

🌱 Seed Test Selector

Select languages to run seed tests for:

  • Python
  • TypeScript
  • Java
  • Go
  • Ruby
  • C#
  • PHP
  • Swift
  • Rust
  • OpenAPI
  • Postman

How to use: Click the ⋯ menu above → "Edit" → check the boxes you want → click "Update comment". Tests will run automatically and snapshots will be committed to this PR.

@jsklan jsklan changed the title feat(python): replace runtime SSE reflection with compile-time discri… feat(python): replace runtime SSE reflection with compile-time discriminant context Mar 27, 2026
@jsklan jsklan marked this pull request as ready for review March 27, 2026 01:51
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

Comment on lines +168 to +174
envelope: Dict[str, Any] = {"event": sse.event}
if sse.data is not None:
try:
parsed_data = json.loads(sse.data)
except (json.JSONDecodeError, ValueError):
parsed_data = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 parse_sse_protocol drops SSE id/retry fields compared to old asdict(sse)

The old parse_sse_obj used asdict(sse) which included ALL fields of the ServerSentEvent dataclass (typically event, data, id, retry). The new parse_sse_protocol only constructs {"event": sse.event, "data": ...}, dropping id and retry. This is a semantic change for protocol-level discrimination. If any union variant model has an id field that should come from the SSE protocol-level id, it would now be missing. The generated seed output (seed/python-sdk/server-sent-events-openapi/with-wire-tests/src/seed/raw_client.py) suggests the current variant models only use event and data, so this appears intentional, but future variants that include id would silently lose that data.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits March 27, 2026 02:50
…s for new SSE parse functions

Co-Authored-By: bot_apk <apk@cognition.ai>
…ads("null") from parse failures

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

Fixed in f5c00ad — all 4 pydantic_utilities.py variants now use _PARSE_FAILED = object() as a sentinel instead of None, so json.loads("null") returning None is correctly distinguished from parse failures. The seed output for server-sent-event-examples has also been regenerated.

…sting mypy bug)

Co-Authored-By: judah <jsklan.development@gmail.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +174 to +177
except (json.JSONDecodeError, ValueError):
parsed_data = None

if parsed_data is not None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 parse_sse_protocol uses None as sentinel instead of unique object, mishandling JSON null

In parse_sse_protocol, parsed_data = None is used as the failure sentinel for json.loads, but json.loads("null") legitimately returns None. When sse.data is the string "null", json.loads succeeds and returns Python None, but parsed_data is not None evaluates to False, so the code falls through to envelope["data"] = sse.data (the raw string "null") instead of envelope["data"] = None (the parsed JSON null). This means JSON null data values are silently treated as unparseable strings.

All four source-of-truth template files correctly use _PARSE_FAILED = object() as a unique sentinel (e.g., generators/python/core_utilities/shared/pydantic_utilities.py:168), but this generated output diverges from the template, suggesting it was not regenerated after the template fix.

Prompt for agents
Regenerate the seed output for server-sent-events-openapi by running: pnpm seed test --generator python-sdk --fixture server-sent-events-openapi

The generated file seed/python-sdk/server-sent-events-openapi/with-wire-tests/src/seed/core/pydantic_utilities.py should match the template at generators/python/core_utilities/shared/pydantic_utilities.py, which correctly uses _PARSE_FAILED = object() as a unique sentinel in parse_sse_protocol (lines 168-176). The current generated output incorrectly uses parsed_data = None as the sentinel on lines 174-177, which breaks when sse.data is the JSON string "null".
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@fern-support fern-support marked this pull request as draft March 27, 2026 13:16
@jsklan jsklan closed this Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant