Citadel Governance Hub - Testing & Validation Guide
Executive Summary
This testing suite provides a comprehensive, end-to-end validation framework for the Citadel Governance Hub — an enterprise-grade AI gateway built on Azure API Management (APIM). The notebooks in this directory enable platform teams to onboard LLM backends, provision access contracts for different business units, test agent framework integrations, verify PII processing capabilities, validate JWT authentication with role-based access control, and test unified AI API routing patterns — all through guided, reproducible Jupyter workflows.
The recommended execution order is:
Strongly recommended baseline (steps 1–4): these four notebooks together exercise the core gateway plumbing — backend onboarding, full model surface area, access-contract provisioning, and real-world agent consumption. Run them in order on every new Governance Hub deployment before moving on to the optional scenario-specific notebooks.
- Backend Contracts (LLM Onboarding) — Register AI backends and deploy routing logic into APIM ⭐ strongly recommended
- Universal LLM API — All-Models Tests — Validate every gateway-configured model (chat / embeddings / Responses API) through
/models⭐ strongly recommended - Access Contracts — Create per-team access contracts with Key Vault and Foundry integrations ⭐ strongly recommended
- Agent Frameworks — Validate agent-based consumption of provisioned contracts (Microsoft Agent Framework, Foundry Agent SDK, LangChain) ⭐ strongly recommended
- Model Aliases — Validate the shared
resolve-model-aliaspolicy fragment across all 3 LLM APIs (priority + weighted strategies, RBAC, discovery) - PII Processing — Test PII anonymization, deanonymization, and blocking policies
- Unified AI API — Test multi-provider routing patterns through the Unified AI Wildcard API
- JWT Authentication — Validate JWT-enforced and role-based access control across all API endpoints
- Extended Providers Backend Onboarding — Onboard and validate non-Microsoft-Foundry AI backends (AWS Bedrock, GCP Gemini, Anthropic Claude) through native and OpenAI-compatible routing to confirm multi-cloud support
Each notebook is self-contained with initialization, deployment, testing, visualization, and cleanup stages, enabling both interactive exploration and repeatable validation.
Prerequisites
Before running any notebook, ensure the following are in place:
- Citadel Governance Hub deployed (Full Deployment Guide or Quick Deployment Guide)
- Azure CLI installed and authenticated (
az login) - Python 3.10+ with a virtual environment activated
- Dependencies installed:
pip install -r ../shared/requirements.txt - VS Code with the Jupyter extension (recommended for running notebooks)
Optional (per notebook)
| Capability | Required By | Details |
|—|—|—|
| Universal LLM API (models) imported in APIM | Universal LLM All-Models Tests, Model Aliases | Required for /models discovery and per-model operation tests |
| Azure Key Vault | Access Contracts, Agent Frameworks | A Key Vault with secrets for LLM endpoint and API key |
| Azure AI Foundry | Access Contracts, Agent Frameworks | A Foundry account and project for connection integration |
| Azure AI Language Service | PII Processing | PII detection endpoint with managed identity access |
| Event Hub | PII Processing | For PII state saving and audit logging |
| Unified AI API (unified-ai) imported in APIM | Unified AI API, Model Aliases (full coverage) | Required for the wildcard /unified-ai/** routing patterns |
| resolve-model-alias policy fragment + alias-aware backend onboarding | Model Aliases | Re-deployed by the notebook itself via the LLM backend onboarding Bicep with modelAliases populated |
| Entra ID App Registration | JWT Authentication | Client credentials (client ID + secret) with app roles configured |
| MSAL Library | JWT Authentication | Optional — for interactive device code flow token acquisition |
| Google Gemini API | Unified AI API | Optional — for testing Gemini routing pattern |
| Unified AI API (unified-ai) imported in APIM | Extended Providers Backend Onboarding | Required for native /bedrock/**, /gemini/**, and /claude/** routing |
| AWS Bedrock / GCP Gemini / Anthropic API keys | Extended Providers Backend Onboarding | Optional per provider — fill the REPLACE_* placeholders to enable each backend’s tests |
| Azure Key Vault (provider secrets) | Extended Providers Backend Onboarding | Optional — recommended over inline secrets to hold non-Azure provider keys |
—
Initializing Variables from azd Environment
Every validation notebook supports a one-line init_from_azd = True toggle in its first code cell that auto-populates Citadel Governance Hub variables (resource group, location, Key Vault name, LLM backend config, Foundry account/project, Entra IDs, Cosmos account, …) directly from your active azd environment.
This is the recommended path when the accelerator was deployed with azd up, because the relevant deployment outputs (AZURE_RESOURCE_GROUP, AZURE_LOCATION, LLM_BACKEND_CONFIG, KEY_VAULT_NAME, AI_FOUNDRY_SERVICES, ENTRA_*, COSMOS_DB_ACCOUNT_NAME, …) are already written to the active azd env file by the Bicep deployment.
How It Works
Each notebook’s init cell follows the same pattern:
init_from_azd = True # Set False to fill REPLACE values manually below
# Manual fallbacks (used when azd env var is missing OR init_from_azd = False)
governance_hub_resource_group = "REPLACE"
location = "REPLACE"
# ... other notebook-specific REPLACE values ...
if init_from_azd:
loaded = utils.load_azd_env({
"resource_group": ["AZURE_RESOURCE_GROUP", "GOVERNANCE_HUB_RESOURCE_GROUP"],
"location": ["AZURE_LOCATION", "LOCATION"],
# ... notebook-specific azd vars ...
}, verbose=False)
# Manually-set values (anything not equal to the REPLACE sentinel) win over azd values.
The shared helper utils.load_azd_env in shared/utils.py wraps azd env get-value <NAME> per variable, supports fallback name lists, and JSON-decodes complex values (LLM_BACKEND_CONFIG, AI_FOUNDRY_SERVICES).
Pre-flight
# Make sure the right azd environment is selected
azd env list
azd env select <env-name>
# (Optional) inspect what will be auto-loaded
azd env get-values
Important: Manually edited values (anything not equal to the
"REPLACE"sentinel string in the init cell) always win over values pulled fromazd. Setinit_from_azd = Falseto disable the lookup entirely and force purely manual configuration.
Per-Notebook Variable Map
The table below summarizes which azd env variables each notebook auto-loads when init_from_azd = True. Variables in italics are optional / only used when the notebook actually exercises the corresponding integration.
| Notebook | azd env variables auto-loaded → notebook variables |
|---|---|
llm-backend-onboarding-runner |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → locationLLM_BACKEND_CONFIG (JSON) → llm_backends_configKEY_VAULT_NAME → key_vault_name |
citadel-universal-llm-api-all-models-tests |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → location |
citadel-access-contracts-tests |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → locationAZURE_SUBSCRIPTION_ID → keyvault_subscription_id / foundry_subscription_idKEY_VAULT_NAME → keyvault_nameAI_FOUNDRY_SERVICES[0] (JSON) → foundry_account_name + foundry_project_name |
citadel-agent-frameworks-tests |
Same as citadel-access-contracts-tests |
citadel-model-aliases-tests |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → locationLLM_BACKEND_CONFIG (JSON) → llm_backends_config |
citadel-pii-processing-tests |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → locationCOSMOS_DB_ACCOUNT_NAME → cosmos_account_endpoint |
citadel-unified-ai-api-tests |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → location |
citadel-jwt-authentication-tests |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → locationAZURE_SUBSCRIPTION_ID → keyvault_subscription_id / foundry_subscription_idENTRA_TENANT_ID → entra_tenant_idENTRA_CLIENT_ID → entra_client_idENTRA_AUDIENCE → entra_audience (defaults to api://<client_id> if missing)KEY_VAULT_NAME → keyvault_nameAI_FOUNDRY_SERVICES[0] (JSON) → foundry_account_name + foundry_project_nameNote: entra_client_secret is intentionally not auto-loaded — set it manually before running JWT tests. |
llm-backend-onboarding-extended-providers-runner |
AZURE_RESOURCE_GROUP → governance_hub_resource_groupAZURE_LOCATION → locationLLM_BACKEND_CONFIG (JSON) → merged with the extended-provider backends in llm_backends_configKEY_VAULT_NAME → key_vault_nameNote: init_from_azd defaults to False here so the in-notebook REPLACE_* provider placeholders are preserved; flip to True to merge your azd-maintained Foundry backends with the new providers. |
Multi-environment teams: Run
azd env select <env-name>before launching the notebook to switch which deployment the notebook talks to. Each.azure/<env-name>/.envfile is fully self-contained.
When to Set init_from_azd = False
- You are validating a Citadel Governance Hub that was not deployed via
azd(e.g. deployed viaaz deployment sub createdirectly, or via a CI/CD pipeline that doesn’t write azd env vars). - You want to point a notebook at a different environment than the currently selected
azdenv. - You are running the notebook on a workstation where
azdis not installed.
In any of these cases, set init_from_azd = False and fill the REPLACE values manually. The shared helpers (utils.azd_env_get, utils.azd_env_get_json, utils.load_azd_env) will silently no-op when azd isn’t available, so leaving init_from_azd = True on a machine without azd is also harmless — it just won’t override your manual values.
Notebooks
1. LLM Backend Onboarding Runner
| Notebook | llm-backend-onboarding-runner.ipynb |
| Purpose | Onboard AI backends into the Citadel Governance Hub and deploy routing logic |
| Run this | First — before any other notebook |
What It Does
This notebook automates the full lifecycle of registering LLM backends with your APIM gateway. It extracts the current backend configuration, generates a Bicep parameter file with per-model metadata (SKU, capacity, model format, version), deploys the backends and policy fragments, and verifies the deployment through multiple API formats.
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group, location, and backend endpoints |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Connect to the existing Governance Hub deployment |
| 3 | Extract current backends — Retrieve existing backend pools and routing configuration |
| 4 | Discover managed identity — Auto-detect the APIM user-assigned managed identity |
| 5 | Generate parameter file — Create a .bicepparam file with full backend definitions |
| 6 | Deploy — Run the Bicep deployment to create backends, pools, and policy fragments |
| 7 | Verify deployment — Confirm backends and policy fragments were created |
| 8 | Verify GET /deployments — Test the get-available-models policy fragment for Foundry integration |
| Test | Test models — Validate via Universal LLM API, Azure OpenAI API, Python SDK, and streaming |
Key Configuration
governance_hub_resource_group = "REPLACE" # Your Governance Hub resource group
location = "REPLACE" # e.g., "eastus", "swedencentral"
llm_backends_config = [
{
"backendId": "aif-citadel-primary",
"backendType": "ai-foundry", # 'ai-foundry' | 'azure-openai' | 'external'
"endpoint": "https://...",
"authScheme": "managedIdentity", # 'managedIdentity' | 'apiKey' | 'token'
"supportedModels": [
{ "name": "gpt-4o", "sku": "GlobalStandard", "capacity": 100, "modelFormat": "OpenAI", "modelVersion": "2024-11-20" }
],
"priority": 1,
"weight": 100
}
]
Output
- Deployed APIM backends with circuit breaker support
- Backend pools with priority/weight-based load balancing
set-backend-poolsandget-available-modelspolicy fragments- Verified model routing through both API formats
2. Universal LLM API — All-Models Tests
| Notebook | citadel-universal-llm-api-all-models-tests.ipynb |
| Purpose | Validate the Universal LLM API (/models) against every model exposed by the gateway |
| Run this | Immediately after backend onboarding to confirm the full model catalogue is reachable |
What It Does
This notebook provisions a single access contract with allowedModels = "" (no model restriction), then dynamically discovers the live model catalogue via GET /models/models and exercises the appropriate OpenAI v1 operation for each model. It is the fastest way to confirm that every onboarded backend pool is end-to-end reachable through the Universal LLM API surface.
Operations Exercised Per Model
| Model name pattern | Operations exercised |
|---|---|
Contains embedding |
POST /models/embeddings |
Contains gpt |
POST /models/chat/completions and the full Responses API trio: POST /models/responses, GET /models/responses/{response_id}, GET /models/responses/{response_id}/input_items?limit=20 |
| Anything else | POST /models/chat/completions |
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group, location, API versions, and optional model cap |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Discover the Universal LLM API and supported models |
| 3 | Provision access contract — Deploy a Bicep-generated APIM product + subscription with allowedModels = "" and a generous capacity allocation |
| 4 | Retrieve API key — Get the subscription key for the unrestricted product |
| 5 | Discover models — Call GET /models/models to enumerate the live model catalogue |
| 6 | Per-model operation loop — Auto-classify each model and run chat / embeddings / Responses API operations |
| 7 | Summary table — Aggregate per-model pass/fail across all exercised operations |
| Cleanup | Delete test products — Optionally remove the unrestricted access contract |
Key Configuration
governance_hub_resource_group = "REPLACE"
location = "REPLACE"
targetInferenceApi = "models" # Universal LLM API
inference_api_version = "2024-05-01-preview"
openai_api_version = "2024-12-01-preview"
# 0 = test every discovered model; set a positive int to cap for quick smoke tests
max_models_to_test = 0
# Delay between POST /responses and the subsequent GET /responses/{id} calls
responses_get_delay_seconds = 0
Output
- One Bicep-deployed APIM product + subscription with no model RBAC restriction
- Live discovery of every gateway-configured model via
GET /models/models - Per-model results for chat, embeddings, and (where applicable) Responses API operations
- Summary table highlighting any model that failed an expected operation
3. Citadel Access Contracts Tests
| Notebook | citadel-access-contracts-tests.ipynb |
| Purpose | Create, deploy, and load-test multiple access contracts with different integration patterns |
| Run this | After backend onboarding and the Universal LLM all-models smoke test |
What It Does
This notebook provisions three distinct access contracts, each representing a different integration pattern. It generates the Bicep parameter files, deploys the contracts as APIM products with subscriptions, performs load testing, and visualizes throttling behavior and token bucket dynamics across all contracts.
Access Contracts Created
| Contract | Business Unit | Integration | Description |
|---|---|---|---|
| Sales-Assistant | Sales | Key Vault only | Secrets (endpoint + API key) resolved from Azure Key Vault |
| HR-ChatAgent | HR | Key Vault + Foundry | Optionally creates a Foundry project connection for agent integration |
| Support-Bot | Support | Direct output | No external integrations — uses direct APIM subscription output |
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group, Key Vault, and Foundry settings |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Discover APIs and supported models |
| 3 | Define contracts — Configure three access contracts with varying integration patterns |
| 4 | Create parameter files — Generate .bicepparam files with policy XML for each contract |
| 5 | Deploy contracts — Run Bicep deployments at subscription scope |
| 6 | Retrieve API keys — Extract subscription keys for each deployed product |
| 7 | Load test — Send concurrent API requests to each contract and record metrics |
| 8 | Visualize results — Compare success/throttled/error rates across contracts |
| 9 | Token bucket analysis — Simulate and visualize token bucket refill behavior |
| Cleanup | Delete test products — Optionally remove all created APIM products and subscriptions |
Key Configuration
governance_hub_resource_group = "REPLACE"
location = "REPLACE"
# Optional integrations
use_keyvault_integration = True
keyvault_name = "REPLACE"
use_foundry_integration = True
foundry_account_name = "REPLACE"
foundry_project_name = "REPLACE"
Output
- Three deployed APIM products with subscription keys
- Key Vault secrets populated (if enabled)
- Foundry connection created (if enabled)
- Performance charts comparing all contracts
- Token bucket behavior visualization
4. Citadel Agent Frameworks Tests
| Notebook | citadel-agent-frameworks-tests.ipynb |
| Purpose | Validate real-world agent consumption of access contracts using three major frameworks |
| Run this | After access contracts are deployed (notebook 3) |
What It Does
This notebook instantiates three AI agents — each using a different framework and integration pattern — and runs multi-turn conversations through the Citadel gateway. It measures token consumption, retry behavior, and call reliability, then produces comparative visualizations across all three frameworks.
Agent Framework Matrix
| Access Contract | Agent Framework | Integration Type | Target Model |
|---|---|---|---|
| Sales-Assistant | Microsoft Agent Framework | Azure Key Vault (endpoint + key) | gpt-4.1 |
| HR-ChatAgent | Microsoft Foundry Agent SDK | Foundry Project Connection | gpt-4o |
| Support-Bot | LangChain | Local (direct endpoint + key) | phi-4 |
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group, Key Vault, Foundry, and model settings |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Discover APIs and supported models |
| 3 | Retrieve API keys — Fetch subscription keys for each access contract |
| 4 | Install packages — Install agent framework dependencies (agent-framework, azure-ai-projects, langchain, langchain-openai) |
| 5 | Microsoft Agent Framework — Sales conversation via Key Vault integration |
| 6 | Foundry Agent SDK — HR conversation via Foundry project connection |
| 7 | LangChain — Support conversation via direct endpoint configuration |
| 8 | Metrics comparison — Token consumption pie charts, calls vs. retries, retry rates |
| 9 | Efficiency analysis — Token efficiency and call reliability per agent |
| Cleanup | Managed by citadel-access-contracts-tests.ipynb |
Key Configuration
governance_hub_resource_group = "REPLACE"
# Key Vault (for Sales-Assistant)
keyvault_name = "REPLACE"
# Foundry (for HR-ChatAgent)
foundry_account_name = "REPLACE"
foundry_project_name = "REPLACE"
foundry_connection_name = "HR-ChatAgent-DEV-LLM"
# Models per agent
sales_model_name = "gpt-4.1"
hr_model_name = "gpt-4o"
support_model_name = "phi-4"
Output
- Multi-turn conversation logs for each agent
- Token usage metrics (prompt, completion, total) per framework
- Retry rate and reliability analysis
- Comparative visualizations saved as
agent_metrics_comparison.png
5. Citadel Model Aliases Tests
| Notebook | citadel-model-aliases-tests.ipynb |
| Purpose | Validate the shared resolve-model-alias policy fragment across Universal LLM, Azure OpenAI, and Unified AI APIs |
| Run this | After backend onboarding (notebook 1). Can be run independently of notebooks 3–4. |
What It Does
This notebook re-runs the LLM backend onboarding Bicep with a modelAliases parameter populated, which (re)deploys the shared resolve-model-alias policy fragment with the configured aliases inlined. It then provisions an access contract scoped to alias names only (least-privilege RBAC) and exercises the same aliases through all 3 LLM API surfaces, inspecting the UAIG-* debug response headers to verify the gateway’s routing decisions end-to-end.
Two alias strategies are validated:
| Alias | Strategy | Behavior |
|---|---|---|
adv-gpt |
priority |
First underlying model wins; remaining models act as cross-model fallback (deterministic per-call) |
gpt-blend |
weighted |
Random-weighted picking across underlying models — useful for A/B model swaps |
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Auto-load from azd env (LLM_BACKEND_CONFIG) or fill manually; define the two aliases and a direct-test model |
| 1 | Verify Azure CLI & APIM Client — Confirm subscription context and discover the APIM resource + managed identity |
| 2 | Generate .bicepparam — Write llm-backends-aliases-validation.bicepparam containing the full backend config + the two aliases |
| 3 | Deploy onboarding — Re-run the LLM backend onboarding Bicep so the resolve-model-alias fragment is regenerated with the aliases inlined |
| 4 | Create access contract — Deploy an APIM product whose allowedModels lists ONLY the alias names + direct_test_model, with enableResponseHeaders=true for UAIG-* debug headers |
| 5 | Resolve API key — Pick the subscription created for the contract |
| 6 | Discover endpoints — Universal LLM (/models), Azure OpenAI (/openai), Unified AI (/unified-ai) |
| Discovery | GET /deployments honors allowedModels — Aliases appear as type: "alias" entries with descriptions; underlying real models that aren’t in allowedModels are filtered out |
| 7 | Direct model control test — Call direct_test_model on all 3 APIs; resolver should be a no-op (no UAIG-Alias header) |
| 8 | Priority alias test — Call adv-gpt on all 3 APIs; resolves deterministically to the first underlying model |
| 9 | Weighted alias single call — Call gpt-blend once on each API |
| 10 | Weighted distribution test — Send N=30 requests through gpt-blend and tally UAIG-Resolved-Model against configured weights |
| 11 | Negative RBAC test — Send a model NOT in allowedModels; expect HTTP 403 unauthorized_model_access |
| Summary | Results overview — Cross-API consistency check + alias resolution observations |
| Cleanup | Delete access contract — do_cleanup flag (default False); LLM backends and the resolve-model-alias fragment are intentionally preserved |
UAIG-* Response Headers Inspected
| Header | Meaning |
|---|---|
UAIG-Model-Id |
Model that was actually routed to (post alias resolution) |
UAIG-Alias |
Original alias name the client sent (only present when alias was used) |
UAIG-Resolved-Model |
Real model the alias resolved to |
UAIG-Backend |
Backend pool / backend that served the request |
UAIG-API-Type |
Detected API type (Unified AI only) |
UAIG-Final-Path |
Reconstructed backend path (Unified AI only) |
UAIG-Auth-Type |
Auth method enforced by security-handler (Unified AI only) |
UAIG-Cache-Operation |
cache-hit / cache-miss for metadata-config |
UAIG-Request-Id |
APIM request id for log correlation |
Key Configuration
governance_hub_resource_group = "REPLACE" # or auto-loaded from azd
location = "REPLACE"
llm_backends_config = [] # auto-loaded from LLM_BACKEND_CONFIG when init_from_azd=True
model_aliases = [
{ "name": "adv-gpt", "models": ["gpt-5.2", "gpt-5.4-mini", "gpt-4.1"], "strategy": "priority" },
{ "name": "gpt-blend", "models": ["gpt-5.4-mini", "gpt-4.1"], "strategy": "weighted", "weights": [70, 30] },
]
direct_test_model = "gpt-4.1" # control test (must exist in llm_backends_config)
inference_api_version = "2024-05-01-preview"
Output
- Re-deployed
resolve-model-aliaspolicy fragment with the two aliases inlined as a staticJObject - Access contract scoped to alias names (proves alias-name RBAC works without exposing underlying real models)
- Per-API report of
UAIG-*headers showing alias → real-model resolution - Distribution table for the weighted alias compared against the configured weights
- Filtered
GET /deploymentsresponse showing aliases as first-class entries withproperties.capabilities.description - 403 negative-test confirmation that unauthorized models are blocked at the gateway
6. Citadel PII Processing Tests
| Notebook | citadel-pii-processing-tests.ipynb |
| Purpose | Verify PII anonymization, deanonymization, and blocking capabilities |
| Run this | After the Governance Hub is deployed with PII policy fragments |
What It Does
This notebook creates two specialized access contracts to test PII processing. The first contract enables PII anonymization and deanonymization with state saving (audit logging to Event Hub). The second contract enables PII blocking, which rejects any request containing detected PII. Both contracts support built-in PII categories and custom regex patterns for domain-specific identifiers.
Use Cases Tested
| Use Case | Mode | Behavior |
|---|---|---|
| PII Masking | Anonymization / Deanonymization | PII in requests is replaced with placeholders (e.g., <Person_0>), sent to the LLM, then restored in the response |
| PII Blocking | Detection + Rejection | Requests containing PII are rejected with HTTP 400 and a list of detected PII categories |
PII Types Covered
- Person names, email addresses, phone numbers
- Physical addresses, IBANs
- Credit card numbers (custom regex)
- Passport numbers (custom regex)
- Emirates ID (custom regex)
- Multiple PII types in a single request
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group, location, and Key Vault settings |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Discover APIs and supported models |
| 3.1 | Define masking contract — Configure PII anonymization with state saving |
| 3.2 | Create masking policy — Generate policy XML with anonymization, deanonymization, and regex patterns |
| 3.3 | Deploy masking contract — Deploy via Bicep with generated parameters |
| 3.4 | Retrieve masking API key — Get the subscription key for the masking product |
| 3.5 | Test PII masking — Send 6 test payloads with various PII types and verify deanonymization |
| 4.1 | Define blocking contract — Configure PII detection and blocking |
| 4.2 | Create blocking policy — Generate policy XML that detects PII and returns HTTP 400 |
| 4.3 | Deploy blocking contract — Deploy via Bicep with generated parameters |
| 4.4 | Retrieve blocking API key — Get the subscription key for the blocking product |
| 4.5 | Test PII blocking — Send 8 test payloads (5 with PII, 3 without) and verify correct blocking/allowing |
| Summary | Results overview — Aggregate pass/fail across both use cases |
| Cleanup | Delete test products — Optionally remove PII access contracts and generated files |
Key Configuration
governance_hub_resource_group = "REPLACE"
location = "REPLACE"
# PII detection settings (configured in policy XML)
# - Confidence threshold: 0.75–0.8
# - Entity exclusions: PersonType
# - Custom regex: Credit cards, passport numbers, Emirates ID
Output
- Deployed PII masking and blocking APIM products
- Test results for 14 PII test payloads
- Validation of custom regex pattern detection
- Pass/fail summary for both anonymization and blocking modes
7. Citadel Unified AI API Tests
| Notebook | citadel-unified-ai-api-tests.ipynb |
| Purpose | Validate the Unified AI Wildcard API across multiple LLM providers and API patterns |
| Run this | After the Governance Hub is deployed with the Unified AI API enabled |
What It Does
This notebook validates the Unified AI Wildcard API (/unified-ai) that enables API pattern flexibility across multiple LLM providers. It deploys a test access contract, then tests Azure OpenAI, AI Foundry inference, and Gemini routing patterns, validates model discovery endpoints, verifies API key authentication, and runs a load test with throttling visualization.
API Patterns Tested
| Pattern | Path | Provider |
|---|---|---|
| Azure OpenAI | /unified-ai/openai/deployments/{model}/chat/completions |
Azure OpenAI |
| Foundry Inference | /unified-ai/models/chat/completions |
AI Foundry |
| Gemini OpenAI | /unified-ai/v1beta/openai/chat/completions |
Google Gemini (optional) |
| Deployment Discovery | GET /unified-ai/deployments |
All providers |
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group, location, and model names per backend |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Discover Unified AI API and retrieve supported models |
| 3 | Deploy access contract — Create and deploy a test APIM product with model-scoped policy via Bicep |
| 4 | Retrieve API key — Get the subscription key and build endpoint URLs |
| Test 1 | Model discovery — GET /unified-ai/deployments to list available models |
| Test 2 | Azure OpenAI pattern — Chat completion via OpenAI-compatible path; expects 200 |
| Test 3 | Foundry inference pattern — Chat completion via inference path with model in body; expects 200 |
| Test 4 | Deployment queries — Get existing deployment (200) and non-existent deployment (404) |
| Test 5 | Gemini pattern — Chat completion via Gemini OpenAI-compatible path (if configured) |
| Test 6 | API key authentication — Valid key (200), missing key (401) |
| Test 7 | Load test — 30-second burst requests with throttling visualization |
| Cleanup | Delete test products — Optionally remove the access contract product |
Key Configuration
governance_hub_resource_group = "REPLACE"
location = "REPLACE"
# Model configuration per backend
openai_model = "gpt-4o"
foundry_inference_model = "Mistral-Large-3"
gemini_model = "gemini-2.5-flash-lite"
# Test toggles
test_gemini = False # Set True if Gemini backend is configured
# Load test
test_duration = 30 # Seconds
Output
- Deployed test access contract with model-scoped policy
- Model discovery results from
GET /deployments - Response validation across Azure OpenAI, Foundry, and Gemini patterns
- API key authentication enforcement
- Load test visualization (bar chart with 200/429 status codes over time)
8. Citadel JWT Authentication Tests
| Notebook | citadel-jwt-authentication-tests.ipynb |
| Purpose | Validate JWT-based authentication and role-based access control (RBAC) across all LLM API endpoints |
| Run this | After the Governance Hub is deployed with JWT configuration and Entra ID app registration |
What It Does
This notebook tests dual authentication modes (API Key + JWT Bearer token) and role-based authorization using Entra ID app roles. It supports both service-to-service (client credentials) and interactive user (device code flow) token acquisition, and validates the unified security-handler fragment across all three API endpoint flavors (Azure OpenAI, Universal LLM, Unified AI).
Use Cases Tested
| Use Case | Mode | Behavior |
|---|---|---|
| JWT-Enforced Access | API Key + JWT | Requires both a valid subscription key and a valid JWT Bearer token |
| Role-Enforced Access | API Key + JWT + App Role | Additionally requires a specific app role (e.g., Models.Read) in the JWT |
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group, location, Entra ID tenant/client IDs, and model settings |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Discover all 3 API endpoints and retrieve supported models |
| 3 | Acquire JWT (Client Credentials) — Obtain a JWT token via client credentials grant from Entra ID |
| 4 | Acquire JWT (Device Code Flow) — Optional interactive sign-in via MSAL for user token acquisition |
| 5 | Inspect JWT tokens — Decode and display token header, payload, roles, and lifetime |
| 6 | Select active token — Choose between client credentials or device flow token for tests |
| 7 | Deploy JWT access contract — Create and deploy a JWT-enforced APIM product via Bicep |
| 8 | Retrieve API key — Get the subscription key for the JWT-enforced product |
| Test 1 | API Key + JWT (Success) — Send requests with both credentials to all endpoints; expects 200 |
| Test 2 | API Key Only (Fail) — Send requests without JWT; expects 401 |
| Test 3 | Invalid JWT (Fail) — Send requests with invalid JWT; expects 401 |
| Test 4 | JWT Only (Fail) — Send requests without API key; expects 401/403 |
| 9 | Deploy role-enforced contract — Create APIM product requiring Models.Read app role |
| 10 | Retrieve role API key — Get the subscription key for the role-enforced product |
| Test 5 | Correct Role (Success) — Send requests with JWT containing Models.Read; expects 200 |
| Test 6 | Missing Role (Fail) — Send requests without required role; expects 403 |
| Test 7 | API Key Only on Role Product (Fail) — Send requests without JWT to role-enforced product; expects 401 |
| Summary | Results overview — Aggregate PASS/FAIL across all 7 tests and 4 endpoints |
| Cleanup | Delete test products — Optionally remove JWT and role-enforced products |
Key Configuration
governance_hub_resource_group = "REPLACE"
location = "REPLACE"
# Entra ID / OAuth
entra_tenant_id = "REPLACE"
entra_client_id = "REPLACE"
entra_client_secret = "REPLACE"
# Model and API versions
model_name = "gpt-4.1"
openai_api_version = "2024-12-01-preview"
# Token source: "client_credentials" or "device_flow"
token_source = "client_credentials"
# Role configuration
requiredRoles = "Models.Read"
Output
- Deployed JWT-enforced and role-enforced APIM products
- Test results for 7 scenarios across 4 API endpoints
- Decoded JWT token inspection (claims, roles, lifetime)
- PASS/FAIL summary table with overall pass rate
9. Extended Providers Backend Onboarding (AWS Bedrock + GCP Gemini + Anthropic Claude)
| Notebook | llm-backend-onboarding-extended-providers-runner.ipynb |
| Purpose | Onboard three non-Microsoft-Foundry LLM backends and validate native + OpenAI-compatible routing across both LLM API surfaces |
| Run this | After backend onboarding (notebook 1). This notebook extends notebook 1 — it keeps the Foundry pipeline and adds the non-Azure backends. |
What It Does
This notebook extends the base LLM backend onboarding flow with three non-Azure backends authenticated by simple API keys (no AWS SigV4 or Workload Identity Federation required for validation). It generates a .bicepparam file with the new backend definitions, re-deploys the onboarding Bicep, provisions an access contract that allows every onboarded model, then validates each backend through its native API surface and its OpenAI-compatible surface. Each test cell self-skips when a provider’s credentials are still REPLACE_*, so partial configurations never produce failures.
Backends Onboarded
| Backend type | Auth | Native path prefix | OpenAI-compat surface |
|---|---|---|---|
aws-bedrock |
api-key-bearer (Bedrock API key) |
/unified-ai/bedrock/model/{modelId}/converse |
— |
aws-bedrock-mantle |
api-key-bearer |
— | /models/chat/completions and /unified-ai/v1/chat/completions |
gemini |
api-key-gemini (x-goog-api-key) |
/unified-ai/gemini/v1beta/models/{model}:generateContent |
— |
gemini-openai |
api-key-bearer |
— | /models/chat/completions and /unified-ai/v1/chat/completions |
anthropic |
api-key-anthropic (x-api-key + anthropic-version) |
/unified-ai/claude/v1/messages |
— |
API Surfaces Validated
| API | Inbound path | Surface | Backends covered |
|---|---|---|---|
| Universal LLM | /models/chat/completions |
OpenAI-compat | Foundry, Bedrock-Mantle, Gemini-OpenAI |
| Unified AI | /unified-ai/v1/chat/completions |
OpenAI-compat | Foundry, Bedrock-Mantle, Gemini-OpenAI |
| Unified AI | /unified-ai/bedrock/model/{modelId}/converse |
Native Bedrock Converse | aws-bedrock |
| Unified AI | /unified-ai/gemini/v1beta/models/{model}:generateContent |
Native Gemini | gemini |
| Unified AI | /unified-ai/claude/v1/messages |
Native Anthropic Messages | anthropic |
Steps
| Step | Description |
|---|---|
| 0 | Initialize variables — Configure resource group and per-provider credentials (inline secret or Key Vault URI); build the backend definitions and opt-in model aliases |
| 1 | Verify Azure CLI — Confirm authentication and subscription context |
| 2 | Initialize APIM Client — Connect to the existing Governance Hub deployment |
| 3 | Extract current backends — Retrieve existing backend pools and routing configuration |
| 4 | Discover managed identity — Auto-detect the APIM user-assigned managed identity |
| 5 | Generate parameter file — Create a .bicepparam with the new backends, auth config, and aliases |
| 6 | Deploy — Re-run the onboarding Bicep to add backends, pools, and policy fragments |
| 7–8 | Verify deployment — Confirm backends, pools, and get-available-models fragment |
| 9 | Provision access contract — Deploy an APIM product allowing every onboarded model + alias |
| Test | Validate routing — OpenAI-compat (Universal LLM + Unified AI), native Bedrock / Gemini / Anthropic, and model-alias scenarios |
Key Configuration
init_from_azd = False # keeps REPLACE_* provider placeholders; set True to merge azd Foundry backends
governance_hub_resource_group = "REPLACE"
location = "REPLACE"
# Provider credentials — leave as REPLACE_* to skip a backend's tests
aws_bedrock_region = "eu-north-1"
aws_bedrock_native_inline = "REPLACE_AWS_BEDROCK_BEARER_VALUE" # full "Bearer sk-..."
gemini_native_inline = "REPLACE_GEMINI_RAW_KEY" # raw key for x-goog-api-key
anthropic_inline = "REPLACE_ANTHROPIC_RAW_KEY" # raw key for x-api-key
anthropic_version = "2023-06-01"
# Optional Key Vault to hold provider secrets (recommended over inline values)
key_vault_name = "kv-REPLACE"
Secret format note: APIM substitutes `` on a Backend resource only when it is the entire header value (no concatenation). Bearer-auth secrets must therefore store the complete header value (
Bearer sk-...), while direct-key headers (x-api-key,x-goog-api-key) store the raw key. A provider with both a native and an OpenAI-compat surface (e.g. Gemini) needs two separate secrets.
Output
- Three native + two OpenAI-compat backends onboarded alongside the existing Foundry backends
- Access contract allowing every onboarded model and alias
- Per-surface validation results (native and OpenAI-compat) with
UAIG-*debug headers - Optional model-alias scenarios (single-cloud weighted, cross-cloud OpenAI-compat, native Anthropic) that self-skip when members are unavailable
Recommended Execution Order
Strongly recommended baseline: run notebooks 1 → 4 in order on every new Citadel Governance Hub deployment. Steps 5 → 8 are optional, scenario-specific validations that can be run independently afterwards.
┌──────────────────────────────────────────────┐
│ 1. llm-backend-onboarding-runner │ ⭐ Onboard LLM backends & routing
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. citadel-universal-llm-api-all-models- │ ⭐ Smoke-test EVERY onboarded model
│ tests │ (chat / embeddings / Responses API)
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 3. citadel-access-contracts-tests │ ⭐ Create access contracts & load test
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. citadel-agent-frameworks-tests │ ⭐ Test agent frameworks
│ │ (uses contracts from step 3)
└──────────────┬───────────────────────────────┘
│ ── End of strongly recommended baseline ──
▼
┌──────────────────────────────────────────────┐
│ 5. citadel-model-aliases-tests │ Optional: alias routing
│ │ (priority + weighted strategies)
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 6. citadel-pii-processing-tests │ Optional: PII masking & blocking
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 7. citadel-unified-ai-api-tests │ Optional: Unified AI wildcard API
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 8. citadel-jwt-authentication-tests │ Optional: JWT auth & RBAC
└──────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 9. llm-backend-onboarding-extended- │ Optional: AWS Bedrock + GCP Gemini +
│ providers-runner │ Anthropic Claude (native + OpenAI-compat)
└──────────────────────────────────────────────┘
Note: Notebooks 5–9 create their own access contracts and can be run independently after backend onboarding. Notebook 5 re-deploys the LLM backend onboarding Bicep with
modelAliasespopulated (theresolve-model-aliasfragment is regenerated; full cross-API coverage requires the Unified AI API to be imported), notebook 6 requires PII policy fragments (pii-anonymization,pii-deanonymization,pii-state-saving), notebook 7 requires the Unified AI API (unified-ai) to be imported into APIM, notebook 8 requires JWT configuration plus an Entra ID app registration, and notebook 9 extends backend onboarding with non-Azure providers (native/bedrock/**,/gemini/**, and/claude/**routing requires the Unified AI API to be imported).
Shared Utilities
All notebooks import shared helper modules from the ../shared/ directory:
| Module | Description |
|---|---|
utils.py |
CLI command runner, formatted output helpers (print_ok, print_error, print_info) |
apimtools.py |
APIMClientTool class for APIM discovery, API key retrieval, policy fragment parsing, and backend management |
Cleanup
Each notebook includes an optional cleanup cell at the end that removes the APIM products and subscriptions created during testing. Cleanup is controlled by a cleanup_enabled flag (default: True).
Important: Cleanup does not remove Azure Key Vault secrets, Foundry connections, or LLM backend configurations. Those resources are managed separately.
Troubleshooting
| Issue | Resolution |
|---|---|
az account show fails |
Run az login and set the correct subscription with az account set --subscription <id> |
| APIM Client Tool initialization fails | Verify the governance_hub_resource_group is correct and your identity has Reader access |
| Model not found in backend pool | Run the backend onboarding notebook to register the model |
| Key Vault access denied | Ensure your identity has Key Vault Secrets User role on the Key Vault |
| Foundry connection fails | Verify the Foundry account, project, and connection names are correct |
| PII detection not working | Confirm the Azure AI Language Service is deployed and the managed identity has access |
| 429 Throttled responses | Expected during load testing — the token bucket policy is working correctly |