Skip to content

test(sdk): add Anthropic Messages integration tests against llm-katan - #601

Open
yossiovadia wants to merge 638 commits into
praxis-proxy:mainfrom
yossiovadia:test/anthropic-llm-katan-sdk
Open

test(sdk): add Anthropic Messages integration tests against llm-katan#601
yossiovadia wants to merge 638 commits into
praxis-proxy:mainfrom
yossiovadia:test/anthropic-llm-katan-sdk

Conversation

@yossiovadia

Copy link
Copy Markdown
Contributor

Summary

  • Add Anthropic Messages API SDK integration tests using the official anthropic Python SDK
  • Tests run through Praxis against llm-katan echo backend
  • Exercises the full Anthropic filter chain end-to-end: anthropic_messages_formatanthropic_validateanthropic_messages_protocoltoken_count (provider: anthropic)

What this covers

Test What it validates
test_non_streaming_basic Request/response cycle, content blocks, stop_reason
test_non_streaming_with_system System message handling
test_streaming_basic SSE event lifecycle (message_start → content_block_delta → message_stop)
test_streaming_collects_full_text Text stream assembly
test_multi_turn Multi-message conversation
test_usage_present Anthropic-format token extraction (input_tokens/output_tokens)

Why

The existing Anthropic integration tests (tests/integration/tests/suite/anthropic_messages.rs) use Backend::fixed() to replay pre-recorded JSON fixtures. They verify Praxis passes through static data but never test against a server that speaks the actual Anthropic Messages protocol.

This test sends real Anthropic SDK requests through the full filter chain to a live Anthropic-format endpoint, validating protocol fidelity end-to-end — including streaming SSE and Anthropic-native token usage extraction.

6 tests, ~1.3 seconds total. Skips gracefully when llm-katan is unreachable.

Usage

cargo build -p praxis-ai-proxy
uv run tests/integration/sdk/anthropic/test_anthropic_messages_llmkatan.py -s -v

Relates to #600

Checklist

  • I reviewed every changed line and can explain the change.
  • Tests are added or updated when behavior changes.
  • New capabilities include an example config and functional example test.
  • User-facing behavior and generated documentation are updated.
  • Performance-sensitive changes include appropriate benchmark or load-test evidence.
  • Commits are signed and include a Signed-off-by trailer.

nerdalert and others added 30 commits July 6, 2026 14:17
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
…nse-store (praxis-proxy#271)

Signed-off-by: Sébastien Han <seb@redhat.com>
Signed-off-by: Sébastien Han <seb@redhat.com>
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
Co-authored-by: Francisco Javier Arceo <farceo@redhat.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
…roxy#293)

Wire the openai_stream_events filter into the Responses API
full-flow pipeline between openai_response_store and
openai_responses_rehydrate.

Signed-off-by: Sébastien Han <seb@redhat.com>
…is-proxy#299)

docs(filters): generate missing openai_stream_events doc and lint in CI

Run `cargo xtask generate-filter-docs` to produce the missing
`openai_stream_events.md` and refresh stale docs (a2a, mcp,
openai_response_store, reference index).

Add `cargo xtask lint-filter-docs` to the `make lint` target so CI
catches stale or missing filter documentation going forward.

Signed-off-by: Sébastien Han <seb@redhat.com>
This also removes CI shims to build and test with the main branch
while we were waiting for this release.

Signed-off-by: Shane Utt <shaneutt@linux.com>
…axis-proxy#308)

The lint-filter-docs check fails in CI because parse_shared_config_items()
hardcodes ../praxis to find shared config types. Since praxis moved from
a path dependency to a crates.io dependency (v0.4.0), ../praxis does not
exist in CI.

Fall back to resolving praxis-proxy-filter source via cargo metadata when
../praxis is not available. Also trim unused core/tls source parsing since
only filter/payload_processing types (OnInvalidBehavior) are referenced by
AI filter configs.

Signed-off-by: Sébastien Han <seb@redhat.com>
…dencies group (praxis-proxy#307)

chore(deps): bump jsonwebtoken in the rust-dependencies group

Bumps the rust-dependencies group with 1 update: [jsonwebtoken](https://github.com/Keats/jsonwebtoken).


Updates `jsonwebtoken` from 9.3.1 to 10.4.0
- [Changelog](https://github.com/Keats/jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](Keats/jsonwebtoken@v9.3.1...v10.4.0)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-version: 10.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
…roxy#306)

Bumps [actions/github-script](https://github.com/actions/github-script) from 7.0.1 to 9.0.0.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@60a0d83...3a2844b)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
…axis-proxy#305)

Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.36.2 to 4.36.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@8aad20d...54f647b)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
…raxis-proxy#303)

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.36.2 to 4.36.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@8aad20d...54f647b)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
…proxy#304)

Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](docker/metadata-action@80c7e94...dc80280)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
…xy#302)

Bumps [docker/login-action](https://github.com/docker/login-action) from 4.2.0 to 4.4.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@650006c...af1e73f)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sébastien Han <seb@redhat.com>
Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
…-proxy#248)

* feat(conversations): rehydrate and append via conversation.id

When a POST /v1/responses request includes a `conversation` field
(string ID or `{"id": "..."}` object), load the stored conversation
and prepend its messages to the input. `previous_response_id` takes
precedence when both are present.

After a successful non-streaming response with status "completed",
input and output items are appended back to the conversation so
subsequent requests see the full accumulated history.

- Rehydrate filter: add conversation-based rehydration path alongside
  existing previous_response_id support
- Proxy filter: strip `conversation` field from outbound body
- Conversations filter: add response-phase append-back with guards
  for streaming, background, and non-completed responses
- Store filter: trigger store init when has_conversation metadata set
- Full-flow config: add openai_conversations filter with
  on_invalid: continue for format classifier compatibility
- Add unit and integration tests for all new paths

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(tests): remove stale ai-inference feature gate from conversations_rehydrate

The ai-inference feature no longer exists in the integration test crate.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test(conversations): add unit tests for append-back response hooks

Cover on_response arming logic (metadata guards, content-type, status
code) and on_response_body paths (skip non-completed, invalid JSON,
empty items, full persist cycle) to restore coverage above 90%.

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(lint): resolve clippy and compile errors after rebase

Suppress large_stack_frames on on_request dispatcher and fix
BackendGuard import path after test-utils restructuring on main.

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(config): restore original backend endpoint port in full-flow example

The full-flow example config endpoint was changed from 3001 to 8000,
breaking the integration tests that use port mapping keyed on
127.0.0.1:3001.

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix: address review feedback on conversation rehydration

- Reject malformed conversation values (e.g. {}, 42) with 400 instead
  of silently forwarding them to the backend
- Defensively strip conversation from proxy passthrough body as a
  safety net against unresolved fields leaking upstream
- Propagate append-back store errors with ? instead of swallowing
  them, consistent with the response store filter
- Replace fixed sleep in integration test with retry/poll loop
- Add tenant metadata mismatch test and make mock store tenant-aware

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix: revert append-back to warn+swallow and add conversation store failure test

Append-back is a best-effort side-effect — propagating store errors
would abort the client's already-successful inference response. Revert
to warn and continue.

Add missing test for store failure during conversation rehydration
(the existing test only covered the previous_response_id path).

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Sébastien Han <seb@redhat.com>
)

* test(mcp): add unit tests for broker config validation (praxis-proxy#278)

Add 63 inline unit tests to config.rs covering CacheScope,
InvalidToolPolicy, serde deserialization, validate_schema_object
edge cases, validate_path, server/tool/exposed name uniqueness,
cache field profile validation, version validation, build_config
integration, and catalog entry construction.

Also fix pre-existing unfulfilled #[expect(dead_code)] on
CatalogTool by using cfg_attr(not(test), expect(...)) since test
builds use all fields.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test(mcp): add coverage for broker config validation gaps

Add tests for uncovered code paths in MCP broker config validation:

- build_config_minimal: assert default_version and supported_versions
  are correctly derived from the current protocol profile
- validate_server_paths: direct unit tests with invalid path and
  multi-server iteration proving the second server is checked
- validate_server_clusters: positive test with valid non-empty cluster
- build_config_with_tools_and_prefix: assert input_schema preservation,
  cluster, backend_path, and server_name on catalog entries

Widen validate_server_paths and validate_server_clusters to pub(super)
so the broker test module can call them directly.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test(mcp): address review feedback on broker config tests

Remove short-form separator comments from the test module — test
function names provide sufficient organization.

Replace fragile Debug-format assertions with direct field access for
cluster and backend_path on catalog entries, and remove the associated
inline comment.

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Sébastien Han <seb@redhat.com>
* test: replay sanitized agent sessions

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* chore: update crossbeam-epoch advisory fix

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* docs: explain replay fixture examples

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* test: share temp sqlite helper

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* test: address replay review comments

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* test: add session replay importer tooling

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* test: assert replayed response body is stored

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* test: harden replay fixture paths

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

* test: avoid replay fixture source path leaks

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>

---------

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
test(a2a): add unit tests for envelope parsing (praxis-proxy#280)

Signed-off-by: Sébastien Han <seb@redhat.com>
…tion (praxis-proxy#309)

* feat(filter): add tool_parse filter for Responses API tool classification

Add a new tool_parse filter that parses the tools array and tool_choice
from Responses API POST /v1/responses requests. Classifies each tool by
type (function, web_search, file_search, code_interpreter, computer_use,
image_generation, tool_search, MCP) and promotes summary facts to
metadata and filter results for branch conditions.

The filter does not mutate the request body and is gated to only process
Responses API create requests. Malformed function tools (missing name)
are still counted by discriminator for has_tools routing. Unsafe values
(oversized, control characters) in tool_choice are silently dropped
rather than failing the filter.

Closes praxis-proxy#42

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(tool_parse): promote function_count to filter results

function_count was written to metadata but never promoted to
filter_results or restored, making it invisible to branch_chains
conditions. Add it to promote_filter_results() and restore via
restore_function_count().

Signed-off-by: Sébastien Han <seb@redhat.com>

* fix(tool_parse): recognize web_search_2025_08_26 as WebSearch

The OpenAI API lists web_search_2025_08_26 as a valid web search tool
type. Without this match arm, requests using that literal fall through
to Unknown, setting has_tools=true but has_web_search=false, causing
branch_chains on has_web_search to silently miss them.

Signed-off-by: Sébastien Han <seb@redhat.com>

* docs(tool_parse): note why accumulate_tool extracts full details

The parser clones names, configs, and MCP entries that tool_parse
does not consume today. Document that tool_dispatch (praxis-proxy#26) will need
these fields.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test(tool_parse): assert FilterAction and add restore unit tests

Assert Release on has-tools paths and Continue on no-tools/skip paths
in every run_filter caller. Add unit tests for restore_presence_flags,
restore_function_count, and restore_tool_choice (present and absent).

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Sébastien Han <seb@redhat.com>
praxis-proxy#261)

feat(apis): add Deserialize to TokenUsageProvider and streaming token extraction

  Add serde Deserialize to TokenUsageProvider for YAML config parsing.
  Add extract_streaming_tokens for providers that spread token counts
  across multiple SSE events (Anthropic message_start/message_delta,
  Bedrock ConverseStream metadata).

Signed-off-by: szedan <szedan@redhat.com>
test(config): add unit tests for remaining config modules (praxis-proxy#283)

Add inline #[cfg(test)] modules to the four config files that had
zero test coverage:

- filters/src/prompt_enrich/config.rs (17 tests)
- apis/src/openai/responses/model_rewrite/config.rs (24 tests)
- apis/src/openai/responses/config.rs (9 tests)
- apis/src/anthropic/messages_format/config.rs (10 tests)

Tests cover serde defaults, deny_unknown_fields enforcement,
validation functions, null-header-disables-promotion, and role/enum
round-trips.

Signed-off-by: Sébastien Han <seb@redhat.com>
…s-proxy#317)

* docs: use language-neutral type names in generated filter docs

Replace Rust-specific type names (u8, u16, u32, u64, usize, i32, f64,
etc.) with language-neutral equivalents (integer, number) in the xtask
filter-doc generator and regenerate all filter documentation.

Signed-off-by: Sébastien Han <seb@redhat.com>

* test(xtask): table-driven coverage for all numeric primitives

Signed-off-by: Sébastien Han <seb@redhat.com>

---------

Signed-off-by: Sébastien Han <seb@redhat.com>
usize and others added 24 commits August 22, 2026 00:25
…y#800)

* test: opt loopback fixtures into allow_private_endpoints

praxis-core main now validates clusters defined inline in load_balancer
filters and rejects private/loopback/link-local endpoints unless the
config sets `insecure_options.allow_private_endpoints: true` (core commit
894ac18, "validate clusters defined inline in load-balancer filters").

Core updated its own conformance fixtures and shipped examples for this
change, but nothing propagated downstream, so the whole `ai` tree fails
`test-praxis-main` (unit, schema, and integration steps) against core
main. The flag already ships in released praxis 0.5.3, so this fix is
release-independent and green on both the 0.5.3 pin and core main.

Mirror core's remediation:

- Append a top-level `insecure_options.allow_private_endpoints: true`
  block to every shipped example config whose inline load_balancer
  cluster targets a loopback/private backend (52 files).
- Port core's `allow_loopback_endpoints()` test helper into
  `praxis-test-utils` so harness-loaded example configs opt in centrally
  at load time.
- Add the flag inline to every raw-parse fixture (schema, integration,
  and the `dump`/`pipelines` unit tests) that builds an inline
  load_balancer cluster on a private endpoint.

Public-endpoint fixtures, listener addresses, ip_acl allow lists, tcp
upstreams, and top-level `clusters:` blocks are left untouched.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* test(inference): merge insecure_options in replay collision tests

Two replay negative tests build their config from the shipped
`responses-proxy.yaml` example and then append their own top-level
`insecure_options:` block (`allow_public_admin` / `allow_private_health_checks`).
Now that the example carries its own `insecure_options:` opt-in, string
concatenation produced a duplicate top-level key, changing the parse
outcome so the intended rejection error no longer fired.

Merge each test's option under the example's existing `insecure_options:`
block instead of emitting a second one, restoring the asserted errors.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

---------

Signed-off-by: usize <mofoster@redhat.com>
praxis-proxy#721)

* fix(guardrails): fail closed on NeMo redact verdict until body replacement is ready

GuardResult::Redact was returning FilterAction::Continue while recording
status=redacted, silently forwarding the original unmodified body. This
leaked content the configured guardrail identified as sensitive and
misled downstream branch logic into treating the request as sanitised.

Change record_verdict to return FilterAction::Reject(403) for
GuardResult::Redact, identical to the GuardResult::Block path, so the
original body is never forwarded. Body replacement with the provider's
modified_text is deferred to praxis-proxy#579.

Update on_request_body_modified_writes_filter_results to assert the 403
rejection and rename it accordingly. Add regression test
on_request_body_modified_never_forwards_original_secret that asserts the
action is Reject, the original SSN is absent from the body buffer, and
status is never recorded as passed.

Fixes: fnd_sig-feat-custom-ai-anthropic-gua_cfe2e666bb

Signed-off-by: mkoushni <mkoushni@redhat.com>

* test(guardrails): assert original secret absent from rejection body

Replace the weaker matches!(action, Reject(_)) assertion in
on_request_body_modified_never_forwards_original_secret with a direct
check that the raw SSN never appears in the rejection body. This makes
the test meaningfully different from on_request_body_modified_rejects_with_403
and directly validates the stated invariant: the unmodified user content
must not surface in any response path.

Reported by praxis-bot review of fix/guardrails-redact-fail-closed.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(guardrails): extract nemo_pii_redact_filter helper to fix too-many-lines lint

Both on_request_body_modified_rejects_with_403 and
on_request_body_modified_never_forwards_original_secret share identical
mock setup. Extract it into nemo_pii_redact_filter() to bring each test
under the 30-line clippy limit.

Also collapse the two-line rejection body setup into one expression
using rejection.body.as_deref().unwrap_or_default().

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(guardrails): format assert chain to satisfy rustfmt

Collapse the split method chain onto a single line as required by
cargo +nightly fmt --check.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(guardrails): drop stale praxis-proxy#579 references from redact-fail-closed path

Issue praxis-proxy#579 is closed. Remove all references to it from the comment,
the warn! log message, the unit-test assertion, and the integration-test
doc comment and assertion. The fail-closed behaviour for GuardResult::Redact
is now the permanent implementation, not a temporary workaround.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(guardrails): update nemo-guardrails.yaml comment for modified verdict

The modified verdict now rejects with 403 (same as blocked). Update the
example config comment to reflect the current behaviour instead of the
stale deferred-to-praxis-proxy#579 note.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* test(guardrails): document helper and drop rejection-body clone

Move nemo_pii_redact_filter next to the other test helpers with a
doc comment, and read the rejection body with as_deref() instead of
clone().

Signed-off-by: mkoushni <mkoushni@redhat.com>

* test(guardrails): backtick NeMo in helper docs for clippy

clippy::doc-markdown treats NeMo as a missing identifier; wrap it so
lint CI can compile praxis-ai-filters tests.

Signed-off-by: mkoushni <mkoushni@redhat.com>

---------

Signed-off-by: mkoushni <mkoushni@redhat.com>
* feat(llm-d): move ext_proc compatibility into AI

Move the llm-d ext_proc compatibility layer into the AI repository under integrations/llmd/ext-proc and gate runtime registration behind the llmd-ext-proc feature.

The crate is publish=false and scoped to llm-d's current EPP protocol rather than general-purpose Envoy ext_proc support. Environment tests cover the mock EPP to endpoint_selector to llm-d-inference-sim path.

Use the published Praxis 0.4.1 crates now that the required support APIs are available from crates.io.

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

* docs: use ascii arrows in llm-d testing guide

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

* docs: use ascii punctuation in llm-d transfer

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

* fix(llm-d): address ext_proc review feedback

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

* test(llm-d): share ext_proc routing mock

Move the duplicated mock routing processor into the feature-gated test utilities module. Reuse it from the example and simulator environment suites while keeping the gRPC dependencies scoped to llmd-ext-proc tests.

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

* fix(llm-d): align rebased test support

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

* fix(llm-d): harden ext-proc boundary handling

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

---------

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
Co-authored-by: Morgan Foster <39788015+usize@users.noreply.github.com>
…raxis-proxy#760)

* feat: http_callout using SubRequestConnector

Signed-off-by: Gabriela Dozortsev <gdozorts@redhat.com>
Signed-off-by: usize <mofoster@redhat.com>

* control character parsing for llama guard

Signed-off-by: Gabriela Dozortsev <gdozorts@redhat.com>
Signed-off-by: usize <mofoster@redhat.com>

* chore: regenerate Cargo.lock after rebase onto main

The lockfile on the PR branch carried dangling references to
thiserror 2.0.19 (no matching package entry), which panicked
cargo-audit in the security-audit and dependency-check jobs.
Reset to main and re-resolve so only the serde_json_path
additions remain.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* feat(callout): scope examples to Lakera Guard

Defer the LlamaGuard example to a follow-up: its verdict format
("unsafe\nS02", "unsafe01".."unsafe13") needs the richer on_result
matching proposed in praxis-proxy/praxis#964. Lakera Guard returns
exact "true"/"false", which works with exact-equality matching
today, and is the guardrails integration required by the AI
Gateway MVP (ai#758, success criterion 4).

Move lakera-guard.yaml back to examples/configs/ (the subdirectory
only existed to group the two guard examples) and regenerate the
examples README table.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): resolve clippy lints and rustfmt drift

Refactor the callout filter to satisfy the workspace lint gates
(clippy -D warnings, nightly rustfmt):

- introduce a CalloutTarget struct in place of the six target_*
  fields and the parse_callout_target six-tuple, splitting scheme
  and host validation into helpers;
- extract callout header assembly, response handling, the network
  round-trip, depth parsing, and the max_body_bytes bound check into
  focused helpers to clear too_many_lines/cognitive_complexity and
  large_stack_frames on execute_callout;
- document and split sanitize_string, replace string indexing with
  checked slicing, and neutralise its LlamaGuard-specific warning;
- hoist DISALLOWED_FORWARD_HEADERS to a module const;
- assert http_callout registration in the AI registry test.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* docs(callout): make filter visible to docs generator and add reference docs

cargo xtask generate-filter-docs discovers filter anchors by the
string literal returned from name(); returning the FILTER_NAME const
made http_callout invisible, so docs/filters/http_callout.md was never
generated (AGENTS.md test requirement praxis-proxy#5). Return the "http_callout"
literal directly, keep FILTER_NAME for internal use with a drift test,
lead the struct doc with a descriptive summary line, and check in the
generated reference docs.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): inject response headers with set semantics

Inject callout response headers via ctx.request_headers_to_set
(overwrite) instead of ctx.extra_request_headers (append), so a
header taken from the trusted callout response replaces any
client-supplied header of the same name rather than being appended
alongside it. Also drops a lossy HeaderValue::to_str() conversion by
pushing the HeaderValue directly. Adds a test that an inject header
absent from the response is not injected.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): validate result_key at config time; skip rejected values

P3: CompiledExtraction::compile now probes result_key against the
FilterResultSet key rules (ASCII alphanumeric/_/- , 1-64 bytes) with
an empty value, so invalid keys such as "lakera.flagged" or an empty
key fail at startup rather than silently on every request.

P4: evaluate no longer returns Result. A coerced value rejected by
the result-set limits is logged (warn) and skipped instead of
propagating through handle_success and failing the whole request via
?, so an oversized/hostile third-party response value is handled per
the on_failure policy. handle_success/handle_response drop their now
unnecessary Result wrappers.

Note: with the key validated at config time and sanitize_string
capping string/array/object coercions at 255 bytes (below the 256
value limit), the value-rejection branch is defense-in-depth and not
reachable through the current coercion pipeline; it degrades
gracefully if those limits ever change.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): remove dead failure_mode alias on on_failure

The #[serde(alias = "failure_mode")] on on_failure could never bind:
core strips failure_mode as a structural pipeline key (see
praxis-proxy-filter factory strip_structural_keys) before the filter
config is parsed, and it controls a different semantic (how the
pipeline reacts to a filter *error*, not how this filter reacts to a
*callout* failure). Remove the misleading alias and document the
distinction.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* test(callout): port coverage gaps from earlier review rounds

Add the test cases that earlier review rounds surfaced but that had not
made it into the scoped branch:

- forward_header_absent_from_request_not_sent: a configured
  forward_header that is absent from the downstream request is not sent
  to the callout (asserts against the mock's received requests).
- body_shaping_non_json_forwards_raw: when body shaping is configured
  but the downstream body is not JSON, the raw body is forwarded
  verbatim rather than dropped, and extraction still succeeds.
- config_rejects_unknown_{target,response,circuit_breaker}_field:
  deny_unknown_fields is enforced on the nested config structs, not
  just the top level.
- lakera_guard_get_bypasses_callout (integration): the example scopes
  the callout to methods: [POST], so a GET reaches the upstream without
  a callout even when Lakera would flag it.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* docs(callout): regenerate http_callout doc for on_failure description

The generated field table lagged the source doc comment on
`on_failure` after the dead `failure_mode` alias was removed. Regenerate
so `docs/filters/http_callout.md` matches the config source and passes
`cargo xtask lint-filter-docs`.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): validate status_on_error is a legal HTTP status

praxis-bot review (praxis-proxy#760): status_on_error accepted any u16, so values
like 0, 99, or 65535 would produce a nonsensical HTTP status on the
rejection path. Validate it falls in 100-599 at config time, alongside
the existing max_body_bytes check; an unset value still defaults to 403.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* docs(callout): note status validation duplication for follow-up

The 100..=599 status check is duplicated across openai_responses_compact,
web_search, and now callout. Record the known duplication and the plan to
promote a shared helper into praxis-ai-apis so the follow-up dedupe is
discoverable from the code. No behavior change.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* test(callout): cover non-2xx callout response forwarding

A completed callout that answers with a non-2xx status forwards that
status to the downstream client, which is distinct from a transport
failure applying status_on_error. The new test mounts a mock returning
500 with on_failure: open and asserts Reject(500) — proving the non-2xx
branch forwards the callout's own status regardless of failure mode.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* test(callout): cover userinfo-in-URL rejection

parse_host rejects URLs containing '@' to prevent embedded credentials
from leaking into logs or being forwarded to the callout target. Add a
test asserting both user:pass@host and bare user@host are rejected at
config time with an error that mentions userinfo.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): stop stripping slashes from extracted values

split_at_first_control removed '/' and '\\' from every extracted
value, mangling legitimate results ("unsafe/S02" -> "unsafeS02",
"safe/clean" -> "safeclean") and silently breaking on_result
matching. The stripping was incidental to the original control-character
work for llama guard and served no security purpose: control-character
truncation already defends against CR/LF/header-injection, and slashes
are ordinary value characters.

Preserve non-control characters verbatim and split at the first control
character only. Add tests pinning slash preservation and confirming a
control character still truncates.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* test(callout): cover https target parsing (TLS/SNI/port)

CalloutTarget::parse had no coverage of the https path. Add tests
asserting https enables TLS, sets SNI to the host, defaults the port to
443 and omits it from the Host authority; that a non-default https port
is kept in the authority while SNI stays host-only; and that http
disables TLS, defaults to port 80, and leaves SNI empty.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* feat(callout): warn on disallowed forward_header at config time

Hop-by-hop and sensitive headers in DISALLOWED_FORWARD_HEADERS are
silently skipped at request time, so an operator who lists one as a
forward_header gets no feedback that it is a no-op. Emit a warning per
such header at config time; the entry remains non-fatal and the
request-time skip is unchanged as defense-in-depth.

Add a test that a disallowed forward_header is accepted (warns, not
errors) and a wiremock test proving a disallowed header is not sent to
the callout while an allowed one is.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): cap response body at configured max_body_bytes

build_subrequest_client used SubRequestClient::new, leaving the client
response-byte ceiling at its 64 MiB default. Because execute() uses
min(per_call_limit, client_ceiling), a configured max_body_bytes above
64 MiB was silently clamped to 64 MiB. Construct the client with
with_max_response_bytes(connector, max_body_bytes) so the effective
response limit always equals the operator's configured value.

Add a test that a response body larger than max_body_bytes fails the
callout (Reject(status_on_error) under on_failure: closed).

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* style(callout): apply nightly rustfmt to fixup commits

Wrap two over-width lines flagged by nightly rustfmt in the
status_on_error validator error and the https target-parse assertion.
No behavior change.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): block resolved private/loopback peers (SSRF/rebinding)

Add opt-in `target.allow_private_addresses` (default true, preserving
current warn-only behavior). When false, resolve_peer rejects a resolved
private/loopback/link-local peer after DNS resolution, closing the
DNS-rebinding gap that the config-time literal-IP check cannot catch
(e.g. a hostname resolving to 169.254.169.254). A blocked peer is treated
as a callout failure and follows on_failure.

Defer to the shared classifier praxis_core::connectivity::is_private_ip
rather than adding another hand-rolled private-address predicate; see
praxis-proxy#771 for unifying the existing copies. Harden the
lakera-guard example accordingly.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(agentic): gate parse_json_rpc_body body arg for praxis-core main

praxis-core `main` changed the first parameter of
`parse_json_rpc_body` from `&Option<Bytes>` (released 0.5.2) to
`Option<&Bytes>`, which breaks the A2A and MCP filters under the
`test-praxis-main` compatibility job (E0308) at a2a/mod.rs:178 and
mcp/mod.rs:148.

This is upstream drift caught by the forward-looking canary, not a
defect in this branch: the default build pins praxis-core 0.5.2 and
stays green, while `test-praxis-main` clones core `main` at HEAD.

Gate the call argument on the existing `praxis-main` feature, matching
the pattern already used in inference/model_to_header.rs: pass
`&*body` against 0.5.2 and `body.as_ref()` against core `main`. A TODO
marks the gate for removal once we pin to a praxis-core release that
ships the new signature.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* docs(callout): note max_body_bytes also caps callout response body

request.max_body_bytes is passed both to the forwarded-request buffer and
to SubRequestClient::with_max_response_bytes (build_subrequest_client) and
the per-request execute limit, so it also bounds how large a callout
response the filter will accept. The field doc only described the request
role; note the dual role and regenerate the reference doc.

Addresses praxis-bot review comment on filters/src/callout/config.rs.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): box-pin large callout futures to satisfy clippy

The praxis-proxy-filter 0.5.3 API changes (merged from main) enlarged the
SubRequestClient::execute future, pushing the callout await chain over
clippy's large_futures / large_stack_frames thresholds under -D warnings.

Box::pin the awaited futures at each flagged site (client.execute,
perform_callout, and the execute_callout calls in on_request /
on_request_body), matching the fix main applied to apis/src/subrequest.rs
in 5b45cc1. Moves the future to the heap so the stack frame stays small.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* test(server): opt dump loopback fixtures into allow_private_endpoints

praxis-core main added validation of clusters defined inline in
load_balancer filters (praxis 894ac183), so a loopback cluster endpoint
like 127.0.0.1:9090 is now rejected at config-parse time unless
insecure_options.allow_private_endpoints is set. This broke the two
credential_injection dump tests under the test-praxis-main canary
(server/src/dump.rs config-parse panic), while the released 0.5.3 build
stays green. The failing tests are unrelated to their subject (credential
redaction); they panic earlier during Config::from_yaml.

Add the same insecure_options.allow_private_endpoints: true opt-in that
praxis-core applied to its own loopback fixtures (praxis 185545ae). The
field exists in released 0.5.3, so the default build is unaffected.

Verified: both tests pass against released 0.5.3 and against core main
(--features praxis-main); full server bin, filters, and apis suites pass
against core main.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* test(callout): opt lakera-guard example into allow_private_endpoints

praxis-core main validates clusters defined inline in load_balancer
filters and rejects private/loopback/link-local endpoints unless the
config sets `insecure_options.allow_private_endpoints: true` (core commit
894ac18). praxis-proxy#800 added this opt-in to every shipped example, but the
lakera-guard example was introduced on this branch and so was missed,
leaving `test-praxis-main` red on the schema parse_configs check:

  examples/configs/lakera-guard.yaml: chain 'routing':
  filter 'load_balancer': cluster 'backend': endpoint '127.0.0.1:3000'
  resolves to a sensitive address; set
  insecure_options.allow_private_endpoints: true to allow

Mirror praxis-proxy#800's remediation: append the top-level insecure_options block.
The flag ships in released praxis 0.5.3, so this is green on both the
pinned dependency and core main. The lakera integration test builds its
config manually (not via the allow_loopback_endpoints helper), so it
inherits the flag from the file with no duplicate-key collision.

Assisted by Opus 4.8

Signed-off-by: usize <mofoster@redhat.com>

* fix(callout): trim trailing whitespace and correct the sanitize docs

The `sanitize_string` docstring claimed it "drops `/` and `\` from the
retained text". It does not, and has not since slash stripping was removed
— `split_at_first_control` splits only on control characters, and its own
doc says slashes are preserved verbatim. The two comments contradicted each
other, which is what prompted the review question.

Fixing the docstring surfaced a real gap next to it: leading whitespace was
trimmed but trailing whitespace was not, so a provider returning `"safe "`
produced `"safe "` and silently failed `on_result` exact-equality matching
against a config saying `safe`. Trim both ends.

The trim runs after the control-character split, so the `warn!` still
reports the untrimmed dropped remainder and truncation still applies to the
final value.

Tests added:

- trailing/surrounding whitespace is trimmed, interior spacing is not
- whitespace sitting just before a control character is removed
- whitespace-only input yields `None`
- truncation at, over, and exactly at `MAX_SANITIZED_LEN`
- the UTF-8 boundary walk, including 2-, 3-, and 4-byte characters, so a
  multi-byte character straddling byte 255 is never split
- `coerce_value` over null, bool, number, string, array, and object

Verified the four behavioral tests fail against the pre-fix implementation
and pass after it.

Assisted by Opus 5

Signed-off-by: usize <mofoster@redhat.com>

* feat(callout)!: gate http_callout behind an experimental build flag

Per review: the filter is a work in progress, so put a build flag around it
and let it soak before it counts as supported surface.

Adds `http-callout-filter` to praxis-ai-filters and praxis-ai-proxy, off by
default. It activates an `experimental` marker feature, mirroring how praxis
core's server crate buckets `basic-auth-filter` under `experimental`, so
consumers can gate on "anything experimental" without naming each feature.

Gated: the `callout` module and its `HttpCalloutFilter` re-export, the
registration in `register_ai_filters`, and the `lakera-guard` example
integration test (it runs the proxy in-process, so the filter must be
compiled in). The registry test now asserts both directions — present with
the feature, absent without it — so the gate cannot silently regress.

Documented in the filter's struct doc, which flows into the generated
`docs/filters/http_callout.md`, and in the example config's usage line.

Verified both configurations: default build 1073 filter tests pass with no
callout code compiled in; with the feature, 1172 pass plus the three lakera
integration tests. Clippy clean both ways; full workspace suite green.

BREAKING CHANGE: `http_callout` is no longer registered in a default build.
Enable `--features http-callout-filter` to use it.

Assisted by Opus 5

Signed-off-by: usize <mofoster@redhat.com>

---------

Signed-off-by: Gabriela Dozortsev <gdozorts@redhat.com>
Signed-off-by: usize <mofoster@redhat.com>
Co-authored-by: Gabriela Dozortsev <gdozorts@redhat.com>
…xis-proxy#802)

* aws: add aws_sigv4_sign filter config scaffold

Signed-off-by: szedan <szedan@redhat.com>

* aws: implement sigv4 canonical-request signing, verified against AWS's published test vector

Signed-off-by: szedan <szedan@redhat.com>

* aws: implement Sigv4SignFilter HttpFilter, wired to sign_headers

Signed-off-by: szedan <szedan@redhat.com>

* aws: register aws_sigv4_sign as a security-class filter

Signed-off-by: szedan <szedan@redhat.com>

* aws: add aws-sigv4 example config and functional integration test

Signed-off-by: szedan <szedan@redhat.com>

* xtask: title-case the AWS filter-doc category as AWS, not Aws

Signed-off-by: szedan <szedan@redhat.com>

* aws: fix broken intra-doc links and reformat with workspace nightly rustfmt

Full verification (make lint / make doc) surfaced issues not caught by
stable cargo fmt/clippy alone: unresolved rustdoc links to
praxis_filter::HttpFilterContext (out of scope in this module), and
three functions that crossed the 30-line clippy::too_many_lines
threshold once reformatted with the workspace's nightly rustfmt
settings.

Signed-off-by: szedan <szedan@redhat.com>

* aws: generate aws_sigv4_sign filter reference docs

Signed-off-by: szedan <szedan@redhat.com>

* aws: opt aws-sigv4 example into allow_private_endpoints

Praxis core now rejects inline load_balancer clusters that target
loopback/private endpoints unless the config sets
insecure_options.allow_private_endpoints: true. The aws-sigv4 example
proxies to a local test double (127.0.0.1:3000), so opt it in the same
way upstream praxis-proxy#800 did for the other shipped example configs.

Signed-off-by: szedan <szedan@redhat.com>

* aws: harden aws_sigv4_sign per review feedback

- Log signing failures with tracing::warn before failing closed with 503,
  so operators can distinguish misconfiguration from an outage. The error
  carries only signing-input diagnostics, never credential material.
- Validate the configured host as a HeaderValue once in new() and reuse it,
  moving the check to startup and off the per-request path.
- Reject max_body_bytes of 0 or above a 64 MiB ceiling at construction.
- Add tests for the runtime signing-failure 503 path and the max_body_bytes
  bounds; extract credential resolution into a helper.

Signed-off-by: szedan <szedan@redhat.com>

* aws: split registry test to satisfy too_many_lines lint

The upstream merge combined the aws_sigv4_sign and http_callout
assertions into build_ai_registry_includes_ai_and_builtin_filters,
pushing it one line over the 30-line clippy limit. Move the
security-filter assertions into a focused test.

Signed-off-by: szedan <szedan@redhat.com>

---------

Signed-off-by: szedan <szedan@redhat.com>
…raxis-proxy#702)

* refactor(responses): centralize usage accumulation

Signed-off-by: kaplan <rivka.kaplan@nokia.com>
Signed-off-by: kaplan <rkaplan@redhat.com>

* style: apply rustfmt to responses usage modules

Signed-off-by: kaplan <rkaplan@redhat.com>

---------

Signed-off-by: kaplan <rivka.kaplan@nokia.com>
Signed-off-by: kaplan <rkaplan@redhat.com>
…ing usage (praxis-proxy#782)

* fix(token_usage): recover from SSE overflow instead of silently dropping usage

Token accounting stopped permanently after a single oversized SSE event
or JSON response, clearing all working state (including the terminal
usage event) with no signal that data was lost.

- The shared SSE scanner (used by token_usage and a2a) now discards only
  the oversized event and resumes at the next event boundary, instead of
  aborting the whole stream. A terminal usage event arriving after an
  oversized one is now captured.
- JSON and any residual SSE overflow (e.g. the usage event itself being
  oversized) now set an explicit token.status=overflow metadata key and
  Praxis-Token-Status response header, so billing consumers cannot
  mistake missing counts for zero usage.
- max_body_bytes/max_scratch_bytes are now configurable per token_count
  filter instance instead of fixed at 1 MiB / 64 KiB.

Fixes praxis-proxy#674

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(token_usage): share SkipPhase encoding and reject zero capture limits

Give A2A and token_count a single SkipPhase metadata codec, reject
zero max_body_bytes/max_scratch_bytes, and regenerate filter docs so
lint CI matches the new config fields.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(token_usage): mark overflow when a dropped SSE event is the tail

Partial Anthropic/Bedrock counts followed by an oversized terminal
usage event now keep the captured maxima and set token.status=overflow,
while a recovered usage event after a drop stays authoritative.

Signed-off-by: mkoushni <mkoushni@redhat.com>

* fix(token_usage): cap max_scratch_bytes at the shared 64 MiB ceiling

Signed-off-by: mkoushni <mkoushni@redhat.com>

---------

Signed-off-by: mkoushni <mkoushni@redhat.com>
test_streaming_basic collected event types into a set, which discards
ordering — a stream emitting the lifecycle events out of order would
still pass. Collect them in arrival order and assert the sequence:
message_start opens, message_stop closes, and content_block_delta
arrives strictly between them. Addresses review feedback on the SSE
lifecycle claim in the PR description.

Signed-off-by: Yossi Ovadia <yovadia@redhat.com>
fix: add missing praxis-main API fields

Praxis main added session_stores and pinned_endpoint_address
to HttpFilterContext, log_level to AdminEndpointOptions, and
a log_level argument to run_server.

Quote on_result.result as "true" so YAML does not parse it
as a boolean.

Signed-off-by: Aslak Knutsen <aslak@4fs.no>
…y#815)

* feat(azure): add azure_ad upstream authentication filter

Add an Entra ID upstream-authentication filter that acquires an OAuth2
bearer token via the client-credentials grant, caches it, refreshes it
in the background before expiry, and injects "Authorization: Bearer
<token>" on every proxied request to Azure OpenAI.

The filter injects only the Authorization header; unlike SigV4, an Entra
token is not bound to Host, path, or body, so routing stays the
operator's responsibility and the filter has no ordering constraint. The
client secret is read from an environment variable, never from YAML.

Token acquisition runs on a dedicated background thread so building or
hot-reloading a pipeline never blocks on a network round-trip. The
filter fails closed with 503 until the first token is acquired and
whenever the cached token is missing or expired.

Scope is the client-secret credential only; managed identity, client
certificates, and OIDC federation slot into the same cache/refresh
machinery as follow-ups.

Signed-off-by: szedan <szedan@redhat.com>

* fix(azure): harden azure_ad token handling per review

Address code-review findings on the azure_ad filter:

- Validate authority_host and tenant_id reject URL-structural characters
  (scheme, path, query, '@' userinfo override) before they are
  interpolated into the token endpoint URL. This prevents a misconfigured
  or malicious value from redirecting the secret-bearing token POST to an
  unintended host. Validation allows both GUID and domain-style tenants.

- Cap the expiry skew at half the token TTL so a short-lived token is
  cached with real remaining validity instead of already-expired, which
  previously caused every request to fail closed with 503 forever while
  the refresher kept fetching.

- Back off exponentially (30s base, doubling, capped at 15m) on repeated
  token-acquisition failures instead of retrying at a flat 30s, so an
  unreachable endpoint does not become a steady poll.

Add unit tests for URL-component validation, skew capping, backoff
growth/cap, and the short-TTL caching regression.

Signed-off-by: szedan <szedan@redhat.com>

* feat(azure): gate azure_ad filter behind experimental build flag

Put the azure_ad (Entra ID) upstream auth filter behind a new
azure-ad-filter cargo feature, off by default and activating the
experimental marker, mirroring http-callout-filter. The filter module,
its registration, and the example integration test are cfg-gated; the
server and integration-test crates forward the feature. Registry tests
assert presence with the feature and absence without it.

This lets the filter merge and iterate while its background token
refresher moves onto a future core primitive (praxis-proxy/praxis#1043).

Signed-off-by: szedan <szedan@redhat.com>

---------

Signed-off-by: szedan <szedan@redhat.com>
…y replac…" (praxis-proxy#821)

This reverts commit 7d1aef9.

Signed-off-by: szedan <szedan@redhat.com>
…s-proxy#799)

* fix(openai): preserve OGX HTTP responses

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
(cherry picked from commit 8738f3bd19717c74d463e4c9ab05c08d9fe7e76c)

* docs(openai): clarify redirect response handling

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
(cherry picked from commit a5bab9cf11a5b6f315b410d9253e11446829c486)

* test(openai): cover non-success file responses

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
(cherry picked from commit 7c4bab199b795907be5dfcf3e2d589eb27c85375)

* test(openai): cover file search callout statuses

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
(cherry picked from commit fa96e6c7248807117ae8fe04d0df14171de58012)

* chore(openai): document response test complexity

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
(cherry picked from commit 57426f8b33b037f0f04092ee732502731042fb5e)

* test(openai): cover example non-2xx file responses

Prove resolver 502 safety, transparent /v1/files 429, and closed file-search failure.

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
(cherry picked from commit 2b0878d5284deb1616aee9285020cdd9600532ea)

* chore(openai): format file resolve test constants

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
(cherry picked from commit 646cf3f6e5bd29e37738109601e1c7ac6b507622)

* test(openai): restore file-search failure-mode asserts

Closed reject should keep searching because it returns before apply_batch.
Open continue still needs the reinference function_call_output. Drop the
stale JSON-decode rustdoc on ApiClientError.

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>

---------

Signed-off-by: Sumanth Kamenani <skamenan@redhat.com>
Co-authored-by: Aslak Knutsen <aslak@4fs.no>
…xis-proxy#818)

Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](docker/setup-buildx-action@bb05f3f...37fe631)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…raxis-proxy#819)

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.7 to 4.37.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@ff2f1c6...db488dd)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…axis-proxy#820)

Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.7 to 4.37.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@ff2f1c6...db488dd)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
Signed-off-by: Shane Utt <shaneutt@linux.com>
)

Bumps rust from 1.97-alpine to 1.98-alpine.

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.98-alpine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@aslakknutsen

aslakknutsen commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

@yossiovadia What is the plan here? Are we starting up a local llm-katan instance at some point, or is this only to be ran manually?

@yossiovadia

Copy link
Copy Markdown
Contributor Author

Re: the July sign-off callout — ec519c1 was replaced by c77ed28 (signed) when the branch was rebased; DCO is green.

@yossiovadia

Copy link
Copy Markdown
Contributor Author

Good question. Current state: the suite is opt-in — it skips when LLM_KATAN_BASE_URL is unset/unreachable, so CI stays green without a backend, and it is meant to be run manually. It is self-contained (PEP 723 header, uv run), and I have a public instance it works against, so it is easy to verify right now:

LLM_KATAN_BASE_URL=https://3-147-232-199.sslip.io uv run tests/integration/sdk/anthropic/test_anthropic_messages_llmkatan.py

(needs a built praxis-ai binary — the test starts its own proxy; the katan URL is the only external dependency). I verified that endpoint serves the suite a few minutes ago.

On the plan (tracked in #600): running it in CI is future work rather than a promise in this PR. The natural shape is a dedicated workflow like vllm-integration.yaml — the echo backend is much lighter than the vLLM container (no model weights, no GPU, just an install and a start). The one prerequisite is pinning llm-katan from somewhere the org controls rather than a personal fork (it is public and Apache-2.0, so that is a repo move or a tagged release). Happy to take that up as a follow-up if the maintainers want these running in CI.

@praxis-bot praxis-bot left a comment

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.

PR Re-Review

Summary: Most findings resolved. The hardcoded IP default is gone (env var with skip), URL parsing consolidated into _parse_llm_katan_url(), event ordering now uses a list with sequence assertions, and inline config has a comment explaining why no example exists. Helper duplication (finding #2) remains open. One new medium finding on the error-path test implementation.

Severity Count
Medium 1

)


def test_malformed_json_rejected(self, anthropic_client, praxis_proxy):

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] This error-path test uses raw httpx instead of the Anthropic SDK client. The original finding requested testing through the SDK (anthropic_client.messages.create with pytest.raises(anthropic.BadRequestError)) to confirm the filter works correctly when exercised by the real SDK, not just raw HTTP.

The SDK may transform errors, add retries, or handle response parsing differently than a raw HTTP client. Testing via httpx bypasses SDK-level error handling entirely.

Add a test that exercises the SDK's error path, for example:

def test_missing_max_tokens_rejected_via_sdk(self, anthropic_client):
    """Verify anthropic_validate rejects missing max_tokens through the SDK."""
    import anthropic
    
    with pytest.raises(anthropic.BadRequestError) as exc_info:
        anthropic_client.messages.create(
            model=LLM_KATAN_MODEL,
            messages=[{"role": "user", "content": "test"}],
            # Missing max_tokens — required by anthropic_validate
        )
    
    assert exc_info.value.status_code == 400

This confirms the SDK correctly surfaces validation errors from the proxy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.