Skip to content

[integrations][openai] Apply OpenAI native structured output - #919

Open
weiqingy wants to merge 1 commit into
apache:mainfrom
weiqingy:280-pr2-openai-native
Open

[integrations][openai] Apply OpenAI native structured output#919
weiqingy wants to merge 1 commit into
apache:mainfrom
weiqingy:280-pr2-openai-native

Conversation

@weiqingy

@weiqingy weiqingy commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Linked issue: #280

Purpose of change

Applies OpenAI's native structured output on top of the structured-output foundation from #843, which has now merged. This PR is rebased onto main and is a single commit.

The OpenAI completions connection overrides the schema-carrying chat and requests response_format json_schema strict when native applies, and declares the capability predicate over the effective model.

Native applies only when the schema is a POJO class (Java) or a BaseModel subclass (Python) and the effective model supports it. Per OpenAI's structured-output guide, json_schema is supported on gpt-4o-mini and the gpt-4o snapshots from 2024-08-06 onward, while gpt-4-turbo and gpt-3.5-turbo have JSON mode only. Because "and later" is a snapshot cutoff rather than a name prefix, the predicate matches gpt-4o-mini by prefix (its whole family post-dates the cutoff) and the capable gpt-4o snapshots exactly, so a pre-cutoff snapshot such as gpt-4o-2024-05-13 falls back to the prompt path instead of failing at the provider. A RowTypeInfo schema stays on the prompt path.

#843 added _reject_unsupported_output_schema so that a connection with no native translation turns a schema it could only drop into an error. OpenAI has that translation now, so it no longer calls the guard, and the connection walk in test_output_schema_param_declared.py exempts connections that translate a schema natively. The exemption keys off the override of supports_native_structured_output, which is how a connection reports that capability in the first place. The other seven connections stay held to the rejection contract.

No path passes a schema on the chat call yet, so behavior is unchanged; native output is exercised by direct callers and the unit tests.

Tests

Java: the request carries response_format json_schema strict when a capable model and a POJO schema are supplied; the capability predicate is model-dependent (an incapable and a pre-cutoff same-family snapshot both resolve to not-capable); null schema and non-POJO schema keep the prompt path.

Python: the same coverage over the BaseModel path, including the pre-cutoff snapshot regression test and policy behavior.

The connection walk still fails if a connection without a native translation stops rejecting a schema, and its existing non-empty assertion now runs on the filtered list, so an exemption that ever swallowed the whole walk would fail rather than pass silently.

Java OpenAI module tests and the Python chat model suites pass after the rebase; ruff and Spotless are clean.

API

Touches the OpenAI connection: overrides the schema-carrying chat and declares the capability predicate. No new user-facing surface beyond the foundation in #843.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

Override the schema-carrying chat on the OpenAI completions connection to
request the provider's native structured output when the caller supplies a
schema the provider can honor natively.

Native applies only when the schema is a POJO class (Java) or a BaseModel
subclass (Python) and the effective model supports it. Capability is a
connection-side predicate over the effective model, since the OpenAI json
schema mode is model dependent: per the provider's structured output guide it
is supported on gpt-4o-mini and the gpt-4o snapshots from 2024-08-06 onward,
while gpt-4-turbo and gpt-3.5-turbo have json mode only. The predicate matches
gpt-4o-mini by prefix (the whole family postdates the cutoff) and the capable
gpt-4o snapshots exactly, so a pre-cutoff snapshot such as gpt-4o-2024-05-13
correctly falls back to the prompt path rather than failing at the provider. A
RowTypeInfo schema stays on the prompt path.

No path passes a schema on the chat call yet, so behavior is unchanged; native
output is exercised by direct callers and the unit tests.
@weiqingy
weiqingy force-pushed the 280-pr2-openai-native branch from da21d08 to 265bc35 Compare July 27, 2026 00:09
@github-actions github-actions Bot added doc-not-needed Your PR changes do not impact docs and removed doc-not-needed Your PR changes do not impact docs labels Jul 27, 2026
@weiqingy

weiqingy commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Hi @wenjin272, could you please help review this PR when you get a chance? It applies OpenAI's native structured output on top of #843, and it's now rebased onto main since that merged. Thanks!

@weiqingy

Copy link
Copy Markdown
Collaborator Author

One thing worth calling out explicitly, since it is deliberately left out of this PR.

StructuredOutputStrategy.NATIVE resolves to native regardless of the connection's reported capability, so a caller with an explicit NATIVE intent expects the schema to be applied even on a model the connection does not recognize as capable. This PR's OpenAI chat re-checks capability itself and skips response_format when the effective model is not capable or the schema is a RowTypeInfo, which under that intent would return an unconstrained response rather than an error. That is the same shape as the silent drop fixed in #843.

I left it unfixed here on purpose. The connection never receives the strategy, so it cannot tell an explicit NATIVE apart from an AUTO that happens to resolve to native, and any decision made inside the connection is a guess at caller intent. The composition site is where the setup's policy meets the connection's capability, which is #912. Fixing it there also means dropping or bypassing this connection-side re-check, otherwise the setup honoring NATIVE would still be downgraded here. I have recorded that on my side so it lands with the wiring.

Nothing dispatches on the strategy today, so this is dormant on main: no path passes a schema on the chat call yet, and the native path here is exercised only by direct callers and the unit tests.

Happy to pull the fix into this PR instead if you would rather it not wait for #912. It would change the two fallback tests, which currently assert the silent degrade.

@wenjin272 wenjin272 left a comment

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.

Thanks for taking this on @weiqingy. I left two minor comments

return false;
}
return effectiveModel.startsWith(NATIVE_STRUCTURED_OUTPUT_FAMILY_PREFIX)
|| NATIVE_STRUCTURED_OUTPUT_MODELS.contains(effectiveModel);

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.

The capability predicate appears to be inaccurate in both directions.

Using startsWith("gpt-4o-mini") also classifies models such as gpt-4o-mini-audio-preview and gpt-4o-mini-realtime-preview as capable, although these variants do not support Structured Outputs. Conversely, models such as gpt-4.1, gpt-5, o3, and o4-mini do support Structured Outputs but currently return false.

Once #912 uses this predicate for AUTO, the false positives may produce provider errors, while the false negatives will unnecessarily fall back to prompting. Could we use stricter alias/snapshot matching, include the currently supported model families, and add tests covering both cases?

References:

// Native structured output applies only for a POJO Class schema on a model the provider
// documents as capable; a RowTypeInfo (wrapped in OutputSchema) or an incapable model keeps
// the prompt-engineering fallback.
if (outputSchema instanceof Class && supportsNativeStructuredOutput(modelName)) {

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.

I agree that the final fix belongs in #912, where AUTO can be distinguished from explicit NATIVE. Could we add a clear TODO referencing #912 at this capability check, noting that NATIVE must bypass the connection-side fallback or fail explicitly? This should help prevent the dormant issue from being missed when the strategy wiring is implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants