A side-by-side analysis of the llm-d Inference Payload Processor (IPP) and Praxis, covering their architectures, design philosophies, integration models, and how they fit together in the broader llm-d and AI Grid ecosystems.
| IPP | Praxis | |
|---|---|---|
| Full name | llm-d Inference Payload Processor | Praxis Proxy |
| One-liner | Pluggable payload processing service for inference routing | High-performance proxy server and framework for AI and cloud-native workloads |
| Language | Go | Rust |
| Foundation | Envoy ext_proc gRPC protocol | Pingora (Cloudflare's production proxy runtime) |
| License | Apache 2.0 (CNCF llm-d project) | MIT |
| Scope | Single-cluster, single-gateway payload decisions | Single-cluster to multi-site grid routing |
| Deployment | Standalone gRPC service, one per Gateway | Standalone proxy binary, per-listener set |
| AI awareness | AI-first: built for inference payload processing | AI-capable: general proxy with AI features in a separate repo |
| IPP | Praxis | |
|---|---|---|
| How it sees traffic | Proxy streams events to IPP over gRPC | Traffic flows through it natively |
| Body access | Full body via ext_proc FULL_DUPLEX_STREAMED |
StreamBuffer: peek-then-stream, zero-copy forwarding |
| Extra network hops | 2+ per request (to/from ext_proc per event) | 0 (all in-process) |
| Body mutation | Yes (rewrites JSON model field) |
Possible via buffer manipulation, not the common path |
| Config hot reload | Restart required | Atomic pipeline swap, zero downtime |
┌────────────────────────┐
"Which model?" │ IPP ModelSelector │
cost, load, capability │ Filter → Score → Pick│
└──────────┬─────────────┘
│
┌───────────▼─────────────┐
"Which pool?" │ IPP header injection │
base-model mapping, HTTPRoute match │ → Proxy routing │
└──────────┬──────────────┘
│
┌───────────▼──────────────┐
"Which pod?" │ EPP / llmd_endpoint_ │
KV cache, queue depth, prefix cache │ picker (in Praxis) │
└──────────┬───────────────┘
│
┌───────────▼──────────────┐
"Which site/provider?" │ AI Grid / grid_route │
locality, cost, capacity, policy │ (Praxis + Grid Operator│
└──────────────────────────┘
| IPP | Praxis | |
|---|---|---|
| Write extensions in | Go | Rust |
| Registration | Runtime via factory function + YAML type |
Compile-time via register_filters! macro |
| Extension interfaces | 10+ interfaces (RequestProcessor, ResponseProcessor, Filter, Scorer, Picker, ProfilePicker, Extractor, Collector, DataSource, ResponseChunkProcessor, ResponseHeadersProcessor) | 2 traits: HttpFilter (6 hooks) and TcpFilter (2 hooks) |
| Config reference | Plugins declared once, referenced by name with pluginRef |
Filters declared in named chains, referenced by listener |
| Body access declaration | Implicit via ext_proc event config | Explicit via request_body_access() at construction |
| Capability | IPP | Praxis |
|---|---|---|
| Payload Processing | ||
| Model name extraction from body | ✓ | ✓ |
| LoRA adapter-to-base-model mapping | ✓ | -- |
| Model selection (Filter/Score/Pick) | ✓ | -- |
| JSON-RPC parsing | -- | ✓ |
| Response body mutation | ✓ | Possible |
| Profile-based traffic splitting | ✓ | -- (use branch chains) |
| Routing & Selection | ||
| Endpoint selection (queue/cache-aware) | -- (defers to EPP) | ✓ |
| Multi-site routing | -- | ✓ |
| Provider API translation | -- | ✓ (planned) |
| Credential injection | -- | ✓ |
| Proxy Infrastructure | ||
| TLS termination / mTLS | Proxy¹ | ✓ |
| Load balancing (RR, P2C, consistent hash) | Proxy¹ | ✓ |
| Circuit breaker with fallback | Proxy¹ | ✓ |
| Rate limiting | Proxy¹ | ✓ |
| Guardrails / content filtering | ✓² | ✓ |
| Operations | ||
| Cross-request data layer (async) | ✓ | Partial (KvStoreRegistry) |
| Dynamic config reload | -- | ✓ |
| TCP/L4 support | Proxy¹ | ✓ |
¹ Handled by the proxy that IPP sits alongside (typically Envoy) ² IPP can do payload-level content filtering; broader guardrails handled by proxy
Current llm-d stack:
Client → Envoy → ext_proc → IPP → ext_proc → EPP → vLLM
Praxis native (primary track):
Client → Praxis → llmd_endpoint_picker (in-process) → vLLM
Praxis + IPP (hybrid):
Client → Praxis → ext_proc → IPP (model selection) → grid_route → Backend
AI Grid (full stack):
Client → Praxis AI Gateway → grid_route → credential_inject → Site/Provider/Model Server
IPP and Praxis are not competing projects. They occupy different layers of the inference data plane and were designed with different constraints. IPP is a payload-aware sidecar service that inspects and mutates inference request and response bodies over Envoy's ext_proc protocol. Praxis is a high-performance proxy server and framework built on Pingora that processes all traffic through composable filter pipelines. When deployed together, IPP handles pool-level model routing decisions while Praxis (or another proxy) handles the actual traffic forwarding, TLS, load balancing, and the broader gateway capabilities.
The key question is not "which one wins" but "where does each one's responsibility begin and end, and what changes when Praxis absorbs IPP-style capabilities natively."
| Dimension | IPP | Praxis |
|---|---|---|
| What it is | Pluggable payload processing service | Proxy server and framework |
| Language | Go | Rust |
| Deployment | Standalone gRPC service, one per Gateway | Standalone proxy binary, per-listener |
| Protocol | Envoy ext_proc (gRPC) | Native HTTP/TCP via Pingora |
| Primary job | Inspect/mutate request/response payloads | Route, forward, and secure all traffic |
| Body access | Full request/response bodies via ext_proc streaming | StreamBuffer (peek-then-stream) with zero-copy forwarding |
| Extension model | Go plugins via YAML config | Rust filters compiled into the binary |
| Scope | Single-cluster, single-gateway payload decisions | Single-cluster to multi-site grid routing |
IPP was born from a specific need in the llm-d stack: the Envoy proxy that fronts inference pools cannot read request bodies to make routing decisions. Someone needs to parse the OpenAI-compatible JSON, extract the model field, and inject a header so Envoy's HTTPRoute rules can select the right InferencePool. IPP fills that gap.
Its design principles reflect this heritage:
- The framework is opinion-free; plugins hold the opinions. The core defines the pipeline and extension points. All concrete behavior lives in swappable plugins.
- Explicit state boundaries. Per-request state is shared through an in-memory "cycle state" for one request's lifetime. Cross-request state lives in a separate data layer.
- Pluggable everything. Request processing, response processing, model selection, profile selection, and data collection are all swappable via YAML configuration.
Praxis was designed as a general-purpose proxy that happens to be excellent at AI workloads, not as an AI-specific tool. Its design principles are:
- Fast. Performance is a primary design goal. Built on Pingora (Cloudflare's production proxy runtime), Praxis inherits connection pooling, zero-copy forwarding, and efficient memory management.
- Secure by default.
unsafe_code = "deny"across the workspace, Rustls (no OpenSSL), SSRF protection on health checks, localhost-default listener binds. - Composable. Every behavior is a filter. Routing, load balancing, rate limiting, guardrails, body-based routing: all filters, all using the same
HttpFilter/TcpFiltertraits, all assembled into pipelines through named chains. - Extensible. Custom filters implement the same traits as built-in filters and are registered with a single macro.
| Principle | IPP | Praxis |
|---|---|---|
| Core identity | Payload processing framework | Proxy server framework |
| Extension mechanism | Runtime plugin loading via YAML | Compile-time filter registration via Rust traits |
| Configuration | PayloadProcessorConfig (YAML, Kubernetes-style) |
YAML config with filter chains and listeners |
| Default posture | Does nothing until plugins are configured | Ships with 25+ built-in filters covering routing, security, observability |
| Performance priority | Correctness and extensibility first | Performance is a primary design constraint |
| Security model | Delegates to proxy and Kubernetes RBAC | Security-first with fail-closed defaults |
IPP implements the Envoy External Processing (ext_proc) gRPC API. The proxy streams HTTP lifecycle events to IPP, which replies with mutations.
Client
--> Proxy (Envoy)
--> ext_proc gRPC stream --> IPP
--> Plugin Pipeline
<-- Mutations (headers, body edits)
<-- Apply mutations
--> Backend (InferencePool / model server)
Processing Pipeline:
PreProcessor Plugins --> ProfilePicker --> Profile Request Plugins
--> [Model Server] --> Profile Response Plugins --> PostProcessor Plugins
Key concepts:
- Profile: A named set of request and response plugins. Exactly one profile runs per request, chosen by a profile picker. This enables different processing logic for different traffic classes.
- ModelSelector: A three-phase
Filter -> Score -> Pickpipeline that selects which model serves a request. Filters remove ineligible candidates, scorers rank the rest, and a picker makes the final choice. - Data Layer: Background collectors, extractors, and datasources maintain cross-request state (in-flight counts, model configs, cost metadata) that scorers and filters consume without adding latency to the request path.
Praxis processes traffic natively through Pingora's HTTP/TCP runtime. No external service call is needed for any routing or processing decision.
Client
--> Praxis Listener (TCP accept, TLS termination)
--> Filter Pipeline (flat Vec<PipelineFilter>)
F0 --> F1 --> F2 --> ... --> FN --> Upstream
FN <-- ... <-- F1 <-- F0 <-- Upstream (response, reverse order)
--> Upstream (via connection pool)
--> Backend
Key concepts:
- Filter Chains: Named, reusable groups of filters defined in YAML. A listener references one or more chains; at startup they are flattened into a single pipeline.
- Pipeline: The runtime execution list. Filters run forward on request, reverse on response. Any filter can short-circuit.
- StreamBuffer: Praxis's differentiated body access mode. Chunks are delivered to filters as they arrive (streaming inspection) but not forwarded upstream until a filter releases the buffer. This enables body-based routing with zero post-decision latency.
- Branch Chains: Conditional branching based on filter results. A filter writes key-value results; branch conditions can skip forward, loop back, terminate, or reject.
- Dynamic Reload: Pipelines are atomically swapped at runtime via
ArcSwap. Config file changes rebuild all pipelines; in-flight requests drain on the old pipeline.
| Aspect | IPP | Praxis |
|---|---|---|
| Execution model | Out-of-process gRPC service | In-process filter pipeline |
| Request flow | Proxy -> gRPC -> IPP -> gRPC -> Proxy -> Backend | Client -> Pipeline -> Backend (single process) |
| Network hops per request | 2+ extra (to/from ext_proc per enabled event) | 0 extra (all in-process) |
| Body access | Full body via FULL_DUPLEX_STREAMED ext_proc mode |
StreamBuffer (peek-then-stream, zero-copy) |
| Latency overhead | gRPC serialization + network hop per event | In-process function calls |
| State model | CycleState (per-request) + DataLayer (cross-request) | HttpFilterContext (per-request) + KvStoreRegistry (cross-request) |
| Pipeline shape | Linear with profile branching | Linear with conditional branch chains |
| Hot reload | Restart required for config changes | Atomic pipeline swap, zero downtime |
| Response processing | Separate response plugin phase | Reverse-order response filter phase |
| Streaming responses | ResponseChunkProcessor for per-chunk processing | on_response_body hook (synchronous, reverse order) |
| Protocol support | HTTP only (via ext_proc) | HTTP + TCP + TLS natively |
IPP organizes plugins into clear categories based on the framework interface they implement:
| Category | Interface | When Executed | Examples |
|---|---|---|---|
| Request Handling | RequestProcessor |
Before routing | body-field-to-header, base-model-to-header, model-selector |
| Response Handling | ResponseProcessor / ResponseHeadersProcessor / ResponseChunkProcessor |
After model server reply | model-name-to-header |
| Model Selector - Filter | Filter |
First phase of ModelSelector | model-group-name-filter |
| Model Selector - Scorer | Scorer |
Second phase, weighted combination | cost-scorer, inflight-requests-scorer |
| Model Selector - Picker | Picker |
Final selection | max-score-picker, random-picker, weighted-random-picker |
| Profile Picker | ProfilePicker |
Before profile execution | single-profile-picker |
| Data Layer - Collector | Collector |
Background timer | (none in-tree yet) |
| Data Layer - Extractor | Extractor |
Background event-driven | request-metadata-extractor |
| Data Layer - Datasource | DataSource |
Background watcher | model-config-datasource |
Plugins are declared once in the plugins list and referenced by name elsewhere. The same type can be instantiated multiple times with different parameters.
Praxis ships with 25+ built-in filters organized by function:
| Category | Filters |
|---|---|
| Observability | access_log, request_id |
| Payload Processing | compression, json_body_field, json_rpc |
| Security | cors, credential_injection, csrf, forwarded_headers, guardrails, ip_acl, policy (CPEX) |
| Traffic Management | circuit_breaker, endpoint_selector, grpc_detection, load_balancer, rate_limit, redirect, router, static_response, timeout |
| Transformation | header, path_rewrite, url_rewrite |
| TCP | tcp_access_log, sni_router, tcp_load_balancer |
| External | ext_proc (Envoy-compatible, opt-in crate) |
All filters implement the same HttpFilter trait. Custom filters use the same trait and register with register_filters!.
| Dimension | IPP | Praxis |
|---|---|---|
| Language | Go | Rust |
| Registration | Factory function registered at startup | register_filters! macro at compile time |
| Discovery | Runtime via YAML type field |
Compile-time via filter registry |
| Parameters | Opaque YAML/JSON passed to factory | Typed YAML deserialized via serde |
| Lifecycle | Instantiated per-config-load | Instantiated per-pipeline-build |
| Trait surface | Multiple interfaces (RequestProcessor, ResponseProcessor, Filter, Scorer, Picker, etc.) | Two traits: HttpFilter (6 hooks) and TcpFilter (2 hooks) |
| Body access declaration | Implicit via ext_proc event config | Explicit via request_body_access() / response_body_access() |
| Custom plugin effort | Implement Go interface, register factory, configure in YAML | Implement Rust trait, register macro, configure in YAML |
Both systems exist because traditional proxies route on headers and paths but cannot read the JSON body to extract model names, estimate complexity, or classify task type. This section compares how each solves the payload problem.
IPP receives the full request body via ext_proc's FULL_DUPLEX_STREAMED mode. The proxy streams body chunks to IPP over gRPC; IPP's plugins inspect and potentially mutate the body, then return the result.
Strengths:
- Full body access with mutation capability (can rewrite the
modelfield in the JSON body) - Response body processing with the same mechanism
- Profile-based routing of different processing pipelines
Trade-offs:
- Every body inspection is a network round-trip
- gRPC serialization/deserialization overhead on the hot path
- Body must be fully available before plugins can make decisions (no partial-parse optimization)
Praxis's StreamBuffer mode delivers body chunks to filters as they arrive but defers upstream forwarding until a filter returns Release. This enables body inspection before the routing decision with zero post-decision latency.
Strengths:
- In-process, zero network overhead
- Chunks inspected as they arrive (streaming inspection)
- Once released, remaining chunks flow through in stream mode (no full-body buffering required)
- Pre-computed at pipeline build time, so the protocol layer knows exactly what to expect
Trade-offs:
- Body mutation is more complex (buffer must be modified before release)
- Read-only access is the common path; mutation requires explicit buffer manipulation
- Rust implementation means higher barrier to extending body processing
| Capability | IPP | Praxis |
|---|---|---|
| Extract JSON field to header | body-field-to-header plugin |
json_body_field filter |
| Model name extraction | body-field-to-header + base-model-to-header |
json_body_field (model to X-Model header) |
| LoRA adapter mapping | base-model-to-header with ConfigMap reconciler |
Not built-in (would be a custom filter) |
| JSON-RPC parsing | Not built-in | json_rpc filter (method, id, kind, batch to headers) |
| Body-based model selection | model-selector (Filter -> Score -> Pick) |
Not built-in as a framework; individual scoring logic in custom filters |
| Streaming field detection | body-field-to-header for stream boolean |
json_body_field for any top-level field |
| Body mutation | Yes (can rewrite model field) |
Possible via StreamBuffer but not the common path |
IPP's flagship capability is structured model selection through a three-phase pipeline:
Filter --> Score --> Pick
- Filters remove ineligible models (e.g.,
model-group-name-filterrestricts by group name or explicit model name) - Scorers assign each candidate a score in [0, 1] with configurable weights (e.g.,
cost-scorerranks by price,inflight-requests-scorerranks by current load) - Pickers make the final selection (e.g.,
max-score-pickerfor deterministic best,weighted-random-pickerfor exploration)
This is a model-level decision: which model should serve this request. The selected model name is written back into the request body's model field, and routing continues as if the client had requested that model directly.
Praxis does not have a dedicated ModelSelector framework. Instead, routing decisions compose from individual filters in the pipeline:
- Signal Extraction:
json_body_fieldextracts model name to header;json_rpcparses JSON-RPC envelopes - Router: Matches path, host, and headers against configured routes; sets
ctx.cluster - Load Balancer: Selects an endpoint within the cluster using round-robin, least-connections, P2C, or consistent-hash
- Branch Chains: Conditional branching based on filter results enables fallback routing
For llm-d integration specifically, the llmd_endpoint_picker filter (behind the ai-inference feature flag) embeds the EPP scheduling pipeline directly:
Parse OpenAI body --> Model-aware filter --> Load-aware scorer --> Pick endpoint --> Set ctx.upstream
| Question | IPP Answers | Praxis Answers |
|---|---|---|
| Which model serves this request? | Yes (ModelSelector framework) | Partially (json_body_field + router, or custom filter) |
| Which pool serves this request? | Yes (header injection for HTTPRoute matching) | Yes (router filter + cluster selection) |
| Which endpoint/pod within a pool? | No (defers to EPP) | Yes (load_balancer filter, or llmd_endpoint_picker) |
| Which site/region serves this request? | No (single-cluster only) | Yes (via AI Grid's grid_route filter) |
| Which provider serves this request? | No | Yes (via AI Grid's backend_scorer + credential_injection) |
IPP sits between the proxy (Envoy) and the EPP (Endpoint Picker Provider) in the current llm-d architecture:
Client -> Envoy -> ext_proc -> IPP (pool selection)
-> ext_proc -> EPP (endpoint selection)
-> Model Server (vLLM, SGLang, etc.)
IPP decides which pool. The EPP decides which pod within the pool. They are deployed and configured independently. IPP's base-model-to-header plugin maps LoRA adapter names back to base models, enabling multi-pool routing behind a single OpenAI-compatible endpoint.
Praxis's llm-d integration takes two forms:
Native EPP (primary track): The llmd_endpoint_picker filter embeds EPP-style scheduling directly in the Praxis process. No ext_proc hop is needed. Praxis parses the OpenAI body, filters by model eligibility, scores by queue depth and KV-cache utilization, and sets ctx.upstream to the selected endpoint.
Client -> Praxis -> llmd_endpoint_picker (in-process)
-> Model Server (vLLM, llm-d-inference-sim)
ext_proc compatibility (back-pocket track): Praxis replaces Envoy as the proxy but still calls the existing Go EPP via ext_proc. This addresses Envoy's ext_proc connection-limit problem while keeping Go EPP behavior unchanged.
Client -> Praxis -> ext_proc -> Go EPP (endpoint selection)
-> Model Server
| Aspect | IPP + Envoy (current llm-d) | Praxis Native EPP | Praxis + ext_proc |
|---|---|---|---|
| Proxy | Envoy | Praxis/Pingora | Praxis/Pingora |
| Pool selection | IPP (ext_proc) | Praxis filter pipeline | IPP or custom ext_proc service |
| Endpoint selection | EPP (ext_proc) | llmd_endpoint_picker filter |
Go EPP (ext_proc) |
| Network hops | 2 ext_proc round-trips | 0 (all in-process) | 1 ext_proc round-trip |
| Body parsing | IPP parses JSON, injects headers | Praxis parses JSON natively | Same as IPP path |
| Metrics source | EPP scrapes vLLM Prometheus | Praxis background worker scrapes vLLM | EPP scrapes vLLM |
| KV cache awareness | EPP (block-level) | Praxis (utilization-level, prefix-cache planned) | EPP (block-level) |
| Kubernetes discovery | EPP watches InferencePool | Praxis discovery worker (planned) | EPP watches InferencePool |
| Config hot reload | Restart required | Atomic pipeline swap | Restart for ext_proc config |
IPP is designed for single-cluster, single-gateway deployments. It has no awareness of multi-site topology, cross-cluster routing, or distributed state. Its job is payload processing within one gateway's filter chain.
Praxis serves as the data plane for AI Grid, which is a distributed peer-to-peer network of inference and agentic services. The architecture splits into three layers:
| Layer | Responsibility | Technology |
|---|---|---|
| Praxis core | Generic proxy runtime, filter pipeline, TLS, connection pooling | Rust/Pingora |
Praxis AI (praxis-proxy/ai) |
AI-specific filters: grid_route, grid_credential_inject, llm-d ext_proc, API translation |
Rust, compiled into praxis-ai-proxy binary |
Grid Operator (praxis-proxy/grid) |
Control plane: SWIM membership, CRDT state propagation, provider inventory, overlay generation | Rust, Kubernetes operator |
The AI Grid routing pipeline through Praxis follows a multi-stage design:
| Stage | Filter | Purpose |
|---|---|---|
| 1. Signal Extraction | json_body_field, json_rpc |
Extract model name, streaming flag, tool calls from body |
| 2. Semantic Classification | semantic_classifier (planned) |
Classify request by task type using embedding model |
| 3. Backend Scoring | backend_scorer (planned) |
Score all eligible backends by metrics, cost, locality |
| 4. Policy Enforcement | rate_limit, guardrails, budget_enforcer, sovereignty_filter |
Apply business rules |
| 5. API Translation | api_translator (planned) |
Rewrite request/response for target provider format |
| 6. Credential Injection | credential_injection, grid_credential_inject |
Inject provider-specific auth |
| 7. Resilience | circuit_breaker + branch chains |
Automatic failover to next-best backend |
IPP can participate in the AI Grid architecture as a standalone filter within the Praxis AI pipeline. The integration model uses Praxis's ext_proc filter to call IPP as an external processing service:
Client -> Praxis AI Gateway
-> ext_proc -> IPP (model selection / LoRA mapping)
-> grid_route (multi-site routing)
-> credential_injection (provider auth)
-> Backend (local llm-d, remote site, or cloud provider)
In this model, IPP provides model-level selection (choosing between models based on cost, load, and capability) while Praxis handles site-level routing, policy enforcement, and provider abstraction.
However, the Praxis native path is the strategic direction. The AI Grid proposal's routing pipeline embeds all the capabilities IPP provides today as native Praxis filters:
json_body_fieldreplacesbody-field-to-headerjson_rpcreplaces JSON-RPC parsingbackend_scorerreplaces thecost-scorer+inflight-requests-scorercombinationgrid_routereplaces pool-level header injection with multi-site-aware routing
The strategic question is whether IPP's pluggable ModelSelector framework (Filter -> Score -> Pick with weighted combination) provides enough value to justify the ext_proc hop, or whether equivalent logic should be implemented as native Praxis filters.
| Aspect | IPP | Praxis |
|---|---|---|
| Binary | Single Go binary | Single Rust binary |
| Deployment unit | Kubernetes Deployment + Service | Standalone binary or Kubernetes Deployment |
| Per-gateway ratio | One IPP per Gateway | One Praxis instance per listener set |
| Packaging | Helm chart with provider-specific integration (Istio/GKE/none) | YAML config file; Helm/OLM planned for Grid |
| Config format | PayloadProcessorConfig (Kubernetes-style YAML) |
Praxis YAML (listeners, filter_chains, clusters) |
| Config reload | Restart required | Atomic hot reload via ArcSwap |
| Health checks | gRPC health port (9005) | HTTP /ready and /healthy on admin listener |
| Metrics | Prometheus on port 9090 | Prometheus on admin listener /metrics |
| Tracing | OpenTelemetry (configurable) | tracing crate with structured logging |
| TLS | Self-signed cert for ext_proc gRPC | Rustls with SNI, mTLS, cert hot-reload, CRL |
| Resource footprint | Go runtime + gRPC server | Rust binary, no GC, lower memory baseline |
| Kubernetes integration | ConfigMap watching, controller-runtime, RBAC | Static config (primary), Kubernetes discovery (planned) |
- You are running the standard llm-d stack with Envoy and need multi-pool routing
- You need model-level selection (choosing between models, not just endpoints) with the structured Filter -> Score -> Pick pipeline
- You need LoRA adapter-to-base-model mapping with runtime ConfigMap reconciliation
- You want to write custom Go plugins without modifying the proxy binary
- You need response body processing with per-chunk streaming access
- Your deployment is single-cluster and the ext_proc overhead is acceptable
- You need a complete proxy solution (TLS, routing, load balancing, security, observability)
- You need multi-site or multi-provider routing (AI Grid)
- You want zero ext_proc overhead on the request path
- You need hot config reload without restarts
- You need native llm-d endpoint picking with in-process model-aware, load-aware, KV-cache-aware scheduling
- You need TCP/L4 support alongside HTTP
- You need branch chain conditional logic (fallback routing, circuit breaker recovery paths)
- You are building a custom proxy using the Praxis framework
- You want IPP's ModelSelector framework for model-level decisions while Praxis handles site/provider-level routing and all proxy concerns
- You are migrating from Envoy and want to keep IPP as a stable ext_proc service while Praxis replaces Envoy as the data plane
- You need independent scaling of payload processing and proxy concerns
The two projects are converging. Praxis's native EPP filter already provides a subset of what IPP does (model extraction, load-aware scoring, endpoint selection), and the AI Grid roadmap calls for native Praxis filters that cover the remaining IPP capabilities.
| IPP Capability | Praxis Equivalent | Status |
|---|---|---|
body-field-to-header (model extraction) |
json_body_field filter |
Shipped |
base-model-to-header (LoRA mapping) |
No direct equivalent | Gap |
| JSON-RPC parsing | json_rpc filter |
Shipped |
| Queue depth scoring | llmd_endpoint_picker (queue + KV cache) |
POC |
| Cost-aware scoring | backend_scorer (planned in AI Grid) |
Planned |
| Inflight requests scoring | llmd_endpoint_picker |
POC |
| Model group filtering | Router filter with header matching | Indirect |
| ConfigMap-based model config | Static YAML config / Kubernetes discovery (planned) | Partial |
| Response header injection | header filter |
Shipped |
- Structured ModelSelector framework (Filter -> Score -> Pick with weighted combination)
- LoRA adapter-to-base-model mapping with runtime ConfigMap reconciliation
- Profile-based traffic splitting (different processing pipelines for different request classes)
- Data Layer with event-driven extractors (asynchronous cross-request state management)
- Response body mutation (rewriting response JSON before returning to client)
The llm-d integration work has established a clear architectural boundary:
- llm-d/EPP level: Endpoint selection within a pool (which pod, considering KV cache, queue depth, prefix cache). This is where the
llmd_endpoint_pickerfilter or Go EPP operates. - IPP level: Model/pool selection (which model, considering cost, load, capabilities). This is IPP's current domain.
- AI Grid level: Site/provider selection (which site, which provider, considering locality, cost, capacity, policy, sovereignty). This is Praxis + Grid Operator territory.
The native Praxis path is the primary track. IPP remains a viable integration option through ext_proc, but the long-term direction is absorbing the useful IPP scheduling patterns into native Praxis filters where they execute in-process with zero network overhead.
| Dimension | IPP | Praxis |
|---|---|---|
| Identity | Payload processing sidecar | Proxy server and framework |
| Language | Go | Rust |
| Runtime | gRPC server (ext_proc) | Pingora HTTP/TCP runtime |
| Latency overhead | gRPC round-trip per event | Zero (in-process) |
| Body access | Full body via ext_proc streaming | StreamBuffer (peek-then-stream) |
| Model selection | Structured Filter -> Score -> Pick | Individual filters (or custom) |
| Endpoint selection | Defers to EPP | Native llmd_endpoint_picker |
| Multi-site routing | No | Yes (AI Grid) |
| Provider abstraction | No | Yes (API translation, credential injection) |
| TLS | Self-signed for gRPC | Full TLS: SNI, mTLS, CRL, hot-reload |
| Hot reload | Restart | Atomic pipeline swap |
| Extension language | Go | Rust |
| Kubernetes native | Yes (controller-runtime, ConfigMap watching) | Config file primary; Kubernetes discovery planned |
| CNCF alignment | Part of llm-d (CNCF Sandbox) | Independent project, upstream Kubernetes alignment |
| Maturity | Production-ready for multi-pool routing | Core proxy production-ready; AI features in active development |
| Grid integration | Standalone ext_proc service via Praxis ext_proc filter | Native data plane for AI Grid |