fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@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

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@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

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@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

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@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

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@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

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@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

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@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

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391) - #392

Merged
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override
May 25, 2026
Merged

fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)#392
moonming merged 5 commits into
mainfrom
fix/platform-adapter-api-base-override

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Two parallel production gaps where ProviderKey.api_base was silently dropped or rejected by the bridge, fixed in lockstep so the Bedrock parity story now covers all three platform adapters.

BridgePre-fixPost-fix
Bedrock✅ Already honored api_base via SDK endpoint_url (bridge.rs:603)unchanged
Vertexresolve_api_base() only consulted #[cfg(test)]api_base_override; production silently fell back to <region>-aiplatform.googleapis.com✅ Adds a ctx_api_base arg with precedence: cfg-test override > ctx.provider_key.api_base > canonical
AzureAzureUpstreamRef::resolve() rejected any api_base whose host didn't end in .openai.azure.com✅ Adds an upstream_override field + verbatim-override branch for non-canonical hosts; canonical Azure path unchanged

Why now

Surfaced by AISIX-Cloud#491 — when writing per-adapter live e2e specs (adapter-{azure,vertex,bedrock}-live.spec.ts) against compose-stack mocks, the Bedrock spec worked because the bridge honored api_base, but Vertex + Azure could not redirect to mock-vertex / mock-llm. Filed as ai-gateway#390 + #391; this PR closes both.

Defence-in-depth

The Azure override branch explicitly rejects ? and # in api_base — the bridge appends ?api-version=… so an operator-supplied query would open an api-version downgrade vector. The canonical Azure path keeps its existing strict resource-name validation.

The Vertex change drops nothing — the #[cfg(test)] with_api_base_override seam is preserved and takes precedence over ctx.provider_key.api_base, so existing tests keep working unchanged.

Tests

Vertex (4 new unit tests in bridge.rs::tests)

  • resolve_api_base_honors_ctx_api_base_in_production_path — pins the new production override path
  • resolve_api_base_trims_trailing_slash_on_ctx_override
  • resolve_api_base_falls_back_to_canonical_on_empty_ctx_value (empty string, whitespace, None)
  • resolve_api_base_cfg_test_override_takes_precedence_over_ctx — proves wiremock-pinning tests stay intact

Azure (5 unit-test changes in bridge.rs::tests)

  • resolve_accepts_verbatim_override_for_non_canonical_host — replaces (renamed + flipped) the pre-fix resolve_rejects_canonical_https_with_wrong_suffix test, with an inline docstring citing Azure-OpenAI bridge hardcodes .openai.azure.com host suffix, blocks BYO api_base override #391
  • resolve_accepts_http_override_for_mock_endpoint — pins the canonical mock-llm e2e shape
  • resolve_strips_trailing_slash_on_verbatim_override
  • resolve_rejects_override_with_query_injection
  • resolve_rejects_override_with_fragment

All pre-existing tests (canonical Azure path, bare-resource shorthand, validators) pass unchanged.

Test plan

  • cargo test --workspace — 1200+ tests pass, 0 failed
  • cargo clippy -p aisix-provider-vertex -p aisix-provider-azure-openai --tests -- -D warnings — clean
  • cargo fmt — applied
  • CI on this PR
  • Independent audit agent review
  • After merge: cut the per-adapter live spec PRs on AISIX-Cloud for Vertex and Azure

Closes

Unblocks

  • AISIX-Cloud follow-up: adapter-vertex-live.spec.ts + adapter-azure-live.spec.ts (per-adapter live e2e specs, mirror of the merged adapter-bedrock-live.spec.ts in AISIX-Cloud#491)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed API base URL resolution for the Vertex provider so operator-supplied api_base overrides are respected in production.
  • New Features

    • Azure OpenAI provider now supports operator-supplied upstream URL overrides (including HTTP), with trailing-slash trimming.
  • Validation & Tests

    • Added validation to reject overrides containing query strings/fragments and added unit and end-to-end tests to cover override behavior.
  • Documentation

    • Expanded google-vertex provider docs: api_base format, URL construction, streaming vs non-streaming endpoints, and OAuth token guidance.

Review Change Stack

Pre-fix, `VertexBridge::resolve_api_base()` consulted
`self.api_base_override` only under `#[cfg(test)]`. Production
builds dropped the line entirely, hardcoding
`https://<region>-aiplatform.googleapis.com`. A BYO operator
who set `ProviderKey.api_base` to a corporate-proxy URL saw
their traffic still go to Google — no error, no warning, no log.
Mirrors the Bedrock pattern (`crates/aisix-provider-bedrock/src/bridge.rs:597-609`)
which already reads `ctx.provider_key.api_base.as_deref()` in
production builds. Precedence on the rewritten Vertex path:
1. `#[cfg(test)] Self::with_api_base_override` — test seam
(keeps the 7 existing wiremock-based unit tests working
without touching ProviderKey state).
2. `ProviderKey.api_base` — production override for
corporate-proxy / private-VPC / mock deployments. Trimmed
of whitespace; trailing `/` stripped so URL stitching
produces a single-slash separator.
3. Canonical `https://<region>-aiplatform.googleapis.com`.
Empty string and whitespace-only `api_base` values are treated
as absent — falls through to canonical, matching the OpenAI
bridge's defensive normalization at `bridge.rs:107`.
## Out of scope (called out in PR body / docs)
- **Token-mint endpoint.** OAuth minting hits
`secret.token_uri` (operator-controlled inside the SA JSON),
NOT `api_base`. An air-gapped operator behind a proxy must
additionally set `token_uri` in the SA JSON to their internal
token endpoint. The fix does not (and intentionally is not
changed to) override `token_uri` from `api_base`. Documented
in `provider-keys.md` row for `google-vertex`.
- **Suffix normalization.** OpenAI bridge's `strip_known_endpoint`
tolerates `api_base="https://api.openai.com/v1/chat/completions"`.
Vertex equivalent (operator pasting a full `:generateContent`
URL) is not in scope — Vertex's URL is parameterized over
project/region/model and the canonical form is the bare host
root.
- **Azure-OpenAI (ai-gateway#391).** Sibling fix; same shape;
separate PR after #390 lands. The branch carries the WIP in
a stash for that next step.
- **Anthropic-on-Vertex.** `adapter=vertex` + Anthropic publisher
(Phase E D5.3) lands later and will pick up the same override
path for free.
## Tests
Four new unit tests on `resolve_api_base`:
- `resolve_api_base_honors_ctx_api_base_in_production_path` —
the new override path. Pre-fix this would have returned the
canonical URL.
- `resolve_api_base_trims_trailing_slash_on_ctx_override` —
defends against operator paste of `http://proxy/`.
- `resolve_api_base_falls_back_to_canonical_on_empty_ctx_value` —
empty, whitespace, and None all fall back to canonical.
- `resolve_api_base_cfg_test_override_takes_precedence_over_ctx` —
preserves the existing 7-test wiremock seam.
Two new end-to-end wiremock tests (production-path coverage,
NO `with_api_base_override`):
- `chat_gemini_honors_provider_key_api_base_in_production_path` —
ProviderKey.api_base = wiremock URI, asserts wiremock observes
the request at the expected path. `expect(1)` would have failed
pre-fix because traffic went to the canonical Google URL instead.
- `chat_gemini_trims_trailing_slash_on_provider_key_api_base` —
operator paste with trailing slash still produces single-slash
URL after stitching.
## Unblocks
AISIX-Cloud platform-adapter live-spec work — once a new
`:dev` image with this fix is published, the per-adapter
`adapter-vertex-live.spec.ts` (issue #390 mentions it as
'blocked') can be authored to assert the customer-visible
contract end-to-end via the `mock-vertex` compose service.
Refs: #390, AISIX-Cloud#492 (real-cloud
Bedrock/Vertex/Azure smoke tests tracking).
@coderabbitai

coderabbitaiBot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 3 reviews of capacity. Refill in 8 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: a1c2510a-2576-4586-9a7d-fe1443ada0b4

📥 Commits

Reviewing files that changed from the base of the PR and between 09f4f49 and 46f6997.

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

Walkthrough

VertexBridge now resolves Vertex API base using test seam, ProviderKey.api_base (trimmed/stripped), or canonical per-region host; both Gemini paths wire the override. AzureOpenAI gains a verbatim upstream_override mode with validation and URL-building changes. Tests and docs updated accordingly.

Changes

Vertex API Base Override Support

Layer / File(s)Summary
API base resolution with three-level precedence
crates/aisix-provider-vertex/src/bridge.rs
resolve_api_base accepts ctx_api_base and resolves test override → ProviderKey.api_base (trim + strip trailing slash) → canonical https://<region>-aiplatform.googleapis.com.
Wire both Gemini request paths to use override
crates/aisix-provider-vertex/src/bridge.rs
chat_gemini and chat_gemini_stream pass ctx.provider_key.api_base.as_deref() into resolve_api_base so operator-supplied api_base is honored.
Unit tests for resolve_api_base precedence
crates/aisix-provider-vertex/src/bridge.rs
Unit tests cover production-path precedence, trimming trailing slashes, empty/whitespace fallback, and test-seam precedence.
End-to-end tests and test helpers
crates/aisix-provider-vertex/src/bridge.rs
Adds sample_pk_with_secret_and_api_base helper and Tokio E2E tests asserting Gemini generateContent calls target ProviderKey.api_base host and trim trailing slash.
Documentation for Vertex api_base behavior
docs/configuration/provider-keys.md
Adds google-vertex canonical api_base entry and explains Vertex URL construction, token minting via secret.token_uri, and air-gapped guidance.

Azure OpenAI upstream_override Support

Layer / File(s)Summary
Add upstream_override field
crates/aisix-provider-azure-openai/src/bridge.rs
Adds pub upstream_override: Option<String> to AzureUpstreamRef with documentation of when it's set.
Resolve logic and validation for overrides
crates/aisix-provider-azure-openai/src/bridge.rs
AzureUpstreamRef::resolve parses canonical *.openai.azure.com into resource or pins non-canonical HTTP(S) inputs as upstream_override; rejects values containing ? or #.
URL construction using upstream_override
crates/aisix-provider-azure-openai/src/bridge.rs
chat_completions_url uses upstream_override root when present, otherwise https://<resource>.openai.azure.com, preserving deployment path and api-version.
Azure override unit tests and test updates
crates/aisix-provider-azure-openai/src/bridge.rs
Test fixtures updated to include upstream_override: None; tests added for HTTPS/HTTP overrides, trailing-slash stripping, and rejection of query/fragment injection.

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

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

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

…#391)
Pre-fix, `AzureUpstreamRef::resolve()` rejected any `api_base` whose
host did not end in `.openai.azure.com` with a `Config` error,
blocking BYO deployments through corporate proxies, private-VPC
endpoints, or mock services. The only override seam was
`#[cfg(test)] pub(crate) with_url_override`, unreachable from
production builds.
This is the parity gap that surfaced from AISIX-Cloud#491 live e2e
work — mirroring Bedrock, the operator-supplied `ProviderKey.api_base`
should be honored as a production override. (Vertex got the same
treatment in the previous commit, #390.)
## What changed
`AzureUpstreamRef` gains an `upstream_override: Option<String>` field.
`resolve()` keeps the canonical Azure path unchanged (a `.openai.azure.com`-
suffixed `api_base` still extracts `resource` and uses the canonical
URL builder), AND adds a verbatim-override branch for any other
HTTP/HTTPS host: `resource` is empty, `upstream_override` holds the
base, and `chat_completions_url()` builds the URL off the override
instead of `<resource>.openai.azure.com`.
Defence-in-depth on the override path: reject any `api_base` that
contains `?` or `#`. The bridge's canonical query-string append
(`?api-version=…`) would otherwise either merge with or be overridden
by an operator-supplied query, opening an api-version downgrade
vector. The bare-resource shorthand and the canonical `.openai.azure.com`
path keep their existing strict validation.
## Tests
- `resolve_accepts_verbatim_override_for_non_canonical_host` —
pins the new contract (was `resolve_rejects_canonical_https_with_wrong_suffix`,
renamed + flipped to match the new behavior, with a docstring
citing #391).
- `resolve_accepts_http_override_for_mock_endpoint` — canonical
e2e shape (`http://mock-llm:8000` redirected to mock-llm's Azure
deployment endpoint).
- `resolve_strips_trailing_slash_on_verbatim_override` — ensures
no `//` between the override base and the appended deployment path.
- `resolve_rejects_override_with_query_injection` — explicit
defence on the api-version downgrade vector.
- `resolve_rejects_override_with_fragment` — defence on URL
fragment injection.
All pre-existing tests pass unchanged.
## References
- Bedrock parity: `crates/aisix-provider-bedrock/src/bridge.rs`
reads `ctx.provider_key.api_base.as_deref()` and forwards it as
the AWS SDK's `endpoint_url`.
- Companion fix: #390 (Vertex `resolve_api_base`).
- Unblocks AISIX-Cloud follow-up: per-adapter `adapter-azure-live.spec.ts`
exercising `dashboard → cp-api → DP → mock-llm Azure-shape` end-to-end.
@moonmingmoonming changed the title fix(vertex): honor ProviderKey.api_base in production builds (#390)fix(platform-adapters): honor ProviderKey.api_base in Vertex + Azure bridges (#390, #391)May 25, 2026
… path preservation (#391)
Independent audit findings from PR #392:
H1 (Typo guard for near-miss canonical hosts)
↳ Documented explicitly in the `upstream_override` field doc-comment.
The audit's suggested heuristic (`rest.contains("openai.azure.com") ||
rest.contains("openai.azure.")`) doesn't actually catch the cited
typo example (`openai.azur.com` contains neither substring) and
would break legitimate Azure cloud variants (`.openai.azure.cn`
Azure China, `.openai.azure.us` Azure Government) plus any
corporate proxy whose URL happens to contain `openai`. Same risk
model as OpenAI / Bedrock bridges that already accept arbitrary
operator-supplied `api_base`. Documented the typo caveat in the
field doc so operators are warned at API surface time.
M1 (Path-preservation contract for verbatim override)
↳ Added `resolve_preserves_path_prefix_on_verbatim_override` test.
The bridge preserves an internal path prefix (e.g. corporate
gateway routing `/azure-passthrough/*` to upstream Azure) and
appends the deployment path verbatim. Test pins the shape.
M2 (Public struct gained field — non_exhaustive)
↳ Added `#[non_exhaustive]` to `AzureUpstreamRef`. Future field
additions no longer require a major version bump for downstream
crates. In-crate test struct-literal at line 924 still works
(`#[non_exhaustive]` only restricts external callers). Added
docstring guidance pointing external callers at `Self::resolve`.
Symmetry with Vertex sibling fix
↳ Added `@` userinfo rejection on the verbatim-override branch.
An operator embedding `user:pass@host` would leak credentials
via logs and bypass the api-key / AAD auth path. Mirrors the
vertex-bridge defence at `resolve_api_base`. Test
`resolve_rejects_override_with_userinfo` pins the rejection.
All pre-existing tests pass; new tests:
- `resolve_preserves_path_prefix_on_verbatim_override`
- `resolve_rejects_override_with_userinfo`
LOW findings (Vertex docstring "mirrors Bedrock" wording, query-injection
asymmetry across canonical/override branches, `resource` field doc) are
informational — not addressed in this commit; can land in a follow-up
docs PR if they surface as real-world confusion.
…on (#390)
Audit findings on PR #392 surfaced 1 HIGH + 1 MEDIUM that needed
in-PR fixes:
**HIGH-1 (streaming-path E2E coverage gap):** the original PR
updated both `chat_gemini` and `chat_gemini_stream` call sites
to thread `ctx.provider_key.api_base`, but the new wiremock test
only exercised the non-streaming path. A regression that reverted
just `chat_gemini_stream` would have compiled clean and passed
all tests. Add
`chat_gemini_stream_honors_provider_key_api_base_in_production_path`
— mirrors the non-streaming version but drains the SSE stream via
`bridge.chat_stream`.
**MEDIUM-1 (api_base SSRF / credential-embed surface):** before
this PR the production code path hardcoded the canonical Google URL
— there was NO operator-supplied URL surface. The fix opened that
surface; the original commit forgot to validate it. `api_base` is
interpolated directly into `format!("{base}/v1/projects/...")` so
malicious / mistaken input could:
- use a non-http(s) scheme (`file:///etc/passwd`, `gs://...`)
- embed userinfo (`https://user:pass@host`) → credential leak
via access-log URLs + auth-header shadow concerns
- inject a query string that silently merges with the bridge's
own `?alt=sse` on the streaming path
- inject a fragment
Add validation that rejects all four classes as `BridgeError::Config`
(matches Azure-OpenAI sibling fix#391's pattern of surfacing
operator-actionable Config errors rather than silently falling
through to canonical). `resolve_api_base` signature now returns
`Result<String, BridgeError>`; both call sites propagate with `?`.
Five new unit tests cover the four rejection classes + a happy-path
sanity check.
## Test run
- `cargo test -p aisix-provider-vertex` — 72/72 green (was 67/67
pre-audit, added 5 tests).
- `cargo clippy -p aisix-provider-vertex --all-targets -- -D warnings`
— clean (fixed one doc-lazy-continuation in the new docstring).
- `cargo fmt -p aisix-provider-vertex` — clean.
Refs: #390, PR #392 audit.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit findings addressed in fe13041.

HIGH-1 (streaming-path E2E gap): added chat_gemini_stream_honors_provider_key_api_base_in_production_path — mirrors the non-streaming wiremock test but drains the SSE stream via bridge.chat_stream. A regression that reverted just one of the two call sites would now fail.

MEDIUM-1 (api_base SSRF / credential-embed surface): before #390 the production code path hardcoded the canonical Google URL — there was no operator-supplied URL surface. The fix opened that surface; the original commit forgot to validate. resolve_api_base now returns Result<String, BridgeError> and rejects 4 classes of input as BridgeError::Config:

  • non-http(s) scheme (file:///etc/passwd, gs://..., bare host)
  • userinfo @ (would leak credentials via access-log URLs + auth-header shadow)
  • query string ? (would silently merge with bridge's ?alt=sse on streaming)
  • fragment #

Mirrors the Azure-OpenAI sibling fix (#391) which rejects the same classes. Surfacing as Config errors (vs silently falling through to canonical) matches the principle of #390 itself: don't silently ignore operator config.

Five new unit tests cover the four rejection classes:

  • resolve_api_base_rejects_non_http_scheme (5 cases incl. bare host)
  • resolve_api_base_rejects_embedded_userinfo
  • resolve_api_base_rejects_query_string
  • resolve_api_base_rejects_fragment

LOW-1 and LOW-2 from the audit are non-blocking; no action.

Test run: 72/72 green (added 5 tests, was 67/67 pre-audit). cargo clippy --all-targets -- -D warnings clean.

… LOW-1)
Re-audit LOW-1 noted that the userinfo-rejection error string echoes
the operator-supplied URL back verbatim via `{b:?}` — so a rejected
`api_base="https://user:secret@host"` produces a Config error
containing "user:secret@host". That string propagates to operator-
facing logs; rejecting `@` exists specifically to keep operator
credentials out of upstream traffic, so the error shouldn't leak
them via the audit/telemetry path either.
Add `redact_userinfo` helper that replaces `scheme://user:pass@host`
with `scheme://<redacted>@host`. Only touches the substring between
`://` and the first `@`; leaves `@` past the first path slash
intact (RFC 3986 allows `@` in path segments).
Tests:
- `redact_userinfo_strips_user_and_password` — happy path
- `redact_userinfo_leaves_non_userinfo_at_alone` — RFC 3986 path-`@`
is preserved (`https://proxy.internal/v1@my-namespace` unchanged)
- The existing userinfo-rejection test now asserts the rejected URL
contains `<redacted>` and does NOT contain the original
`user:secret` substring.
74/74 Vertex tests green (+2 redactor tests on top of the +5
SSRF-validation tests from the prior audit-fix commit).
`cargo clippy --all-targets -- -D warnings` clean.
Refs: #390, PR #392 re-audit LOW-1.
@moonming

Copy link
Copy Markdown
MemberAuthor

Re-audit LOW-1 addressed in 46f6997.

The userinfo-rejection branch's error message echoed the operator-supplied URL verbatim via {b:?} — so a rejected https://user:secret@host produced a Config error containing user:secret@host. Since the entire point of rejecting @ is to keep operator credentials out of upstream traffic, the error shouldn't leak them via the audit/telemetry path either.

Added a small redact_userinfo helper that replaces scheme://user:pass@host with scheme://<redacted>@host. Preserves RFC 3986 path-@ (e.g. https://proxy/v1@my-namespace stays unchanged because the @ is past the first path slash).

74/74 Vertex tests green. The re-audit's LOW-2 note (b.contains('@') over-rejecting RFC 3986 path-@) is operationally moot for Vertex's bare-host-root convention; not changed.

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

Labels

None yet

Projects

None yet

1 participant

@moonming