Skip to content

feat(azure): add azure_to_openai translation filter - #910

Merged
leseb merged 5 commits into
praxis-proxy:mainfrom
noalimoy:feat/886-azure-openai-translation
Sep 10, 2026
Merged

feat(azure): add azure_to_openai translation filter#910
leseb merged 5 commits into
praxis-proxy:mainfrom
noalimoy:feat/886-azure-openai-translation

Conversation

@noalimoy

@noalimoy noalimoy commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an azure_to_openai HttpFilter under apis/src/azure/ that lets standard Chat Completions clients talk to Azure OpenAI deployments through Praxis. Azure's wire format is nearly identical to Chat Completions — the filter handles the small but operationally critical differences:

  • Request: injects api-version query parameter, strips the model field (Azure uses the deployment name in the URL path), removes Accept-Encoding to prevent double-compressed bodies.
  • Response (JSON): strips prompt_filter_results and per-choice content_filter_results/content_filter_offsets.
  • Response (SSE): reuses the shared SseFrameParser rather than hand-rolling line splitting — the parser already handles cross-chunk buffering, bare \r and split \r\n line endings, data: without space after colon, multi-line data: joining, and buffer-overflow protection. The filter calls parse_chunk, strips Azure fields from each frame's data payload via the same strip_azure_fields used for non-streaming JSON, and re-emits clean SSE.
  • Errors: fills type: null from the HTTP status code and strips innererror, normalizing Azure errors to the standard Chat Completions error envelope.

SSE handled inline, not as a separate filter

Unlike anthropic_stream_events (which is a standalone filter doing full format translation between OpenAI and Anthropic event schemas), Azure SSE is already Chat Completions format — the only work is stripping the same content_filter_results fields that strip_azure_fields already removes from non-streaming JSON. A separate filter would add config surface and registration overhead for no new logic, so the SSE path lives alongside the JSON and error paths in on_response_body.

Follows the same {provider}/to_openai module structure established by anthropic/to_openai. This is the first provider filter for Epic #114 (Schema translation); the remaining providers (Bedrock, Vertex AI, Cohere) follow separately.

Related issue

Part of #114
Closes #886

Validation

  • Unit tests — 34 Azure-specific tests in praxis-ai-apis (config parsing, request/response stripping, SSE frame rebuild, partial-line buffering via SseFrameParser, multi-byte UTF-8 boundary, error normalization)
  • Integration or functional tests — 5 tests via azure_translation.rs (request forwarding with api-version, content-filter stripping, SSE event stripping, error normalization with type: null+innererror, pass-through of already-valid errors)
  • make lint — clippy (default + all-features), nightly fmt, rustdoc all pass

Checklist

  • I reviewed every changed line and can explain the change.
  • 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.

Breaking changes

None. New filter only; no existing behavior or API is changed.

@noalimoy
noalimoy force-pushed the feat/886-azure-openai-translation branch 2 times, most recently from afccb81 to 590bb7d Compare September 3, 2026 10:46

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(azure): add azure_to_openai translation filter

Well-structured filter following established {provider}/to_openai module patterns. Clean separation of request, response, SSE, and error normalization. Good reuse of the shared SseFrameParser for streaming. Unit test coverage (34 tests) and integration tests (5 tests) are thorough.

Findings

# Severity File Description
1 Medium apis/src/azure/wire.rs normalize_error_response does not handle entirely-missing type field
2 Medium apis/src/azure/to_openai/mod.rs rebuild_sse_frames silently drops event_type from SSE frames

Comment thread apis/src/azure/wire.rs
Comment thread apis/src/azure/to_openai/mod.rs

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Azure-to-OpenAI translation filter — re-review focusing on new findings only (2 prior comments already posted).

1 new finding below.

Comment thread apis/src/azure/to_openai/mod.rs
@noalimoy
noalimoy force-pushed the feat/886-azure-openai-translation branch 2 times, most recently from 280f2f5 to 726b1ec Compare September 6, 2026 11:23
@noalimoy
noalimoy requested a review from szedan-rh September 7, 2026 19:06
@leseb

leseb commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

@noalimoy do not force push - it's hard to determine which commits addressed reviews, PTAL https://github.com/praxis-proxy/ai/blob/main/CONTRIBUTING.md#pr-review-process

@leseb leseb 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.

A few finding from AI which look legit:

[P1] api-version is lost. Both the Azure filter and subsequent path_rewrite overwrite ctx.rewritten_path from the original URI. The documented ordering drops the required query parameter; reversing it drops the deployment path. Existing api-version values are also duplicated instead of replaced. The integration test never inspects the forwarded URI. Code, test, Azure requirement.

[P1] Asynchronous moderation evidence is deleted after content was streamed. The response transformation strips content_filter_results and content_filter_offsets. Azure asynchronous filtering emits text before moderation annotations; those offsets identify which emitted range must be redacted. Preserve/translate these annotations or reject unsupported asynchronous mode. Code, Azure streaming semantics.

Additionally, could we add an integration test using the official OpenAI Python SDK? It should cover non-streaming, streaming, and errors through the proxy, with a strict local fake Azure backend asserting the rewritten path, api-version, and request body. No live Azure endpoint is needed. Since client compatibility is the core contract of this filter, I consider this required before merging.

@noalimoy
noalimoy force-pushed the feat/886-azure-openai-translation branch from 726b1ec to 6b192bb Compare September 9, 2026 13:19
@leseb

leseb commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

@noalimoy please read https://github.com/praxis-proxy/ai/blob/main/CONTRIBUTING.md#pr-review-process on how to resolve praxis bot discussions. Thanks

@leseb leseb 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.

please resolve all the conversations when ready

@leseb
leseb enabled auto-merge September 9, 2026 13:25
@noalimoy

noalimoy commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

@noalimoy please read https://github.com/praxis-proxy/ai/blob/main/CONTRIBUTING.md#pr-review-process on how to resolve praxis bot discussions. Thanks

@leseb sorry, that last force-push was a mistake. review fixes are in 6b192bb; :)

@leseb

leseb commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

@noalimoy lint fails

@noalimoy

noalimoy commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

@noalimoy lint fails

fixed

auto-merge was automatically disabled September 9, 2026 15:12

Head branch was pushed to by a user without write access

Normalize Azure OpenAI requests and responses into standard Chat
Completions form: inject api-version, strip model from request body,
remove content-filter fields from JSON and SSE responses, and fill
type: null in error envelopes.

SSE streaming reuses the shared SseFrameParser for correct cross-chunk
buffering, CRLF handling, and buffer-overflow protection.

Signed-off-by: noalimoy <nlimoy@redhat.com>
…K coverage

- Compose with rewritten_path and replace api-version instead of duplicating it
- Drop async-filter annotation chunks; keep finish_reason
- Drop unstripped SSE on parse error
- Add OpenAI Python SDK tests for path, query, body, streaming, and errors

Signed-off-by: noalimoy <nlimoy@redhat.com>
Signed-off-by: noalimoy <nlimoy@redhat.com>
Signed-off-by: noalimoy <nlimoy@redhat.com>
@noalimoy
noalimoy force-pushed the feat/886-azure-openai-translation branch from be596b4 to bb6843c Compare September 10, 2026 05:45
@noalimoy
noalimoy requested a review from a team as a code owner September 10, 2026 05:45
@leseb
leseb enabled auto-merge September 10, 2026 08:19
@leseb
leseb added this pull request to the merge queue Sep 10, 2026
Merged via the queue into praxis-proxy:main with commit 3f34f80 Sep 10, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API translation: Azure OpenAI ↔ Chat Completions

3 participants