feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F) - #319

Merged
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire
May 17, 2026
Merged

feat(provider-azure-openai): wire D6 chat + stream + api-key auth (#302 Phase F)#319
moonming merged 2 commits into
mainfrom
feat/azure-openai-wire

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-azure-openai with real HTTP dispatch against Azure OpenAI Service. Closes the Phase F (D6) wire-implementation work on api7/AISIX-Cloud#302.

The wire shape is OpenAI chat-completions; Azure differs on three axes that this PR handles end-to-end:

  1. URL pattern — deployment-keyed: https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version> (the AzureUpstreamRef::chat_completions_url() helper that landed in feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) #313 is unchanged)
  2. Auth headerapi-key: <secret> (NOT Authorization: Bearer), with defense-in-depth via the reserved-headers list in aisix-provider-openai::overrides (covers api-key, authorization, x-api-key)
  3. Response extension — Azure injects prompt_filter_results / content_filter_results blocks; the reused OpenAiResponse / OpenAiStreamChunk parsers tolerate these transparently (no deny_unknown_fields)

Implementation strategy

Promote the OpenAI wire types and conversion helpers from pub(crate) to pub and reuse them in the Azure crate — they're JSON contract types, not OpenaiBridge-specific implementation. This keeps a single source of truth for the chat-completions wire format.

Items promoted to pub in aisix-provider-openai::wire:

  • OpenAiRequest / OpenAiMessage / OpenAiContent — request types
  • OpenAiResponse / OpenAiChoice / OpenAiResponseMessage / OpenAiUsage / OpenAiPromptDetails / OpenAiCompletionDetails — response types
  • OpenAiStreamChunk / OpenAiStreamChoice / OpenAiStreamDelta — stream types
  • build_request / messages_from / response_into_chat_response / stream_chunk_into_chat_chunk — converters

The OpenaiBridge's own dispatch is unchanged. Embedding wire types stay pub(crate) (out of scope for D6).

The override apply pipeline (param_renames / param_constraints / default_body_fields / default_headers / content_list_to_string / stream_done_marker / reasoning_field) is reused verbatim from aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge::build_chunk_stream.

Deferred (tracked in lib.rs Status section)

  • D6.4 — per-PK api_version override. Today the bridge pins DEFAULT_API_VERSION = \"2024-10-21\" (GA). Follow-up will accept an explicit version from provider_key.api_base query string or a dedicated PK field.
  • D6.6 — AAD Bearer auth as a second auth scheme. Today the bridge supports api-key only (the common case). AAD support will land alongside the cp-api auth_scheme field becoming routable.

References

Test plan

34 unit tests, all passing. Coverage:

AzureUpstreamRef parsing (12 tests — unchanged from #313 skeleton)

  • canonical https / bare resource / trailing slash / pasted endpoint tolerance
  • URL-injection rejection (deployment, resource, query, slash, hash)
  • host-suffix enforcement (openai.azure.com only)
  • missing api_base / empty api_base / empty deployment error messages
  • DEFAULT_API_VERSION is GA shape (YYYY-MM-DD, no -preview)

build_request_headers (6 tests — new)

  • api-key set, Authorization NOT set
  • SSE Accept: text/event-stream set only when streaming
  • default_headers.api-key override blocked by reserved-headers list
  • default_headers.authorization override blocked
  • non-reserved custom headers pass through (x-custom-trace)
  • invalid api-key chars rejected (e.g. newline → header injection guard)
  • invalid request_id chars rejected

Wire-shape dispatch against wiremock (8 tests — new)

  • api-key header + deployment URL + api-version query reach upstream
  • JSON body's model field = deployment name (Azure ignores it, but kept for log clarity)
  • content_filter_results / prompt_filter_results in response tolerated
  • param_renames applied to outbound body (max_tokensmax_completion_tokens)
  • 4xx maps to UpstreamStatus with body content
  • 429 maps with Retry-After parsed
  • req.model ignored, ctx.model.model_name used for URL deployment
  • SSE streaming yields chunks until [DONE]

Negative pre-dispatch (2 tests — new)

  • missing api_base errors before HTTP
  • empty secret errors before HTTP

Compile-only proof (1 test — new)

  • bridge.chat() reaches the network layer for a canonical api_base (not Config-errored)

Test plan TODO

  • cargo test -p aisix-provider-azure-openai passes locally (34/34)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Azure OpenAI provider is now fully functional with complete chat and streaming capabilities
    • Updated to the latest Azure OpenAI API version (2024-10-21)
  • Documentation

    • Updated provider implementation status documentation

Review Change Stack

…rs + api-key auth (D6, #302 Phase F)
Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs with real HTTP dispatch against Azure OpenAI Service. The wire
shape is OpenAI chat-completions; Azure differs on three axes that
this crate now handles end-to-end:
1. URL pattern — deployment-keyed:
`https://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=<version>`
built by AzureUpstreamRef::chat_completions_url() (kept as-is from
the skeleton; pinned by chat_completions_url_matches_azure_api_path)
2. Auth header — `api-key: <secret>` (NOT `Authorization: Bearer`).
Set by build_request_headers() with defense-in-depth via the
reserved-headers list in aisix-provider-openai::overrides (covers
`api-key`, `authorization`, `x-api-key` so an operator's
`default_headers` override cannot exfil traffic by rewriting auth)
3. Response extension — Azure injects `prompt_filter_results` /
`content_filter_results` blocks on responses. The reused
`OpenAiResponse` / `OpenAiStreamChunk` parsers tolerate these
transparently (no `deny_unknown_fields` on the parser types),
pinned by chat_tolerates_content_filter_results_in_response
Implementation strategy: promote the OpenAI wire types and conversion
helpers (build_request, messages_from, response_into_chat_response,
stream_chunk_into_chat_chunk, OpenAiResponse, OpenAiStreamChunk) from
`pub(crate)` to `pub` and reuse them — they're JSON contract types,
not OpenaiBridge-specific implementation. This keeps a single source
of truth for the chat-completions wire format. The OpenaiBridge's
own dispatch is unchanged.
The override apply pipeline (param_renames / param_constraints /
default_body_fields / default_headers / content_list_to_string /
stream_done_marker / reasoning_field) is reused verbatim from
aisix_provider_openai::overrides. SSE decoding mirrors OpenaiBridge's
build_chunk_stream.
Deferred to follow-ups (tracked in lib.rs Status section):
- D6.4 — per-PK `api_version` override (currently DEFAULT_API_VERSION
GA pin `2024-10-21`)
- D6.6 — AAD Bearer auth as second auth scheme
## Test coverage
34 unit tests cover:
- AzureUpstreamRef parsing (canonical https, bare resource shorthand,
trailing slash tolerance, pasted-endpoint tolerance, URL-injection
rejection across deployment/resource/query/slash/hash, host-suffix
enforcement, missing api_base error message)
- DEFAULT_API_VERSION shape (GA, YYYY-MM-DD, no `-preview`)
- build_request_headers: api-key set (NOT Authorization), reserved-
headers list blocks api-key/authorization overrides, SSE Accept,
invalid api-key chars rejected, invalid request_id chars rejected
- Bridge wire-shape dispatch against wiremock:
- api-key header + deployment URL + api-version query reach upstream
- JSON body's `model` field = deployment name
- content_filter_results / prompt_filter_results tolerated
- param_renames applied (max_tokens → max_completion_tokens)
- 4xx maps to UpstreamStatus with body
- 429 maps with Retry-After parsed
- req.model ignored, ctx.model.model_name used for URL deployment
- SSE streaming yields chunks until [DONE]
- bridge.chat() end-to-end reaches network layer for canonical api_base
CopilotAI review requested due to automatic review settings May 17, 2026 12:20
@coderabbitai

coderabbitaiBot commented May 17, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9ff02d5c-c9ef-4798-b0ff-79d3f681f21a

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa158f and 1e076da.

📒 Files selected for processing (2)
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-openai/src/wire.rs
📝 Walkthrough

Walkthrough

This PR implements a functional Azure OpenAI bridge by exposing shared OpenAI wire types as public API, adding HTTP dispatch logic with Azure-specific configuration validation and request handling, comprehensive test coverage via wiremock for both streaming and non-streaming operations, and updating documentation to reflect completion status.

Changes

Azure OpenAI Bridge Implementation

Layer / File(s)Summary
OpenAI Wire Module Public API
crates/aisix-provider-openai/src/lib.rs, crates/aisix-provider-openai/src/wire.rs
Wire types (OpenAiRequest, OpenAiMessage, OpenAiResponse, OpenAiStreamChunk) and conversion functions (build_request, response_into_chat_response, stream_chunk_into_chat_chunk) are promoted from pub(crate) to pub, enabling reuse by the Azure bridge and other providers.
Azure Bridge Infrastructure & Dependencies
crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/bridge.rs
Cargo.toml adds reqwest, tokio, futures, async-stream, bytes, http, serde, serde_json to normal dependencies and wiremock to dev-dependencies. AzureOpenAiBridge struct now contains a Client field, with with_client() and default_client() helpers for configuration.
Azure Configuration & URL Resolution
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef validation rejects URL-control characters and mismatched host suffixes, accepts canonical HTTPS base URLs with trailing slashes, builds Azure chat-completions URLs, and pins DEFAULT_API_VERSION to GA shape "2024-10-21".
Azure HTTP Request/Response Handling & Main Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
Request/response helpers extract Azure api-key from secrets, construct Azure-specific HeaderMap (api-key, content-type, request-id, SSE Accept), apply RequestOverrides/ResponseOverrides to request bodies, map HTTP errors with retry-after propagation, and decode OpenAiResponse into ChatResponse. chat() and chat_stream() methods dispatch over HTTP with optional request deadlines and SSE streaming support.
Tests – Configuration & URL Validation
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Unit tests validate AzureUpstreamRef resolution, URL path construction, deployment token injection rejection, resource-name query-injection defense, canonical HTTPS suffix with trailing-slash/endpoint-path acceptance, and DEFAULT_API_VERSION GA-shape enforcement.
Tests – Wire-Level Dispatch & Streaming
crates/aisix-provider-azure-openai/src/bridge.rs (tests)
Wiremock-based tests verify api-key header usage (no Bearer token), SSE Accept behavior, reserved-header override defenses, param_renames in request bodies, 4xx/429 error mapping with retry-after extraction, Azure content-filter block tolerance, and end-to-end streaming with [DONE] marker detection. Includes test helpers for building mock Model, ProviderKey, and BridgeContext inputs.
Documentation & Status Updates
crates/aisix-provider-azure-openai/src/lib.rs
Crate-level documentation updated to replace skeleton TODOs with issue #302 Phase F status checklist (D6.1/D6.2/D6.3/D6.5 complete, D6.4/D6.6 remaining). Documentation reference changed from LiteLLM Azure to OpenAI Python SDK's Azure module.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…+ L1/L3/L5)
PR #319 audit surfaced gaps the original PR's tests didn't catch
because most "dispatch" tests bypassed `bridge.chat()` via a
`run_dispatch_against_mock` helper that rebuilt the request from
scratch. This commit closes those gaps:
## HIGH
H1 — All dispatch tests now go through bridge.chat() / chat_stream()
- Added a #[cfg(test)] `url_override` field + `with_url_override`
constructor on AzureOpenAiBridge so wiremock can stand in for
`<resource>.openai.azure.com`. resolve/validation/header/body
paths still run normally against the canonical api_base.
- Removed `run_dispatch_against_mock` helper (rebuilt request,
bypassed bridge entry point).
- Rewrote 7 dispatch tests to call bridge.chat() / chat_stream()
directly.
H2 — Streaming deadline now enforced per-chunk, not only on the
initial POST. `build_chunk_stream` now takes (deadline, started)
and wraps each `stream.next().await` in `timeout_at`. Pinned by
new `chat_stream_enforces_per_chunk_deadline` test (200ms deadline,
2s mock body delay → BridgeError::Timeout).
H3 — Stream test extended with inline `prompt_filter_results` (top-
level Azure prelude chunk) + per-choice `content_filter_results`.
Pins OpenAiStreamChunk tolerance for Azure's in-stream filter
blocks, not just response-body filter blocks.
## MEDIUM
M1 — Upstream error body no longer echoed verbatim. Azure error
envelopes include the deployment name + resource hostname; piping
them into customer-visible BridgeError::UpstreamStatus.message
leaks operator-internal taxonomy. `map_http_error` now maps to
canned status-keyed phrases ("upstream deployment or model not
found", "upstream rate limited", etc.). The upstream body is
drained and discarded; full body still reachable via tracing on
the operator side via request_id correlation.
- New test `chat_maps_upstream_400_to_canned_message_not_body_echo`
asserts the body's deployment-name + resource leak into the
error message is blocked.
- New test `chat_maps_404_to_deployment_not_found_canned_message`.
- Updated `chat_maps_429_with_retry_after_and_canned_message` to
assert both the redacted message AND retry-after.
M2 — param_renames test now asserts the renamed key carries the
ORIGINAL VALUE (100), not just that the key swap happened. A
buggy apply_param_renames that nukes the old key without
inserting a value would have passed the old assertion.
M3 — chat_dispatch_sends_api_key_header_and_deployment_url now
asserts `Authorization` is absent at the wire (not just absent
from the helper output) by extending CapturingResponder to also
capture inbound headers.
M4 — chat_body_full_shape_on_the_wire replaces the
body_partial_json({"model": "gpt4o-prod"}) check with a full
body shape inspection: `messages` array length + role + content,
`stream: false` for non-streaming. body_partial_json no longer
used; import removed.
## LOW
L1 — Added doc comment to wire.rs's module-level header explaining
why request/response/stream-chunk types are pub (sibling crate
reuse, not a stability promise).
L3 — chat_against_full_bridge_dispatch (renamed to
chat_against_real_azure_reaches_network) marked `#[ignore]` —
it called real Azure DNS, flaky on CI runners with corporate
proxies. Run manually via `cargo test -- --ignored`.
L5 — `truncate` removed (no longer used after M1's body-discard).
L5's UTF-8 boundary-panic risk is moot now.
## Result
cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit follow-up pushed (1e076da): Independent audit surfaced 3 HIGH + 4 MEDIUM + 5 LOW findings; all addressed:

HIGH (all fixed)

  • H1 — Most "dispatch" tests bypassed bridge.chat() via a run_dispatch_against_mock helper. Added a #[cfg(test)]url_override field + with_url_override(url) constructor; rewrote 7 dispatch tests to call bridge.chat() / bridge.chat_stream() directly. AzureUpstreamRef::resolve still runs against the canonical acme-west.openai.azure.com api_base (host-suffix check, validation, etc.); only the final POST URL is rewritten to the mock.
  • H2 — Streaming deadline now per-chunk, not just on the initial POST. build_chunk_stream takes (deadline, started) and wraps each stream.next() in tokio::time::timeout_at. Pinned by new chat_stream_enforces_per_chunk_deadline test.
  • H3 — Stream test extended with inline prompt_filter_results (Azure prelude chunk) + per-choice content_filter_results. Renamed to chat_stream_yields_chunks_until_done_marker_with_inline_content_filters.

MEDIUM (all fixed)

  • M1 — Upstream error body no longer echoed verbatim. map_http_error maps to canned status-keyed phrases ("upstream deployment or model not found", "upstream rate limited", etc.) so Azure's deployment name + resource hostname in error envelopes don't leak into customer-visible BridgeError::UpstreamStatus.message. Pinned by chat_maps_upstream_400_to_canned_message_not_body_echo + chat_maps_404_to_deployment_not_found_canned_message.
  • M2chat_applies_param_renames_to_outbound_body now asserts the renamed key carries the ORIGINAL VALUE (100), not just the key swap.
  • M3chat_dispatch_sends_api_key_header_and_deployment_url asserts Authorization is absent on the wire (via CapturingResponder extended to capture inbound headers).
  • M4chat_body_full_shape_on_the_wire replaces body_partial_json with full shape inspection: messages length + role + content, stream: false.

LOW (all fixed)

  • L1wire.rs module doc explains the pub visibility (sibling crate reuse, not a stability promise).
  • L2_docs_only dead-code function + its preamble removed (replaced by the url_override doc on the bridge struct).
  • L3 — Real-Azure-DNS test marked #[ignore]; runnable manually via cargo test -- --ignored.
  • L4with_client documented as a public-surface constructor.
  • L5 — Moot — truncate removed entirely as part of M1's body-discard.

Verification

cargo test -p aisix-provider-azure-openai → 35 passed, 1 ignored (real-Azure)
cargo clippy --workspace -- -D warnings → clean
cargo fmt --check → clean

Sign up for freeto 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.

2 participants

@moonming