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 parsing and encoding helpers
- sse_translation.rs — application adapter between SSE bytes and structured translation events
- formatting.rs — formatting helpers
Directoryerror/ — domain errors and rendering
- …
Directoryhttp_support/ — HTTP carrier helpers
- …
Directoryprovider/ — provider request projection/serialization, response observation, and HTTP transport
- …
Directoryupstream/ — upstream response reading
- …
Directorypipeline/ — typed proxy pipeline, including HTTP ingress adaptation
- …
Directoryobserve/ — capture, logging, diagnostics
- …
Directorymcp/ — MCP control surface
- …
- main.rs — entry point; delegates to
Directorycrates/
Directoryproxai-core/
Directorysrc/
Directoryingress/ — carrier-independent inbound normalization and validation
- …
Directorypipeline/ — carrier-independent composition across ingress, routing, translation, and provider adaptation
- …
Directoryprotocol/ — protocol wire types and protocol enums
- …
Directoryrouting/ — compiled protocol/model routing and provider-label selection
- …
Directoryprovider/ — carrier-independent provider request preparation and response/error normalization
- …
Directorytranslation/ — carrier-independent cross-protocol conversion
- …
The application keeps HTTP ingress, provider projections/serialization, transport lookup, and SSE carrier adaptation in src/. proxai-core::pipeline composes structured ingress, provider-label routing, provider request/response adaptation, and translation into a reusable Value/StreamEvent façade.
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 .parse_inbound()? // app: path + JSON bytes .prepare_provider_request( // core: ingress + route + translate + adapt &state.core_pipeline, &state.providers, )?;
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_requestModulessrc/pipeline/inbound.rsproxai-core/pipeline/proxai-core/ingress/ResponsibilityRead body bytes, detect protocol, and parse JSON in the app; normalize and validate the structured request through the core pipeline.
- 2RoutingModules
pipeline/provider_request.rsproxai-core/pipeline/proxai-core/routing/ResponsibilityResolve the provider label, protocol, compatibility policy, and upstream model in core, then select the application transport.
- 3
provider_requestModulespipeline/provider_request.rsproxai-core/pipeline/proxai-core/translation/requestproxai-core/provider/requestprovider/requestResponsibilityReturn a prepared provider value and matching response pipeline from core, then project diagnostics and serialize the body in the application.
- 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.rssse_translation.rsproxai-core/pipeline/proxai-core/translation/responseproxai-core/translation/streamResponsibilityRun structured response normalization and translation through
ResponsePipeline, adapt events to/from SSE bytes, observe failures, and rebuild HTTP.
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”proxai-core/protocol/Core crate protocol wire shapes and shared protocol enums. Models JSON only; no conversion and no HTTP carrier types.
src/pipeline/inbound.rsHTTP path detection, JSON byte parsing, and carrier error mapping before structured core preparation.
proxai-core/ingress/Carrier-independent structured request normalization, validation, model extraction, and typed ingress observations.
proxai-core/pipeline/Reusable structured façade that composes ingress, routing, request preparation, response normalization, and translation.
proxai-core/observe/Shared `Observer` contract, closed `Observation` variants, and no-op default; no concrete sinks.
proxai-core/routing/Carrier-independent provider-label selection from request protocol, model pattern, defaults, and route configuration.
proxai-core/provider/Carrier-independent provider request value preparation, response compatibility policy, structured response/event and error-payload normalization, and typed adaptation observations.
proxai-core/translation/Core crate request, response, and structured-event conversion across explicit protocol pairs. Exposes the `Translator` façade and emits typed observations through the shared `Observer`.
provider/Provider projection/summary extraction, request serialization, response observation, auth headers, upstream URL construction, and transport.
upstream/Reading upstream status, headers, complete bodies, or streaming byte carriers.
sse_translation.rsApplication carrier adapter: parses SSE bytes into `StreamTranslationInput`, invokes core `ResponsePipeline`, retains raw failure context, and encodes output back to SSE.
http_support/Protocol-neutral HTTP helpers such as content-type checks, response reconstruction, and boxed byte 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”proxai-core/protocol/is low-level wire modeling: JSON shapes only, no conversion.proxai-core/ingress/accepts structured JSON values and owns protocol normalization and validation; it never receives HTTP body bytes.src/pipeline/inbound.rsis the application adapter for path detection and JSON byte parsing.proxai-core/pipeline/composes the complete structured request/response path and binds the request-scoped core observer without owning transport.src/pipeline/coordinates HTTP carriers and transport around that core façade while keeping phase order explicit.proxai-core/provider/owns carrier-independent provider request value preparation, response compatibility policy, structured response/event and error-payload normalization, and typed adaptation observations.proxai-core/translation/is carrier-independent:Translatoraccepts JSONValues or structuredStreamEvents and never receives HTTP responses, bodies, SSE bytes, or provider-private structs.sse_translation.rsis the application adapter between upstream byte streams and core provider/translation APIs. It owns raw triggering frames and SSE output encoding.provider/owns application-side provider projections/summaries, request serialization, response observation, and transport details such as auth headers, upstream URLs, and idle-read timeout.proxai-core/observe/defines the sharedObservercontract and closedObservationenum. Core ingress, provider normalization, and translation emit typed variants;src/observe/supplies the downstream implementation and owns logging, diagnostics, and capture.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 --> core_pipeline[proxai-core/pipeline/] core_pipeline --> core_ingress[proxai-core/ingress/] core_pipeline --> core_provider[proxai-core/provider/] core_pipeline --> routing[proxai-core/routing/] pipeline --> provider[provider/] pipeline --> upstream[upstream/] pipeline --> sse_translation[sse_translation.rs] core_pipeline --> translation[proxai-core/translation/] pipeline --> observe[observe/] pipeline --> http_support[http_support/] pipeline --> error[error/]
core_ingress --> protocol[proxai-core/protocol/] core_provider --> protocol translation --> protocol provider --> protocol upstream --> http_support provider --> http_support sse_translation --> http_support sse_translation --> sse[sse.rs] sse_translation --> core_pipeline upstream --> sse observe --> error lib --> observe lib --> errorKey rules:
proxai-core/protocol/is low-level wire modeling.proxai-core/ingress/is the structured value boundary for inbound normalization and validation;src/pipeline/inbound.rsadapts HTTP bytes and application diagnostics.proxai-core/provider/is the structured provider-response compatibility boundary.proxai-core/pipeline/is the carrier-independent composition root;src/pipeline/coordinates the HTTP lifecycle and maps the selected provider label to transport.proxai-core/translation/depends only on protocol models, structured values/events, and the core observation contract; it does not depend on HTTP, SSE bytes, provider transport, or concrete application observation.sse_translation.rsadapts byte carriers to the coreResponsePipeline; the app pipeline owns diagnostics and client-facing error rendering.observe/implements the core observation trait but does not make protocol or routing decisions.
Translation selection
Section titled “Translation selection”Translator selects translation from two protocol values:
- inbound
request_protocol, detected bypipeline/inbound.rs - provider
protocol, configured on the selected provider
Rules:
- Same protocol: pass through without protocol conversion.
- Different protocols: dispatch to
crates/proxai-core/src/translation/<inbound_protocol>/to_<provider_protocol>/. - Unsupported pairs fail explicitly.
Translator is the single public façade. It directly owns the selected route and observation hook. Each entrypoint derives a phase-bound TranslationScope: request, non-streaming response, or streaming response. The scope is explicit and operation-local—never global, thread-local, or task-local—and is passed only into semantic projection paths that may emit adapted or dropped; pure conversions and outbound constructors remain scope-free. Pair code therefore calls observation methods without separately supplying a phase, so an operation cannot accidentally report telemetry under the wrong pipeline phase. Value translation borrows its derived scope; structured stream translation consumes Translator, moves a streaming-response scope into the returned stream, and lazily creates a private closed PairStreamingState containing exactly one of the six cross-protocol state machines or the identity path. This avoids allocating stream state for value-only translation and prevents pair state from being reused across streams. TranslationRoute and TranslationScope definitions live below the façade in translation/context.rs; top-level dispatch and pair code receive only &TranslationScope, so dispatch, phase, and observation cannot disagree about the active operation. Protocol-neutral event, error, terminal, identity, and lifecycle types live in dependency-free translation/stream.rs. Pair implementations depend only on context.rs and stream.rs, while Translator depends on those foundations plus pair states, so every dependency edge remains one-directional. Carrier adapters call Translator::translate_stream rather than driving pair events or terminal handling directly.
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 core streaming APIs as
StreamEventin/out; useByteStreamonly in the application carrier layer. - Keep provider names separate from protocol names.