Skip to content

Add Microsoft Entra ID authentication for Azure OpenAI#3250

Open
hellovai wants to merge 11 commits intocanaryfrom
entra-id-auth
Open

Add Microsoft Entra ID authentication for Azure OpenAI#3250
hellovai wants to merge 11 commits intocanaryfrom
entra-id-auth

Conversation

@hellovai
Copy link
Contributor

@hellovai hellovai commented Mar 16, 2026

Summary

  • Add Entra ID (OAuth2 bearer token) authentication to the azure-openai provider, enabling enterprise customers to authenticate via service principal credentials, managed identity, or Azure CLI
  • Detect auth strategy at parse time based on tenant_id/client_id property presence — errors if both API key and Entra ID fields are specified
  • Native token acquisition via azure_identity crate with global credential caching; WASM support via JS callback bridge for playground
  • Regenerate all language baml clients and add Python + TypeScript integration tests

Test plan

  • Existing Azure API-key integration tests pass unchanged
  • Entra ID integration test passes with AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET set for a real Azure service principal
  • cargo build -p baml-runtime and cargo test -p baml-runtime --lib pass (147+ tests)
  • Playground loads without errors (stub Azure callback wired up)
  • Verify Authorization: Bearer <token> replaces api-key header for Entra ID clients

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Azure Entra ID auth added for Azure OpenAI: service-principal and system-default credential flows (alternative to API key) across runtimes and clients.
    • VS Code and WASM tooling can request Azure credentials for browser-based flows.
  • Documentation

    • Provider docs updated with Entra ID examples and environment mappings.
  • Tests

    • New integration and unit tests exercising Entra ID scenarios across languages.
  • Chores

    • Ignore rule added for cloud-synced Riptide artifacts.

Add UnresolvedAzureAuthStrategy and ResolvedAzureAuthStrategy enums
to support Entra ID authentication for azure-openai provider. Wire
auth strategy detection into create_azure() based on presence of
tenant_id/client_id properties vs api_key. No runtime behavior change
yet - API key auth continues to work, Entra ID configs are parsed but
will be wired to token acquisition in the next phase.
Add azure_identity/azure_core 0.27.0 as native-only dependencies.
Create std_auth.rs with AzureAuth struct that caches credential
objects globally and acquires bearer tokens at request time.
Wire into build_request() behind cfg(not(wasm32)) so Entra ID
clients send Authorization: Bearer instead of api-key header.
Extend JsCallbackProvider with AzureCredResult and azure channels.
Add AzureCredProvider to js_callback_bridge.rs with loadAzureCreds
callback. Create wasm_auth.rs as WASM counterpart to std_auth.rs.
Unify build_request() to use azure_auth module alias (std_auth on
native, wasm_auth on WASM). Wire loadAzureCreds into playground TS
with a stub implementation for now.
Add AzureEntraId and AzureEntraIdSystemDefault client configs to
integ-tests with corresponding BAML functions and TypeScript tests.
Tests skip gracefully when Azure env vars are not set. Update Azure
provider docs with Entra ID configuration example and param fields
for tenant_id, client_id, and client_secret.
…creds

Regenerate all language baml_client outputs to include new Entra ID
test functions. Add Python integration test for Azure OpenAI Entra ID
auth. Wire up Azure credential loading in vscode extension and update
pnpm-lock.
@vercel
Copy link

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Mar 17, 2026 1:03am
promptfiddle Error Error Mar 17, 2026 1:03am

Request Review

@github-actions
Copy link

@github-actions
Copy link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Azure OpenAI authentication via Entra ID (service principal and system-default) and API-key paths, implements credential resolution and token acquisition (cached native, JS bridge for WASM), propagates azure_auth through OpenAI client config, and generates cross-language test clients and bindings.

Changes

Cohort / File(s) Summary
Gitignore
engine/.gitignore
Adds .humanlayer/tasks/ Riptide artifacts ignore rule.
OpenAI client schema
engine/baml-lib/llm-client/src/clients/openai.rs
Adds UnresolvedAzureAuthStrategy/ResolvedAzureAuthStrategy, azure_auth fields on UnresolvedOpenAI/ResolvedOpenAI; propagates azure_auth through builders and resolution; env collection and header population updated.
Runtime deps & module aliasing
engine/baml-runtime/Cargo.toml, engine/baml-runtime/src/internal/llm_client/primitive/openai/mod.rs
Adds azure_identity/azure_core deps (v0.27.0) and conditional std_auth / wasm_auth modules with unified azure_auth alias.
Native Azure auth & integration
engine/baml-runtime/src/internal/llm_client/primitive/openai/std_auth.rs, engine/baml-runtime/src/internal/llm_client/primitive/openai/openai_client.rs
Implements AzureAuth with global cache, builds ClientSecret/ManagedIdentity/AzureCli credentials per strategy, exposes token(), and injects bearer tokens into OpenAI requests when EntraId/SystemDefault selected.
WASM Azure auth bridge
engine/baml-runtime/src/internal/llm_client/primitive/openai/wasm_auth.rs
WASM-facing AzureAuth that delegates token acquisition to JS callback bridge; no native caching.
JS callback provider & bridge
engine/baml-runtime/src/types/js_callback_provider.rs, engine/baml-runtime/src/types/mod.rs, engine/baml-schema-wasm/src/js_callback_bridge.rs
Adds AzureCredResult, azure request/response channels, JsCallbackProvider::azure_req, loop/bridge for loadAzureCreds callback, and re-exports.
VSCode/webview integration
typescript/apps/vscode-ext/..., typescript/packages/playground-common/...
Adds @azure/identity usage, DefaultAzureCredential in webview host, RPC types LoadAzureCredsRequest/Response, VSCode API wrapper loadAzureCreds, and WASM init bridge wiring.
Docs / provider schema
fern/03-reference/baml/clients/providers/azure.mdx
Documents Entra ID params (tenant_id/client_id/client_secret), notes about API-key exclusion when using Entra ID.
BAML clients & tests
integ-tests/baml_src/clients.baml, integ-tests/baml_src/test-files/providers/azure.baml
Adds AzureEntraId and AzureEntraIdSystemDefault client definitions and corresponding test functions.
Generated language bindings
multiple files under integ-tests/ (Go, Python v1, Python, TypeScript/React/ESM, Ruby, Rust)
Generates TestAzureEntraId and TestAzureEntraIdSystemDefault across sync/async/streaming/HTTP request/parse surfaces for many target languages and client layers.
OpenAPI & integration examples/tests
integ-tests/openapi/baml_client/openapi.yaml, integ-tests/typescript/tests/providers/azure.test.ts, integ-tests/python/azure_openai_entra_example.py
Adds OpenAPI endpoints for new functions, test suite guarded by env vars, and Python example demonstrating Entra ID usage.
Inlined BAML / source maps updates
various inlinedbaml.*, baml_source_map.* files across integ-tests
Updates embedded clients.baml content and source maps; several files simplified or replaced content.
Minor
integ-tests/python/README.md
Whitespace fix in README.

Sequence Diagram

sequenceDiagram
    participant Client as Client Code
    participant Runtime as BAML Runtime / OpenAI client
    participant AzureAuth as AzureAuth (std_auth / wasm_auth)
    participant CredCache as Credential Cache
    participant JSBridge as JS Callback Provider / Azure SDK
    participant OpenAI as OpenAI API

    Client->>Runtime: Invoke function with azure_auth config
    Runtime->>AzureAuth: get_or_create(auth_strategy)
    alt std target & cache hit
        AzureAuth->>CredCache: lookup(key)
        CredCache-->>AzureAuth: credential
    else std target & cache miss
        AzureAuth->>Azure SDK: build credential (ClientSecret / ManagedIdentity / AzureCli)
        Azure SDK-->>AzureAuth: TokenCredential
        AzureAuth->>CredCache: store(key, credential)
    else wasm target
        AzureAuth->>JSBridge: request token via loadAzureCreds
        JSBridge-->>AzureAuth: access_token
    end
    AzureAuth->>Runtime: token() -> bearer token
    Runtime->>OpenAI: HTTP request with Authorization: Bearer {token}
    OpenAI-->>Runtime: Response
    Runtime-->>Client: Result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch entra-id-auth
📝 Coding Plan
  • Generate coding plan for human review comments

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 26


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bf01657a-c2f0-4800-bc79-a1171f5d3423

📥 Commits

Reviewing files that changed from the base of the PR and between 6b13cfd and f4009b6.

⛔ Files ignored due to path filters (2)
  • engine/Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (64)
  • engine/.gitignore
  • engine/baml-lib/llm-client/src/clients/openai.rs
  • engine/baml-runtime/Cargo.toml
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/mod.rs
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/openai_client.rs
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/std_auth.rs
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/wasm_auth.rs
  • engine/baml-runtime/src/types/js_callback_provider.rs
  • engine/baml-runtime/src/types/mod.rs
  • engine/baml-schema-wasm/src/js_callback_bridge.rs
  • fern/03-reference/baml/clients/providers/azure.mdx
  • integ-tests/baml_src/clients.baml
  • integ-tests/baml_src/test-files/providers/azure.baml
  • integ-tests/go/baml_client/baml_source_map.go
  • integ-tests/go/baml_client/functions.go
  • integ-tests/go/baml_client/functions_build_request.go
  • integ-tests/go/baml_client/functions_build_request_stream.go
  • integ-tests/go/baml_client/functions_parse.go
  • integ-tests/go/baml_client/functions_parse_stream.go
  • integ-tests/go/baml_client/functions_stream.go
  • integ-tests/openapi/baml_client/openapi.yaml
  • integ-tests/python-v1/baml_client/async_client.py
  • integ-tests/python-v1/baml_client/inlinedbaml.py
  • integ-tests/python-v1/baml_client/parser.py
  • integ-tests/python-v1/baml_client/sync_client.py
  • integ-tests/python/README.md
  • integ-tests/python/baml_client/async_client.py
  • integ-tests/python/baml_client/inlinedbaml.py
  • integ-tests/python/baml_client/parser.py
  • integ-tests/python/baml_client/sync_client.py
  • integ-tests/python/test_azure_openai_entra.py
  • integ-tests/react/baml_client/async_client.ts
  • integ-tests/react/baml_client/async_request.ts
  • integ-tests/react/baml_client/inlinedbaml.ts
  • integ-tests/react/baml_client/parser.ts
  • integ-tests/react/baml_client/react/hooks.tsx
  • integ-tests/react/baml_client/react/server.ts
  • integ-tests/react/baml_client/react/server_streaming.ts
  • integ-tests/react/baml_client/react/server_streaming_types.ts
  • integ-tests/react/baml_client/sync_client.ts
  • integ-tests/react/baml_client/sync_request.ts
  • integ-tests/ruby/baml_client/client.rb
  • integ-tests/rust/src/baml_client/baml_source_map.rs
  • integ-tests/rust/src/baml_client/functions/async_client.rs
  • integ-tests/rust/src/baml_client/functions/sync_client.rs
  • integ-tests/typescript-esm/baml_client/async_client.ts
  • integ-tests/typescript-esm/baml_client/async_request.ts
  • integ-tests/typescript-esm/baml_client/inlinedbaml.ts
  • integ-tests/typescript-esm/baml_client/parser.ts
  • integ-tests/typescript-esm/baml_client/sync_client.ts
  • integ-tests/typescript-esm/baml_client/sync_request.ts
  • integ-tests/typescript/baml_client/async_client.ts
  • integ-tests/typescript/baml_client/async_request.ts
  • integ-tests/typescript/baml_client/inlinedbaml.ts
  • integ-tests/typescript/baml_client/parser.ts
  • integ-tests/typescript/baml_client/sync_client.ts
  • integ-tests/typescript/baml_client/sync_request.ts
  • integ-tests/typescript/tests/providers/azure.test.ts
  • typescript/apps/vscode-ext/package.json
  • typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts
  • typescript/apps/vscode-ext/src/webview-to-vscode-rpc.ts
  • typescript/packages/playground-common/src/sdk/runtime/BamlRuntime.ts
  • typescript/packages/playground-common/src/shared/baml-project-panel/vscode.ts
  • typescript/packages/playground-common/src/shared/baml-project-panel/webview-to-vscode-rpc.ts
👮 Files not reviewed due to content moderation or server errors (5)
  • integ-tests/typescript-esm/baml_client/inlinedbaml.ts
  • integ-tests/typescript/baml_client/inlinedbaml.ts
  • integ-tests/python-v1/baml_client/inlinedbaml.py
  • integ-tests/rust/src/baml_client/baml_source_map.rs
  • integ-tests/go/baml_client/baml_source_map.go

Comment on lines +399 to +434
let (azure_auth, api_key_for_header) = match (&api_key, &tenant_id, &client_id) {
// Both api_key and Entra ID fields provided — error
(Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
properties.push_option_error(
"Cannot specify both api_key and tenant_id/client_id. Use either API key auth or Entra ID auth, not both.",
);
(None, None)
}
// Entra ID: tenant_id or client_id present (without api_key)
(None, Some(_), _) | (None, _, Some(_)) => (
Some(UnresolvedAzureAuthStrategy::EntraId {
tenant_id: tenant_id
.unwrap_or_else(|| StringOr::EnvVar("AZURE_TENANT_ID".into())),
client_id: client_id
.unwrap_or_else(|| StringOr::EnvVar("AZURE_CLIENT_ID".into())),
client_secret,
}),
None,
),
// Explicit api_key provided, no Entra ID fields
(Some(_), None, None) => {
let key = api_key.clone().unwrap();
(
Some(UnresolvedAzureAuthStrategy::ApiKey(key.clone())),
Some(key),
)
}
// Neither — default to API key auth (current behavior)
(None, None, None) => {
let key = StringOr::EnvVar("AZURE_OPENAI_API_KEY".to_string());
(
Some(UnresolvedAzureAuthStrategy::ApiKey(key.clone())),
Some(key),
)
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding unit tests for auth strategy detection logic.

The match logic determining authentication strategy has multiple branches and edge cases. Unit tests would help verify:

  • Error raised when both api_key and tenant_id/client_id are provided
  • EntraId strategy selected when only Entra ID fields present
  • ApiKey strategy selected when only api_key present
  • Default to ApiKey with AZURE_OPENAI_API_KEY when nothing specified
  • Env var fallback for partial Entra ID configuration

As per coding guidelines: "Prefer writing Rust unit tests over integration tests where possible".

Comment on lines +407 to +417
// Entra ID: tenant_id or client_id present (without api_key)
(None, Some(_), _) | (None, _, Some(_)) => (
Some(UnresolvedAzureAuthStrategy::EntraId {
tenant_id: tenant_id
.unwrap_or_else(|| StringOr::EnvVar("AZURE_TENANT_ID".into())),
client_id: client_id
.unwrap_or_else(|| StringOr::EnvVar("AZURE_CLIENT_ID".into())),
client_secret,
}),
None,
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Partial Entra ID configuration silently falls back to environment variables.

When only tenant_id or only client_id is provided, the code silently defaults the missing field to an environment variable. For example, providing only tenant_id will cause client_id to default to AZURE_CLIENT_ID.

This could lead to confusing runtime errors if users don't realize partial configuration triggers env var lookup. Consider either:

  1. Requiring both fields when one is provided (explicit validation), or
  2. Documenting this fallback behavior clearly in user-facing docs.

Comment on lines +453 to +475
// Entra ID bearer token acquisition — uses azure_identity on native, JS callback bridge on WASM.
{
use internal_llm_client::openai::ResolvedAzureAuthStrategy;
match &self.properties.azure_auth {
Some(
ResolvedAzureAuthStrategy::EntraId { .. }
| ResolvedAzureAuthStrategy::SystemDefault,
) => {
let azure_auth = super::azure_auth::AzureAuth::get_or_create(
self.properties.azure_auth.as_ref().unwrap(),
)
.await
.map_err(|e| {
anyhow::anyhow!("Azure Entra ID authentication failed: {e}")
})?;
let token = azure_auth.token().await.map_err(|e| {
anyhow::anyhow!("Azure Entra ID token acquisition failed: {e}")
})?;
req = req.bearer_auth(&token);
}
_ => {}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add a unit test for this bearer-auth branch.

This is the feature’s critical path, but the local test updates in this file only add azure_auth: None; nothing here asserts that Entra ID/SystemDefault produce an Authorization header or that the Azure API-key path is skipped. A focused request-builder unit test would catch regressions without live Azure credentials.

As per coding guidelines, "Prefer writing Rust unit tests over integration tests where possible".

Comment on lines +7305 to +7321
const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) }
const __signal__ = __options__.signal;

if (__signal__?.aborted) {
throw new BamlAbortError('Operation was aborted', __signal__.reason);
}

// Check if onTick is provided and reject for sync operations
if (__options__.onTick) {
throw new Error("onTick is not supported for synchronous functions. Please use the async client instead.");
}

const __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : [__options__.collector]) : [];
const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env };
const __env__: Record<string, string> = Object.fromEntries(
Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix env propagation in these generated Entra ID sync wrappers.

Lines 7318-7321 and 7368-7371 rebuild __rawEnv__ from __baml_options__?.env only, so b.withOptions({ env: ... }) never reaches these calls. For the new Entra ID paths, that can silently drop Azure credential vars and fall back to bare process.env.

Suggested fix
-      const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env };
+      const __rawEnv__ = {
+        ...process.env,
+        ...(this.bamlOptions.env ?? {}),
+        ...(__baml_options__?.env ?? {}),
+      };

Apply the same generator fix to both methods.

Also applies to: 7355-7371

Run cargo fmt and remove dead else-if branch in create_azure() that
could never execute (api_key with Entra ID fields errors earlier,
api_key alone sets api_key_for_header).
- Fix token scope from ai.azure.com to cognitiveservices.azure.com
  in vscode extension and Python test script
- Remove unused azure_identity imports (AzureCliCredentialOptions,
  ClientSecretCredentialOptions)
- Fix AzureCliCredential double-creation in SystemDefault chain
- Replace JWT-like test fixture with plain string to avoid Gitleaks
- Fix docs contradiction about api_key being "ignored" vs compile error
- Remove unreachable else-if branch in create_azure()
- Run cargo fmt
@github-actions
Copy link

@github-actions
Copy link

- Fix token scope from ai.azure.com to cognitiveservices.azure.com
  in vscode extension and Python test script
- Remove unused azure_identity imports (AzureCliCredentialOptions,
  ClientSecretCredentialOptions)
- Fix AzureCliCredential double-creation in SystemDefault chain
- Replace JWT-like test fixture with plain string to avoid Gitleaks
- Fix docs contradiction about api_key being "ignored" vs compile error
- Remove unreachable else-if branch in create_azure()
- Convert match single_pattern to if-let per clippy
- Run cargo fmt
@github-actions
Copy link

@github-actions
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts (1)

729-730: 🧹 Nitpick | 🔵 Trivial

Cache DefaultAzureCredential instead of constructing per request.

Line 729 creates a new credential chain on every LOAD_AZURE_CREDS call. Caching a class-level credential avoids repeated chain setup and matches the existing cached GCP pattern.

#!/bin/bash
set -euo pipefail

# Verify whether DefaultAzureCredential is created per request and whether a cached member exists.
rg -n --type=ts -C2 'new DefaultAzureCredential\(' typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts
rg -n --type=ts -C2 '_azureCredential' typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts
integ-tests/python/test_azure_openai_entra.py (1)

1-88: ⚠️ Potential issue | 🔴 Critical

This is not a pytest integration test and won’t run in the intended integ-test flow.

The file is currently a standalone script (main() + if __name__ == "__main__"), so it contributes no pytest-discovered tests in integ-tests/python. It should be rewritten as pytest test cases (and wired to the uv+maturin integration test path) so CI executes it consistently.

As per coding guidelines, integ-tests/python/**/*.py: "For Python integration tests in integ-tests/python, use uv + maturin to build and pytest to run tests".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 882e5cf5-6f04-4ccd-9a20-1e0f1b0b426a

📥 Commits

Reviewing files that changed from the base of the PR and between f4009b6 and e16796c.

📒 Files selected for processing (9)
  • engine/baml-lib/llm-client/src/clients/openai.rs
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/mod.rs
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/openai_client.rs
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/std_auth.rs
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/wasm_auth.rs
  • engine/baml-runtime/src/types/js_callback_provider.rs
  • fern/03-reference/baml/clients/providers/azure.mdx
  • integ-tests/python/test_azure_openai_entra.py
  • typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts

Comment on lines +81 to +82
If `client_secret` is omitted, BAML uses the system default credential chain (managed identity first,
then Azure CLI / azd CLI).
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the credential behavior in EntraId mode without client_secret
rg -n -A 10 "No client_secret provided" engine/baml-runtime/

Repository: BoundaryML/baml

Length of output: 1552


🏁 Script executed:

#!/bin/bash
# Check SystemDefault mode credential chain behavior
rg -n -B 5 -A 15 "ResolvedAzureAuthStrategy::SystemDefault" engine/baml-runtime/src/internal/llm_client/primitive/openai/std_auth.rs

Repository: BoundaryML/baml

Length of output: 2295


🏁 Script executed:

#!/bin/bash
# Check the actual documentation context around lines 81-82 and 103-104
sed -n '75,110p' fern/03-reference/baml/clients/providers/azure.mdx

Repository: BoundaryML/baml

Length of output: 1279


Documentation does not match code behavior when client_secret is omitted.

The documentation claims that without client_secret, BAML uses "the system default credential chain (managed identity first, then Azure CLI / azd CLI)." However, examining the implementation in engine/baml-runtime/src/internal/llm_client/primitive/openai/std_auth.rs lines 78-86, when client_secret is omitted in Entra ID mode (with tenant_id/client_id set), the code uses only ManagedIdentityCredential, not the full chain with Azure CLI fallback.

The full credential chain (managed identity → Azure CLI) is implemented only in SystemDefault mode (lines 88-103), which requires all three fields (tenant_id, client_id, client_secret) to be omitted.

Update lines 81-82 and 103-104 to clarify the two distinct authentication paths: service principal when all three fields are set, managed identity only when client_secret is omitted with Entra ID, and the full credential chain only when using system default authentication (all fields omitted).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
engine/baml-runtime/src/internal/llm_client/primitive/openai/openai_client.rs (1)

449-472: 🧹 Nitpick | 🔵 Trivial

Consider guarding against double bearer auth (defensive coding).

If both api_key (line 450) and Entra ID auth (line 470) are configured, both bearer_auth calls execute and the second overwrites the first. While the PR summary states the parser prevents this combination, adding an explicit guard or else branch would make the mutual exclusivity clearer at runtime and prevent subtle bugs if parser validation is bypassed (e.g., dynamic clients).

💡 Suggested defensive refactor
         if let Some(key) = &self.properties.api_key {
             req = req.bearer_auth(key.render(expose_secrets));
-        }
-
-        // Entra ID bearer token acquisition — uses azure_identity on native, JS callback bridge on WASM.
-        {
+        } else if let Some(
+            ResolvedAzureAuthStrategy::EntraId { .. }
+            | ResolvedAzureAuthStrategy::SystemDefault,
+        ) = &self.properties.azure_auth
+        {
+            // Entra ID bearer token acquisition — uses azure_identity on native, JS callback bridge on WASM.
             use internal_llm_client::openai::ResolvedAzureAuthStrategy;
-            if let Some(
-                ResolvedAzureAuthStrategy::EntraId { .. }
-                | ResolvedAzureAuthStrategy::SystemDefault,
-            ) = &self.properties.azure_auth
-            {
-                let azure_auth = super::azure_auth::AzureAuth::get_or_create(
-                    self.properties.azure_auth.as_ref().unwrap(),
-                )
-                .await
-                .map_err(|e| anyhow::anyhow!("Azure Entra ID authentication failed: {e}"))?;
-                let token = azure_auth
-                    .token()
-                    .await
-                    .map_err(|e| anyhow::anyhow!("Azure Entra ID token acquisition failed: {e}"))?;
-                req = req.bearer_auth(&token);
-            }
+            let azure_auth = super::azure_auth::AzureAuth::get_or_create(
+                self.properties.azure_auth.as_ref().unwrap(),
+            )
+            .await
+            .map_err(|e| anyhow::anyhow!("Azure Entra ID authentication failed: {e}"))?;
+            let token = azure_auth
+                .token()
+                .await
+                .map_err(|e| anyhow::anyhow!("Azure Entra ID token acquisition failed: {e}"))?;
+            req = req.bearer_auth(&token);
         }

Add a unit test for the bearer-auth branch.

This is the feature's critical path, but the existing tests only set azure_auth: None. A focused unit test asserting that Entra ID / SystemDefault produces an Authorization: Bearer header would catch regressions without requiring live Azure credentials.

As per coding guidelines: "Prefer writing Rust unit tests over integration tests where possible".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9cb62249-b089-4b4d-b303-8f7db6e7cb10

📥 Commits

Reviewing files that changed from the base of the PR and between e16796c and f07baa9.

📒 Files selected for processing (1)
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/openai_client.rs

- Rename test_azure_openai_entra.py to azure_openai_entra_example.py
  so pytest doesn't discover it (standalone script, not a test)
- Fix docs: client_secret omitted uses ManagedIdentity only, not the
  full credential chain (chain is only for SystemDefault mode)
- Normalize Azure error payload in WebviewPanelHost.ts to match the
  LoadAzureCredsResponse type shape
- Clean up README duplicate line
@github-actions
Copy link

@github-actions
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
integ-tests/python/azure_openai_entra_example.py (1)

9-19: ⚠️ Potential issue | 🟠 Major

This integration-test file bypasses the repo’s pytest + maturin workflow.

This is implemented as a standalone script (main()), not a pytest test entrypoint, so it won’t participate in the expected integration-test execution path. Please either convert it to a pytest test module or move it outside integ-tests/python if it is intentionally just an example.

As per coding guidelines, "For Python integration tests in integ-tests/python, use uv + maturin to build and pytest to run tests".

Also applies to: 34-88

typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts (1)

726-730: 🧹 Nitpick | 🔵 Trivial

Reuse a cached DefaultAzureCredential instance instead of recreating it per call.

Line 729 creates a new credential each request; this repeats credential-chain initialization and can reduce token-cache effectiveness. This same concern was raised earlier and is still applicable.

#!/bin/bash
# Verify whether DefaultAzureCredential is recreated in handlers
# and whether a class-level cached credential exists.
rg -n --type=ts -C3 '\bDefaultAzureCredential\b|_azureCredential|getToken\('

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e3cce3f9-9538-4752-87ea-a004d18db5a6

📥 Commits

Reviewing files that changed from the base of the PR and between f07baa9 and fdbbd0a.

📒 Files selected for processing (4)
  • fern/03-reference/baml/clients/providers/azure.mdx
  • integ-tests/python/README.md
  • integ-tests/python/azure_openai_entra_example.py
  • typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts

…re/identity, cache credential

- Fix all stale filename references in azure_openai_entra_example.py
- Add TestAzureEntraId/SystemDefault to TestAzureClients test suite
- Use env.AZURE_OPENAI_API_VERSION instead of hard-coded api_version
- Use it.skip pattern in azure.test.ts for proper skip reporting
- Upgrade @azure/identity to ^4.2.1 (CVE-2024-35255 fix)
- Cache DefaultAzureCredential as class member in WebviewPanelHost
- Log warning on Azure credential cache lock poisoning
- Clarify partial Entra ID config env var fallback in docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
integ-tests/python/azure_openai_entra_example.py (1)

1-19: ⚠️ Potential issue | 🟠 Major

Convert this script into a pytest-discoverable integration test (or move it out of integ-tests/python).

This file is currently executable-example style (main() + __main__) and won’t run as a standard pytest integration test in the required test workflow.

As per coding guidelines "integ-tests/python/**/*.py: For Python integration tests in integ-tests/python, use uv + maturin to build and pytest to run tests".

Also applies to: 34-88

engine/baml-runtime/src/internal/llm_client/primitive/openai/std_auth.rs (1)

27-37: 🧹 Nitpick | 🔵 Trivial

Add unit tests for cache_key logic.

This is a pure function that maps auth strategies to cache keys and is ideal for unit testing without mocking dependencies. As per coding guidelines, "Prefer writing Rust unit tests over integration tests where possible."

🧪 Example test module
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cache_key_api_key() {
        let strategy = ResolvedAzureAuthStrategy::ApiKey;
        assert_eq!(AzureAuth::cache_key(&strategy), "api_key");
    }

    #[test]
    fn test_cache_key_entra_id() {
        let strategy = ResolvedAzureAuthStrategy::EntraId {
            tenant_id: "tenant-123".to_string(),
            client_id: "client-456".to_string(),
            client_secret: Some("secret".to_string()),
        };
        assert_eq!(AzureAuth::cache_key(&strategy), "tenant-123:client-456");
    }

    #[test]
    fn test_cache_key_system_default() {
        let strategy = ResolvedAzureAuthStrategy::SystemDefault;
        assert_eq!(AzureAuth::cache_key(&strategy), "system_default");
    }
}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f39a7f45-fe9b-4471-b7f7-e96f900bb360

📥 Commits

Reviewing files that changed from the base of the PR and between fdbbd0a and e77ca0b.

📒 Files selected for processing (8)
  • engine/baml-runtime/src/internal/llm_client/primitive/openai/std_auth.rs
  • fern/03-reference/baml/clients/providers/azure.mdx
  • integ-tests/baml_src/clients.baml
  • integ-tests/baml_src/test-files/providers/azure.baml
  • integ-tests/python/azure_openai_entra_example.py
  • integ-tests/typescript/tests/providers/azure.test.ts
  • typescript/apps/vscode-ext/package.json
  • typescript/apps/vscode-ext/src/panels/WebviewPanelHost.ts

Comment on lines +132 to +139
Err(e) => {
errors.push(format!("ManagedIdentityCredential (init): {e}"));
log::debug!(
"Azure SystemDefault: ManagedIdentityCredential init failed, trying AzureCliCredential"
);
AzureCliCredential::new(None)
.context("Failed to create AzureCliCredential")?
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing token probe for AzureCliCredential in init-failure path.

When ManagedIdentityCredential::new(None) fails, the code falls back to AzureCliCredential but does not probe it with get_token before caching. This is inconsistent with the other fallback path (lines 112-128) where the CLI credential is tested.

This could cache a non-working credential if the user hasn't run az login, leading to confusing failures on subsequent requests. Additionally, if AzureCliCredential::new fails, the error message doesn't include the accumulated errors from the previous failures.

🐛 Proposed fix to add token probe and improve error reporting
                     Err(e) => {
                         errors.push(format!("ManagedIdentityCredential (init): {e}"));
                         log::debug!(
                             "Azure SystemDefault: ManagedIdentityCredential init failed, trying AzureCliCredential"
                         );
-                        AzureCliCredential::new(None)
-                            .context("Failed to create AzureCliCredential")?
+                        let cli_cred = match AzureCliCredential::new(None) {
+                            Ok(c) => c,
+                            Err(e) => {
+                                errors.push(format!("AzureCliCredential (init): {e}"));
+                                anyhow::bail!(
+                                    "Azure SystemDefault credential chain exhausted. Tried:\n{}",
+                                    errors.join("\n")
+                                );
+                            }
+                        };
+                        match cli_cred.get_token(&[AZURE_OPENAI_TOKEN_SCOPE], None).await {
+                            Ok(_) => {
+                                log::debug!(
+                                    "Azure SystemDefault: AzureCliCredential succeeded"
+                                );
+                                cli_cred
+                            }
+                            Err(e) => {
+                                errors.push(format!("AzureCliCredential: {e}"));
+                                anyhow::bail!(
+                                    "Azure SystemDefault credential chain exhausted. Tried:\n{}",
+                                    errors.join("\n")
+                                );
+                            }
+                        }
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Err(e) => {
errors.push(format!("ManagedIdentityCredential (init): {e}"));
log::debug!(
"Azure SystemDefault: ManagedIdentityCredential init failed, trying AzureCliCredential"
);
AzureCliCredential::new(None)
.context("Failed to create AzureCliCredential")?
}
Err(e) => {
errors.push(format!("ManagedIdentityCredential (init): {e}"));
log::debug!(
"Azure SystemDefault: ManagedIdentityCredential init failed, trying AzureCliCredential"
);
let cli_cred = match AzureCliCredential::new(None) {
Ok(c) => c,
Err(e) => {
errors.push(format!("AzureCliCredential (init): {e}"));
anyhow::bail!(
"Azure SystemDefault credential chain exhausted. Tried:\n{}",
errors.join("\n")
);
}
};
match cli_cred.get_token(&[AZURE_OPENAI_TOKEN_SCOPE], None).await {
Ok(_) => {
log::debug!(
"Azure SystemDefault: AzureCliCredential succeeded"
);
cli_cred
}
Err(e) => {
errors.push(format!("AzureCliCredential: {e}"));
anyhow::bail!(
"Azure SystemDefault credential chain exhausted. Tried:\n{}",
errors.join("\n")
);
}
}
}

Comment on lines +67 to +69

Not used when `tenant_id` or `client_id` is set (Entra ID auth takes over).
</ParamField>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent logic operator with Note at lines 42-44.

Line 68 uses "or" (Not used when tenant_id or client_id is set), but the Note at line 42 was corrected to use "and" (When tenant_id and client_id are present). This creates confusion about whether setting only one of these fields triggers Entra ID mode.

Given the env var fallback behavior described in lines 78-79 and 92-93, it appears either field can initiate Entra ID auth (with the other falling back to env). If so, "or" is correct here but the Note should be updated. Otherwise, align this with the Note.

📝 Suggested fix (if both are required)
-  Not used when `tenant_id` or `client_id` is set (Entra ID auth takes over).
+  Not used when `tenant_id` and `client_id` are set (Entra ID auth takes over).

Comment on lines +81 to +83
If `client_secret` is also provided, BAML uses service principal (client credentials) authentication.
If `client_secret` is omitted, BAML uses managed identity authentication only
(via `ManagedIdentityCredential` — suitable for Azure VMs, App Service, and AKS).
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor style: Three successive sentences begin with "If".

Consider varying the sentence structure for improved readability. This is a minor style concern flagged by static analysis.

✏️ Suggested revision
   If `client_secret` is also provided, BAML uses service principal (client credentials) authentication.
-  If `client_secret` is omitted, BAML uses managed identity authentication only
+  Without `client_secret`, BAML uses managed identity authentication only
   (via `ManagedIdentityCredential` — suitable for Azure VMs, App Service, and AKS).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
If `client_secret` is also provided, BAML uses service principal (client credentials) authentication.
If `client_secret` is omitted, BAML uses managed identity authentication only
(via `ManagedIdentityCredential` — suitable for Azure VMs, App Service, and AKS).
If `client_secret` is also provided, BAML uses service principal (client credentials) authentication.
Without `client_secret`, BAML uses managed identity authentication only
(via `ManagedIdentityCredential` — suitable for Azure VMs, App Service, and AKS).
🧰 Tools
🪛 LanguageTool

[style] ~82-~82: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... (client credentials) authentication. If client_secret is omitted, BAML uses m...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

Comment on lines +503 to +516
// Azure client using Entra ID system default credential chain
// Tries ManagedIdentityCredential first, then DeveloperToolsCredential (Azure CLI / azd)
// No max_tokens so server default is used (works for both GPT-5 and o1-style deployments)
client<llm> AzureEntraIdSystemDefault {
provider azure-openai
options {
resource_name env.AZURE_OPENAI_RESOURCE_NAME
deployment_id env.AZURE_OPENAI_DEPLOYMENT_ID
api_version env.AZURE_OPENAI_API_VERSION
tenant_id env.AZURE_TENANT_ID
client_id env.AZURE_CLIENT_ID
max_tokens null
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usages of AzureEntraIdSystemDefault to assess rename impact
rg -n "AzureEntraIdSystemDefault" --type-add 'baml:*.baml' --type baml
rg -n "AzureEntraIdSystemDefault" --type ts --type py

Repository: BoundaryML/baml

Length of output: 50372


Update client name and comment for accuracy.

The client name AzureEntraIdSystemDefault is misleading. When tenant_id and client_id are set without client_secret, the client uses ManagedIdentityCredential only—not the full credential chain (managed identity → developer tools). The full chain is only used in true "SystemDefault" mode where all three Entra ID fields are omitted.

Rename the client to AzureEntraIdManagedIdentity and update the comment:

Suggested fix
-// Azure client using Entra ID system default credential chain
-// Tries ManagedIdentityCredential first, then DeveloperToolsCredential (Azure CLI / azd)
+// Azure client using Entra ID managed identity authentication
+// Uses ManagedIdentityCredential only (suitable for Azure VMs, App Service, AKS)
 // No max_tokens so server default is used (works for both GPT-5 and o1-style deployments)
-client<llm> AzureEntraIdSystemDefault {
+client<llm> AzureEntraIdManagedIdentity {
   provider azure-openai
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Azure client using Entra ID system default credential chain
// Tries ManagedIdentityCredential first, then DeveloperToolsCredential (Azure CLI / azd)
// No max_tokens so server default is used (works for both GPT-5 and o1-style deployments)
client<llm> AzureEntraIdSystemDefault {
provider azure-openai
options {
resource_name env.AZURE_OPENAI_RESOURCE_NAME
deployment_id env.AZURE_OPENAI_DEPLOYMENT_ID
api_version env.AZURE_OPENAI_API_VERSION
tenant_id env.AZURE_TENANT_ID
client_id env.AZURE_CLIENT_ID
max_tokens null
}
}
// Azure client using Entra ID managed identity authentication
// Uses ManagedIdentityCredential only (suitable for Azure VMs, App Service, AKS)
// No max_tokens so server default is used (works for both GPT-5 and o1-style deployments)
client<llm> AzureEntraIdManagedIdentity {
provider azure-openai
options {
resource_name env.AZURE_OPENAI_RESOURCE_NAME
deployment_id env.AZURE_OPENAI_DEPLOYMENT_ID
api_version env.AZURE_OPENAI_API_VERSION
tenant_id env.AZURE_TENANT_ID
client_id env.AZURE_CLIENT_ID
max_tokens null
}
}

"update-test:syntaxes": "vscode-tmgrammar-snap --config package.json syntaxes/all.test.baml --updateSnapshot"
},
"dependencies": {
"@azure/identity": "^4.2.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update the lockfile with this dependency change to unblock CI.

Adding @azure/identity at Line 214 requires committing the updated pnpm-lock.yaml; CI is currently failing with frozen-lockfile specifier mismatch.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant