# Implementation Plan: Adapter: Azure OpenAI

**Branch**: `016-adapter-azure` | **Date**: 2026-04-02 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/016-adapter-azure/spec.md`

## Summary

Extend the existing Azure adapter stub (`adapters/src/azure.rs`) from basic API-key-only auth to a full-featured Azure OpenAI v1 GA adapter with Azure AD/Entra ID OAuth2 support and content filter detection. Cross-cutting change: add `ContentFiltered` error variant to core crate (`StreamErrorKind` + `AgentError`) for use by all adapters with safety filters.

## Technical Context

**Language/Version**: Rust 1.88, edition 2024
**Primary Dependencies**: `reqwest` (HTTP + token acquisition), `tokio` (async runtime), `serde`/`serde_json` (serialization), `tracing` (diagnostics), `swink-agent` (core types). All workspace deps.
**Storage**: N/A (in-memory token cache only)
**Testing**: `cargo test --workspace`, wiremock for mock HTTP, live tests with `#[ignore]`
**Target Platform**: Any (library crate)
**Project Type**: Library (adapter crate within workspace)
**Performance Goals**: Token refresh must not block concurrent stream requests
**Constraints**: No new external dependencies. `#[forbid(unsafe_code)]`. Zero clippy warnings.
**Scale/Scope**: Single adapter module (~300-400 lines) + ~30 lines core changes

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

| Principle | Status | Notes |
|---|---|---|
| I. Library-First | PASS | Adapter crate, independently compilable, no reverse deps |
| II. Test-Driven | PASS | Tests before implementation (wiremock + live) |
| III. Efficiency | PASS | Token cache avoids redundant HTTP calls; `Arc<RwLock>` for concurrent reads |
| IV. Leverage Ecosystem | PASS | Uses `reqwest` for OAuth2 (no new crate needed) |
| V. Provider Agnosticism | PASS | `StreamFn` boundary maintained; `ContentFiltered` is provider-agnostic |
| VI. Safety & Correctness | PASS | `#[forbid(unsafe_code)]`, non-retryable `ContentFiltered` |

**Architectural Constraints**:
- Crate count: No new crate (extends adapters crate) — PASS
- MSRV 1.88: PASS
- Concurrency: Token refresh is async, doesn't block streams — PASS
- Events outward-only: No mutation via events — PASS
- No global mutable state: Token cache uses `Arc<RwLock>` — PASS

## Project Structure

### Documentation (this feature)

```text
specs/016-adapter-azure/
├── plan.md              # This file
├── spec.md              # Feature specification
├── research.md          # Phase 0 research findings
├── data-model.md        # Entity definitions
├── quickstart.md        # Usage examples
├── contracts/
│   └── public-api.md    # Public API contract
└── tasks.md             # Implementation tasks (generated by /speckit.tasks)
```

### Source Code (repository root)

```text
src/
├── error.rs             # Add AgentError::ContentFiltered variant
├── stream.rs            # Add StreamErrorKind::ContentFiltered + constructor
└── loop_.rs             # Map ContentFiltered in error handling

adapters/src/
├── azure.rs             # Extend: AzureAuth, token cache, content filter
├── lib.rs               # Re-export AzureAuth (already exports AzureStreamFn)
└── classify.rs          # Optional: content filter body detection

adapters/tests/
├── azure.rs             # Unit tests (wiremock)
└── azure_live.rs        # Live integration tests (#[ignore])
```

## Implementation Phases

### Phase 1: Core Changes (ContentFiltered)

Small, cross-cutting change to the core crate. Must be done first as the adapter depends on it.

1. Add `StreamErrorKind::ContentFiltered` variant
2. Add `AssistantMessageEvent::error_content_filtered()` constructor
3. Add `AgentError::ContentFiltered` variant (non-retryable)
4. Update loop error mapping in `loop_.rs`
5. Tests for new variant

### Phase 2: Azure AD Token Cache

Internal module for OAuth2 client credentials flow.

1. `CachedToken` struct (token + expiry)
2. `TokenCache` with `Arc<RwLock<Option<CachedToken>>>` + refresh logic
3. Token acquisition via `reqwest::Client` POST to Entra ID endpoint
4. Proactive refresh margin (5 min before expiry)
5. Tests with mock token endpoint

### Phase 3: AzureAuth & Constructor

Replace the current `new(base_url, api_key)` with `new(base_url, AzureAuth)`.

1. `AzureAuth` enum definition
2. New `AzureStreamFn` struct fields (auth + token_cache)
3. Updated `new()` constructor
4. Updated `Debug` impl (redact all credentials)
5. Breaking change to existing stub API (acceptable — stub status)

### Phase 4: Stream Enhancement

Extend the streaming pipeline for content filter detection.

1. Check `finish_reason: "content_filter"` in SSE chunks
2. Check `content_filter_results.*.filtered == true` in chunks
3. Emit `ContentFiltered` error event instead of normal Done
4. Parse Azure error bodies for `ContentFilterBlocked` code on HTTP errors
5. Auth header selection based on `AzureAuth` variant (api-key vs Bearer)
6. Tests for content filter scenarios

### Phase 5: Live Tests

Integration tests against real Azure OpenAI deployment.

1. Text streaming test
2. Tool call test
3. Error handling test (invalid key, bad deployment)
4. Content filter test (if triggerable)

## Complexity Tracking

No constitution violations — no entries needed.
