weiqingy opened a new pull request, #1265:
URL: https://github.com/apache/flink-agents/pull/1265
Linked issue: Fixes #1253
### Purpose of change
A schema with an open-keyed field now keeps the prompt path on providers
whose native schema cannot express one, instead of going native and losing the
field. In Java that is a `Map` or `JsonNode` field, rendered as an empty object
allowing no keys, so the model could only return `{}`. In Python it is a `dict`
field or an `extra="allow"` model, which OpenAI and Azure OpenAI strict mode
reject and Anthropic closes to the same empty object.
#### Runtime flow
Each connection's native predicate, which feeds both
`supportsNativeStructuredOutput` and the request, gains one last check:
- **Java:** `PojoJsonSchemaGenerator.reachesAny(type, targets, options)`
runs the victools generator with the connection's native options and reports
whether it visits an instance of a target. Strict providers pass `Map` and
`JsonNode`; Gemini passes `JsonNode`.
- **Python:** `contains_open_object(model)` walks `model_json_schema()` for
an object whose `additionalProperties` is not `false`, or that has
`patternProperties`.
#### Key decisions
- **Java asks the generator, not the rendered schema**, because a rendered
`Map<String,Object>` has no `additionalProperties` key to read. It uses the
generation options, which change what is reached: on Gemini,
`Map<String,JsonNode>` reaches `JsonNode`.
- **Python reads the rendered schema**, where pydantic states openness,
including `extra="allow"`.
- **Fall back, not raise**: the prompt path can honour a map.
- **Gemini keeps `Map` native**: its schema keeps the value type.
- **vLLM splits by language.** Java vLLM reuses the OpenAI renderer, which
closes a `Map` to `{}`. Python vLLM sends pydantic's typed
`additionalProperties`, which vLLM's guided decoding enforces, so its predicate
drops the check.
### Behavioral Semantics
#### Interaction decisions
| Schema contains (any depth) | OpenAI, Azure, Anthropic | vLLM | Gemini
(Java) | Others |
|---|---|---|---|---|
| Only declared fields | unchanged | unchanged | unchanged | unchanged |
| Java `Map` (values not `JsonNode`) | `INFEASIBLE` | `INFEASIBLE` |
unchanged | unchanged |
| Java `JsonNode`, incl. as a `Map` value | `INFEASIBLE` | `INFEASIBLE` |
`INFEASIBLE` | unchanged |
| Python `dict` field or `extra="allow"` | `INFEASIBLE` | unchanged | n/a |
unchanged |
The check adds to the existing blockers; any one gives `INFEASIBLE`.
#### Behavioral contracts
1. Java OpenAI, Azure OpenAI, vLLM and Anthropic answer `INFEASIBLE` for a
schema class reaching a `Map` or `JsonNode`, and send no `response_format` /
`output_config`.
2. Java Gemini answers `INFEASIBLE` for one reaching a `JsonNode`, and sends
no response schema. A `Map` of other values is unchanged.
3. Python OpenAI, Azure OpenAI and Anthropic answer `INFEASIBLE` for a model
with a `dict` field or `extra="allow"` at any depth, and send no native
parameter.
4. Python vLLM sends such a model natively, keeping its
`additionalProperties`.
5. A blocked schema uses the prompt path under `AUTO` and `PROMPT`, and
raises before the call under `NATIVE`.
6. Anthropic keeps a requested JSON prefill for a blocked schema (Java and
Python).
7. A `@JsonIgnore`d `Map` field does not block (Java).
8. Every other schema is unchanged on every provider.
#### Failure behavior
- `NATIVE` with a blocked schema raises the existing early error naming the
connection and schema.
- Python: a model whose `model_json_schema()` raises is not reported open,
so the render step still raises its `TypeError`.
- Java: the query now runs the generator, uncaught, under every strategy,
`PROMPT` included.
### Tests
| Contract | Tests |
|---|---|
| 1 | `testNativeNotAppliedForOpenObjectSchema` (OpenAI, Azure),
`testOpenObjectSchemaKeepsFallback` (Anthropic),
`testQueryRecommendsNativeForServedModels` (vLLM),
`testQueryAgreesWithTheNativeBranch` |
| 2 | `nativeSchemaSkippedForJsonNode`, `queryAgreesWithTheNativeBranch`
(Gemini); `reachesMapValuesOnlyUnderTheirOption` |
| 3 | `test_output_schema_render.py`;
`test_schema_admitting_undeclared_keys_keeps_the_prompt_path`,
`test_query_agrees_with_the_native_branch` |
| 4 | `test_map_schema_is_sent_natively_with_its_value_type` |
| 5 | Existing strategy tests |
| 6 | `testOpenObjectSchemaKeepsFallback`;
`test_json_prefill_applied_when_open_schema_falls_back` |
| 7 | `doesNotReachTypesTheSchemaLeavesOut` |
| 8 | Unchanged rows of each agreement matrix; the suites below |
Tests pinning a map schema as native on a blocked provider now pin the
prompt path. Each new explicit test fails with its gate or override reverted;
the agreement tests pass either way, as query and request move together.
Run, all passing: `mvn test` on
`integrations/chat-models/{common,openai,anthropic,gemini,ollama,watsonx,bedrock}`
with `-am`; `pytest -m "not integration"` on
`flink_agents/{api,plan,runtime,integrations/chat_models}`; `ruff check`;
Spotless; `./tools/check-license.sh`.
Not verified:
- No live provider call; responses follow documented strict-mode rules.
- vLLM's acceptance of a typed `additionalProperties` comes from its
guided-decoding source, not a running server.
- Whether Gemini enforces a `Map`'s typed values.
- A Python `Any` field is not flagged (it renders with no `type`), though
Java's `JsonNode` is.
<details>
<summary>Implementation invariants and supporting detail</summary>
- The generator skips `@JsonIgnore`d fields and includes subtypes, as the
native schema does. The new check composes with the Azure api-version,
Anthropic caller `output_config` and Gemini tools blockers as one more conjunct.
- Python vLLM does not apply OpenAI's strict rules; its guided-decoding
backend (xgrammar) lists no `additionalProperties` among its unsupported JSON
features.
- `reachesAny` adds a custom definition provider that records an
`isInstanceOf` match and returns `null`, so the generated schema is identical
to `generate`'s. `generate` and `reachesAny` share one private `configure`.
- Each Java connection keeps one `NATIVE_SCHEMA_OPTIONS` constant used by
both generation and the query, so the two cannot drift. The cheap checks run
first in every `canApply…`, and `reachesAny` runs only for a `Class` schema.
- Recursive classes terminate: victools emits a `$ref` for a type it has
already visited.
- `contains_open_object` follows only keywords that hold subschemas
(`properties` and `$defs` values, `items`, `prefixItems`, `anyOf`, `oneOf`,
`allOf`, `not`), so a field named `additionalProperties` does not count.
- Python vLLM's override returns `_native_output_model(output_schema) is not
None`, the OpenAI predicate without the check; its request path reads the same
predicate.
- `JsonNode` on Ollama, watsonx and Bedrock is unchanged and not examined.
- Suite sizes: common 11, OpenAI 152, Anthropic 104, Gemini 71, api 680,
Ollama 35, watsonx 52, Bedrock 90; Python 2157 passed, 15 skipped.
</details>
### API
No public API is removed or changed. Added helpers:
`PojoJsonSchemaGenerator.reachesAny` and
`flink_agents.api.agents.types.contains_open_object`.
Compatibility: under `AUTO`, a schema with an open-keyed field moves from
native to the prompt path on OpenAI, Azure OpenAI, Anthropic and Java vLLM, as
does a `JsonNode` on Gemini. Under `NATIVE` it fails before the call, instead
of returning `{}` (Java, Python Anthropic) or a provider 400 (Python OpenAI,
Azure OpenAI).
### Documentation
- [ ] `doc-needed` <!-- Your PR changes impact docs -->
- [ ] `doc-not-needed` <!-- Your PR changes do not impact docs -->
- [x] `doc-included` <!-- Your PR already contains the necessary
documentation updates -->
### Was this patch authored or co-authored using generative AI tooling?
- [x] Yes
- [ ] No
Generated-by: GitHub Copilot CLI 1.0.98 (Claude Opus 5.5)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]