Repository navigation
[integrations][watsonx] Support watsonx.ai native structured output - #1117
Merged
Merged
Conversation
Collaborator
Author
|
The one failing check is unrelated to this PR.
This is a repo-wide break rather than something in this branch. Re-running does not clear it. I re-triggered the job once and it failed with the same error, so this needs the image reference fixed rather than another attempt. #1116 changes that image reference to |
The watsonx.ai connection rejected a caller-supplied output schema, so a caller passing one fell back to prompt engineering even though the chat endpoint accepts a response_format carrying a JSON Schema and enforces it during decoding. The connection now reports native structured-output capability and translates a POJO schema into the endpoint's json_schema envelope. Unlike the OpenAI and Azure connections, the capability predicate is unconditional rather than gated on a model allowlist. IBM publishes no per-model structured-output table and exposes no capability flag on the model specification, and no client gates by model. IBM does state that Chat API support requires the vLLM runtime, so reaching this endpoint implies the runtime that applies the constraint. An allowlist here would encode a gate nobody documents and would report false for models that do work. That diverges from the base contract, which documents capability as model-dependent and asks an unrecognized model to report false. The Ollama connection already diverges the same way, and the override records why. A caller-supplied response_format has always reached the service through either the model parameters or the additional kwargs. Now that a schema can also produce one, a request carrying both raises rather than silently choosing between them: reporting capability while not applying the caller's schema would make the capability contract untrue. A caller-supplied value with no schema is forwarded unchanged. There is no vendor SDK in this module, so the schema is generated directly, using the generator configuration the Ollama connection established. The required set marks every field except an Optional one, and additionalProperties is deliberately not forbidden, because pydantic's plain schema omits it and emitting it on one side alone would make the two languages disagree. Nothing changes at runtime yet. The four-argument chat has no production caller in either language. Coverage asserts the serialized request body against a local stub server, which establishes the wire shape but not that the service accepts the document. The live test that would establish acceptance is gated on credentials and has not been run. Generated-by: Claude Code 2.1.260 (Claude Opus 5)
…Python The Python connection rejected a caller-supplied output schema outright, so the capability was unavailable even though the chat endpoint accepts a response_format carrying a JSON Schema. The Java side gained the native path in the preceding commit; this brings the two to parity. Adds the capability predicate, the json_schema envelope, and a guard that raises when a caller supplies a response_format alongside a schema, then removes the rejection call. The removal and the predicate override are one change: overriding the predicate is how a connection reports native support, and that is what exempts it from the cross-connection test asserting a connection which cannot translate a schema rejects one. A test restores that coverage by passing a schema form this connection does not translate. The schema is rendered with pydantic's plain output rather than the strict renderer the OpenAI and Azure connections use. That renderer rewrites the required list to name every property, satisfying a rule OpenAI imposes and watsonx does not, and applying it here would report every optional field as mandatory. A response_format valued None is not treated as a conflict. The derived envelope overwrites it before the request is built, so it can never reach the service, and raising on it would reject a request that has no conflict in it. The Java side draws the line in the same place. The two languages emit different schema metadata, so the parity test asserts property names, types and the required set rather than document equality. Nothing changes at runtime yet. No production caller passes an output schema in either language. Coverage stubs the vendor SDK, so it pins the parameters handed to it rather than the bytes the service receives. The live test that would establish acceptance is gated on credentials and has not been run. Generated-by: Claude Code 2.1.260 (Claude Opus 5)
weiqingy
force-pushed
the
280-pr8-watsonx-native
branch
from
September 14, 2026 16:33
cfea383 to
53873cd
Compare
2 of 5 tasks
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.
Linked issue: #280
Purpose of change
Passing an
output_schemato the watsonx.ai connection now constrains the reply through the provider instead of raising. Both languages rejected a schema outright before, so every caller fell back to prompt engineering, even though the chat endpoint accepts aresponse_formatcarrying a JSON Schema and enforces it during decoding.Runtime flow
The connection derives a JSON Schema from the caller's type, wraps it as
{"type": "json_schema", "json_schema": {"name", "schema", "strict": true}}, and sets it at the request root. The branch runs only for a Java POJO class or a pydantic model, and only when the capability predicate answers true. It sits before both parameter-merge steps, so a caller's own value is read before anything is written. Java generates the schema directly, having no vendor SDK here; Python renders it from pydantic throughrender_output_schema.Key decisions
No model allowlist. OpenAI and Azure gate this on a list of model names; this connection reports it for every model. IBM publishes no per-model structured-output table and no capability flag on the model specification, and no client I could find gates by model. IBM does state Chat API support requires the vLLM runtime, so reaching this endpoint implies the runtime that applies the constraint. An allowlist would encode a gate nobody documents and answer "unsupported" for models that work. Ollama reports unconditionally for a comparable reason. This diverges from the base contract, which asks an unrecognized model to report false; the override records that.
Plain schema rendering. OpenAI and Azure rewrite the required list to name every property, which OpenAI's strict mode demands and watsonx does not; reusing that renderer would report every optional field as mandatory. Relatedly,
additionalPropertiesis not forbidden, since emitting it on one side only would make the two languages send different documents for the same type.Behavioral Semantics
Interaction decisions
Output schema kind against a caller-supplied
response_format:RowTypeInfoThe middle-right cell is the only new refusal. A null is excluded because the derived envelope overwrites it before the request is built. The bottom row is deliberate: the conflict fires only where a schema was applied.
Behavioral contracts
response_format,stricttrue, named for the type.response_formatkey.response_formatcarrying a value raises.response_formatalone reaches the service unchanged.Failure behavior
An untranslatable schema degrades to the prompt fallback silently, matching existing behavior. A schema plus a caller
response_formatvalue raises before the request is built. A pydantic model with no JSON Schema raisesTypeErrornaming the model rather than a translation failure. Provider and transport errors, including retries, are unchanged.Two cases do not fail loudly. A self-referential type generates a self-referencing document and is sent as-is; no generator setting here prevents it. An empty completion is not distinguished from a normal one, which is existing behavior for every reply.
Tests
supportsNativeStructuredOutputIsUnconditionaltest_supports_native_structured_output_is_unconditionalbuildPayloadWritesResponseFormatForPojoSchema,serializedRequestBodyCarriesResponseFormatAtRoottest_params_carry_response_format_for_basemodel_schemabuildPayloadOmitsResponseFormatWithoutSchematest_params_omit_response_format_without_schemabuildPayloadOmitsResponseFormatForRowTypeInfotest_row_type_info_schema_returns_and_sends_no_response_formatchatWithSchemaAndCallerResponseFormatRaisestest_schema_and_caller_response_format_raisescallerResponseFormatSurvivesWhenNoSchemaIsSenttest_caller_response_format_survives_when_no_schema_is_sentderivedSchemaHonorsJacksonAnnotationstest_derived_schema_matches_java_semanticallyCoverage by risk. The highest risk is sending a document the provider rejects or ignores, so the Java suite asserts the full serialized request body through a local stub server rather than the builder's output. Next is a schema constraining the model to a document the deserializer then refuses, covered by contract 7. Conflict handling is covered on both parameter channels in both languages.
Not verified. No live call was made against watsonx, because I hold no credentials. An environment-gated live test ships for each language and neither has run, here or in CI. Nothing here establishes that IBM accepts the document, that an incapable model is rejected rather than ignoring the schema, or how a self-referential schema behaves at the provider. Python's stubbing sits above the SDK's payload assembly, so it pins the parameters handed to the SDK, not the bytes sent.
Implementation invariants and supporting evidence
Not user-observable, so recorded here rather than as contracts.
The Java payload seam widened from a static three-argument method to a four-argument instance method, because the capability predicate it now consults is an instance method.
The generated Java schema omits
additionalPropertiesand keeps the document root inline rather than behind a$ref. Neither is required by the endpoint; both keep the two languages emitting the same shape. The generator marks every field required except anOptionalone, which matches pydantic treating a field without a default as required.The two languages emit different schema metadata: the Java document carries
$schema, the pydantic document carriestitle. Contract 8 is therefore asserted semantically rather than as document equality.A caller
response_formatvalued null with no schema at all reaches the Python request body, where the Java write loops drop it. Those lines are untouched by this change and the divergence predates it.Suite results: Java 44 run with 2 skipped, both skipped being the live tests. Python 27 passed with 2 deselected. The cross-connection guard asserting that a connection which cannot translate a schema rejects one still passes; contract 4's test is what keeps that covered once the predicate is overridden.
API
No new public classes, and nothing changes for an existing caller. No production code path passes an output schema in either language today, so this adds a capability rather than altering behavior, and a caller supplying
response_formatdirectly is unaffected.What becomes possible is passing an
output_schemawithout it raising. The only new refusal is supplying both an output schema and aresponse_formatvalue in one call.Documentation
doc-neededdoc-not-neededdoc-includedThe capability is not reachable by a user until the policy layer is wired up, so the user-facing documentation belongs with that change.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code 2.1.260 (Claude Opus 5)