Skip to content

fix(conversations): model official input and output item unions - #747

Draft
leseb wants to merge 596 commits into
praxis-proxy:mainfrom
leseb:leseb/issue-567-conversation-item-unions
Draft

fix(conversations): model official input and output item unions#747
leseb wants to merge 596 commits into
praxis-proxy:mainfrom
leseb:leseb/issue-567-conversation-item-unions

Conversation

@leseb

@leseb leseb commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #567.

  • Replace opaque-object Conversation item contracts with the official OpenAI InputItem and ConversationItem unions using oneOf variants with type discriminators.
  • Generate item schemas from the projected OpenAI specification via a new xtask command (sync-item-contracts), compiled at startup with jsonschema for runtime validation.
  • Validate items at the owned API boundary: InputItem on create, ConversationItem on normalize/append-back, rejecting structurally invalid items with OpenAI-style error responses.
  • Reconcile with aligned request contracts from fix(conversations): align create and update request contracts #625: add nullable_vec deserializer for null-as-empty items, remove dead nullable_initial_items_schema, fix integration test to include required OutputTextContent fields.

Test plan

  • make lint passes (clippy, fmt, deps, docs, examples)
  • make test passes (2534 unit + 457 integration + schema tests)
  • Integration test append_back_persists_items_after_response fixed and passing
  • Conformance report updated with refreshed drift counts

leseb and others added 30 commits June 30, 2026 15:49
Signed-off-by: Sébastien Han <seb@redhat.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
…SSE parser (praxis-proxy#739)

Signed-off-by: Sébastien Han <seb@redhat.com>
…d Content-Type (praxis-proxy#745)

Signed-off-by: Sébastien Han <seb@redhat.com>
To migrate to this repository we simply based on the core repository,
and this patch removes, modifies and updates everything to match the
new AI filters and capabilities crates we want here. This method allowed
us to keep all git history and attribution.

Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
Signed-off-by: dependabot[bot] <support@github.com>
…axis-proxy#225)

Signed-off-by: Dimitri Saridakis <dimitri.saridakis@gmail.com>
Signed-off-by: dimakis <dimitri.saridakis@gmail.com>
Signed-off-by: Sébastien Han <seb@redhat.com>
Signed-off-by: Alex Snaps <alex@wcgw.dev>
Signed-off-by: Sébastien Han <seb@redhat.com>
leseb and others added 12 commits August 12, 2026 14:11
praxis-proxy#690)

The openai_mcp_tool_resolve filter applied max_body_bytes only when
buffering the original client body via StreamBuffer, but after expanding
MCP tool definitions into function tools the rewritten body could exceed
the limit unchecked. Add a post-serialization size guard that returns
HTTP 413 before committing the expanded body.

Signed-off-by: Sébastien Han <seb@redhat.com>
…-proxy#689)

* fix: reject invalid query params in conversation item listing

Make parse_item_list_params fallible. Reject malformed limit values,
invalid order enums, unknown parameters, duplicates, and key-only
params with 400 invalid_request_error instead of silently defaulting.

Move param parsing before the conversation store lookup so malformed
requests consistently return 400 rather than sometimes 404.

Remove effective_limit clamping (now validated at parse time) and
decode_query_component (replaced by strict decoding).

Closes praxis-proxy#547

Signed-off-by: Sébastien Han <seb@redhat.com>

* test: update and add unit tests for strict query validation

Update existing tests to assert errors instead of silent defaults.
Remove effective_limit and decode_query_component tests (dead code).
Add tests for: limit=0, limit above max, duplicates, unknown params,
key-only params, empty after, invalid UTF-8, empty components, and
encoded duplicate keys.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test: add integration tests for query validation error responses

Verify handler-level 400 responses for invalid limit, invalid order,
unknown params, and duplicate params. Also verify limit=0 returns an
empty 200 page with has_more=false.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test: add validation precedence regression test

Verify that an invalid query on a nonexistent conversation returns 400,
not 404, confirming that query validation runs before the store lookup.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test: add boundary test for limit at MAX_PAGE_LIMIT

Addresses praxis-bot review comment on PR praxis-proxy#689.

Signed-off-by: Sébastien Han <seb@redhat.com>

* style: fix rustfmt formatting in boundary test

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Sébastien Han <seb@redhat.com>
Increase all timeout values in the vLLM Responses SDK integration tests
to 300 seconds to reduce flaky failures from slow model inference:

- read_timeout_ms: 120000 → 300000
- timeout_ms: 120000 → 300000
- step_timeout_ms: 120000 → 300000
- SDK client timeout: 180 → 300 (all three client fixtures)

Signed-off-by: Sébastien Han <seb@redhat.com>
…raxis-proxy#704)

feat(openai): resolve connector_id through configured MCP connectors (praxis-proxy#316)

Add connector_id resolution to the openai_mcp_tool_resolve filter,
allowing requests to reference named MCP connectors instead of embedding
raw server_url values. Configured connectors map short IDs to validated
server URLs, keeping client payloads free of infrastructure details.

Key changes:
- ConnectorConfig with validation (max 64 connectors, 128-byte ID limit)
- EntryResolution enum (PassThrough vs Resolved) preventing connector_id leak
- resolve_connector_ids() with request-side ID length cap
- URL redaction in client-facing connector errors
- Zero-tool tool_choice rejection for resolved labels
- Integration tests and example config

Signed-off-by: Sébastien Han <seb@redhat.com>
* feat(openai): support finite chat continuations

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* fix(openai): fail closed for unresolved stream history

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* fix(test): add assertion messages and Responses live recording coverage

Add diagnostic messages to all bare assert! calls introduced in the
chat continuation feature to comply with repository conventions.

Extend live recording tests with a two-turn Responses scenario that
exercises the ${PREVIOUS_RESPONSE_ID} placeholder binding during
record_live, verifying the first response ID reaches the second
request.

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
Signed-off-by: Sébastien Han <seb@redhat.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
* Limit MCP dynamic values and add boundary tests

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>

* fix(mcp): bound method and name filter results

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>

---------

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
…xy#711)

* fix(responses): disable parallel tools on first inference

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>

* test(inference): add fixture coverage for agentic parallel_tool_calls

Add a controlled synthetic scenario and recording that proves the
agentic loop injects `parallel_tool_calls: false` into the upstream
request when the client omits it. This closes the inference fixture
coverage gap for the dirty-marker implementation.

- Add `agentic-loop-fixture.yaml` example config (replay-safe subset
  of the agentic loop pipeline without external callout filters)
- Add `agentic_loop` and `iterative_request_router` to the replay
  filter allowlist (they make no external callouts)
- Add scenario, recording, and coverage.yaml entry for
  `responses.agentic.parallel_tool_calls`
- Update the snapshot test in `coverage.rs` for the new scope,
  feature, scenario, and recording counts
- Regenerate example and inference READMEs

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
Signed-off-by: Sébastien Han <seb@redhat.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
* feat(filters): add OpenTelemetry routing spans

Add feature-gated routing.select spans for successful intelligent_route decisions while leaving request lifecycle, propagation, sampling, and export ownership in Praxis core.

Keep the default build unchanged and avoid OpenTelemetry SDK dependencies in the AI filters. Record only validated, bounded routing attributes and document the ownership and privacy boundaries.

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

* test(filters): align telemetry test conventions

Document the private routing-selection fields used to project bounded
OpenTelemetry attributes.

Move the candidate fixture below the tests, use the standard
test-utilities separator, and add diagnostic messages to every
assertion.

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

---------

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
…-proxy#625)

* fix(conversations): align create and update request contracts

Align the locally owned Conversation create and update request bodies with confirmed OpenAI behavior. Create requests accept an absent body and nullable optional fields, while update requests require a non-null metadata object.

Generate the matching OpenAPI schemas from the shared runtime contracts, retain the 20-item limit, cover absent, null, empty, valid, and invalid body shapes, and document the verified live behavior where it contradicts the pinned upstream specification.

Closes praxis-proxy#566

Signed-off-by: Sébastien Han <seb@redhat.com>

* docs(conformance): remove unnecessary probe cleanup detail

Signed-off-by: Sébastien Han <seb@redhat.com>

* test(conversations): expect 400 for update without metadata

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(conversations): return OpenAI error codes on update validation

Map update-conversation parse and validation errors to the OpenAI error
contract with code (missing_required_parameter, invalid_type) and param
fields. Add inner anyOf assertions for the two-layer nullable metadata
schema and an array-type rejection test.

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(conversations): narrow error code/param scope in update responses

- Remove hardcoded "but got null" from classify_update_error message
  since the error applies to all non-object types, not just null
- Restore invalid_input_response to not include code/param fields,
  keeping those only for update-specific errors via
  invalid_input_response_with
- Use invalid_type code only for actual type violations from
  validate_metadata, not for constraint violations (key/value length,
  key count)

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(conversations): address praxis-bot review findings

- Replace string-matching heuristic (msg.contains("must be")) with
  MetadataError enum that structurally distinguishes type errors from
  constraint violations
- Add maxItems assertion to OpenAPI schema test ensuring the 20-item
  bound is preserved in the generated document
- Handle empty update body with missing_required_parameter error code
  matching the {} case, as documented in conformance README

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Sébastien Han <seb@redhat.com>
…proxy#725)

v0.5.2 is now published on crates.io, so remove the temporary git tag
workaround introduced in praxis-proxy#701.

Signed-off-by: Sébastien Han <seb@redhat.com>
…raxis-proxy#733)

* fix(a2a): defer route commit until EOS to prevent prefix-poisoning

GuardResult::Redact was returning FilterAction::Continue while recording
status=redacted (guardrails fix). Separately, the A2A non-streaming
capture path committed task-route ownership as soon as the JSON balance
scanner reported a structurally complete value, regardless of
end_of_stream. A backend could deliver a valid JSON task response with
end_of_stream=false, have the route stored, then append trailing garbage
to produce an overall-invalid response — poisoning task ownership while
the complete body was not valid JSON.

When the scanner reports is_complete but end_of_stream is false,
parse_to_tentative() now stores the parsed value in filter metadata under
a2a.response.tentative_json without committing to the route store.
Subsequent chunks are inspected:
- non-whitespace bytes → tentative discarded (trailing content proves
  the response is not valid JSON)
- end_of_stream with no further non-whitespace → commit_tentative_capture()
  promotes the held value into the store

If end_of_stream arrives together with is_complete, try_capture_from_buffer()
is called directly as before, preserving the common fast path.

Update tests:
- json_response_split_across_chunks_defers_capture_until_eos
- many_single_byte_chunks_capture_route_at_eos
- split_json_response_with_context_stores_context_route
- assert_capture_scratch_cleared includes a2a.response.tentative_json
Add regression tests:
- complete_json_prefix_then_garbage_does_not_capture_route
- complete_json_followed_by_whitespace_at_eos_captures_route

Fixes: fnd_sig-feat-custom-ai-agentic-class_e1322f3e62

Signed-off-by: mkoushni <mkoushni@redhat.com>

* test(a2a): add tentative-survives-whitespace-chunk regression test

Covers the keep-waiting branch of the tentative guard: a whitespace-only
chunk with end_of_stream=false must leave the tentative JSON intact without
committing the route; only the subsequent EOS callback should commit.

Reported by praxis-bot review of fix/a2a-defer-route-commit-until-eos.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* docs(a2a): correct try_capture_from_buffer doc comment

The previous comment claimed the function was called after a tentative parse
was promoted by a whitespace-only EOS callback; that path actually calls
commit_tentative_capture. Update to accurately describe the two real call
sites: the fast path (is_complete && end_of_stream simultaneously) and the
fallback where EOS arrives with an incomplete buffer.

Reported by praxis-bot review of fix/a2a-defer-route-commit-until-eos.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(a2a): resolve clippy too-many-lines and needless-pass-by-ref-mut

Extract the tentative-guard logic from handle_non_streaming_capture into a
dedicated handle_pending_tentative helper. This reduces handle_non_streaming_capture
to under the 30-line clippy limit while improving readability.

Drop the unused &mut on ctx in commit_tentative_capture — the function only
reads filter_metadata, so &HttpFilterContext<'_> is sufficient.

Remove two inline comments from json_response_split_across_chunks_defers_
capture_until_eos that violated the project convention (no inline comments
in test bodies) and pushed the function over the 30-line limit.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(a2a): clear capture state on serde_json serialization failure in parse_to_tentative

If serde_json::to_string fails inside parse_to_tentative, the tentative_json
key is not set but buffer_hex, balance state, and capture_enabled all remain.
Subsequent chunks re-enter the main flow and when EOS arrives is_complete ||
end_of_stream fires, committing the route via try_capture_from_buffer and
bypassing the tentative guard entirely.

In practice this is effectively infallible for a Value that was just
deserialized, but add a defense-in-depth else branch that calls
clear_capture_metadata so the tentative guard cannot be bypassed under
any failure mode.

Reported by praxis-bot review of fix/a2a-defer-route-commit-until-eos.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(a2a): extract assert_tentative_pending helper to fix too-many-lines lint

json_response_split_across_chunks_defers_capture_until_eos was 32 non-blank
lines (limit 30). Extract the paired tentative-state assertions into a shared
assert_tentative_pending helper, bringing the test well under the limit.

Also restore the #[expect(clippy::too_many_lines)] attribute to its correct
position on assert_capture_scratch_cleared after it was accidentally
displaced during the helper insertion.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(a2a): use RFC 8259 whitespace predicate and remove test inline comments

Use a JSON-specific whitespace predicate (`b' ' | b'\t' | b'\n' | b'\r'`)
instead of `is_ascii_whitespace()` which also matches vertical tab (\x0B)
and form feed (\x0C). RFC 8259 defines only four insignificant-whitespace
code points; using the broader ASCII predicate could allow non-JSON
trailing bytes to be silently treated as whitespace, bypassing the
tentative-guard discard branch.

Move the Clawpatch finding ID for `complete_json_prefix_then_garbage_
does_not_capture_route` into the first assertion message where it is
visible as traceability. Remove all other inline comments from new and
modified test function bodies per project conventions.

Signed-off-by: mkoushni <mkoushni@redhat.com>

---------

Signed-off-by: mkoushni <mkoushni@redhat.com>
Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb
leseb requested review from a team and aslakknutsen August 14, 2026 09:46
leseb added 3 commits August 14, 2026 11:52
Replace the broad opaque Conversation item schema with the official OpenAI InputItem and ConversationItem unions at locally owned API boundaries.

Generate deterministic recursive item contracts from the pinned OpenAI specification, compile runtime validators from the same artifact, validate accepted inputs and normalized stored outputs, and merge the official discriminator-based components into the generated OpenAPI document. Cover representative messages, tool calls, tool outputs, reasoning items, supported extensions, and malformed or unknown variants, then refresh the conformance report.

Closes praxis-proxy#567

Signed-off-by: Sébastien Han <seb@redhat.com>
Regenerate the Conversations conformance report after the include-projection changes landed on main. Remove stale query-parameter drift for item endpoints and record the current item-union results: two exact operations, four request-drift operations, and four response-drift operations.

Closes praxis-proxy#567

Signed-off-by: Sébastien Han <seb@redhat.com>
Resolve rebase conflicts between the request-contract alignment
(praxis-proxy#566/praxis-proxy#625) and the item union modeling (praxis-proxy#567). Removes the dead
nullable_initial_items_schema, adds a null-to-default deserializer
for create items, fixes the integration test to include required
OutputTextContent fields, and drops unused imports.

Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb
leseb marked this pull request as draft August 14, 2026 09:52
@leseb
leseb force-pushed the leseb/issue-567-conversation-item-unions branch from d92dae5 to ac19b50 Compare August 14, 2026 09:56
leseb added 2 commits August 14, 2026 12:01
The jsonschema crate brings in borrow-or-share which uses the MIT-0
(MIT No Attribution) license. Add it to the deny.toml allowlist.

Signed-off-by: Sébastien Han <seb@redhat.com>
…ovement

Signed-off-by: Sébastien Han <seb@redhat.com>

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR Review

Summary: Reviewed all 18 changed files covering the InputItem/ConversationItem type split, generated JSON Schema validation at both API boundaries, the nullable_vec deserializer, LazyLock-based schema compilation, the oneOf-to-anyOf relaxation for runtime input validation, schema alignment across all item types (message, function_call, reasoning, program), the normalize_item -> validate_output_item pipeline, xtask tooling for schema generation and verification, and test coverage.

Overall: Clean. The design correctly separates input and output item types with schema-validated boundaries. The anyOf relaxation for InputItem allows overlapping EasyInputMessage and Item variants while ConversationItem retains strict oneOf. Normalization bridges the gap by adding id and status, and output validation confirms the result. Test coverage is thorough -- including the non_message_item_type_skips_normalization handler test that exercises function_call items through the full normalize-validate pipeline.

Severity Count
Critical 0
Large 0
Medium 0

@praxis-bot

Copy link
Copy Markdown
Collaborator

We haven't heard back from you on this in some time, so we're going to mark it closed for now. However, if you're still interested in pushing this forward please don't hesitate to re-open and update 🖖

@franciscojavierarceo franciscojavierarceo 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.

the current head still fails the required OpenAI conformance gate: it introduces six new POST /conversations request-drift fingerprints for the items schema, while ALLOW_NEW_FAILURES=false and no acknowledgement label is present. we need to align the generated contract with the reference or add the repository’s explicit acknowledgement, then rerun the failing integration fixture before this is mergeable.

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

Model official Conversation input and output item unions