Infrastructure

Microsoft Foundry

Both directions on one page, plus the whole Microsoft service map. Outbound — foundry() calls a Foundry project over the api-version-free v1 route, and foundryLocal() calls an on-device model with no key at all. Inbound — foundryResponsesHost serves the Foundry Toolkit's hosted-agent contract on the AgentHost port. entraIdentity() vends Azure tokens to tools. And an honest status column — nothing here has been run against live Azure.

Foundry turns up twice in an agent's life, and the two halves are independent:

  • Outbound — the model your agent calls. foundry() for a Foundry project, foundryLocal() for a model running on this machine. Both are ordinary LLMProviders, so the agent code above them does not change.
  • Inbound — the door requests arrive at. foundryResponsesHost() speaks the Foundry Toolkit's Agent Inspector contract on the same AgentHost port every other host uses.

Take either, both, or neither. Nothing on one half constrains the other. Beside them sits entraIdentity(), which vends Azure access tokens to your tools — a third thing again, and the identity section below says where its edge is.

Read the Status column before you read anything else

This is the youngest of the three provider columns, and the one honest sentence that governs the whole page is this: no adapter here has ever been run against live Azure. There is no Azure subscription behind this repository. The AWS and Google Cloud pages carry rows marked field-validated because those adapters answered real requests from real services in live accounts; that phrase appears nowhere on this page, and it will not until somebody runs one. What these adapters do carry is written out row by row below, and it is worth having — it is just not that.

Service → adapter map

Every row: what Microsoft calls it, which adapter fills it, which door exports it, which peer dependency it lazily requires, what it covers — and what it does not.

Microsoft serviceAdapterDoorPeer depOps coveredStatusCapability page
Foundry project inferencefoundry()agentfootprint/providersopenai — plus @azure/identity, loaded lazily and only on the zero-config path where you pass neither credential nor apiKeyChat completions, complete() and stream(), on the v1 route derived from the project endpoint by simple suffixing (+ /openai/v1), with the deployment name travelling as the wire's model field. Not the classic deployment-scoped route and not an api-version query — that is the row below. Not multi-modal input, not JSON mode, not listing deployments or any other control-plane callPinned by unit tests, and wire-verified against a LOCAL FAKE — never against live Azure. test/adapters/integration/foundry-wire.test.ts drives the real openai SDK over a real socket into a fake of the /openai/v1 route and records what actually left the process: the path under /api/projects/…, Authorization: Bearer and no api-key header, max_completion_tokens and never max_tokens, stream_options.include_usage on a stream, a rotated token reaching the second request, and the audience the credential was asked for. That proves the request this library sends. It proves nothing about how Azure answers — see Verified howbelow
Classic Azure OpenAI (a resource endpoint)azureOpenai() — keyless since 9.74.0agentfootprint/providersopenai. @azure/identity only because you construct the credential — this door never loads itDeployment-scoped chat completions, {endpoint}/openai/deployments/{d}/…, where apiVersion is required (option or AZURE_OPENAI_API_VERSION). A static key or an Entra credential, never both — the pair is refused by name as the config bug it is. Its default audience is the classic https://cognitiveservices.azure.com/.default, because that is the audience its route documentsPinned by unit tests, and wire-verified against a LOCAL FAKE — never against live Azure. test/adapters/integration/azure-openai-wire.test.ts drives the real openai SDK against a local fake: both endpoint spellings resolve to one identical URL, the keyed path sends the api-key header and never a Bearer, the keyless path sends Authorization: Bearer with no api-key header, an ambient AZURE_OPENAI_API_KEY does not collide with an explicit credential, and a streamed turn reports the usage the wire sentOpenAI
Foundry Local (on-device)foundryLocal()agentfootprint/providersnonefetch onlyPOST {endpoint}/v1/chat/completions, complete and SSE stream, with stream_options: { include_usage: true } always sent so streamed token counts are real numbers. Alias → variant resolution through GET {endpoint}/foundry/list, first match wins, cached per provider hit or miss. Not forced tool choice (carriesForcedToolChoice is false), not multi-modal, not prompt caching, not a structured thinking fieldPinned by unit tests against an injected fake fetch, so the suite runs offline — never run against a real Foundry Local install here. Endpoint and catalog resolution, SSE streaming with usage on the final chunk, a mid-stream error frame raised rather than reported as a clean stop, and both typed refusals are coveredbelow
Microsoft Entra ID (identity for tools)entraIdentity()agentfootprint/security@azure/identity — an optional peer, required the first time this provider vendsMachine tokens from the DefaultAzureCredential chain — environment service principal, workload identity, managed identity, VS Code, Azure CLI, Azure PowerShell, Azure Developer CLI. Scopes ride per call; AZURE_AI_SCOPE is the default. mode: 'user' and a per-request userToken are REFUSED BY NAME — there is no on-behalf-of surface yet, and a machine token in their place would be a silent downgrade9.74.0 — contract-shaped and injection-tested; awaiting field use. The _credential and _sdk seams are how the suite runs with no package and no Azure account — which is also to say that no token has ever been minted here by real EntraIdentity & credentials
Foundry Toolkit hosted agent (Agent Inspector)foundryResponsesHost() + responsesWireagentfootprint/hostingnone — plain HTTPPort 8088; POST /responses takes a turn, a bare HEAD on the same path answers the capability probe with 204, GET /readiness answers {"status":"healthy"}. stream: true in the body selects the nine-event Responses lifecycle. Not the full Responses API — no retrieval or storage, no background mode, no tool-call continuation — and not the Workflow Visualizer9.65.0 — really verified, on the one thing here that could be. Passes the same host conformance suite as nodeHost and agentCoreRuntimeHost over a real socket, plus a contract suite carrying shapes captured from a real Toolkit 1.6.9 Agent Inspector session, where connection, streaming and final-answer rendering were observed live. That is evidence about VS Code on a laptop — not about Azure. Cloud-hosted Inspector behaviour and Foundry Hosted Agent deployment are not covered and not claimedHosting & runtime
Azure Monitor / Application Insightsagentfootprint/observe🔴 No adapter — queued for a later train. The App Insights sink was deliberately held out of 9.74.0 rather than shipped unverifiedObservability sinks
Azure AI Searchagentfootprint/memory🔴 No adapter — queued for a later train. Bring your own behind the MemoryStore port until thenMemory & stores
Foundry Toolbox (managed MCP)agentfootprint/providers🔴 No adapter — queued for a later train. The port it would fill is mcpClient, which exists and is not Foundry-specificTools & gateways
Evaluations🔴 No adapter — queued for a later train. This library ships the typed event stream and never a judge; the grader stays yoursObservability sinks
The A2A dooragentfootprint/hosting⏸️ Parked — preview, and unverifiable without a live account. There is no Foundry A2A adapter. The vendor-neutral a2aWire does ship and composes with httpHost on paths of your ownHosting & runtime
Browser automation / computer use⏸️ Parked — preview-only, and there is no account here to verify it against. Wrap what you already have as a defineTool or an MCP toolTools
Managed Memoryagentfootprint/memory⏸️ Parked — preview-only, unverifiable without a live account. Every other MemoryStore adapter still fits behind the same portMemory & stores
Control plane (creating resources, projects, deployments)🔴 Bridge it yourself — azd, the Azure SDK, Bicep, Terraform or the portal. agentfootprint is runtime-only, and the control plane is a different token audience besides

The two rungs this page reaches, and the one it does not

Pinned by unit tests is the floor every row stands on: behaviour, refusals and option handling exercised through injected seams — _client, _credential, _sdk, a fake fetch. Nothing about a real service is proved by it.

Wire-verified against a local fake is one rung up, and only foundry() and azureOpenai() reach it. The real openai SDK talks over a real socket to a local fake of the route, and the test asserts what left the process rather than what the code meant to send. It is a strong proof of its own kind — it is what pins the dialect trap, where a bare baseURL would have quietly sent max_tokens and withheld stream_options on a route that wants neither. It is not a proof about the response Azure returns, the role a resource needs, or whether a deployment name resolves.

Field-validated — the rung the other two provider pages carry, where an adapter answered a real request from a real service in a live account — is absent here on purpose, because there is nothing to point at. An honest "not yet run against live Azure" is the whole value of this column; borrowing the other columns' credit would empty it.

The single exception is the inbound half, and only because the thing it talks to runs on a laptop rather than in a subscription: foundryResponsesHost was observed live against a real Foundry Toolkit Agent Inspector.

Before you start

This column's adapters do not earn a ten-step setup page of their own, so the whole of it is here. Everything below is a documented Microsoft fact this library is built on, rather than something measured here — which is exactly the distinction the Status column above is keeping.

Keyless inference needs a role on the resource, and it is not the obvious one. An Entra identity that can sign in is not an identity that can infer. The assignment is Cognitive Services OpenAI User (or Contributor) on the resource; the Foundry Models surface names Cognitive Services User. Without it the token mints perfectly and the inference call comes back 403.

Entra auth requires the resource's CUSTOM SUBDOMAIN. A regional endpoint answers 401 for a bearer token no matter how correct that token is. Keys work on either; only the keyless doors care.

A role assignment takes up to about five minutes to propagate. A 403 in the first minutes after assigning one is the expected shape of "not yet", not a misconfiguration. Wait it out before you change anything, or you will spend the afternoon debugging a problem that was fixing itself.

Locally, DefaultAzureCredential resolves your az login. In a hosted Foundry container the platform creates a per-agent Entra identity and injects FOUNDRY_PROJECT_ENDPOINT, so a deployed container needs neither — which is precisely what makes foundry() with no arguments a complete configuration there.

@azure/identity is an optional peer, and nothing loads it for you unless a keyless door needs it. entraIdentity() requires it the first time it vends; foundry() requires it only on the zero-config path where you name neither credential nor apiKey. Build a credential yourself and you are installing the package for your own new DefaultAzureCredential(), not for this library. An api key needs it not at all.

npm install @azure/identity     # only for the keyless doors

Foundry Local needs no key, and its port is DYNAMIC. There is no account and nothing to authenticate; foundry server status prints the live URL, because every foundry server start may pick a new port.

Outbound — calling a Foundry model

foundry() — a Foundry project

Point at a Foundry project, name a deployment, say which identity signs. Inside a hosted Foundry container the first two are already there, so this is a complete configuration:

import { Agent } from 'agentfootprint';
import { foundry } from 'agentfootprint/providers';

const agent = Agent.create({
  provider: foundry(),   // endpoint injected by the platform; managed identity signs
  model: 'foundry',      // the shorthand for "the deployment this provider was configured with"
}).build();

Two environment variables stand in for the two required options. FOUNDRY_PROJECT_ENDPOINT carries the project — hosted Foundry containers get it auto-injected, which is what makes the call above zero-config — and AZURE_AI_MODEL_DEPLOYMENT_NAME (the azd scaffolding convention, so a template-provisioned app needs no extra wiring), then MODEL_NAME, carries the deployment. Neither is guessed: a missing one is refused by name where you typed foundry(...).

Anywhere else — a laptop, CI, your own container — name them and hand over a credential:

import { DefaultAzureCredential } from '@azure/identity';
import { foundry } from 'agentfootprint/providers';

const provider = foundry({
  projectEndpoint: 'https://my-acct.services.ai.azure.com/api/projects/my-proj',
  deployment: 'gpt-4o-128k',
  credential: new DefaultAzureCredential(),
});

FoundryProviderOptions in full:

OptionWhat it does
projectEndpointThe project endpoint, https://{account}.services.ai.azure.com/api/projects/{project}, copied from the portal's project Overview page. Env fallback FOUNDRY_PROJECT_ENDPOINT. Required (option or env)
deploymentFoundry's "model" is the deployment name. Env fallbacks AZURE_AI_MODEL_DEPLOYMENT_NAME, then MODEL_NAME. Required (option or env)
credentialKeyless Entra ID — any @azure/identity credential (DefaultAzureCredential, ManagedIdentityCredential, …). Duck-typed, so this file never imports that SDK. A token is minted before every request, so MSAL's cache does the pacing and an expired token is a fresh token rather than a 401
apiKeyA static key — the v1 route accepts a key as a Bearer too. A function here is re-read before every request
scopeThe token audience for the credential doors. Default https://ai.azure.com/.default
reasoningDeclare an o-series deployment (o1 / o3 / o4-mini)
defaultMaxTokensToken cap when the request doesn't set one

Three doors, and one refused ambiguity. credential is keyless Entra ID; apiKey is a static key or a callback; neither constructs a DefaultAzureCredential from the optional @azure/identity peer, which is the platform's own blessed pattern inside a hosted container. Passing both credential and apiKey is refused by name — two credentials is a config bug, not extra security, and whichever one this factory silently preferred would be the one you did not think was in use.

The URL derivation, stated so it is not a mystery. A project endpoint serves the v1 inference route by simple suffixing: + /openai/v1. No api-version query, no deployment-scoped path — the wire's model field carries the deployment name instead. foundryInferenceUrl(projectEndpoint) is exported if you want the derived URL yourself; it trims trailing slashes, is idempotent (an endpoint already ending in /openai/v1 comes back unchanged), and refuses anything that is not https:// with /api/projects/ in it — cleartext to loopback is the one carve-out, because that is how the wire tests drive a local fake. That refusal is worth having: a resource root or an ARM URL passed here would fail as a 404 or a 401 far away from the typo.

One audience, and it is not interchangeable. The tokens minted here are for https://ai.azure.com/.default — the data plane, which is what every Foundry and Azure OpenAI inference call accepts. The ARM control plane (https://management.azure.com/.default) is a different audience, and Azure validates it on every call, so a management token against inference is a 401 that no retry fixes. Both names are exported from agentfootprint/security as AZURE_AI_SCOPE and AZURE_MANAGEMENT_SCOPE, so nobody has to remember which string is which — see Identity & credentials.

Three ceilings, named rather than worked around:

  • Deployment names hide the model. o-series auto-detection cannot work on an arbitrary deployment name, so reasoning: true must be declared to omit temperature and send the developer role.
  • Inference only. Listing deployments or creating resources is control-plane work on a different audience, and this provider does not do it.
  • Everything openai() does not do, this does not do either — no multi-modal input, no JSON mode. It is the same machinery underneath.

foundryLocal() — a model on this machine

Foundry Local is Microsoft's on-device runtime: ONNX under the hood, models pulled with foundry model run <alias>, no key and no account. The adapter talks to it over fetch alone — no SDK, no peer dependency — the same way ollama() does:

import { Agent } from 'agentfootprint';
import { foundryLocal } from 'agentfootprint/providers';

const agent = Agent.create({
  provider: foundryLocal('qwen2.5-0.5b'),
  model: 'qwen2.5-0.5b',
}).build();

The port is dynamic, and that is the first thing to know. Every foundry server start may pick a new port. http://localhost:5272 is the default here because it is the port in Microsoft's own REST walkthrough — it is an example, never a promise. foundry server status prints the live URL; foundry server start --port <p> pins one. When the service is somewhere else, say so:

foundryLocal('qwen2.5-0.5b', { endpoint: 'http://127.0.0.1:57127' });
OptionWhat it does
endpointThe service ROOT — this adapter appends /v1/chat/completions and its own lookup paths. A trailing /v1 is trimmed and a bare host:port gets http://. Defaults to FOUNDRY_LOCAL_ENDPOINT, then FOUNDRY_LOCAL_BASE_URL, then http://localhost:5272
managerA foundry-local-sdk FoundryLocalManager, duck-typed — this package never imports that SDK. manager.urls[0] is the manager's discovered URL, so a consumer already using the SDK gets the real dynamic port for free. An explicit endpoint still wins
defaultModelModel used when the request model is the 'foundry-local' shorthand. The positional form (foundryLocal('qwen2.5-0.5b')) sets this
defaultMaxTokensToken cap when the request doesn't set one (maps to max_tokens)
timeoutMsHow long to wait for the service to answer — default 10 000 ms. It bounds the wait for response headers, not generation: a laptop model may take minutes to finish a long answer and that is fine

Aliases and full model ids are different things. The catalog speaks in aliases (qwen2.5-0.5b) that fan out to hardware variants (qwen2.5-0.5b-instruct-generic-cpu:1), and REST chat calls take the full variant id. So an alias is resolved through the service's own catalog — first matching variant wins, because the list's order is the service's priority order — and the answer is cached for the life of the provider. A name that already carries a variant's execution-provider suffix (-cpu, -gpu, -npu, optionally :version) is used as-is with no catalog round-trip. If the catalog is silent, the name you wrote is sent unchanged, so the chat call's own error still names the model you actually asked for.

No API key exists on this wire, so none is sent. Microsoft's own samples pass placeholders; this adapter sends no Authorization header at all. There is nothing to put in one, and an invented value would only end up in somebody's proxy log.

The factory's option bag is FoundryLocalProviderOptions, and the class form FoundryLocalProvider wraps the factory the way OllamaProvider does. The credential option on foundry() is typed by two small duck interfaces, TokenCredentialLike and AccessTokenLike — the @azure/core-auth shapes, declared in this library so no import of @azure/identity is ever required just to name a type.

The two failures it actually has get a type. FoundryLocalUnavailableError carries reason ('service-unreachable' or 'model-not-available'), the endpoint it tried, the model, and — when the service could tell us — the models this machine does have cached. The messages are the fix: an unreachable service teaches foundry server start and foundry server status, because with a dynamic port "nothing is answering" is usually a stale URL rather than a stopped service; a missing model teaches foundry model run <model>.

Ceilings, stated rather than discovered in a demo:

  • No forced tool choice. tool_choice is undocumented on this wire, so carriesForcedToolChoice is false and an agent using .outputSchema(parser, { strategy: 'tool-forced' }) refuses at run start, naming this provider.
  • Tool calling is model-dependent, and this adapter does not preflight-refuse on it — a wrong refusal is worse than a weak answer. Pick a tool-capable variant.
  • No multi-modal input, no prompt caching, no structured thinking. A reasoning model's <think> tags ride the answer text untouched.

Streaming does report real token counts: stream_options: { include_usage: true } is always sent, because this is a documented Foundry Local surface rather than an arbitrary OpenAI-compatible server — so cost recorders and budgets read numbers instead of zeros.

Letting the environment choose

providerFromEnv() reads the environment and returns the configured provider, so a deployment switches by .env rather than by code. Both Foundry doors sit in its order:

PositionIf these are setResolves to
1OLLAMA_MODELollama()
2FOUNDRY_LOCAL_MODEL (+ optional FOUNDRY_LOCAL_ENDPOINT | FOUNDRY_LOCAL_BASE_URL)foundryLocal()
3FOUNDRY_PROJECT_ENDPOINT + (AZURE_AI_MODEL_DEPLOYMENT_NAME | MODEL_NAME)foundry()
4AZURE_OPENAI_API_KEY + an Azure endpoint + api-version + deploymentazureOpenai()
5ANTHROPIC_API_KEYanthropic()
6OPENAI_API_KEYopenai()

Two placements are deliberate, and both are about what a variable means:

  • A local model name outranks everything, FOUNDRY_PROJECT_ENDPOINT included. FOUNDRY_LOCAL_MODEL is a name somebody chose and typed for this run; the project endpoint can be injected by the platform, and an injected variable must never beat a hand-typed one. (An endpoint variable alone is not a trigger either — people export those just to run the service.)
  • The Foundry endpoint beats a leftover Azure key — when a deployment is named. The credential arms trigger on a credential, and credentials linger in a shell by accident; FOUNDRY_PROJECT_ENDPOINT + AZURE_AI_MODEL_DEPLOYMENT_NAME is a pair of product-specific spellings nobody exports by mistake, so together they make Foundry the declared destination. The generic MODEL_NAME alone does not carry the endpoint past a bootable Azure config — an env that resolved to azureOpenai() yesterday keeps resolving to it when the hosted platform injects the endpoint today.

FOUNDRY_PROJECT_ENDPOINT with no deployment named holds its refusal rather than throwing: the hosted platform injects that variable into every container it runs, including containers whose agent never asked for Foundry inference, so every arm below still answers exactly as it always did — and only if nothing else in the environment resolves is the held refusal raised, naming the one variable to set. Detection is env-only: no socket is opened to see whether anything is up, so the answer is identical on a laptop and in CI, and a service that is down reports itself from the call, with the fix in the message.

Identity — entraIdentity()

The doors above authenticate the model call. entraIdentity() is the other half: it vends an Entra access token to your tools, through the CredentialProvider port, so a tool that calls an Azure API uses the deployment's own identity without holding a secret.

import { entraIdentity } from 'agentfootprint/security';

const agent = Agent.create({ provider, model, credentials: entraIdentity() }).build();

What it vends: a bearer token from the DefaultAzureCredential chain — environment service principal, workload identity, managed identity, VS Code, Azure CLI, Azure PowerShell, Azure Developer CLI, in that order. On a laptop that is your az login; on a deployed container it is the managed identity, with no configuration in between. It caches the credential, never a token, so MSAL's own refresh does the pacing.

The audience split, in one sentence: a token is minted for exactly one audience, so AZURE_AI_SCOPE (https://ai.azure.com/.default, the inference data plane, and this provider's default) is a 401 on the ARM control plane and AZURE_MANAGEMENT_SCOPE (https://management.azure.com/.default) is a 401 on inference — which is why both are exported by name instead of left as strings to remember.

What it refuses, by name: mode: 'user' and a per-request userToken. Entra's on-behalf-of flow needs a confidential client app registration this adapter does not hold, and answering a delegated request with a machine token would succeed and be wrong — the call would run with the agent's access rather than the person's, with nothing downstream able to tell. OBO is a later train, and it will be its own provider rather than a flag here. A services allowlist is refused the same way when a request names something outside it.

A bring-your-own TokenCredential for inference does not go through this provider at all: pass it straight to the door, as foundry({ credential }) or azureOpenai({ credential }).

The full treatment — the port, the three exported scope constants, the worked tool, and how this sits beside agentCoreIdentity and googleIdentity — is on Identity & credentials.

Inbound — hosting your agent for Agent Inspector

This half is a hosting adapter, not a model provider. It is about the door requests arrive at. Which model the agent calls is the outbound decision above — foundry() for a project, foundryLocal() for a model on this machine — and nothing in this section changes it.

The Foundry Toolkit for VS Code talks to a locally hosted agent over the Responses protocol. foundryResponsesHost speaks that contract on the same AgentHost port every other host uses, so the Inspector can invoke an agent you already built:

import { foundryResponsesHost, memorySessions, standingAgent } from 'agentfootprint/hosting';

const handle = await standingAgent({
  agent,
  sessions: memorySessions(),
  host: foundryResponsesHost(),   // 8088 — HEAD/POST /responses, GET /readiness
});
process.on('SIGTERM', () => void handle.close());

Point Agent Inspector at http://localhost:8088 and chat. The runnable, self-testing version is examples/deploy/foundry-responses.ts.

What the contract is, exactly

foundryResponsesHost is a configuration of httpHost — the shared HTTP machinery keeps its own promises (draining on close, aborting on disconnect, failing a handler that throws or answers nothing), and this adapter supplies only what is genuinely this runtime's:

  • FOUNDRY_INVOKE_PATH (/responses) takes a turn by POST, and answers the Inspector's capability probe — a bare HEAD — with 204.
  • FOUNDRY_READINESS_PATH (/readiness) answers {"status":"healthy"}.
  • DEFAULT_FOUNDRY_PORT is 8088. Pass port: 0 in tests for an ephemeral socket; every option lives on FoundryResponsesHostOptions (port, hostname, model, maxBodyBytes, or attach to your own server).
  • FOUNDRY_SESSION_FIELDS is the session-alias precedence the contract reads: conversation (object or bare string), then agent_session_id, then session_id — first present wins, and whichever alias the caller used lands on the same HostRequest.sessionId.
  • Streaming is chosen by stream: true in the body — not the Accept header — and a streamed success walks the nine-event Responses lifecycle from response.created through response.completed with stable ids and a monotonic sequence_number; a failure ends with response.failed.
  • A request body over the ceiling (default one MiB) is refused with the typed RequestTooLargeError refusal, read about on Hosting & runtime.
  • An uncaught handler exception is reported as a failed response carrying PUBLIC_FAILURE_MESSAGE — the thrown text never travels. A handler that chose reply.fail(error) chose its words for the caller, and those travel unchanged.

What it refuses, by name

Image and file input parts, non-message items such as function_call_output, and non-user roles are each answered 400 before your agent runs — never silently reduced to the text around them. The awaiting, artifact and sessions reply terminals have no shape in this protocol, so they fail explicitly rather than answering with an invented success.

Two more limits, stated so nobody discovers them in a demo: the adapter is the captured local-Inspector text subset, not the full Responses API (no response retrieval or storage, no background mode, no tool-call continuation) — and it does not feed the Toolkit's Workflow Visualizer. The Inspector renders the conversation; your agent's internal topology is readable from agentfootprint's own recorders, not from this wire.

responsesWire — the protocol without the vendor

Under the adapter sits responsesWire, the Responses protocol as a plain HttpWire with no vendor's spelling in it. A deployment that speaks Responses on paths of its own composes it with httpHost directly:

import { httpHost, responsesWire } from 'agentfootprint/hosting';

const host = httpHost({
  name: 'myResponsesHost',
  wire: responsesWire({ defaultModel: 'my-agent' }),
  invokePath: '/responses',
  healthPath: '/health',
});

ResponsesWireOptions carries the three seams a deployment configures: defaultModel (echoed in response objects when the request names none), sessionFields (the alias precedence; DEFAULT_SESSION_FIELDS is ['conversation', 'session_id'], and the Foundry adapter passes its own three-alias list), and health (the probe body).

Two of its readers are exported for tests and for dialects built beside it: readResponsesInput turns a Responses input — a bare string or user-message items — into the turn's text, refusing what it cannot carry, and readResponsesSession resolves a body's session aliases in precedence order.

What is NOT here

Absences are facts, so here they are stated rather than discovered. Two reasons recur below, and both are positions rather than apologies: several Microsoft surfaces are preview, and this library does not ship against an API it cannot verify; the rest are simply queued behind the auth-and-inference tier this column started with.

  • No live-Azure validation, of anything. There is no Azure subscription behind this repository, so no row above says field-validated and none will until somebody runs one. What the doors are built on is Microsoft's own documented wire, named on this page so you can check it against your resource before you trust it.
  • No Application Insights / Azure Monitor sink. Queued for a later train, and deliberately held out of 9.74.0 rather than shipped as an unverified recipe.
  • No Azure AI Search store. Queued. The MemoryStore port is what it will fill, and it is open today.
  • No Foundry Toolbox (managed MCP) transport. Queued. mcpClient and gatewayTransport are the vendor-neutral port underneath, and they are not Foundry-specific.
  • No Evaluations adapter. Queued — and when it comes it will ship the machinery, not a judge. Grading stays yours.
  • No A2A door for Foundry, no browser automation or computer use, and no managed Memory adapter. All three are preview surfaces, and none of them can be verified from here without a live account. Refusing to ship against an API nobody in this repository can call is the position, not an oversight — the vendor-neutral a2aWire ships regardless, because it is a protocol rather than a product.
  • No on-behalf-of, and no user-delegated Entra token. entraIdentity() refuses mode: 'user' and a per-request userToken by name rather than quietly vending the deployment's own identity in their place.
  • No control plane. Creating a resource, a project or a deployment, and listing what exists, are azd / Azure SDK / Bicep / Terraform work — on a different token audience, which is its own reason the inference doors do not reach for it.
  • No multi-modal input and no JSON mode on foundry(). It is openai()'s machinery underneath, so it inherits that ceiling exactly.
  • No forced tool choice, no prompt caching and no structured thinking on foundryLocal(). tool_choice is undocumented on that wire, so carriesForcedToolChoice is false and a 'tool-forced' output schema refuses at run start naming this provider — rather than claiming an undocumented field works.
  • No Workflow Visualizer topology from the inbound half. The Responses protocol carries no channel for it, and inventing one would claim a compatibility this has never demonstrated. Your agent's internal structure is readable from agentfootprint's own recorders instead.

Verified how

The hosting adapter passes the same host conformance suite as nodeHost and agentCoreRuntimeHost, over a real socket, plus a contract suite carrying the request and lifecycle shapes captured from a real Toolkit 1.6.9 Agent Inspector session — connection, streaming and final-answer rendering were observed live. Cloud-hosted Inspector behavior and Foundry Hosted Agent deployment are not covered, and are not claimed.

foundry() is proven against a fake Foundry wire driven by the real openai SDK over a real socket: the request lands on /openai/v1/chat/completions under the project path, auth arrives as Authorization: Bearer, the body carries max_completion_tokens and never max_tokens, a stream asks for stream_options.include_usage, and a token that rotates between calls reaches the second request. foundryLocal() is proven the same way its refusals promise — endpoint resolution, catalog resolution, SSE streaming with usage on the final chunk and both typed refusals — against an injected fake fetch, so the suite runs offline. Neither door is claimed as field-verified against a live Foundry account or a live Foundry Local install: the wire facts they are built on are Microsoft's own documented ones, named above so you can check them against your resource before you trust them.

azureOpenai() is proven the same way foundry() is, against its own route: the real openai SDK over a real socket into a local fake, where both endpoint spellings resolve to one identical deployment-scoped URL, the keyed path sends the api-key header and never a Bearer, the keyless path sends Authorization: Bearer with no api-key header and asks its credential for the classic https://cognitiveservices.azure.com/.default, an ambient AZURE_OPENAI_API_KEY does not collide with an explicit credential, and a streamed turn reports the usage the wire sent.

entraIdentity() is contract-shaped and injection-tested and nothing more: its _credential and _sdk seams are how the suite runs with no @azure/identity installed and no Azure account, which also means no token has ever been minted here by real Entra. Its refusals — mode: 'user', a per-request userToken, a service outside the allowlist, a credential that resolves but vends no token — are each pinned by name.

So the ceiling on this page, said once more because it is the point: what is proven is the request each door sends and the refusal each adapter raises. What is not proven — anywhere on this page — is how Azure answers.

Next steps

  • Identity & credentialsentraIdentity() in full, beside agentCoreIdentity and googleIdentity under one port.
  • OpenAIazureOpenai()'s own page, and the legacyEndpoint dial that foundry() sets for you.
  • Hosting & runtime — the AgentHost port foundryResponsesHost fills, the SessionLifecycle beside it, and the concurrency table.
  • Ollama — the other on-device door, and the adapter foundryLocal() is modelled on.
  • Infrastructure: ports & adapters — the pattern this page is one instance of.
  • AWS & Bedrock AgentCore · Google Cloud & Gemini — the same map for the two older columns, including the rows this one cannot yet claim.

On this page