Use Case 028: LLM Provider Account & Deployment Management
Overview
| Property |
Value |
| Use Case ID |
UC-028 |
| Use Case Name |
LLM Provider Account & Deployment Management |
| Module |
Agent Identity — LLM Gateway |
| Priority |
High |
| Status |
✅ Implemented |
| Version |
1.0 |
| Last Updated |
June 15, 2026 |
Implementation status (agent-identity release, June 2026). Implemented: provider accounts, the model catalog (ModelCatalogEntry), and model deployments (ModelDeployment), administered from the AI Gateway group (/ai-gateway/provider-accounts, /ai-gateway/models, /ai-gateway/deployments). Several names below differ from the shipped code: the account field is ApiKeySecretName (plus the KeySource/EncryptedApiKey additions in the next note), not Credential; the account uses Enabled (bool), not a Status enum, and TenantId scoping rather than an OwnedBy enum. Gaps vs. this spec: pricing is a single per-model rate (InputCostPer1KTokens / OutputCostPer1KTokens) with no PricingVersion / PricingAuthority / override layering; deployment health-endpoint tracking (UC-028e) is not implemented; provider subscriptions live in a separate ProviderSubscription entity (UC-035).
Description
This use case describes how administrators and platform stewards register and govern the LLM provider surface that the Application Manager (AM) gateway dispatches against: provider accounts (the credentialed relationship with Anthropic, OpenAI, AWS Bedrock, Azure OpenAI, Google, OpenRouter), the model catalog (per-model pricing, context windows, capabilities), and model deployments (the concrete, classification-ceilinged endpoints that task-type model chains resolve to). AM holds every provider credential; agents never see them.
This is part of the agent-identity release. The LLM Gateway subsystem absorbs the RAF-SVC-LLM specification — that service no longer ships standalone, and its model-registry / provider-account / deployment responsibilities become AM's gateway design. See the SDK master design riptide-sdk/docs/plans/AGENT-IDENTITY-ARCHITECTURE.md and the AM plan docs/internal/agent-identity-plan.md (§2).
Implementation note — API-key storage. This document is the original design spec; some field and command names below differ from the shipped code. In particular, a provider account does not always store the key itself. Each account carries a Key source that selects where its API key lives: Environment variable (the default — the account stores only the secret/env-var name and the key is read from the environment or configuration at call time, never persisted by AM), Database (the key is stored in the Identity database encrypted at rest via ASP.NET Core Data Protection under the purpose LlmProviderKeys), or Vault (the account stores a secret name resolved from AM's configured Azure Key Vault). Accounts created before this option default to Environment variable. For the administrator-facing workflow — including the guided setup wizard and live connectivity check — see AI Gateway & Model Governance.
Actors
| Actor |
Description |
Role |
| Administrator / Platform Steward |
Holds llm.providers.write / llm.deployments.write / llm.gateway.config capabilities; manages accounts, models, deployments |
Primary |
| AM LLM Gateway |
The AM subsystem that stores accounts/models/deployments and resolves them at dispatch time |
Supporting |
| LLM Providers |
Anthropic, OpenAI, AWS Bedrock, Azure OpenAI, Google, OpenRouter (and local vLLM/RayServe endpoints) |
External |
| AuditLog |
AM's append-only audit surface; records every config change with before/after values and acting principal |
Supporting |
| AI Agent |
Downstream consumer whose AllowedTaskTypes chains resolve to the deployments registered here (read-only with respect to this UC) |
Supporting |
Preconditions
- AM is running; the Identity database has the LLM Gateway tables (
LlmProvider seed, LlmModelCatalog, ProviderAccount, ModelDeployment) applied via EF migration.
- The administrator is authenticated to the AM admin UI and holds the relevant capability (
llm.providers.write, llm.deployments.write, or llm.gateway.config).
- The
LlmProvider enum has been seeded (Anthropic, OpenAI, AwsBedrock, AzureOpenAi, Google, OpenRouter).
- Bundled
LlmModelCatalog rows (shipped with AM) are present from the seed migration.
- For tenant-scoped operations, the tenant exists and has a Top-tier budget cap (see UC-032).
Postconditions
Success Postconditions
- A
ProviderAccount is persisted with its credential encrypted under the ProviderAccountCredentials Data Protection purpose; the plaintext is never stored or returned.
LlmModelCatalog rows (bundled or tenant override) exist with a PricingVersion and EffectiveFrom, queryable by PricingAuthority.
ModelDeployment rows exist, each pointing at a ProviderAccount and LlmModelCatalog model, with a maxClassification ceiling and (optionally) a health-check endpoint.
- Every create / rotate / disable / override / deployment edit writes an
AuditLog row with before/after values and the acting principal.
- Deployments are resolvable by the task-type routing layer (UC-029) and chargeable by the cost-governance layer (UC-032/UC-034).
Failure Postconditions
- No
ProviderAccount persisted if the credential fails to encrypt or a required field is missing.
- Credential rotation that fails leaves the prior credential intact (rotation is atomic).
- A
ModelDeployment referencing an unknown (ProviderId, ModelId) catalog row is rejected (fail-closed).
- A tenant pricing override with an
EffectiveFrom in the past relative to an existing version, or a malformed price, is rejected; no row written.
- Error surfaced to the administrator via the admin UI; no partial state.
Primary Flow Sequence
sequenceDiagram
participant Admin
participant Web as ProviderAccountsController
participant MediatR
participant Protector as ProviderAccountCredentialProtector
participant Repo as IProviderAccountRepository
participant Audit as AuditLog
Admin->>Web: Submit Create Provider Account (provider, name, ownedBy, credential)
Web->>MediatR: Send CreateProviderAccountCommand
MediatR->>MediatR: Validate (provider enum, ownedBy, name, credential vs authMethod)
MediatR->>Protector: Protect(credential) [purpose=ProviderAccountCredentials]
Protector-->>MediatR: Encrypted credential blob
MediatR->>Repo: Add(ProviderAccount{status=Active})
Repo-->>MediatR: Persisted (Id)
MediatR->>Audit: Write CreateProviderAccount (no plaintext)
MediatR-->>Web: ProviderAccountId
Web-->>Admin: Redirect to account detail (credential masked, write-only)
UC-028a: Create a Provider Account
Triggers
- Administrator navigates to LLM Gateway > Provider Accounts > New and submits the form.
Basic Flow
- Administrator selects a provider from the
LlmProvider enum and an ownedBy value (Tenant = tenant supplies and pays the provider directly; Agency = agency operates a shared account and rebills).
- Administrator enters a display name, optional tenant scope (
tenantId nullable for agency-owned accounts), and the provider API key/credential.
- For local
authMethod=none deployments, the credential field may be left blank (nullable credential).
- Administrator optionally attaches a
Subscription value object (Monthly Commitment / Token Quota / Prepaid Credits — detail in UC-035).
CreateProviderAccountCommand validates the provider, ownership, name, and that a credential is present unless authMethod=none.
- The credential is encrypted via
ProviderAccountCredentialProtector (Data Protection purpose ProviderAccountCredentials).
- The
ProviderAccount is persisted with status = Active; multiple accounts per (tenant, provider) are allowed.
- An
AuditLog row is written (no plaintext credential).
- Administrator is redirected to the account detail page; the credential field renders as write-only (masked, never echoed).
Alternative Flows
- A1: Missing credential for cloud account — validation rejects when
authMethod requires a credential but none supplied; form redisplays with error.
- A2: Duplicate display name within (tenant, provider) — allowed (multi-account first-class), but a warning is surfaced.
- A3: Tenant lacks a Top-tier cap — agency-owned accounts proceed; tenant-owned creation surfaces a reminder that agent provisioning will block without cap headroom (UC-032).
UC-028b: Rotate or Disable a Provider Credential
Triggers
- Administrator clicks Rotate Credential or Disable on a provider account detail page.
Basic Flow (Rotate)
- Administrator enters the new credential into the write-only field and confirms.
RotateProviderAccountCredentialCommand encrypts the new value via ProviderAccountCredentialProtector.
- The new encrypted blob atomically replaces the old; the prior credential is overwritten only after the new one is successfully protected and persisted.
- An
AuditLog row records the rotation (acting principal, timestamp; never the secret value).
- In-flight dispatches complete against the configuration they started with; new dispatches use the rotated credential.
Basic Flow (Disable)
- Administrator clicks Disable; AM prompts for confirmation.
DisableProviderAccountCommand sets status = Disabled.
- Deployments backed by the disabled account become non-viable in the resolution cascade (UC-029); affected dispatches fall through to the next chain entry or return a terminal failure.
- An
AuditLog row is written.
Alternative Flows
- A1: Rotation fails mid-write — the prior credential remains valid; no partial state; error surfaced.
- A2: Disable an account with active deployments — allowed; the admin is warned which deployments lose their backing account.
sequenceDiagram
participant Admin
participant Web as ProviderAccountsController
participant MediatR
participant Protector as ProviderAccountCredentialProtector
participant Repo as IProviderAccountRepository
participant Audit as AuditLog
Admin->>Web: Submit Rotate Credential (new secret)
Web->>MediatR: Send RotateProviderAccountCredentialCommand
MediatR->>Protector: Protect(newSecret)
Protector-->>MediatR: New encrypted blob
MediatR->>Repo: Update credential (atomic swap)
alt Persist succeeds
Repo-->>MediatR: OK
MediatR->>Audit: Write RotateCredential (no plaintext)
MediatR-->>Web: Success
Web-->>Admin: Toast: credential rotated
else Persist fails
Repo-->>MediatR: Error
MediatR-->>Web: Failure (prior credential intact)
Web-->>Admin: Error; rotation not applied
end
UC-028c: Register or Override Model Catalog Pricing
Triggers
- Administrator navigates to LLM Gateway > Models and registers a new model or adds a tenant pricing override.
Basic Flow
- Administrator views the catalog; the
PricingAuthority filter toggles between External (pulled from a provider's published pricing) and Internal (set by IT for local-infrastructure chargeback).
- To register a model, the administrator supplies
(ProviderId, ModelId), input/output/cache-read/cache-write prices, contextWindow, maxOutputTokens, supportsStructuredOutput, and optional tokenizerRef.
RegisterModelCommand assigns a PricingVersion and EffectiveFrom, then persists the catalog row.
- To override pricing for a tenant, the administrator opens the override editor;
OverrideModelPricingCommand (tenant-scoped) writes a tenant override row layered over the bundled row, with a new PricingVersion and EffectiveFrom.
- Two pricing rows over the same model in the same period are normal once subscriptions are in play (the active subscription rate is snapshotted on the ledger — UC-034/UC-035).
- An
AuditLog row records the registration/override.
Alternative Flows
- A1: Unknown model referenced at dispatch — if a deployment or task chain references a
(ProviderId, ModelId) with no catalog row, the call is rejected (fail-closed) — never priced at $0 or guessed.
- A2: Override
EffectiveFrom in the past — rejected to preserve version monotonicity; the admin must use a future or current effective date.
- A3: Malformed price (negative / non-numeric) — validation rejects; no row written.
UC-028d: Create or Update a Model Deployment
Triggers
- Administrator navigates to LLM Gateway > Deployments > New (or edits an existing deployment).
Basic Flow
- Administrator selects a catalog model
(ProviderId, ModelId) and a backing ProviderAccount.
- Administrator sets
target (Cloud or Local), endpoint, authMethod, and maxClassification (the classification ceiling this deployment may handle).
- Administrator optionally sets a
healthCheckEndpoint.
CreateModelDeploymentCommand validates that the referenced catalog model exists (fail-closed if not) and that the provider account is Active.
- The
ModelDeployment is persisted with healthStatus = Unknown and lastHealthCheck = null until the first health probe.
- The deployment becomes a resolution target for task-type chains (UC-029) and a charge point for model-tier caps (UC-032).
- An
AuditLog row is written.
UpdateModelDeploymentCommand follows the same flow for edits, recording before/after values.
Alternative Flows
- A1: Catalog model missing — rejected (fail-closed); the admin is directed to register the model first (UC-028c).
- A2: Backing account disabled — rejected; the admin must re-enable the account or pick another.
- A3:
maxClassification lower than a task type's classificationFloor — allowed at create time, but the deployment will simply be filtered out for those task types at resolution (UC-029b).
UC-028e: Deployment Health Tracking
Triggers
- AM's health probe runs against a deployment's
healthCheckEndpoint, or GET /api/llm/health is called (sub-1s SLA, UC-030).
Basic Flow
- AM probes the deployment's
healthCheckEndpoint.
UpdateDeploymentHealthCommand records the result, setting healthStatus (Healthy, Degraded, Unhealthy) and lastHealthCheck timestamp.
- The resolution cascade (UC-029b) skips
Unhealthy deployments and prefers Healthy ones.
- The admin UI (
ModelDeploymentsController) surfaces the current healthStatus and lastHealthCheck.
Alternative Flows
- A1: No
healthCheckEndpoint configured — healthStatus stays Unknown; the deployment is treated as eligible but unverified.
- A2: Probe times out —
healthStatus = Unhealthy; deployment excluded from resolution until a subsequent successful probe.
API Endpoints
Provider-account, model, and deployment administration is performed through the AM admin UI controllers, which dispatch CQRS commands/queries via MediatR. The read endpoints below are exposed on the API project (X-Api-Key) for SDK and gateway consumption.
| Method |
Path |
Auth |
Purpose |
GET |
/api/llm/models |
X-Api-Key |
Model registry view (catalog rows, pricing, capabilities) |
POST |
/api/llm/admin/providers |
X-Api-Key + llm.providers.write |
Create provider account (credential write-only) |
POST |
/api/llm/admin/providers/{id}/rotate |
X-Api-Key + llm.providers.write |
Rotate provider credential |
POST |
/api/llm/admin/providers/{id}/disable |
X-Api-Key + llm.providers.write |
Disable provider account |
POST |
/api/llm/admin/models |
X-Api-Key + llm.gateway.config |
Register a model catalog row |
POST |
/api/llm/admin/models/{id}/override-pricing |
X-Api-Key + llm.gateway.config |
Tenant-scoped pricing override |
POST |
/api/llm/admin/deployments |
X-Api-Key + llm.deployments.write |
Create model deployment |
PUT |
/api/llm/admin/deployments/{id} |
X-Api-Key + llm.deployments.write |
Update model deployment |
GET |
/api/llm/admin/deployments |
X-Api-Key |
List deployments with health status |
Representative Request / Response
// POST /api/llm/admin/providers
// Request
{
"provider": "Anthropic",
"name": "Acme Tenant — Anthropic Prod",
"ownedBy": "Tenant",
"tenantId": "8f1c9e2a-4b6d-4a11-9c3e-2d7f5a8b1c20",
"authMethod": "ApiKey",
"credential": "sk-ant-REDACTED-PLAINTEXT-ONLY-IN-REQUEST",
"subscription": {
"type": "MonthlyCommitment",
"monthlyFee": 5000.0,
"overageBehavior": "BilledOverage"
}
}
// Response 201 Created
{
"id": "c4a7b210-9d3f-4e58-bf21-6a0c1e9d4477",
"provider": "Anthropic",
"name": "Acme Tenant — Anthropic Prod",
"ownedBy": "Tenant",
"tenantId": "8f1c9e2a-4b6d-4a11-9c3e-2d7f5a8b1c20",
"status": "Active",
"credentialMasked": "sk-ant-••••••••••••4477",
"subscriptionAttached": true,
"createdAt": "2026-06-09T14:32:11Z"
}
Business Rules
| Rule |
Description |
| BR-1 |
An account's Key source determines where its API key lives: EnvironmentVariable (default) and Vault store only a secret name and resolve the key at call time (AM never persists it); Database stores the key encrypted at rest via IDataProtector (purpose LlmProviderKeys). In no mode is a plaintext key returned to the UI/API or written to logs or the audit trail. |
| BR-2 |
The credential purpose string is distinct from AgentCredentials so a scoped key compromise cannot cross secret classes. |
| BR-3 |
Multiple ProviderAccounts per (tenant, provider) are first-class; no uniqueness constraint on that pair. |
| BR-4 |
ownedBy = Tenant accounts are tenant-paid; ownedBy = Agency accounts are agency-operated and rebilled. |
| BR-5 |
An unknown (ProviderId, ModelId) referenced at dispatch is rejected (fail-closed) — never priced or guessed. |
| BR-6 |
Model catalog rows are versioned (PricingVersion + EffectiveFrom); tenant override rows layer over bundled rows. |
| BR-7 |
PricingAuthority = External means provider-published pricing; Internal means IT-set local-infrastructure chargeback. |
| BR-8 |
A ModelDeployment must reference an existing catalog model and an Active provider account. |
| BR-9 |
Unhealthy deployments are excluded from the resolution cascade until a subsequent successful health probe. |
| BR-10 |
Credential rotation is atomic — a failed rotation leaves the prior credential intact. |
| BR-11 |
Every create / rotate / disable / override / deployment edit writes an AuditLog row with before/after values and acting principal. |
Data Requirements
LlmProvider (seeded enum)
| Field |
Type |
Constraints |
| Value |
enum |
One of Anthropic, OpenAI, AwsBedrock, AzureOpenAi, Google, OpenRouter (Google is one provider; Vertex vs. Gemini split at model level) |
LlmModelCatalog
| Field |
Type |
Constraints |
| Id |
Guid (UUIDv7) |
Primary key |
| ProviderId |
enum (LlmProvider) |
Part of logical key (ProviderId, ModelId) |
| ModelId |
string |
Max 200 chars; part of logical key |
| InputPrice |
decimal |
Per-token (or per-million) input price; ≥ 0 |
| OutputPrice |
decimal |
Per-token output price; ≥ 0 |
| CacheReadPrice |
decimal |
Cache-read price; ≥ 0 |
| CacheWritePrice |
decimal |
Cache-write price; ≥ 0 |
| PricingVersion |
int / string |
Monotonic per model |
| EffectiveFrom |
DateTimeOffset |
Not in the past relative to an existing version |
| PricingAuthority |
enum |
External or Internal |
| ContextWindow |
int |
> 0 |
| MaxOutputTokens |
int |
> 0 |
| SupportsStructuredOutput |
bool |
— |
| TokenizerRef |
string? |
Optional; used for local models |
| TenantId |
Guid? |
Null = bundled row; non-null = tenant override |
ProviderAccount
| Field |
Type |
Constraints |
| Id |
Guid (UUIDv7) |
Primary key |
| TenantId |
Guid? |
Null for agency-owned accounts |
| OwnedBy |
enum |
Tenant or Agency |
| Provider |
enum (LlmProvider) |
Required |
| Name |
string |
Max 200 chars, required |
| KeySource |
enum |
EnvironmentVariable (default), Database, or Vault — selects where the API key lives (see the implementation note above) |
| Credential |
string |
For EnvironmentVariable / Vault: the secret name to resolve (no key stored by AM). For Database: the key itself, encrypted at rest via Data Protection (purpose LlmProviderKeys). Nullable when authMethod = none. |
| Subscription |
value object? |
Optional (see UC-035) |
| Status |
enum |
Active, Disabled |
| CreatedAt |
DateTimeOffset |
Set on create |
ModelDeployment
| Field |
Type |
Constraints |
| Id |
Guid (UUIDv7) |
Primary key |
| ModelId |
string |
References a catalog (ProviderId, ModelId) |
| ProviderAccountId |
Guid |
FK to ProviderAccount (must be Active) |
| Target |
enum |
Cloud or Local |
| Endpoint |
string |
Max 500 chars |
| AuthMethod |
string |
e.g. ApiKey, Sigv4, none |
| MaxClassification |
enum/int |
Classification ceiling this deployment may handle |
| HealthCheckEndpoint |
string? |
Optional |
| HealthStatus |
enum |
Unknown, Healthy, Degraded, Unhealthy |
| LastHealthCheck |
DateTimeOffset? |
Null until first probe |
Security Considerations
- Authentication. Admin UI access uses AM cookie auth; API/gateway access uses
X-Api-Key. No anonymous access to provider, model, or deployment management.
- Authorization / capabilities. Gated by
llm.providers.write, llm.deployments.write, and llm.gateway.config capabilities composed into admin roles via the existing Role / Capability / RoleCapabilityMapping model.
- Data protection. Provider credentials encrypted via
IDataProtector purpose ProviderAccountCredentials, distinct from AgentCredentials. The credential field is write-only in the UI and API — never echoed, masked on display.
- Audit. Every create, rotate, disable, pricing override, and deployment edit is folded into
AuditLog with before/after values and acting principal. Plaintext credentials are never logged.
- PII handling. Provider account and deployment metadata carry no end-user PII; PII enforcement happens at dispatch time (UC-030). Pricing and chargeback data are tenant-scoped and not cross-tenant readable.
- Fail-closed. Unknown models and disabled/unhealthy backing infrastructure are excluded from dispatch rather than defaulted — no silent fallback to an unpriced or unverified path.
Testing Scenarios
| ID |
Scenario |
Expected Result |
| T-1 |
Create a cloud provider account with a valid credential |
Account persisted Active; credential encrypted under ProviderAccountCredentials; audit row written; credential never returned in plaintext |
| T-2 |
Create a local deployment account with authMethod=none and no credential |
Accepted; nullable credential allowed |
| T-3 |
Create a cloud account with authMethod=ApiKey and no credential |
Rejected by validation (UC-028a A1) |
| T-4 |
Create two accounts for the same (tenant, provider) |
Both persisted (multi-account first-class, BR-3) |
| T-5 |
Rotate a credential successfully |
New blob replaces old atomically; audit row written; in-flight dispatches unaffected |
| T-6 |
Rotation fails at persist |
Prior credential intact; no partial state; error surfaced (UC-028b A1) |
| T-7 |
Disable an account backing active deployments |
Status Disabled; deployments become non-viable; admin warned; audit row written |
| T-8 |
Register a new catalog model with all prices and capabilities |
Row persisted with PricingVersion + EffectiveFrom; audit row written |
| T-9 |
Add a tenant pricing override layered over a bundled row |
Override row written tenant-scoped; both visible under their PricingAuthority |
| T-10 |
Override with EffectiveFrom in the past |
Rejected (UC-028c A2) |
| T-11 |
Register a model with a negative input price |
Rejected (UC-028c A3) |
| T-12 |
Create a deployment referencing an unregistered (ProviderId, ModelId) |
Rejected fail-closed (BR-5, UC-028d A1) |
| T-13 |
Dispatch resolves to a deployment whose catalog row was deleted |
Call rejected fail-closed; never priced at $0 (BR-5) |
| T-14 |
Create a deployment on a disabled provider account |
Rejected (UC-028d A2) |
| T-15 |
Filter the model catalog by PricingAuthority = Internal |
Only IT-set local-chargeback rows returned |
| T-16 |
Health probe succeeds against a deployment |
healthStatus = Healthy, lastHealthCheck updated; deployment preferred in cascade |
| T-17 |
Health probe times out |
healthStatus = Unhealthy; deployment excluded from cascade (UC-028e A2) |
| T-18 |
Deployment with no healthCheckEndpoint |
healthStatus = Unknown; treated as eligible but unverified (UC-028e A1) |
| T-19 |
User without llm.providers.write attempts to create an account |
403 / access denied; no row written |
| T-20 |
Credential value appears in any API response, log, or audit row |
Never present — masked everywhere (security, BR-1) |
| T-21 |
GET /api/llm/models returns catalog |
Pricing, context window, capabilities present; tenant overrides reflected for the calling tenant |
| T-22 |
Attach a MonthlyCommitment subscription on account create |
Subscription value object stored; surfaced to UC-035 utilization tracking |
| T-23 |
Boundary: model with contextWindow = 0 |
Rejected by validation (must be > 0) |
| T-24 |
maxClassification below a task type's classificationFloor |
Deployment created but filtered out for that task type at resolution (UC-029b) |
- UC-029: Task-Type Routing & Agent Provisioning — task-type chains resolve to the
ModelDeployments registered here.
- UC-030: LLM Request Dispatch — the dispatch lifecycle that consumes provider accounts, models, and deployments.
- UC-035: Provider Subscriptions & Utilization — the
Subscription value object attached to ProviderAccount.
- UC-032: Budget Cap Lattice — model-tier caps point at
ModelDeployment rows.
Revision History
| Version |
Date |
Author |
Notes |
| 1.0 |
June 9, 2026 |
Platform Architecture Team |
Initial draft |