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
31 changes: 31 additions & 0 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,37 @@ jobs:
- name: Run Azure OpenAI Python SDK tests
run: uv run tests/integration/sdk/openai/test_azure_chat_completions.py -s

models-sdk:
needs: [changes]
if: |
always() &&
(needs.changes.result == 'skipped' || needs.changes.outputs.relevant == 'true')
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup Rust
uses: praxis-proxy/conventions/.github/actions/setup-rust@c5837c3fdd35066b05d337594fabe9d641d08df2 # v0.3.0
with:
cache-suffix: models-sdk-cargo

- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
cache-dependency-glob: tests/integration/sdk/openai/test_openai_models.py

- name: Build Praxis
run: cargo build -p praxis-ai-proxy --features basic-auth-filter,model-catalog-filter

- name: Run OpenAI models Python SDK tests
run: uv run tests/integration/sdk/openai/test_openai_models.py -s

# ----------------------------------------------------------------------------
# Responses/agentic feature-boundary checks
#
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ LINT_FMT_CARGO = PATH="$$(dirname "$$(rustup which --toolchain nightly cargo)"):
# Experimental filter features are package-specific and off by default.
# Basic Auth is exposed by praxis-ai-proxy and forwarded by the integration-test
# crate; it is not a praxis-ai-filters feature.
FILTER_EXPERIMENTAL_FEATURES := azure-ad-filter,gcp-adc-filter,http-callout-filter,token-rate-limit-filter,token-ceiling-filter,inflight-tracker-filter,vertex-anthropic-filter
INTEGRATION_EXPERIMENTAL_FEATURES := azure-ad-filter,basic-auth-filter,gcp-adc-filter,http-callout-filter,token-rate-limit-filter,token-ceiling-filter,inflight-tracker-filter,vertex-anthropic-filter
FILTER_EXPERIMENTAL_FEATURES := azure-ad-filter,gcp-adc-filter,http-callout-filter,token-rate-limit-filter,token-ceiling-filter,inflight-tracker-filter,model-catalog-filter,vertex-anthropic-filter
INTEGRATION_EXPERIMENTAL_FEATURES := azure-ad-filter,basic-auth-filter,gcp-adc-filter,http-callout-filter,token-rate-limit-filter,token-ceiling-filter,inflight-tracker-filter,model-catalog-filter,vertex-anthropic-filter
# Experimental praxis-ai-apis features, whose unit tests live in that crate.
APIS_EXPERIMENTAL_FEATURES := vertex-anthropic
# Release and container builds exclude experimental Responses and agents.
Expand Down Expand Up @@ -272,7 +272,7 @@ test-integration-experimental:
PRAXIS_AI_BIN=$(abspath $(or $(CARGO_TARGET_DIR),target)/debug/praxis-ai) \
cargo test -p praxis-tests-integration --features openai-all,store-all,$(INTEGRATION_EXPERIMENTAL_FEATURES) --test suite \
-- examples::azure_ad examples::gcp_adc examples::lakera_guard examples::token_rate_limit examples::token_ceiling examples::inflight_tracking \
examples::vertex_anthropic \
examples::model_catalog examples::vertex_anthropic \
$(if $(V),--nocapture)

test-inference-fixtures: test-inference-fixtures-core test-inference-xtask
Expand Down Expand Up @@ -371,7 +371,7 @@ lint: lint-clippy lint-xtask
lint-clippy: setup-lint-toolchains
$(LINT_CARGO) clippy --workspace --all-targets -- -D warnings
$(LINT_CARGO) clippy --workspace --all-targets \
--features $(STORE_ALL_WORKSPACE_FEATURES),xtask/openai-all,praxis-ai-proxy/azure-ad-filter,praxis-ai-proxy/basic-auth-filter,praxis-ai-proxy/gcp-adc-filter,praxis-ai-proxy/http-callout-filter,praxis-ai-proxy/token-rate-limit-filter,praxis-ai-proxy/token-ceiling-filter,praxis-ai-proxy/inflight-tracker-filter,praxis-ai-proxy/vertex-anthropic-filter,praxis-tests-integration/azure-ad-filter,praxis-tests-integration/basic-auth-filter,praxis-tests-integration/gcp-adc-filter,praxis-tests-integration/http-callout-filter,praxis-tests-integration/token-rate-limit-filter,praxis-tests-integration/token-ceiling-filter,praxis-tests-integration/inflight-tracker-filter,praxis-tests-integration/vertex-anthropic-filter \
--features $(STORE_ALL_WORKSPACE_FEATURES),xtask/openai-all,praxis-ai-proxy/azure-ad-filter,praxis-ai-proxy/basic-auth-filter,praxis-ai-proxy/gcp-adc-filter,praxis-ai-proxy/http-callout-filter,praxis-ai-proxy/token-rate-limit-filter,praxis-ai-proxy/token-ceiling-filter,praxis-ai-proxy/inflight-tracker-filter,praxis-ai-proxy/model-catalog-filter,praxis-ai-proxy/vertex-anthropic-filter,praxis-tests-integration/azure-ad-filter,praxis-tests-integration/basic-auth-filter,praxis-tests-integration/gcp-adc-filter,praxis-tests-integration/http-callout-filter,praxis-tests-integration/token-rate-limit-filter,praxis-tests-integration/token-ceiling-filter,praxis-tests-integration/inflight-tracker-filter,praxis-tests-integration/model-catalog-filter,praxis-tests-integration/vertex-anthropic-filter \
-- -D warnings
$(MAKE) lint-lean
$(MAKE) check-dep-budget
Expand Down
31 changes: 31 additions & 0 deletions docs/filters/model_catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!-- Generated by: cargo xtask generate-filter-docs -->
<!-- Do not edit manually -->

# `model_catalog`

Serves an OpenAI-compatible `GET /v1/models` catalog filtered per principal.

## Configuration Notes

The proxy answers list (`GET {list_path}`) and lookup (`GET {list_path}/{id}`) requests itself from configured entries; nothing is forwarded upstream. Each entry may restrict visibility to authenticated subjects, roles, or teams. Hidden and unknown models both return the same `404 model_not_found`, so callers cannot probe for models they cannot see. Catalog metadata (provider, version, capabilities, cost tier) is operator-declared and appears under a `praxis` key that OpenAI clients ignore. Other paths pass through untouched.

## Configuration

| Field | Type | Required | Description |
|-------|------|---------|-------------|
| `list_path` | string | no | Path serving the model list. `{list_path}/{id}` serves one model. |
| `created` | integer | no | Unix timestamp reported as `created` when an entry omits its own. |
| `default_visibility` | `public` \| `authenticated` | no | Who may see entries that have no `visible_to` rule. |
| `models` | object[] | yes | Catalog entries in listing order. IDs should be the client-facing model IDs that inference routing accepts. |
| `models[].id` | string | yes | Client-facing model ID. |
| `models[].owned_by` | string | no | OpenAI `owned_by` value. Defaults to `provider`, then `praxis`. |
| `models[].created` | integer | no | Unix timestamp reported as `created`. Defaults to the top-level value. |
| `models[].provider` | string | no | Provider serving the model, reported in catalog metadata. |
| `models[].version` | string | no | Model version, reported in catalog metadata. |
| `models[].capabilities` | (`chat_completions` \| `completions` \| `responses` \| `messages` \| `embeddings` \| `audio_speech` \| `audio_transcriptions` \| `images` \| `realtime`)[] | no | Operator-declared API capabilities. These are not verified upstream. |
| `models[].cost_tier` | string | no | Coarse cost tier label, reported in catalog metadata. |
| `models[].aliases` | string[] | no | Additional IDs listed as their own objects with the same visibility. |
| `models[].visible_to` | object | no | Restrict visibility to matching principals. Absent uses `default_visibility`. |
| `models[].visible_to.subjects` | string[] | no | Authenticated subject IDs. |
| `models[].visible_to.roles` | string[] | no | Authenticated roles. |
| `models[].visible_to.teams` | string[] | no | Authenticated teams. |
6 changes: 6 additions & 0 deletions docs/filters/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ see the [Praxis core filter reference][core-ref].
|--------|-------------|
| [`http_callout`](http_callout.md) | Calls an external HTTP service during request processing and feeds its response into branch-chain evaluation. |

### Catalog

| Filter | Description |
|--------|-------------|
| [`model_catalog`](model_catalog.md) | Serves an OpenAI-compatible `GET /v1/models` catalog filtered per principal. |

### GCP

| Filter | Description |
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ for how these are authored and regenerated.
| [llmisvc-model-provider-resolver.yaml](configs/llmisvc-model-provider-resolver.yaml) | Rewrites publisher-ID body `model` values to the short model name for LLMISvc / KServe routing; the routing header (default `X-Model`) is left unchanged so routing can still use the publisher ID |
| [mcp-classifier-routing.yaml](configs/mcp-classifier-routing.yaml) | Routes MCP requests by body-derived method and tool name |
| [mcp-stateless-broker.yaml](configs/mcp-stateless-broker.yaml) | Configurable stateless MCP broker using the final MCP 2026-07-28 stateless profile |
| [model-catalog.yaml](configs/model-catalog.yaml) | Serve an OpenAI-compatible GET /v1/models listing from the proxy, filtered by the authenticated caller. basic_auth publishes the verified subject, and model_catalog lists only the entries that subject may see |
| [model-to-header-routing.yaml](configs/model-to-header-routing.yaml) | Routes LLM API requests to different backends based on the "model" field in the JSON request body |
| [model-to-header-trust-existing.yaml](configs/model-to-header-trust-existing.yaml) | Routes on a model header that a trusted hop in front of Praxis has already set, skipping the filter entirely when the header is present |
| [model-to-provider.yaml](configs/model-to-provider.yaml) | Map a stable public model ID to an existing upstream cluster and target model |
Expand Down
75 changes: 75 additions & 0 deletions examples/configs/model-catalog.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Principal-aware model catalog
#
# Serve an OpenAI-compatible GET /v1/models listing from the proxy, filtered by
# the authenticated caller. basic_auth publishes the verified subject, and
# model_catalog lists only the entries that subject may see. A hidden model
# returns the same 404 as an unknown one. Catalog IDs match the public IDs that
# model_to_provider routes, aliases included. Capabilities and cost tiers are
# operator-declared metadata, not verified upstream. The catalog controls
# discovery only; enforcing model access on inference requests is a separate
# policy concern.
#
# Usage:
# cargo run -p praxis-ai-proxy --features basic-auth-filter,model-catalog-filter -- -c examples/configs/model-catalog.yaml
# curl -u viewer:example-password http://localhost:8080/v1/models
# curl -u ml-engineer:example-password http://localhost:8080/v1/models/claude-sonnet-4-5

listeners:
- name: default
address: "127.0.0.1:8080"
filter_chains:
- main

filter_chains:
- name: main
filters:
- filter: basic_auth
strip_authorization: true
credentials:
- username: viewer
password: "example-password"
- username: ml-engineer
password: "example-password"

- filter: model_catalog
created: 1735689600
models:
- id: gpt-4o
provider: openai
capabilities: [chat_completions, responses]
cost_tier: standard
aliases: [gpt4o]
- id: claude-sonnet-4-5
owned_by: anthropic
provider: vertex
capabilities: [messages]
cost_tier: premium
visible_to:
subjects: [ml-engineer]

- filter: model_to_provider
models:
- model: gpt-4o
upstream: openai
target_model: gpt-4o
paths: [/v1/chat/completions, /v1/responses]
- model: gpt4o
upstream: openai
target_model: gpt-4o
paths: [/v1/chat/completions, /v1/responses]
- model: claude-sonnet-4-5
upstream: vertex
target_model: claude-sonnet-4-5@20250929
paths: [/v1/messages]

- filter: load_balancer
clusters:
- name: openai
endpoints:
- "127.0.0.1:3000"
- name: vertex
endpoints:
- "127.0.0.1:3001"

insecure_options:
allow_private_endpoints: true
3 changes: 3 additions & 0 deletions filters/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ token-ceiling-filter = ["experimental", "dep:tiktoken-rs"]
# backing store is still an open question, so the filter itself stays gated
# until both land. Activates the `experimental` marker.
inflight-tracker-filter = ["experimental"]
# Experimental: the model_catalog filter, serving a principal-filtered
# GET /v1/models from static config (ai#1067). Activates the `experimental` marker.
model-catalog-filter = ["experimental"]
# Marker feature activated transitively by any experimental feature, mirroring
# praxis core's server crate. Lets consumers gate on "anything experimental".
experimental = ["praxis-ai-apis/experimental"]
Expand Down
Loading
Loading