feat(openai): generalize the operation registry and matcher - #772
Conversation
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>
593d3e2 to
edee244
Compare
praxis-bot
left a comment
There was a problem hiding this comment.
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 |
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_bodyinferred the body shape fromowned_contract, so anoperation 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.
OpenAiRequestBodynow records the runtime shape (none, JSON, multipart orbinary, 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 ineach declaration, so the eight operations are unchanged and the runtime shape
cannot drift from the generated contract.
Path parameters are no longer Conversations-specific
RouteParamsheld two named fields,conversation_idanditem_id, andinsertmatched on those literal names, so no other family could capture aparameter. 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 bareOpenAiOperationSpecvalues orwrap them in its own type. Operations carry an
OpenAiApiFamilyand anOpenAiTransport, which is what makes a WebSocket handshake distinguishablefrom 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_tokensand/v1/responses/{response_id}both match the same request — so declaration orderalone 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_idanditem_idaccessors over theshared representation, so
openai_conversationsis untouched. No runtimebehaviour changes.
Testing
cargo test --workspace— all suites pass, including 2547 inpraxis-ai-apismake lint— passes end to end (clippy-D warnings, nightlyfmt --check,cargo machete, dependency and doc checks, example and filter doc sync,markdown links, inference coverage)
make doc— cleanNew 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.