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 ordinaryLLMProviders, 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 sameAgentHostport 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 service | Adapter | Door | Peer dep | Ops covered | Status | Capability page |
|---|---|---|---|---|---|---|
| Foundry project inference | foundry() | agentfootprint/providers | openai — plus @azure/identity, loaded lazily and only on the zero-config path where you pass neither credential nor apiKey | Chat 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 call | Pinned 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 how | below |
| Classic Azure OpenAI (a resource endpoint) | azureOpenai() — keyless since 9.74.0 | agentfootprint/providers | openai. @azure/identity only because you construct the credential — this door never loads it | Deployment-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 documents | Pinned 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 sent | OpenAI |
| Foundry Local (on-device) | foundryLocal() | agentfootprint/providers | none — fetch only | POST {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 field | Pinned 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 covered | below |
| Microsoft Entra ID (identity for tools) | entraIdentity() | agentfootprint/security | @azure/identity — an optional peer, required the first time this provider vends | Machine 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 downgrade | 9.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 Entra | Identity & credentials |
| Foundry Toolkit hosted agent (Agent Inspector) | foundryResponsesHost() + responsesWire | agentfootprint/hosting | none — plain HTTP | Port 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 Visualizer | 9.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 claimed | Hosting & runtime |
| Azure Monitor / Application Insights | — | agentfootprint/observe | — | — | 🔴 No adapter — queued for a later train. The App Insights sink was deliberately held out of 9.74.0 rather than shipped unverified | Observability sinks |
| Azure AI Search | — | agentfootprint/memory | — | — | 🔴 No adapter — queued for a later train. Bring your own behind the MemoryStore port until then | Memory & 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-specific | Tools & gateways |
| Evaluations | — | — | — | — | 🔴 No adapter — queued for a later train. This library ships the typed event stream and never a judge; the grader stays yours | Observability sinks |
| The A2A door | — | agentfootprint/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 own | Hosting & 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 tool | Tools |
| Managed Memory | — | agentfootprint/memory | — | — | ⏸️ Parked — preview-only, unverifiable without a live account. Every other MemoryStore adapter still fits behind the same port | Memory & 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 doorsFoundry 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:
| Option | What it does |
|---|---|
projectEndpoint | The 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) |
deployment | Foundry's "model" is the deployment name. Env fallbacks AZURE_AI_MODEL_DEPLOYMENT_NAME, then MODEL_NAME. Required (option or env) |
credential | Keyless 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 |
apiKey | A static key — the v1 route accepts a key as a Bearer too. A function here is re-read before every request |
scope | The token audience for the credential doors. Default https://ai.azure.com/.default |
reasoning | Declare an o-series deployment (o1 / o3 / o4-mini) |
defaultMaxTokens | Token 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: truemust be declared to omittemperatureand send thedeveloperrole. - 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' });| Option | What it does |
|---|---|
endpoint | The 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 |
manager | A 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 |
defaultModel | Model used when the request model is the 'foundry-local' shorthand. The positional form (foundryLocal('qwen2.5-0.5b')) sets this |
defaultMaxTokens | Token cap when the request doesn't set one (maps to max_tokens) |
timeoutMs | How 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_choiceis undocumented on this wire, socarriesForcedToolChoiceisfalseand 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:
| Position | If these are set | Resolves to |
|---|---|---|
| 1 | OLLAMA_MODEL | ollama() |
| 2 | FOUNDRY_LOCAL_MODEL (+ optional FOUNDRY_LOCAL_ENDPOINT | FOUNDRY_LOCAL_BASE_URL) | foundryLocal() |
| 3 | FOUNDRY_PROJECT_ENDPOINT + (AZURE_AI_MODEL_DEPLOYMENT_NAME | MODEL_NAME) | foundry() |
| 4 | AZURE_OPENAI_API_KEY + an Azure endpoint + api-version + deployment | azureOpenai() |
| 5 | ANTHROPIC_API_KEY | anthropic() |
| 6 | OPENAI_API_KEY | openai() |
Two placements are deliberate, and both are about what a variable means:
- A local model name outranks everything,
FOUNDRY_PROJECT_ENDPOINTincluded.FOUNDRY_LOCAL_MODELis 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_NAMEis a pair of product-specific spellings nobody exports by mistake, so together they make Foundry the declared destination. The genericMODEL_NAMEalone does not carry the endpoint past a bootable Azure config — an env that resolved toazureOpenai()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_PORTis 8088. Passport: 0in tests for an ephemeral socket; every option lives onFoundryResponsesHostOptions(port,hostname,model,maxBodyBytes, or attach to your ownserver).FOUNDRY_SESSION_FIELDSis the session-alias precedence the contract reads:conversation(object or bare string), thenagent_session_id, thensession_id— first present wins, and whichever alias the caller used lands on the sameHostRequest.sessionId.- Streaming is chosen by
stream: truein the body — not theAcceptheader — and a streamed success walks the nine-event Responses lifecycle fromresponse.createdthroughresponse.completedwith stable ids and a monotonicsequence_number; a failure ends withresponse.failed. - A request body over the ceiling (default one MiB) is refused with the typed
RequestTooLargeErrorrefusal, 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 chosereply.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
MemoryStoreport is what it will fill, and it is open today. - No Foundry Toolbox (managed MCP) transport. Queued.
mcpClientandgatewayTransportare 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
a2aWireships regardless, because it is a protocol rather than a product. - No on-behalf-of, and no user-delegated Entra token.
entraIdentity()refusesmode: 'user'and a per-requestuserTokenby 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 isopenai()'s machinery underneath, so it inherits that ceiling exactly. - No forced tool choice, no prompt caching and no structured thinking on
foundryLocal().tool_choiceis undocumented on that wire, socarriesForcedToolChoiceisfalseand 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 & credentials —
entraIdentity()in full, besideagentCoreIdentityandgoogleIdentityunder one port. - OpenAI —
azureOpenai()'s own page, and thelegacyEndpointdial thatfoundry()sets for you. - Hosting & runtime — the
AgentHostportfoundryResponsesHostfills, theSessionLifecyclebeside 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.
Google Cloud & Gemini
A provider column. What agentfootprint adapts on Google Cloud today — the native Gemini provider, Gemini embeddings, Cloud Storage artifacts, Agent Runtime sessions, Firestore sessions, Memory Bank and ADC-based identity — plus every remaining boundary with the port that fills it and the honest reason it is not an adapter yet.
On-premises & self-hosted
The provider column for a deployment that owns its own machines. Every service — LLM, stores, embeddings, hosting, code execution, telemetry, credentials, tools — with the adapter that fills it without a cloud account, its door, its dependency and its honest status.
