OpenAI
openai() — GPT provider via the openai SDK. Also covers OpenAI-compatible endpoints (Ollama, llama.cpp, vLLM, Together, Groq, LM Studio) via baseURL, the legacyEndpoint dial that declares which dialect such a URL speaks, and azureOpenai() with either an API key or a keyless Entra credential.
The OpenAI SDK covers GPT-4, GPT-4o, and the wider ecosystem of OpenAI-compatible endpoints. agentfootprint's
openai()factory wraps it as anLLMProvider— same shape asanthropic()andmock().
Install
npm install openaiPeer dep, lazy-required, optional in peerDependenciesMeta. Friendly install hint at first call if missing.
Use
openai() is a vendor-SDK provider, so it lives at the agentfootprint/providers subpath (this keeps the lazy openai peer-dep require out of the main barrel). Agent comes from the main barrel as usual:
import { Agent } from 'agentfootprint';
import { openai } from 'agentfootprint/providers';
const provider = openai({
apiKey: process.env.OPENAI_API_KEY!,
});
const agent = Agent.create({
provider,
model: 'gpt-4o',
}).build();OpenAI-compatible endpoints (Ollama, llama.cpp, vLLM, Together, Groq, LM Studio)
Local models, self-hosted servers, and most inference clouds all speak the same
Chat Completions shape OpenAI does. Point baseURL at any of them — no separate
adapter, no new API to learn:
// llama.cpp's llama-server: llama-server -m model.gguf --port 8080
const = ({
: 'http://localhost:8080/v1',
: 'not-needed-for-local', // local servers ignore it; the SDK just requires non-empty
: 'local-model', // the model is whatever .gguf the server loaded
});
// vLLM: vllm serve meta-llama/Llama-3.1-8B-Instruct --port 8000
const = ({
: 'http://localhost:8000/v1',
: 'not-needed-for-local',
: 'meta-llama/Llama-3.1-8B-Instruct',
});
// Groq (cloud, OpenAI-compatible — a real key is required)
const = ({
: 'YOUR_GROQ_API_KEY',
: 'https://api.groq.com/openai/v1',
});For Ollama specifically, the convenience factory (also from agentfootprint/providers). It defaults baseURL to http://localhost:11434/v1 and names the provider ollama; set the model via defaultModel (or pass a custom host):
const = ({ : 'llama3.1' });$0 to iterate. Local inference costs nothing and needs no key — great for
exploring. It's usually still the wrong choice for CI: slower, and small models
are less reliable at tool-calling. Reach for mock() once
you're past manual poking and writing tests.
legacyEndpoint — declaring the dialect instead of implying it
Any baseURL puts the adapter in legacy-endpoint mode by default: it sends
the older max_tokens field instead of max_completion_tokens, skips
stream_options, and does not declare forced tool choice. That default exists
because the compatible servers this option was built for (Ollama, vLLM, Together,
Groq) are exactly the ones that break on the modern fields, and a hard failure is
worse than a conservative request.
It is a default, not a law — the dial lives on OpenAIProviderOptions, and
the OpenAIProvider class form takes the same options as the factory. Some
custom base URLs speak the current OpenAI dialect, and legacyEndpoint: false
says so:
import { DefaultAzureCredential } from '@azure/identity';
import { openai } from 'agentfootprint/providers';
const credential = new DefaultAzureCredential();
const provider = openai({
baseURL: 'https://my-acct.services.ai.azure.com/api/projects/my-proj/openai/v1',
legacyEndpoint: false, // this URL is current OpenAI wire, not a legacy-compatible server
apiKey: async () => (await credential.getToken('https://ai.azure.com/.default'))!.token,
});Azure's v1 inference route is the worked example — and if that is where you are
pointing, prefer foundry(), which sets this flag, the
URL and the auth for you. legacyEndpoint: true with no baseURL is legal and
means what it says. Unset, nothing changes anywhere: the default is exactly
!!baseURL.
Interplay with streamUsage: a non-legacy endpoint already asks for
stream_options.include_usage on streams, so streamUsage remains the opt-in
for endpoints that stay legacy — this flag changes nothing about what either of
its values does.
Azure OpenAI
An Azure resource is not a drop-in OpenAI-compatible URL: it uses a
deployment-scoped path, an api-version query parameter, and its own auth. That
is what azureOpenai() is for — it wraps the SDK's AzureOpenAI client and
reuses the identical completion, streaming and tool-call logic as openai().
import { Agent } from 'agentfootprint';
import { azureOpenai } from 'agentfootprint/providers';
const agent = Agent.create({
provider: azureOpenai({
endpoint: process.env.AZURE_OPENAI_ENDPOINT, // the resource ROOT, https://my-co.openai.azure.com
apiKey: process.env.AZURE_OPENAI_API_KEY,
apiVersion: process.env.AZURE_OPENAI_API_VERSION, // e.g. 2024-12-01-preview
deployment: process.env.MODEL_NAME, // e.g. gpt-4o-128k
}),
model: 'azure', // the shorthand for the configured deployment
}).build();The request's model is the Azure deployment name; 'azure' /
'azure-openai' / 'openai' resolve to the configured deployment, and a
concrete deployment id passes through, so one provider can target several.
AZURE_OPENAI_ENDPOINT and OPENAI_BASE_URL are two spellings of the same
resource root and reach the identical URL. Deployment names are arbitrary and
hide the underlying model, so a reasoning deployment needs reasoning: true
declared — it cannot be detected.
Keyless: sign with Entra ID instead of a key
A resource that has key auth turned off — or a deployment that would rather not
hold a long-lived secret — passes a credential instead. Any @azure/identity
credential works; the type is duck-typed, so this library never imports that SDK:
import { DefaultAzureCredential } from '@azure/identity';
import { azureOpenai } from 'agentfootprint/providers';
const provider = azureOpenai({
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
apiVersion: process.env.AZURE_OPENAI_API_VERSION,
deployment: process.env.MODEL_NAME,
credential: new DefaultAzureCredential(), // az login, managed identity, workload identity, …
});Underneath, the credential becomes the SDK's azureADTokenProvider, which is
called before every request, so a long-lived agent process never holds an
expired token — MSAL's own cache does the pacing. scope sets the audience and
defaults to AZURE_COGNITIVE_SERVICES_SCOPE
(https://cognitiveservices.azure.com/.default) — the audience Microsoft's own
keyless guidance names for the classic deployment-scoped route this door builds.
Each keyless door defaults to the audience its route documents: foundry()'s
v1/project route uses AZURE_AI_SCOPE (https://ai.azure.com/.default)
instead, and current resources widely accept both — but an older
*.openai.azure.com resource may only accept the classic one. The ARM control
plane is a third audience whose tokens are a 401 here. @azure/identity is an
optional peer dependency — install it only if you use this door.
Passing both credential and apiKey is refused by name at construction.
They are two answers to one question — which identity signs the request — and
whichever one this factory silently preferred would be the one you did not think
was in use.
For a Foundry project endpoint rather than a classic *.openai.azure.com
resource, use foundry(): same credential, no
api-version, and the deployment carried in the request's model field.
Tools (function calling)
OpenAI's Chat Completions API has native function calling. The provider translates Tool[] into the API's tools format and round-trips assistant tool_calls.
Streaming
provider.stream(req) uses the SDK's native SSE streaming; tokens land as they arrive. Final chunk carries the full LLMResponse.
Browser variant
BrowserOpenAIProvider — fetch-based, zero peer deps. CORS depends on the endpoint; OpenAI requires the user-supplied key in the Authorization header, which the consumer must set explicitly.
Limitations
- Multi-modal not exposed yet (
LLMMessage.contentisstring). responseFormat(JSON-mode) not exposed yet — pass schema instructions viasystemPrompt.
Next steps
- Resilience — wrap with
withRetry/withFallback - Streaming — token-by-token UI rendering
- Ollama — local models via OpenAI-compatible endpoint
- Microsoft Foundry —
foundry()for a Foundry project,foundryLocal()for a model on this machine
