Architecture
Architecture
Section titled “Architecture”ProxAI is a small local compatibility proxy. It accepts local OpenAI-compatible or Anthropic-style requests, normalizes protocol-specific request shapes, forwards them to a configured upstream provider, and translates upstream responses back to the client-facing protocol when needed.
Related docs
Section titled “Related docs”Two-axis model
Section titled “Two-axis model”The codebase is easiest to understand as two independent axes.
Phase axis
Section titled “Phase axis”The phase axis describes where data is in the proxy pipeline:
inbound_request— the original client request received by ProxAIprovider_request— the request ProxAI prepares for the upstream providerupstream_response— the response returned by the upstream provideroutbound_response— the response ProxAI returns to the client
Protocol axis
Section titled “Protocol axis”The protocol axis describes the wire protocol used at a given phase:
openai_responsesopenai_chat_completionsanthropic_messages
Each phase has its own protocol:
inbound_request.protocolis what the client sent.provider_request.protocolis what ProxAI sends upstream, controlled by the selected provider.upstream_response.protocolis what the provider returns.outbound_response.protocolis what ProxAI returns to the client.
Provider names are user labels. They are not semantic protocol identifiers.
Top-level source layout
Section titled “Top-level source layout”Directorysrc/
- main.rs — entry point; delegates to
cli::main - lib.rs —
AppState, axumRouter, proxy handler Directorycli/ — CLI parsing and startup
- …
- config.rs —
config.tomlschema and loading - paths.rs — app directory resolution
- request.rs — shared request carrier types
- sse.rs — SSE helpers
- formatting.rs — formatting helpers
Directoryerror/ — domain errors and rendering
- …
Directoryhttp_support/ — HTTP carrier helpers
- …
Directoryingress/ — inbound protocol parsing and normalization
- …
Directoryprotocol/ — protocol wire types and protocol enums
- …
Directoryrouting/ — route matching
- …
Directoryprovider/ — provider request preparation and HTTP transport
- …
Directoryupstream/ — upstream response reading
- …
Directorytranslation/ — cross-protocol conversion
- …
Directorypipeline/ — typed proxy pipeline
- …
Directoryobserve/ — capture, logging, diagnostics
- …
Directorymcp/ — MCP control surface
- …
- main.rs — entry point; delegates to
Request lifecycle
Section titled “Request lifecycle”src/lib.rs registers these routes and sends all of them to the same proxy handler:
/v1/responses /responses/v1/chat/completions /chat/completions/v1/messages /messagesThe simplified inbound path is:
let prepared_provider = inbound_http .prepare_inbound()? // ingress: parse + normalize .route_to_provider(...)? // routing: choose provider .prepare_provider_request()?; // provider/request + translation
run_provider_flow(prepared_provider).awaitrun_provider_flow then handles the provider side:
let provider_http = prepared_provider .send_to_upstream().await? // provider/transport + upstream .handle_upstream_response().await?; // upstream: read body / stream
provider_http.translate_to_outbound().await? // translation + http_supportPipeline stages
Section titled “Pipeline stages”- 1
inbound_requestModulespipeline/inbound.rsingress/ResponsibilityRead body, detect protocol, normalize request shape, create
InboundHttpFlow. - 2RoutingModules
pipeline/inbound.rsrouting/ResponsibilityMatch by protocol and model, resolve default provider.
- 3
provider_requestModulespipeline/provider_request.rsprovider/requesttranslation/requestResponsibilityTranslate request payload, rewrite provider model, serialize body.
- 4Send upstreamModules
pipeline/provider_request.rsprovider/transportResponsibilityBuild auth headers, construct upstream URL, send via
reqwest. - 5
upstream_responseModulespipeline/upstream_response.rsupstream/ResponsibilityRead status, headers, and body or stream.
- 6
outbound_responseModulespipeline/provider_response.rstranslation/responsetranslation/streamingResponsibilityTranslate response back to inbound protocol and rebuild HTTP response.
pipeline/ uses a typed ProxyFlow<S> state machine. Each phase consumes one flow state and returns the next one, keeping the phase order explicit.
Module responsibility map
Section titled “Module responsibility map”protocol/Protocol wire shapes and shared protocol enums. Models JSON only; no conversion and no HTTP carrier types.
ingress/Inbound protocol detection, parsing, and normalization before routing and translation.
routing/Provider selection from request protocol, model pattern, defaults, and route configuration.
translation/Pure request, response, and streaming conversion across explicit protocol pairs.
provider/Provider request rendering, model rewrite, auth headers, upstream URL construction, and transport.
upstream/Reading upstream status, headers, complete bodies, or streaming byte carriers.
http_support/Protocol-neutral HTTP helpers such as content-type checks, response reconstruction, and boxed streams.
observe/Capture artifacts, structured logging, request hints, and privacy-preserving diagnostics.
error/Domain-specific error types and client-facing response rendering.
Boundary rules
Section titled “Boundary rules”protocol/is low-level wire modeling: JSON shapes only, no conversion.pipeline/coordinates the full request lifecycle and owns phase order.translation/is pure at the HTTP carrier boundary: it accepts protocol values and payload/stream carriers, not HTTPResponse/Bodyor provider-private structs.provider/owns provider request rendering and transport details such as auth headers, upstream URLs, and idle-read timeout.observe/cuts across the pipeline for diagnostics, but does not make routing or protocol decisions.- Semantic stream and HTTP errors should use domain errors instead of being hidden inside
std::io::Error.
Dependency direction
Section titled “Dependency direction”flowchart TD cli[cli/] --> lib[lib.rs AppState] config[config.rs] --> lib paths[paths.rs] --> cli lib --> pipeline[pipeline/]
pipeline --> ingress[ingress/] pipeline --> routing[routing/] pipeline --> provider[provider/] pipeline --> upstream[upstream/] pipeline --> translation[translation/] pipeline --> observe[observe/] pipeline --> http_support[http_support/] pipeline --> error[error/]
ingress --> protocol[protocol/] translation --> protocol provider --> protocol upstream --> http_support provider --> http_support translation --> http_support ingress --> sse[sse.rs] upstream --> sse translation --> sse observe --> error lib --> observe lib --> errorKey rules:
protocol/is low-level wire modeling.pipeline/is the coordinator that knows about the full request lifecycle.translation/does not depend on HTTPResponse/Bodyor provider-private types.observe/cuts across the pipeline but does not make protocol or routing decisions.
Translation selection
Section titled “Translation selection”Translation is selected from two protocol values:
- inbound
request_protocol, detected byingress/ - provider
protocol, configured on the selected provider
Rules:
- Same protocol: pass through without protocol conversion.
- Different protocols: dispatch to
translation/<inbound_protocol>/to_<provider_protocol>/. - Unsupported pairs fail explicitly.
Data type conventions
Section titled “Data type conventions”- Prefer top-level enums keyed by protocol for protocol-specific request/response data.
- Avoid parallel
protocol/payload/projection/summaryfields that can drift into impossible states. - Keep streams as
ByteStreamacross HTTP carrier boundaries. - Keep provider names separate from protocol names.