Skip to content

feat(openai): generalize the operation registry and matcher - #772

Draft
cdoern wants to merge 1 commit into
praxis-proxy:mainfrom
cdoern:feat/openai-operation-body-kind
Draft

feat(openai): generalize the operation registry and matcher#772
cdoern wants to merge 1 commit into
praxis-proxy:mainfrom
cdoern:feat/openai-operation-body-kind

Conversation

@cdoern

@cdoern cdoern commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Closes #746

Summary

The OpenAI operation registry was shaped around Conversations being the only
registered API family. This generalizes it so Responses, Files, and Vector
Stores can declare operations against the same foundation.

Request-body shape no longer implies contract ownership

has_request_body inferred the body shape from owned_contract, so an
operation reported a body only when Praxis owned its OpenAPI contract. A proxied
multipart upload — a request that is almost entirely body — reported no body at
all. OpenAiRequestBody now records the runtime shape (none, JSON, multipart or
binary, each required or optional) as its own field. Contract metadata keeps its
existing role of generating the implementation OpenAPI document, so runtime
facts stay separate from conformance metadata.

Conversations derives the new field from the request: token already present in
each declaration, so the eight operations are unchanged and the runtime shape
cannot drift from the generated contract.

Path parameters are no longer Conversations-specific

RouteParams held two named fields, conversation_id and item_id, and
insert matched on those literal names, so no other family could capture a
parameter. It now holds fixed-capacity name/value pairs. Matching stays
allocation-free and still fails on repeated parameter names rather than
overwriting.

Matching moves into the shared module

Path-template matching now lives in the shared operation module behind
OperationEntry, so a family may register bare OpenAiOperationSpec values or
wrap them in its own type. Operations carry an OpenAiApiFamily and an
OpenAiTransport, which is what makes a WebSocket handshake distinguishable
from a plain request at the same method and path.

Precedence is now defined rather than accidental

Matching previously took the first registry hit in declaration order. With only
Conversations registered nothing collided, but /v1/responses/input_tokens and
/v1/responses/{response_id} both match the same request — so declaration order
alone decided whether a static endpoint was reachable or silently swallowed as
an identifier.

Matching now selects the candidate with the most literal segments, so a literal
segment always outranks a parameter. A test declares the parameter template
first on purpose to prove order no longer matters.

Path normalization applies one documented policy: query strings are ignored, and
a single trailing slash is tolerated on non-root paths.

Compatibility

Conversations keeps its typed conversation_id and item_id accessors over the
shared representation, so openai_conversations is untouched. No runtime
behaviour changes.

Testing

  • cargo test --workspace — all suites pass, including 2547 in praxis-ai-apis
  • make lint — passes end to end (clippy -D warnings, nightly fmt --check,
    cargo machete, dependency and doc checks, example and filter doc sync,
    markdown links, inference coverage)
  • make doc — clean

New coverage: static-before-parameter precedence with the parameter declared
first, parameters still matching genuine identifiers, transport separating
operations that share a method and path, unsupported methods not matching,
query-string and trailing-slash normalization, extra and missing segments,
proxied operations reporting a body without owning a contract, optional vs
required bodies, declared shape versus generated contract in both directions,
duplicate parameter names, and capacity overflow.

The OpenAI operation registry was shaped around Conversations being the only
registered API family. Generalize it so other families can declare operations
against the same foundation.

Request-body shape no longer implies contract ownership. `has_request_body`
inferred the shape from `owned_contract`, so an operation reported a body only
when Praxis owned its OpenAPI contract; a proxied multipart upload reported no
body at all. `OpenAiRequestBody` now records the runtime shape (none, JSON,
multipart or binary, each required or optional) as its own field. Contract
metadata keeps its existing role of generating the implementation OpenAPI
document, so runtime facts stay separate from conformance metadata.

Path parameters are no longer Conversations-specific. `RouteParams` held two
named fields and matched on those literal names, so no other family could
capture a parameter. It now holds fixed-capacity name/value pairs, keeping
matching allocation-free and still failing on repeated parameter names.

Path-template matching moves into the shared operation module behind
`OperationEntry`, so a family may register bare specs or its own wrapper type.
Operations carry an API family and a transport, making a WebSocket handshake
distinguishable from a plain request at the same method and path.

Matching selects the candidate with the most literal segments rather than the
first declared, so a static endpoint such as `/v1/responses/input_tokens` is
never consumed as a `{response_id}` parameter. Path normalization applies one
documented policy: query strings are ignored and a single trailing slash is
tolerated on non-root paths.

Conversations keeps its typed `conversation_id` and `item_id` accessors over the
shared representation, and its eight operation declarations are unchanged. No
runtime behaviour changes.

Closes praxis-proxy#746

Signed-off-by: Charlie Doern <cdoern@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: Generalizes the OpenAI operation registry from Conversations-only to support multiple API families (Responses, Files, Vector Stores). Introduces OpenAiRequestBody for runtime body shape independent of contract ownership, OpenAiApiFamily/OpenAiTransport enums for operation identity, generic RouteParams with fixed-capacity name/value pairs, and a shared match_operation function with static-segment precedence.

Overall: Clean. The precedence logic correctly ranks by literal segment count so static endpoints are never consumed as parameters regardless of declaration order. RouteParams handles capacity overflow and duplicate names by failing the match rather than panicking or silently overwriting. Path normalization is well-scoped (query strings stripped, single trailing slash tolerated on non-root). Test coverage is thorough: precedence with parameter declared first, transport discrimination, method filtering, normalization variants, capacity overflow, duplicate parameters, body shape independence from contract ownership, and contract/shape consistency validation.

Severity Count
Critical 0
Large 0
Medium 0

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

refactor(openai): generalize the operation registry and matcher

3 participants