Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ New capabilities require:

1. Create module under `filters/src/` or `apis/src/`
2. Implement `HttpFilter` from `praxis-filter`
3. Register in `server/src/lib.rs` via
`register_filters!` macro
3. Register in `praxis_ai_filters::register_ai_filters`
(`filters/src/register.rs`) via `register_filters!`
4. Add unit tests and doctests
5. Add example config in `examples/configs/`

Expand Down
4 changes: 2 additions & 2 deletions docs/developing/adding-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ first.
2. Implement `HttpFilter` (from `praxis-filter`). Add a
`from_config` factory that deserializes a
`serde_yaml::Value` into your config struct.
3. Register it in `server/src/lib.rs` inside the
`register_ai_filters` function.
3. Register it in `praxis_ai_filters::register_ai_filters`
(in `filters/src/register.rs`).
4. Add unit tests and doctests.
5. Add an example config in the appropriate category
under `examples/configs/`.
Expand Down
39 changes: 34 additions & 5 deletions docs/filters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,45 @@ in the [Praxis core filter reference][praxis-filters], not duplicated here.

## Registration

`server/src/lib.rs` builds the registry in three steps:
In-tree AI filters are registered by
`praxis_ai_filters::register_ai_filters`. Downstream
consumers that only need AI filters (for example an
Envoy ExtProc) can depend on `praxis-ai-filters` without
the proxy crate:

```rust
use praxis_filter::FilterRegistry;

let mut registry = FilterRegistry::with_builtins();
praxis_ai_filters::register_ai_filters(&mut registry);
// Or: let registry = praxis_ai_filters::build_ai_registry();
```

`praxis-ai-proxy` builds the full registry in three
ownership layers:

```rust
let mut registry = FilterRegistry::with_builtins();
register_ai_filters(&mut registry);
register_external_filters(&mut registry);
praxis_ai_filters::register_ai_filters(&mut registry);
register_external_filters(&mut registry); // proxy-only
```

This keeps core filters, in-tree AI filters, and
auto-discovered extensions at clear ownership
boundaries. External filter auto-discovery stays
proxy-only.

Pipelines that use OpenAI store or rehydrate filters
must also install the response-store extension:

```rust
pipeline.add_pipeline_extension(
Box::new(praxis_ai_apis::store::ResponseStoreRegistry::new()),
);
```

This keeps core filters, in-tree AI filters, and auto-discovered extensions at
clear ownership boundaries.
The AI proxy does this in `server/src/pipelines.rs`.
Other hosts (such as ExtProc) must do the same.

## Related documentation

Expand Down
2 changes: 2 additions & 0 deletions filters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ pub mod agentic;
pub mod guardrails;
pub mod inference;
pub mod prompt_enrich;
mod register;
mod time_to_first_token;
mod token_usage;

pub use agentic::{a2a::A2aFilter, mcp::McpFilter};
pub use guardrails::AiGuardrailsFilter;
pub use inference::ModelToHeaderFilter;
pub use prompt_enrich::PromptEnrichFilter;
pub use register::{build_ai_registry, register_ai_filters};
pub use time_to_first_token::TimeToFirstTokenFilter;
pub use token_usage::{TokenCountFilter, TokenUsageHeadersFilter};

Expand Down
199 changes: 199 additions & 0 deletions filters/src/register.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Praxis Contributors

//! Public AI filter registration for consumers outside `praxis-ai-proxy`.

use praxis_filter::FilterRegistry;

use crate::{
A2aFilter, AiGuardrailsFilter, McpFilter, ModelToHeaderFilter, PromptEnrichFilter, TimeToFirstTokenFilter,
TokenCountFilter, TokenUsageHeadersFilter,
};

/// Register all in-tree AI HTTP filters into `registry`.
///
/// Does not call [`FilterRegistry::with_builtins`].
/// Does not register auto-discovered external filters.
///
/// Pipelines that use OpenAI store or rehydrate filters must also install:
///
/// ```rust,ignore
/// pipeline.add_pipeline_extension(
/// Box::new(praxis_ai_apis::store::ResponseStoreRegistry::new()),
/// );
/// ```
pub fn register_ai_filters(registry: &mut FilterRegistry) {
register_agentic_filters(registry);
register_general_ai_filters(registry);
register_anthropic_filters(registry);
register_openai_filters(registry);
}

/// Build a [`FilterRegistry`] with core builtins and in-tree AI filters.
///
/// Equivalent to [`FilterRegistry::with_builtins`] followed by
/// [`register_ai_filters`]. Does not register auto-discovered external
/// filters.
///
/// Pipelines that use OpenAI store or rehydrate filters must also install
/// [`praxis_ai_apis::store::ResponseStoreRegistry`] as a pipeline extension.
#[must_use]
pub fn build_ai_registry() -> FilterRegistry {
let mut registry = FilterRegistry::with_builtins();
register_ai_filters(&mut registry);
registry
}

/// Register agentic protocol filters (A2A, MCP).
fn register_agentic_filters(registry: &mut FilterRegistry) {
praxis_filter::register_filters!(
@register registry,
http "a2a" => A2aFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "mcp" => McpFilter::from_config
);
}

/// Register general-purpose AI filters.
fn register_general_ai_filters(registry: &mut FilterRegistry) {
praxis_filter::register_filters!(
@register registry,
http "ai_guardrails" => AiGuardrailsFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "model_to_header" => ModelToHeaderFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "prompt_enrich" => PromptEnrichFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "token_count" => TokenCountFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "token_usage_headers" => TokenUsageHeadersFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "time_to_first_token" => TimeToFirstTokenFilter::from_config
);
}

/// Register Anthropic-specific filters.
fn register_anthropic_filters(registry: &mut FilterRegistry) {
praxis_filter::register_filters!(
@register registry,
http "anthropic_messages_format" => praxis_ai_apis::anthropic::AnthropicMessagesFormatFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "anthropic_messages_protocol" => praxis_ai_apis::anthropic::AnthropicMessagesProtocolFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "anthropic_stream_events" => praxis_ai_apis::anthropic::AnthropicStreamEventsFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "anthropic_to_openai" => praxis_ai_apis::anthropic::AnthropicToOpenaiFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "anthropic_validate" => praxis_ai_apis::anthropic::AnthropicValidateFilter::from_config
);
}

/// Register OpenAI Responses API request-path filters.
fn register_openai_filters(registry: &mut FilterRegistry) {
register_openai_responses_filters(registry);
praxis_filter::register_filters!(
@register registry,
http "openai_conversations" => praxis_ai_apis::openai::OpenaiConversationsFilter::from_config
);
}

/// Register OpenAI Responses API filters.
fn register_openai_responses_filters(registry: &mut FilterRegistry) {
praxis_filter::register_filters!(
@register registry,
http "openai_doc_extract" => praxis_ai_apis::openai::DocExtractFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_file_resolve" => praxis_ai_apis::openai::FileResolveFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_responses_format" => praxis_ai_apis::openai::ResponsesFormatFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_responses_model_rewrite" => praxis_ai_apis::openai::ModelRewriteFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_responses_validate" => praxis_ai_apis::openai::OpenaiResponsesValidateFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_responses_rehydrate" => praxis_ai_apis::openai::RehydrateFilter::from_config
);
register_openai_response_filters(registry);
}

/// Register OpenAI Responses API response-path and persistence filters.
fn register_openai_response_filters(registry: &mut FilterRegistry) {
praxis_filter::register_filters!(
@register registry,
http "openai_response_store" => praxis_ai_apis::openai::ResponseStoreFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_stream_events" => praxis_ai_apis::openai::OpenaiStreamEventsFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_responses_proxy" => praxis_ai_apis::openai::ResponsesProxyFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_mcp_tool_resolve" => praxis_ai_apis::openai::McpToolResolveFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_tool_parse" => praxis_ai_apis::openai::ToolParseFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_web_search" => praxis_ai_apis::openai::WebSearchFilter::from_config
);
praxis_filter::register_filters!(
@register registry,
http "openai_mcp_dispatch" => praxis_ai_apis::openai::McpDispatchFilter::from_config
);
}

#[cfg(test)]

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.

[Medium] Missing // Tests separator comment before the #[cfg(test)] block. Convention requires a full-width separator:

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]

mod tests {
use super::build_ai_registry;

#[test]
fn build_ai_registry_includes_ai_and_builtin_filters() {
let registry = build_ai_registry();
let names = registry.available_filters();
assert!(names.contains(&"ai_guardrails"), "expected ai_guardrails in registry");

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.

[Medium] The test verifies filters from 2 of the 4 registration groups (general via ai_guardrails, OpenAI via openai_responses_validate) but has zero assertions for the Anthropic and agentic groups. If register_anthropic_filters or register_agentic_filters is accidentally removed from register_ai_filters, this test will not detect it.

Add assertions for at least one filter from each uncovered group:

assert!(names.contains(&"a2a"), "expected agentic filter a2a in registry");
assert!(names.contains(&"anthropic_validate"), "expected anthropic filter in registry");

assert!(
names.contains(&"openai_responses_validate"),
"expected openai_responses_validate in registry"
);
assert!(
names.contains(&"request_id"),
"expected core builtin request_id in registry"
);
}
}
Loading
Loading