fix: preserve discriminated union variants - #370
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a03969729a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
apcha-oai
left a comment
There was a problem hiding this comment.
The keepalive fix is needed, but filtering out every keyed variant after discriminator lookup fails is not safe for existing generated SDK models. Several real unions register keyed variants whose discriminator is intentionally optional or nullable.
Repro 1: valid Responses message without type
require "openai"
state = OpenAI::Internal::Type::Converter.new_coerce_state
value = OpenAI::Internal::Type::Converter.coerce(
OpenAI::Responses::ResponseInputItem,
{role: "user", content: "hello"},
state: state
)
p value.class
p state[:error]Current behavior: OpenAI::Models::Responses::EasyInputMessage, with no error.
With this change: the original Hash plus ArgumentError: no matching variant.
EasyInputMessage#type is optional, and the SDK's own README and examples/structured_outputs_responses.rb use message hashes without type.
The corresponding model accessor also becomes a real exception:
OpenAI::Responses::ResponseCreateParams.new(
input: [{role: "user", content: "hello"}]
).input
# => OpenAI::Errors::ConversionErrorRepro 2: nullable item-reference discriminator
state = OpenAI::Internal::Type::Converter.new_coerce_state
value = OpenAI::Internal::Type::Converter.coerce(
OpenAI::Responses::ResponseInputItem,
{id: "item_123", type: nil},
state: state
)
p value.class
p state[:error]Current behavior: OpenAI::Models::Responses::ResponseInputItem::ItemReference, with no error.
With this change: Hash plus ArgumentError: no matching variant. The generated ItemReference#type is explicitly optional and nullable.
Repro 3: existing Realtime unions also have optional discriminators
[
[OpenAI::Realtime::RealtimeAudioFormats, {rate: 24_000}],
[OpenAI::Realtime::RealtimeAudioFormats, {}],
[OpenAI::Realtime::RealtimeToolsConfigUnion, {name: "weather"}]
].each do |union, input|
state = OpenAI::Internal::Type::Converter.new_coerce_state
value = OpenAI::Internal::Type::Converter.coerce(union, input, state: state)
p [value.class, state[:error]]
endThese currently resolve to RealtimeAudioFormats::AudioPCM, RealtimeAudioFormats::AudioPCM, and RealtimeFunctionTool, respectively. With this change, all three return a hash with ArgumentError.
Repro 4: unknown nested variants break forward compatibility
OpenAI::Responses::Response.new(
output: [{id: "item_future", type: "future_item"}]
).output
# => OpenAI::Errors::ConversionErrorReturning the hash is not enough if the union also sets state[:error]: array/model accessors propagate that error. The new SSE test only covers a top-level union, where the error state is ignored.
Please preserve structural fallback when the discriminator is missing or nullable for variants that allow that, and preserve genuinely unknown discriminator values without recording a conversion error. Alternatively, scope the unknown-event handling to SSE parsing. Regression coverage should use the actual generated Responses and Realtime unions, including an unknown variant nested in Response#output.
|
@apcha-oai Addressed the requested changes in
Validation is clean on Ruby 3.3.12, 3.4.10, and 4.0.6 (628 tests / 2,381 assertions each), plus Sorbet, RBS validation, RuboCop, and gem packaging. |
## Changes - Remove Stainless attribution from the SDK security policy and repository-owned infrastructure. - Remove Stainless-only CI artifact uploads, token permissions, mirror-specific workflow behavior, and legacy `.stats.yml`. - Keep Castiron generation metadata, required Ruby CI checks, public release workflows, and mock-server validation intact. ## Out of scope - Renaming `X-Stainless-*` request headers, retry/idempotency identifiers, OpenAPI `x-stainless-*` extensions, generated fixture identifiers, changelog history, or third-party provenance.
HAYDEN-OAI
left a comment
There was a problem hiding this comment.
I found one forward-compatibility regression in the new unknown-discriminator fallback; leaving an inline comment only.
HAYDEN-OAI
left a comment
There was a problem hiding this comment.
Re-reviewed the current head; the follow-up fixes the prior fallback regression and I found no remaining substantive issues.
Source: openai/openai#1280093 Adds the new `tenant.workload_identity.access_token.issued` audit-log event to generated request and response types. Generated with Castiron from OpenAPI SHA `11854aef674352d3f9cd5c0a7038f079a7bbac06` and validated with the public-baseline dry-run workflow. --------- Co-authored-by: Castiron Bot <castiron@openai.com>
|
Pushed the forward fix in
Validation: 635 tests / 2,427 assertions on Ruby 4.0 after the merge; the pre-merge code-equivalent suite also passed on Ruby 3.3 and 3.4. RuboCop (2,611 files), Sorbet, RBS validation (1,212 files), and gem packaging are clean. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d996666fd0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Addressed the coercion-contract feedback with dedicated regression coverage and a forward fix. Raw model contract:
Union/discriminator contract:
Validation is green on Ruby 3.3.12, 3.4.10, and 4.0.6 (650 tests / 2,493 assertions each), plus RuboCop, Sorbet, RBS validation, and gem build. |
|
Generator compatibility audit complete for this draft as well: no Castiron PR is required.
After #373 lands, this branch still needs the planned refresh so its review surface is limited to discriminator behavior, but it does not need a companion generator change. |
|
Thanks for iterating on this. I’ve spent a considerable amount of time investigating this change with Codex, and I honestly can’t vouch for its correctness across the full range of existing schemas and discriminator configurations. Given that history, I don’t think this particular streaming bug warrants rewriting global union coercion at this time. Can we instead special-case keepalive, handle unknown events in the streaming decoder, or scope the fallback behavior specifically to the streaming event union? (*) For example, given These also (unfortunately) are not very theoretical. E.g. our core responses / input items rely on this behavior. |
|
Closing this draft as part of the decision to roll back #295 and re-evaluate the model system from first principles. The global union/discriminator rewrite here is too broad for schemas with optional discriminators and multiple variants sharing a discriminator value. #373 now performs the literal rollback. #287 remains open and should receive a narrower, streaming-scoped fix after the underlying design is specified and characterized. No Castiron PR is being closed: the generated Ruby surface is unchanged, and this runtime is intentionally SDK-owned. |
Status
Draft — blocked by #373. The raw
BaseModelcontract fix in #373 must land first. After it merges, this branch will be refreshed so #370 contains only the union/discriminator follow-up.Summary
nildiscriminators used by optional/nullable generated schemasCoverage
The dedicated discriminator contract suite exercises real Responses, Beta Responses, and Realtime schemas, including duplicate message tags, optional and nullable discriminators, unkeyed enum variants, nested unknown variants, symbol/string key and value combinations, and truly unknown future tags.
Validation
The current stacked head is green across Ruby 3.3, 3.4, and 4.0, plus RuboCop, Sorbet, RBS validation, gem packaging, required CI, and CodeQL.
Fixes #287