feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F) - #313

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

feat(provider-azure-openai): add aisix-provider-azure-openai skeleton crate (D6, #302 §3 Phase F)#313
moonming merged 2 commits into
mainfrom
feat/azure-openai-bridge-skeleton

Conversation

@moonming

@moonmingmoonming commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Phase F / D6 of api7/AISIX-Cloud#302 — scaffolds the Azure OpenAI Service family bridge so a `provider_key` row with `adapter: "azure-openai"` resolves to a real bridge in the Hub.

Skeleton PR: crate exists, compiles, registered in workspace + Hub. Real HTTP dispatch + auth lands in follow-up D6.1–D6.5.

Why a separate bridge (not OpenAiBridge::with_name)

DifferenceOpenAIAzure OpenAI
Auth header`Authorization: Bearer ``api-key: `
URL pattern`{base}/chat/completions``https://.openai.azure.com/openai/deployments//chat/completions?api-version=`
Model fieldOpenAI model id (e.g. `gpt-4o`)Operator-defined deployment name (e.g. `prod-gpt4`)
ResponsePlain chat.completionInjects `prompt_filter_results` / `content_filter_results`

OpenAiBridge's header builder and URL composition hard-code Bearer + simple path. Reusing it for Azure would either 401 or 404 every request.

What lands

  • `crates/aisix-provider-azure-openai/` (workspace member, registered)
  • `AzureOpenAiBridge` with `name() == "azure-openai"` (kebab-case matches the Adapter enum's wire form)
  • `AzureUpstreamRef` parses + validates the URL components from `provider_key.api_base` (accepting both `https://.openai.azure.com` and bare `` shorthand) + the request's deployment name
  • `AzureUpstreamRef::chat_completions_url()` builds the per-request URL with the exact path Azure expects — pinned by a snapshot test to catch positioning regressions
  • `DEFAULT_API_VERSION` constant pinned (with doc-comment linking to Azure's deprecation schedule)
  • `chat()` / `chat_stream()` return `BridgeError::Config` referencing refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302; resolve-time guards fire before the not-implemented stub
  • `Hub::register_family(Adapter::AzureOpenai, ...)` in `build_hub()`
  • `wire.rs` reserved query params (`api-version`) + reserved auth headers (`api-key`) — same defense-in-depth pattern as OpenAiBridge's RESERVED_DEFAULT_HEADERS

Tests (11 passing)

  • Resolve accepts canonical https-resource form + bare resource shorthand
  • Resolve rejects empty deployment / missing api_base / empty api_base
  • `chat_completions_url` matches the exact Azure REST path
  • Bridge name stable at `"azure-openai"` (metrics label contract)
  • Chat surfaces clear "not yet implemented + refactor(server): inline DeepSeek/Google bridge factories + delete wrapper crates (Phase A) #302" error
  • Chat with missing api_base errors before dispatch (proves resolve-time guard fires)
  • Wire reserved_query_params / reserved_auth_headers pin Azure's auth conventions

References (per CLAUDE.md §7)

Out of scope — D6 follow-up tracker

TaskDescription
D6.1`api-key` header auth (NOT Bearer)
D6.2Full URL pattern dispatch (deployments / chat/completions / api-version query)
D6.3`upstream_id`-as-deployment-name parsing
D6.4`api_version` parameter from provider_key config
D6.5Content filter response surfacing

Test plan

  • `cargo build --workspace` clean
  • `cargo test --workspace --lib` — 828 + 11 new = 839 total, all green
  • `cargo clippy --workspace --all-targets -- -D warnings` clean
  • `cargo fmt --check --all` clean
  • Independent audit per CLAUDE.md §8 (will spawn after this PR is open)

Related

Summary by CodeRabbit

  • New Features
    • Azure OpenAI provider added; validates Azure resource, deployment and API version. Current implementation is a skeleton that returns configuration errors for chat operations.
  • Chores
    • Provider added to the workspace and registered so adapter key "azure-openai" resolves.
  • Behavior
    • Prevents overriding Azure's api-version and auth headers via defaults.
  • Tests
    • Unit and async tests added for parsing, validation and error paths.

Review Change Stack

CopilotAI review requested due to automatic review settings May 17, 2026 05:22
@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 3 minutes and 43 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: 8ce951e8-9504-4c08-a51e-3a616fab6ba6

📥 Commits

Reviewing files that changed from the base of the PR and between bf05ae3 and 6c717e4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/aisix-provider-azure-openai/Cargo.toml
  • crates/aisix-provider-azure-openai/src/bridge.rs
  • crates/aisix-provider-azure-openai/src/lib.rs
  • crates/aisix-provider-azure-openai/src/wire.rs
  • crates/aisix-server/Cargo.toml
  • crates/aisix-server/src/main.rs
📝 Walkthrough

Walkthrough

This PR adds a new aisix-provider-azure-openai crate implementing a skeletal AzureOpenAiBridge with upstream parsing, URL construction, reserved wire helpers, tests, and registers the bridge in the server hub for adapter-based routing. The bridge currently validates configuration and returns "not implemented" errors.

Changes

Azure OpenAI Provider Bridge

Layer / File(s)Summary
Crate Foundation & Module Structure
Cargo.toml, crates/aisix-provider-azure-openai/Cargo.toml, crates/aisix-provider-azure-openai/src/lib.rs
Workspace membership is registered, crate manifest declares dependencies on aisix-core, aisix-gateway, async-trait, thiserror, and tracing, and the library entrypoint establishes crate lint policies and publicly re-exports AzureOpenAiBridge and AzureUpstreamRef.
Azure Bridge & Upstream Resolver Implementation
crates/aisix-provider-azure-openai/src/bridge.rs
AzureOpenAiBridge implements the Bridge trait with a stable "azure-openai" name. AzureUpstreamRef parses the Azure resource name from api_base (supporting canonical https://<resource>... and bare resource forms), validates deployment and api_base, enforces token safety, and constructs the chat-completions endpoint URL. chat()/chat_stream() resolve upstream and return configuration errors; comprehensive unit and async tests cover parsing, URL construction, and error cases.
Wire Shape & Reserved Parameters
crates/aisix-provider-azure-openai/src/wire.rs
Crate-internal helpers define Azure-reserved query parameters (api-version) and authentication headers (api-key, authorization) to prevent accidental mutation during dispatch wiring. Unit tests verify both reserved sets.
Server Integration & Hub Registration
crates/aisix-server/Cargo.toml, crates/aisix-server/src/main.rs
The Azure provider crate is added as a path dependency and imported in main.rs. During hub construction, AzureOpenAiBridge is registered via hub.register_family(Adapter::AzureOpenai, ...) to enable adapter-schema-based dispatch for "azure-openai" catalog rows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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.

@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — 3 HIGH + 2 MEDIUM + 2 LOW

Fixed in `bf05ae3`

  • HIGH-1 — `chat()` used `req.model` instead of `ctx.model.model_name`: ✅ introduced `upstream_model(ctx)` helper mirroring OpenAiBridge. Dispatch reads the operator-pinned deployment from Model.model_name. `req.model` (customer-typed display name) is now `_req` and explicitly ignored. New regression test `chat_ignores_req_model_and_uses_ctx_model_name` pins this by setting `req.model="foo bar/../etc"` (which the URL-token validator would reject if it were the source of truth) — the not-implemented stub fires instead, proving model_name was used.

  • HIGH-2 — URL-injection via operator/customer-controlled strings: ✅ `validate_url_token()` enforces `[A-Za-z0-9_-]+` on both `deployment` and `resource`. The canonical-https resolver now requires the host suffix to be exactly `openai.azure.com` — `https://acme.evil.com\` and similar attacker-host attempts get a clear Config error. 5 new tests cover query-injection (deployment + bare resource), slash-injection, hash-fragment, and wrong-suffix host.

  • HIGH-3 — `DEFAULT_API_VERSION` was a preview: ✅ bumped to GA shape `2024-10-21`. New test `default_api_version_is_ga_shape` asserts the constant is exactly 10 chars with hyphens at positions 4 and 7 AND does not contain "preview" — a future bump can't silently re-introduce a preview default.

  • MEDIUM-1 — `reserved_auth_headers` missing AAD bearer mode: ✅ list now includes both `api-key` (legacy auth) AND `authorization` (Entra RBAC). Test renamed + extended to assert both names are present.

Justified without code changes

  • MEDIUM-2 — No build_hub integration test: ⏸️ acknowledged. Filed as a shared follow-up across D5 feat(provider-vertex): add aisix-provider-vertex skeleton crate (D5, #302 §3 Phase E) #312 / D6 / D7 feat(provider-bedrock): add aisix-provider-bedrock skeleton crate (D7, #302 §3 Phase G) #314 skeletons; the gap applies equally to all three and the fix belongs in a single PR that registers all family bridges + asserts dispatch_two_tier returns Some for every Adapter variant.

  • LOW-1 — `BridgeError::Config` semantically wrong for "not implemented": ⏸️ kept as-is. Same justification as D5's LOW-1: adding `BridgeError::NotImplemented` ripples through every Bridge impl, the proxy error mapping, the OpenAI-shaped error envelope translator, and metrics labels. The error message starts with "azure-openai bridge is not yet implemented" so SREs triaging the log know exactly what happened.

  • LOW-2 — `sample_model` uses `"provider": "openai"` for an Azure bridge: ⏸️ kept as-is by design. The legacy Provider enum doesn't have an Azure variant; `Adapter::AzureOpenai` routing happens off ProviderKey.adapter, not Model.provider. Doc comment in `sample_model()` explains this.

Test results

```
cargo build --workspace # clean
cargo test -p aisix-provider-azure-openai # 20 passed (was 11; 9 new from audit response)
cargo clippy --workspace --all-targets # clean (-D warnings)
cargo fmt --check --all # clean
```

All HIGH/MEDIUM either fixed or justified per CLAUDE.md §8 merge gate.

HIGH-1 also applies to D5 (#312) and D7 (#314) — D7 was written after this audit landed so it already has the fix; D5 backport coming as a follow-up commit on that PR.

moonming added a commit that referenced this pull request May 17, 2026
…el.model_name)
D6 audit on PR #313 surfaced a wire-shape bug that applies equally
to D5's skeleton: chat() and chat_stream() resolved the publisher
from req.model (customer-typed display name) instead of from
ctx.model.model_name (operator-pinned upstream id).
Once dispatch lands in D5.2 (Gemini publisher), the URL builder
would have produced `.../publishers/google/models/<customer-facing-
name>:streamGenerateContent` and 404 on every Vertex request. The
skeleton tests previously pinned the wrong contract (passed
`req.model="gemini-1.5-pro"` so the matcher accidentally got the
right input), so a future dispatch PR would have inherited the
broken wire.
Fix:
- Add upstream_model(ctx) helper mirroring OpenAiBridge.
- chat() / chat_stream() resolve from ctx.model.model_name.
- req.model is now _req and explicitly ignored.
- sample_model() renamed to sample_model_with(model_name) so tests
can pin "display_name differs from model_name" by construction.
- New regression test chat_ignores_req_model_and_uses_ctx_model_name
sets req.model="gpt-4o" (would fail publisher resolution if it
were the source of truth) and asserts the not-implemented stub
fires — proving model_name was the actual input.
- New defense test chat_with_missing_model_name_errors_before_dispatch
ensures Option<String> = None on Model.model_name surfaces a
clear error rather than panicking.
D6 (#313) already fixed; D7 (#314) was authored after D6's audit
landed so it already has the fix.
… crate (#302 Phase F / D6)
Wave 5 D6 — scaffolds the Azure OpenAI Service family bridge so a
`provider_key` row with `adapter: "azure-openai"` resolves to a
real bridge instead of falling through to the legacy fallback.
Actual HTTP dispatch + GCP-style auth lands in follow-up D6.x PRs.
Why Azure-OpenAI is a separate bridge (not OpenAiBridge::with_name):
1. Auth header differs — `api-key: <key>`, not `Authorization: Bearer`
2. URL pattern differs — `https://<resource>.openai.azure.com/openai/
deployments/<deployment>/chat/completions?api-version=<version>`
3. Model field semantics — upstream_id is a deployment name (operator-
defined), not an OpenAI model id
4. Content filter injection — Azure injects prompt_filter_results /
content_filter_results that the OpenAI SDK doesn't expect
Implementation:
- AzureOpenAiBridge struct with name "azure-openai"
- AzureUpstreamRef::resolve(deployment, api_base) parses + validates:
canonical https://<resource>.openai.azure.com OR bare resource name
- AzureUpstreamRef::chat_completions_url() builds the per-request URL
- DEFAULT_API_VERSION constant pinned to current stable (with doc
comment linking to Azure's deprecation schedule)
- chat() / chat_stream() return BridgeError::Config referencing #302
- Hub::register_family(Adapter::AzureOpenai, ...) in build_hub()
- wire::reserved_query_params (api-version) + reserved_auth_headers
(api-key) — same defense-in-depth pattern as OpenAiBridge's
RESERVED_DEFAULT_HEADERS, for the eventual override apply path
Tests (11 passing):
- resolve_accepts_canonical_https_resource / bare_resource_name
- resolve_rejects_empty_deployment / missing_api_base / empty_api_base
- chat_completions_url_matches_azure_api_path (URL fragment pinned —
any typo in resource/deployment/api-version positioning would
surface as a 404 from every Azure dispatch)
- bridge_name_is_stable
- chat_surfaces_clear_not_implemented_error
- chat_with_missing_api_base_errors_before_dispatch (proves resolve-
time guard fires before the not-implemented stub)
- wire reserved_query_params / reserved_auth_headers coverage
References (per CLAUDE.md §7):
- Azure OpenAI REST API — https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
- api-version deprecation schedule — https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
- Content filter shape — https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter
- LiteLLM azure/ reference — https://github.com/BerriAI/litellm/tree/main/litellm/llms/azure
Out of scope, tracked under #302 Phase F follow-ups:
- D6.1 api-key header auth
- D6.2 Full Azure URL pattern dispatch
- D6.3 upstream_id-as-deployment-name parsing
- D6.4 api_version parameter handling
- D6.5 Content filter response surfacing
… + MEDIUM-1
HIGH-1 — chat() resolved deployment from req.model (display name)
instead of ctx.model.model_name (operator-pinned upstream id). Once
dispatch lands the URL builder would produce
`/openai/deployments/<customer-display-name>/...` and 404 on every
request. Fix: introduce upstream_model(ctx) helper mirroring
OpenAiBridge, resolve deployment from Model.model_name. New
regression test chat_ignores_req_model_and_uses_ctx_model_name pins
the contract with req.model="foo bar/../etc" (would be rejected by
the URL-token validator if it were the source of truth) — the
not-implemented stub fires instead, proving model_name was used.
HIGH-2 — `chat_completions_url()` URL-injection via operator/
customer-controlled strings. Format! of unvalidated `resource` +
`deployment` into the URL host + path lets:
- `api_base = "acme?evil=1"` corrupt the host
- `deployment = "foo?api-version=evil"` override the api-version
- `api_base = "https://acme.evil.com"` redirect to attacker host
Fix: validate_url_token() enforces [A-Za-z0-9_-]+ on both deployment
and resource; canonical-https resolver now requires the host suffix
to be exactly `openai.azure.com` (rejecting `acme.evil.com`). Tests
cover query-injection in deployment, slash-injection, hash-fragment,
query-injection in bare-resource, and the wrong-suffix host case.
HIGH-3 — DEFAULT_API_VERSION was "2024-08-01-preview" (preview!).
Azure rotates preview versions aggressively per the published
deprecation schedule; shipping a preview as the implicit default
means silent breakage on Azure's cadence. Fix: bumped to GA shape
"2024-10-21". New test default_api_version_is_ga_shape asserts the
constant matches `YYYY-MM-DD` exactly (no `-preview` suffix) so a
future bump can't accidentally re-introduce a preview default.
MEDIUM-1 — wire::reserved_auth_headers() only listed `api-key`.
Azure supports both `api-key: <key>` (legacy) and
`Authorization: Bearer <aad-token>` (Entra RBAC); a future AAD-mode
operator would have silently been able to inject Authorization via
default_headers. Fix: list now includes both. Test renamed +
extended to assert both are present.
Justifications (LOW + MEDIUM-2):
- LOW-1 (BridgeError::Config semantically wrong for "not
implemented"): kept as-is, same justification as D5's LOW-1 —
a new BridgeError::NotImplemented variant ripples through every
Bridge impl + proxy error mapping. Will revisit alongside D5/D7
if the variant becomes needed for other reasons.
- LOW-2 (sample_model uses "provider": "openai" for an Azure
bridge): doc comment notes this is by design; the legacy
Provider enum doesn't have an Azure variant; Adapter::AzureOpenai
routing happens off ProviderKey.adapter, not Model.provider.
- MEDIUM-2 (no build_hub integration test): filed as a shared
follow-up across D5/D6/D7 skeletons.
@moonming
moonmingforce-pushed the feat/azure-openai-bridge-skeleton branch from bf05ae3 to 6c717e4CompareMay 17, 2026 07:52
CopilotAI review requested due to automatic review settings May 17, 2026 07:52
@moonming
moonming merged commit 148fce2 into mainMay 17, 2026
@moonming
moonming deleted the feat/azure-openai-bridge-skeleton branch May 17, 2026 07:52

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.

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