diff --git a/AGENTS.md b/AGENTS.md index 2ef7ab75..86add35b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/` diff --git a/docs/developing/adding-filters.md b/docs/developing/adding-filters.md index f5012374..7ed7936b 100644 --- a/docs/developing/adding-filters.md +++ b/docs/developing/adding-filters.md @@ -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/`. diff --git a/docs/filters/README.md b/docs/filters/README.md index 95e2d0d4..23de86d8 100644 --- a/docs/filters/README.md +++ b/docs/filters/README.md @@ -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 diff --git a/filters/src/lib.rs b/filters/src/lib.rs index 3ceac787..13295ce9 100644 --- a/filters/src/lib.rs +++ b/filters/src/lib.rs @@ -12,6 +12,7 @@ pub mod agentic; pub mod guardrails; pub mod inference; pub mod prompt_enrich; +mod register; mod time_to_first_token; mod token_usage; @@ -19,6 +20,7 @@ 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}; diff --git a/filters/src/register.rs b/filters/src/register.rs new file mode 100644 index 00000000..31faf5fc --- /dev/null +++ b/filters/src/register.rs @@ -0,0 +1,208 @@ +// 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 + ); +} + +// ----------------------------------------------------------------------------- +// 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"); + assert!( + names.contains(&"openai_responses_validate"), + "expected openai_responses_validate in registry" + ); + assert!(names.contains(&"a2a"), "expected agentic filter a2a in registry"); + assert!( + names.contains(&"anthropic_validate"), + "expected anthropic filter in registry" + ); + assert!( + names.contains(&"request_id"), + "expected core builtin request_id in registry" + ); + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 4af79a63..567e437d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -52,153 +52,11 @@ include!(concat!(env!("OUT_DIR"), "/external_filters.rs")); #[must_use] pub fn build_full_registry() -> praxis_filter::FilterRegistry { let mut registry = praxis_filter::FilterRegistry::with_builtins(); - register_ai_filters(&mut registry); + praxis_ai_filters::register_ai_filters(&mut registry); register_external_filters(&mut registry); registry } -/// Register all AI filters into the registry. -fn register_ai_filters(registry: &mut praxis_filter::FilterRegistry) { - register_agentic_filters(registry); - register_general_ai_filters(registry); - register_anthropic_filters(registry); - register_openai_filters(registry); -} - -/// Register agentic protocol filters (A2A, MCP). -fn register_agentic_filters(registry: &mut praxis_filter::FilterRegistry) { - praxis_filter::register_filters!( - @register registry, - http "a2a" => praxis_ai_filters::A2aFilter::from_config - ); - praxis_filter::register_filters!( - @register registry, - http "mcp" => praxis_ai_filters::McpFilter::from_config - ); -} - -/// Register general-purpose AI filters. -fn register_general_ai_filters(registry: &mut praxis_filter::FilterRegistry) { - praxis_filter::register_filters!( - @register registry, - http "ai_guardrails" => praxis_ai_filters::AiGuardrailsFilter::from_config - ); - praxis_filter::register_filters!( - @register registry, - http "model_to_header" => praxis_ai_filters::ModelToHeaderFilter::from_config - ); - praxis_filter::register_filters!( - @register registry, - http "prompt_enrich" => praxis_ai_filters::PromptEnrichFilter::from_config - ); - praxis_filter::register_filters!( - @register registry, - http "token_count" => praxis_ai_filters::TokenCountFilter::from_config - ); - praxis_filter::register_filters!( - @register registry, - http "token_usage_headers" => praxis_ai_filters::TokenUsageHeadersFilter::from_config - ); - praxis_filter::register_filters!( - @register registry, - http "time_to_first_token" => praxis_ai_filters::TimeToFirstTokenFilter::from_config - ); -} - -/// Register Anthropic-specific filters. -fn register_anthropic_filters(registry: &mut praxis_filter::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 praxis_filter::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 praxis_filter::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 praxis_filter::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 - ); -} - // ----------------------------------------------------------------------------- // Tests // -----------------------------------------------------------------------------