agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
179 lines (178 loc) • 9.38 kB
TypeScript
/**
* FoundryProvider — Microsoft Foundry (Azure AI Foundry) projects over the
* api-version-free v1 OpenAI-compatible inference route.
*
* Pattern: Adapter (GoF) + Ports-and-Adapters (Cockburn 2005), composed —
* a thin vendor file over `openai()`'s machinery, the exact
* `azureOpenai()` shape.
* Role: Outer ring — owns EVERY Foundry spelling (the project-endpoint
* shape, the env-var names, the token audience, the `/openai/v1`
* derivation) so the `LLMProvider` port and the rest of the library
* never learn one. Knows nothing about agents, recorders, or
* compositions.
* Emits: N/A.
*
* ─── Why this exists ─────────────────────────────────────────────────
*
* This is the JS answer to Microsoft's
* `FoundryChatClient(project_endpoint, model, credential)`: point at a
* Foundry PROJECT, name a DEPLOYMENT, hand over a credential, done. The
* doc-verified facts it is built on (learn.microsoft.com, Aug 2026):
*
* • A Foundry project endpoint
* (`https://{account}.services.ai.azure.com/api/projects/{project}`)
* itself 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 — Foundry's
* "model" is a deployment, exactly as on classic Azure OpenAI.
* • Auth is `Authorization: Bearer <Entra token or api key>` — the v1
* route accepts a key as a Bearer too.
*
* Because that route IS current OpenAI wire, this file is `openai({ baseURL,
* legacyEndpoint: false, … })` plus Foundry's spellings — the same
* composition `azureOpenai()` uses, with the modern dialect declared instead
* of implied away by the custom `baseURL`.
*
* ─── Auth: three doors, one refused ambiguity ────────────────────────
*
* • `credential` — any `@azure/identity` credential (duck-typed; the SDK
* is never imported for this door). Tokens are minted per request
* through `openai()`'s credential-callback seam.
* • `apiKey` — a static key, or a callback re-read per request.
* • NEITHER — `new DefaultAzureCredential()` via the optional
* `@azure/identity` peer: the platform's own blessed zero-config pattern
* inside a hosted Foundry container (the platform injects
* `FOUNDRY_PROJECT_ENDPOINT` and gives the agent a managed identity, so
* `foundry()` with no arguments is a complete configuration there).
* • BOTH `credential` and `apiKey` — refused by name. Two credentials is a
* config bug, not extra security.
*
* ─── Ceilings (stated, not worked around) ────────────────────────────
*
* • DEPLOYMENT NAMES HIDE THE MODEL. o-series auto-detection cannot work on
* an arbitrary deployment name, so `reasoning` must be declared — the same
* rationale, word for word, as `azureOpenai()`.
* • INFERENCE ONLY. The token audience here is the data plane
* ({@link AZURE_AI_SCOPE}); the ARM control plane is a different audience
* and a different job — see `entraIdentity()` in the identity adapters.
* • Everything `openai()` does not do (multi-modal, JSON-mode), this does
* not do either — it is the same machinery.
*/
import type { LLMProvider } from '../../provider-contract/types.js';
import type { OpenAIProviderOptions } from './OpenAIProvider.js';
import type { TokenCredentialLike } from '../identity/azure.js';
export interface FoundryProviderOptions {
/**
* The Foundry PROJECT endpoint —
* `https://{account}.services.ai.azure.com/api/projects/{project}`, copied
* from the Foundry portal. Env fallback: `FOUNDRY_PROJECT_ENDPOINT`, which
* hosted Foundry containers get AUTO-INJECTED by the platform — inside one,
* this option can simply be omitted. Required (option or env); refused by
* name otherwise.
*/
readonly projectEndpoint?: string;
/**
* The DEPLOYMENT name (Foundry's "model"). Env fallbacks:
* `AZURE_AI_MODEL_DEPLOYMENT_NAME` — the `azd` scaffolding convention, so a
* template-provisioned app needs no extra wiring — then `MODEL_NAME`.
* Required (option or env); refused by name otherwise.
*/
readonly deployment?: string;
/**
* Keyless (Microsoft Entra ID) auth — any `@azure/identity` credential
* (`DefaultAzureCredential`, `ManagedIdentityCredential`, …), duck-typed so
* this file never imports that SDK. Consulted before EVERY request through
* `openai()`'s credential-callback seam, so MSAL's cache does the pacing
* and an expired token is a fresh token, never a 401.
*
* Mutually exclusive with `apiKey` — both together are refused by name.
* NEITHER given constructs a `DefaultAzureCredential` (peer-dep
* `@azure/identity`), the zero-config path hosted containers are built for.
*/
readonly credential?: TokenCredentialLike;
/**
* Static api key — the v1 route accepts a key as a Bearer too. A FUNCTION
* here is re-read before every request (`openai()`'s 9.29.0 contract).
* Mutually exclusive with `credential`.
*/
readonly apiKey?: string | (() => string | Promise<string>);
/**
* Token audience for the credential doors. Default {@link AZURE_AI_SCOPE}
* (`https://ai.azure.com/.default`) — the ONE data-plane audience every
* Foundry / Azure OpenAI inference call accepts. The ARM control plane
* (`https://management.azure.com/.default`) is a DIFFERENT audience whose
* tokens do NOT work here: Azure validates the audience on every call.
* Ignored when `apiKey` is the door in use.
*/
readonly scope?: string;
/**
* Set when the DEPLOYMENT is a **reasoning model** (o1/o3/o4-mini).
* Deployment names are arbitrary and hide the underlying model, so this
* cannot be auto-detected — declare it to omit `temperature` and send the
* `developer` role. (Same rationale as `azureOpenai()`.)
*/
readonly reasoning?: boolean;
/** Default max tokens when the request doesn't set it. Optional. */
readonly defaultMaxTokens?: number;
/** @internal Pre-built client for testing — the same duck type `openai()`
* takes, spelled as such so the two seams can never drift apart. */
readonly _client?: OpenAIProviderOptions['_client'];
}
/**
* Project endpoint → the base URL the v1 inference route serves.
*
* `https://acct.services.ai.azure.com/api/projects/proj`
* → `https://acct.services.ai.azure.com/api/projects/proj/openai/v1`
* …with trailing slashes → the same
* …already ending in `/openai/v1` → the same (idempotent — `azureUrl.ts`'s
* rule, applied to Foundry's suffix)
*
* Validation lives HERE, beside the derivation, so there is exactly ONE owner
* of what a Foundry endpoint looks like: it must be `https://` and it must
* contain `/api/projects/` — anything else is some OTHER Azure endpoint
* (a resource root, an ARM URL) that would 404 or 401 far from the typo.
* Endpoints are NOT secrets, so a malformed one is echoed back: seeing what
* arrived is the fastest fix. The one carve-out is cleartext to LOOPBACK
* (127.0.0.1 / localhost / [::1]) — that is how the wire tests drive the
* REAL SDK against a local fake, and refusing bytes that cannot leave the
* machine would buy no safety.
*
* A `?` or `#` is refused too, and for a reason particular to a SUFFIXING
* derivation: `…/api/projects/p?x=1` + `/openai/v1` is
* `…/api/projects/p?x=1/openai/v1`, where the route lives entirely inside the
* query string. That URL is well-formed, constructs without complaint, and
* 404s on every request — the exact "late and far from the typo" failure this
* validator exists to convert into an early named one. The v1 inference route
* takes no query parameters at all (that is the point of it: no
* `api-version`), so there is nothing legitimate to preserve.
*/
export declare function foundryInferenceUrl(projectEndpoint: string): string;
/**
* Build an `LLMProvider` for a **Microsoft Foundry project**.
*
* Inside a hosted Foundry container this is a COMPLETE configuration:
*
* @example
* import { foundry } from 'agentfootprint/providers';
*
* const agent = Agent.create({
* provider: foundry(), // endpoint injected, managed identity signs
* model: 'foundry', // → the configured deployment
* }).build();
*
* Anywhere else, name the project and the deployment (and sign in, or pass a
* credential):
*
* @example
* const provider = foundry({
* projectEndpoint: 'https://my-acct.services.ai.azure.com/api/projects/my-proj',
* deployment: 'gpt-4o-128k',
* credential: new DefaultAzureCredential(),
* });
*
* The request's `model` is the DEPLOYMENT name: the shorthand `'foundry'`
* resolves to the configured default, and a concrete deployment id passes
* through untouched (so one provider can target several deployments — the
* azureOpenai precedent).
*/
export declare function foundry(options?: FoundryProviderOptions): LLMProvider;