Chapter 02 · Infrastructure

Compute & AI Services

Where the harness actually runs, and how it talks to large language models. The agent client is provider-agnostic — the same code path works against OpenRouter, Azure OpenAI, plain OpenAI, or any OpenAI-compatible gateway, selected by a single configuration switch.

Hosting options

The harness is a standard ASP.NET Core application, so it can run on any Azure compute service that supports .NET 10 containers. The choice comes down to how much operational overhead you want to take on, what scaling model fits your workload, and whether you need Docker-in-Docker for the sandbox executor.

Factor Container Apps App Service AKS
Best for Most deployments Simple / single-app Enterprise at scale
Scaling Event-driven, 0 → N Fixed / auto rules Full Kubernetes
Cost model Consumption or dedicated Always-on tiers Node pools
SignalR Built-in WebSocket Needs ARR affinity Ingress controller
Sandbox (Docker.DotNet) Needs dedicated plan Not supported Native Docker socket
Complexity Low–Medium Low High
Our recommendation

Container Apps for most teams. It handles the SignalR WebSocket requirement natively, scales to zero in dev, and supports the dedicated compute plan needed for Docker.DotNet sandbox isolation.

Container Apps layout

The harness deploys as three Container Apps, each with a distinct responsibility. Splitting them lets you scale the API and SignalR hub independently and isolate the MCP server for external consumers.

Container Apps Environment
harness-api
Main ASP.NET Core API. Hosts the agent runtime, MediatR pipeline, CQRS handlers.
Ingress: external, port 8080
harness-hub
AgentHub SignalR server. Real-time agent-to-UI streaming. Needs sticky sessions.
Ingress: external, port 8081
harness-mcp
MCP Server. Exposes tools/prompts/resources via Model Context Protocol over HTTP. JWT-authenticated.
Ingress: internal or external
i
Health probes on harness-mcp: don't point them at an authed endpoint

The MCP server is fail-closed: when authentication is configured, a fallback authorization policy (RequireAuthenticatedUser) protects every endpoint that lacks explicit authorization metadata. An unauthenticated liveness/readiness probe hitting the MCP endpoint (or any other route) therefore gets a 401, which Container Apps reads as an unhealthy replica and will restart in a crash loop. Configure the probe as a TCP probe against the container port, or expose a dedicated anonymous /health endpoint (map it with .AllowAnonymous()) and point the probe there. Do not relax the fallback policy to make an authed route probe-friendly.

Bicep example

A representative Container App resource definition for the main API. Note how secrets reference Key Vault via managed identity rather than embedding values.

bicep
resource harnessApi 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'harness-api'
  location: location
  properties: {
    environmentId: containerAppEnv.id
    configuration: {
      ingress: {
        external: true
        targetPort: 8080
        transport: 'auto'
      }
      secrets: [
        {
          name: 'openai-key'
          keyVaultUrl: '${keyVault.properties.vaultUri}secrets/AzureOpenAI-ApiKey'
          identity: managedIdentity.id
        }
      ]
    }
    template: {
      containers: [
        {
          name: 'harness-api'
          image: '${containerRegistry}/harness-api:${imageTag}'
          resources: { cpu: json('1.0'), memory: '2Gi' }
          env: [
            {
              name: 'AI__AgentFramework__AzureOpenAI__Endpoint'
              value: openAiEndpoint
            }
            {
              name: 'AI__AgentFramework__AzureOpenAI__ApiKey'
              secretRef: 'openai-key'
            }
          ]
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 10
        rules: [
          {
            name: 'http-rule'
            http: { metadata: { concurrentRequests: '50' } }
          }
        ]
      }
    }
  }
}

AI providers

The harness needs two model surfaces: a chat-completion model for the agent runtime, and an embedding model for the RAG pipeline. The chat surface is provider-agnostic — one configuration block (AI:AgentFramework) selects between OpenRouter, Azure OpenAI, plain OpenAI, Anthropic, or any OpenAI-compatible gateway. Embeddings use a separate block (AI:Embedding) and currently expect an OpenAI or Azure OpenAI endpoint.

Agent model (chat completion)

Powers conversation, tool selection, and reasoning — the model that decides when to call tools, how to interpret results, and what to respond. The ClientType switch picks the SDK path; everything else is just an endpoint, a model identifier, and credentials.

Provider ClientType Endpoint Notes
OpenRouter (default) OpenAI https://openrouter.ai/api/v1 OpenAI-compatible gateway. DefaultDeployment is a slug like anthropic/claude-sonnet-4.6. Single key unlocks ~100 model families.
Azure OpenAI AzureOpenAI Your Azure OpenAI resource URL DefaultDeployment is the deployment name from Azure AI Foundry, not the model name. Supports managed identity.
OpenAI (direct) OpenAI https://api.openai.com/v1 DefaultDeployment is the model id (gpt-4o etc.).
Anthropic / Claude (via Azure Foundry) Anthropic AppConfig:AI:AgentFramework:Endpoint (your Foundry resource) DefaultDeployment is the model id (e.g. claude-sonnet-4-6). Claude ships through Azure AI Foundry, not a direct connection: the Anthropic SDK builds api.anthropic.com URLs, and AzureFoundryRewritingHandler rewrites each request to the configured Foundry endpoint (native Messages API at /anthropic/v1/messages), keeping the x-api-key header.
Azure AI Foundry (inference) AzureAIInference Your Foundry project inference endpoint Non-OpenAI models (Claude, Mistral, …) deployed via Foundry, called through the Azure AI Inference SDK.
Azure AI Foundry (hosted agent) PersistentAgents Foundry project endpoint Pre-configured, server-side-stateful agents managed by Foundry. Entra ID auth from AppConfig:AI:AIFoundry.
Azure AI Foundry (responses agent) FoundryResponses Foundry project endpoint Direct-inference Foundry agent built via AIProjectClient.AsAIAgent(...) with harness-composed model/instructions/tools. Yields an AIAgent (not an IChatClient), so it wires in at the agent-factory level.
i
Foundry hosted agents

The harness ships Microsoft.Agents.AI.Foundry support and a dedicated Presentation.FoundryHost host. The PersistentAgents and FoundryResponses client types let you run agents backed by Azure AI Foundry — either server-managed (persistent state) or built at runtime from a Foundry project endpoint — while keeping the full harness middleware pipeline (OpenTelemetry, content safety, function-invocation limits) via the client-factory hook.

Embedding model

Generates vector representations for document chunks during ingestion and queries during retrieval. Configured separately from the chat client and currently expects an OpenAI or Azure OpenAI endpoint — OpenRouter does not yet expose an embeddings surface, so even with OpenRouter selected as the chat provider you still need an OpenAI-style embedding endpoint if you want RAG or the knowledge graph.

Configuration mapping

Each code-level config path maps to a specific resource or setting. Use this table to trace from appsettings.json (or user-secrets) to the upstream provider.

Code Config Path Upstream What It Controls
AI:AgentFramework:ClientType Selector — OpenAI / AzureOpenAI / Anthropic Which SDK path the chat client takes
AI:AgentFramework:Endpoint Provider URL Where chat completions are sent
AI:AgentFramework:DefaultDeployment Model identifier or deployment name Which model is invoked
AI:AgentFramework:ApiKey Provider API key (or managed identity for Azure OpenAI) Authentication
AI:Embedding:Endpoint OpenAI or Azure OpenAI resource endpoint Embedding generation
AI:Embedding:DeploymentName Embedding deployment / model id Embedding model
AI:Embedding:Dimensions N/A (client-side config) Vector dimensions (1536 / 3072)
i
Missing-credentials are surfaced, not silenced

When credentials aren't configured, the harness exposes the gap in three places: the /api/config/status endpoint (consumed by the WebUI banner), the /health/ai health check, and — in Development only — a surfaced chat error with the offending config keys. See AiProviderStatus and AiProviderNotConfiguredException.

!
Token limits matter

Most chat models have large context windows, but the harness uses a context budget system (see the Developer Guide) to stay well under. Plan your provider quota (TPM — Tokens Per Minute) based on expected concurrent users. A single agent turn can consume 4K–20K tokens depending on tool use and RAG context. OpenRouter exposes a single account-wide credit pool; Azure OpenAI quotas are per-deployment.

Azure-specific patterns

When the chat provider is Azure OpenAI (directly or via Azure AI Foundry), two Azure-native controls apply that the third-party gateways don't expose.

i
Prefer managed identity

For Azure OpenAI in production, managed identity beats API keys. Set AI:AgentFramework:UseManagedIdentity: true and assign the Cognitive Services OpenAI User role to your Container App's managed identity. The ApiKey field can then be left empty.

i
"Deployment" in Azure OpenAI

In Azure OpenAI, a deployment is a named instance of a specific model. The harness's DefaultDeployment takes that deployment name — not the model name. If your Azure portal shows no deployments, create one in Azure AI Foundry first.