Providers
One canonical message shape, ten provider paths: OpenAI (Chat Completions + Responses), Anthropic, Google Gemini, AWS Bedrock, Ollama, Hugging Face, Azure AI Foundry (Chat + Responses), and Foundry Local. Switch providers by changing the model id — the conversation, tools, and governance don’t change.
The support matrix
| Provider | Install extra | Response normalization | Tool formatting |
|---|---|---|---|
| OpenAI (Chat Completions) | [openai] | ✅ | ✅ functions |
| OpenAI (Responses API) | [openai] | ✅ | ✅ functions |
| Anthropic (Messages) | [anthropic] | ✅ | ✅ tools |
| Google Gemini | [google] | ✅ | ✅ function declarations |
| AWS Bedrock | [bedrock] | ✅ | ✅ toolConfig |
| Ollama (local) | [ollama] | ✅ | ✅ functions |
| Hugging Face | [huggingface] | ✅ (OpenAI-shape) | ✅ functions |
| Azure AI Foundry (Chat) | [azure] | ✅ (OpenAI-shape) | ✅ functions |
| Azure AI Foundry (Responses) | [azure] | ✅ (OpenAI-shape) | ✅ functions |
| Foundry Local (on-device) | [foundry-local] | ✅ (OpenAI-shape) | ✅ functions |
Normalization is isolated in providers.py (Python) / providers.ts (TypeScript) with
per-provider fixtures, so response-shape drift is contained. All ten paths ship in @cendor/sdk
behind the same Provider seam — Hugging Face, Ollama, Gemini, and Bedrock drive the model today,
with their end-to-end token/cost capture in JS activating alongside the matching @cendor/core
detection release. See the parity matrix.
Async (run.aio) runs natively for OpenAI (Chat + Responses), Anthropic, Ollama, and Hugging
Face. Google Gemini and AWS Bedrock have no native async client in their SDKs, so run.aio executes
them synchronously for now — correct results, but without a concurrency benefit.
Core concepts
Provider inference — and when to be explicit
The provider is inferred from the model id: gpt-*/o* → OpenAI, claude-* → Anthropic,
gemini-* → Google, and so on. Two families are not prefix-inferable — Hugging Face Hub ids
and Azure deployment names are arbitrary strings — so those always take provider= explicitly
(sections below).
Provider params & reasoning — Agent.extra
Agent.extra merges arbitrary request kwargs into every model call — the passthrough for
anything the SDK doesn’t model first-class: tool_choice, reasoning_effort, top_p, stop,
seed, response_format, extra_body. o-series models that reject temperature have it
omitted automatically.
agent = Agent(name="a", model="o3-mini", extra={"reasoning_effort": "high"})
picky = Agent(name="p", model="gpt-4o", tools=[...], extra={"tool_choice": "required"})
const agent = new Agent({ name: 'a', model: 'o3-mini', extra: { reasoning_effort: 'high' } });
const picky = new Agent({ name: 'p', model: 'gpt-4o', tools: [/* ... */],
extra: { tool_choice: 'required' } });
// extra keys are raw provider request kwargs, so they stay snake_case in both languages
Pricing unpriced models
Model ids absent from the bundled price table (Hub ids, deployment names, custom gateways) cost
$0, so a USD budget(...) can’t bind. Register a rate and cost +
budgets work:
from cendor.sdk import register_model_price, budget
register_model_price("my-gpt4o-deployment", input=2.50, output=10.00) # USD per 1M tokens
with budget(usd=0.10, on_exceed="block"):
run(agent, "...")
import { registerModelPrice, withBudget } from '@cendor/sdk';
registerModelPrice('my-gpt4o-deployment', { input: 2.50, output: 10.00 }); // USD per 1M tokens
await withBudget({ usd: 0.10, onExceed: 'block' }, () =>
run(agent, '...'));
Or use a tokens= cap (tokens are counted regardless of price), or
configure(on_unpriced="raise") to reject unpriced calls outright.
Hugging Face
TypeScript usage capture. All three providers below ship in
@cendor/sdk. Azure AI Foundry and Foundry Local wrap the standardopenaiclient, so cost and usage are captured end-to-end in TypeScript today; Hugging Face parses responses now, with token/cost capture arriving alongside the matching@cendor/coredetection. See the parity matrix.
Uses huggingface_hub.InferenceClient.chat_completion — the response is OpenAI-shaped, and the
call is attributed to huggingface, not openai. The model is a Hub id or an Inference
Endpoint URL. Install with pip install "cendor-sdk[huggingface]" (Python); in TypeScript add the
client peer with npm i @cendor/sdk @huggingface/inference.
from cendor.sdk import Agent, run
# Serverless Inference API — token from api_key= or the HF_TOKEN / HUGGINGFACEHUB_API_TOKEN env var
agent = Agent(
name="hf",
model="meta-llama/Llama-3.1-8B-Instruct",
provider="huggingface", # required — Hub ids aren't prefix-inferable
api_key="hf_...", # optional; falls back to HF_TOKEN
)
result = run(agent, "Summarize the plot of Hamlet in two lines.")
# A dedicated Inference Endpoint (or a third-party provider) — point base_url at it:
agent = Agent(name="hf", model="tgi", provider="huggingface",
base_url="https://<your-endpoint>.endpoints.huggingface.cloud")
import { Agent, run } from '@cendor/sdk';
// Serverless Inference API — token from apiKey= or the HF_TOKEN / HUGGINGFACEHUB_API_TOKEN env var
const agent = new Agent({
name: 'hf',
model: 'meta-llama/Llama-3.1-8B-Instruct',
provider: 'huggingface', // required — Hub ids aren't prefix-inferable
apiKey: 'hf_...', // optional; falls back to HF_TOKEN
});
const result = await run(agent, 'Summarize the plot of Hamlet in two lines.');
// A dedicated Inference Endpoint (or a third-party provider) — point baseURL at it:
const endpoint = new Agent({ name: 'hf', model: 'tgi', provider: 'huggingface',
baseURL: 'https://<your-endpoint>.endpoints.huggingface.cloud' });
Route through a specific inference provider (e.g. together, fireworks-ai) with the
HF_PROVIDER env var.
Azure AI Foundry
Microsoft’s current guidance — the AzureOpenAI client and azure-ai-inference are being
retired — is to consume Foundry deployments with the standard openai SDK pointed at the
Foundry /openai/v1/ endpoint. So provider="azure" is the OpenAI provider with Foundry-aware
construction. Install with pip install "cendor-sdk[azure]" (it just pulls openai); in TypeScript
the Azure path reuses the openai peer you already have. Two rules:
modelis your deployment name, not the underlying model name (Azure keys on deployment).base_urlis the Foundry endpoint —/openai/v1/is appended for you; also read fromAZURE_OPENAI_ENDPOINT. Noapi-versionneeded (the v1 GA API infers it).
from cendor.sdk import Agent, run, budget
# Azure OpenAI models: https://<res>.openai.azure.com
# Foundry Models (DeepSeek, Grok, Llama, …): https://<res>.services.ai.azure.com — both work
agent = Agent(
name="foundry",
model="my-gpt4o-deployment", # your Foundry DEPLOYMENT name
provider="azure",
base_url="https://my-resource.openai.azure.com", # or AZURE_OPENAI_ENDPOINT
api_key="<resource-key>", # or AZURE_OPENAI_API_KEY / AZURE_INFERENCE_CREDENTIAL
)
with budget(usd=0.10, on_exceed="block"):
result = run(agent, "Draft a one-line release note.")
import { Agent, run, withBudget } from '@cendor/sdk';
// Azure OpenAI models: https://<res>.openai.azure.com
// Foundry Models (DeepSeek, Grok, Llama, …): https://<res>.services.ai.azure.com — both work
const agent = new Agent({
name: 'foundry',
model: 'my-gpt4o-deployment', // your Foundry DEPLOYMENT name
provider: 'azure',
baseURL: 'https://my-resource.openai.azure.com', // or AZURE_OPENAI_ENDPOINT
apiKey: '<resource-key>', // or AZURE_OPENAI_API_KEY / AZURE_INFERENCE_CREDENTIAL
});
const result = await withBudget({ usd: 0.10, onExceed: 'block' }, () =>
run(agent, 'Draft a one-line release note.'));
Microsoft Entra ID (keyless): pass a refreshing bearer-token provider — Python takes it as
api_key (the v1 client refreshes it), TypeScript as azureADTokenProvider — or build the client
yourself and hand it to Agent(client=...):
from openai import OpenAI
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
token = get_bearer_token_provider(DefaultAzureCredential(), "https://ai.azure.com/.default")
client = OpenAI(base_url="https://my-resource.openai.azure.com/openai/v1/", api_key=token)
agent = Agent(name="foundry", model="my-gpt4o-deployment", provider="azure", client=client)
import { Agent, run } from '@cendor/sdk';
// A refreshing Entra-ID bearer-token provider — e.g. @azure/identity's
// getBearerTokenProvider(new DefaultAzureCredential(), 'https://cognitiveservices.azure.com/.default')
const azureADTokenProvider = async () => '<entra-id-bearer-token>';
const agent = new Agent({
name: 'foundry',
model: 'my-gpt4o-deployment', // your Foundry DEPLOYMENT name
provider: 'azure',
baseURL: 'https://my-resource.openai.azure.com', // or AZURE_OPENAI_ENDPOINT
azureADTokenProvider, // keyless — the token is refreshed on every request
});
const result = await run(agent, 'Draft a one-line release note.');
For an OpenAI-family deployment (gpt-*, o*) you can drive the Responses API instead with
provider="azure_responses" — same construction, Responses semantics. Keep provider="azure"
(Chat Completions) for the broadest reach across non-OpenAI Foundry models.
Foundry Local
Microsoft Foundry Local runs a model on the device and exposes an OpenAI-compatible REST
server — the local counterpart to Ollama. provider="foundry_local" points the OpenAI Chat
provider at that endpoint; no key needed. Install with pip install "cendor-sdk[foundry-local]" (Python); in
TypeScript it uses the same openai peer as the OpenAI provider.
from cendor.sdk import Agent, run
from foundry_local import FoundryLocalManager
# FoundryLocalManager starts the service, downloads/loads the model, exposes the endpoint.
mgr = FoundryLocalManager("qwen2.5-0.5b") # a catalog alias
agent = Agent(
name="local",
model=mgr.get_model_info("qwen2.5-0.5b").id, # the resolved model id, not the alias
provider="foundry_local",
base_url=mgr.endpoint, # or set FOUNDRY_LOCAL_ENDPOINT
api_key=mgr.api_key, # not required locally; defaults to "none"
)
result = run(agent, "Give me one tip for faster cold starts.")
import { Agent, run } from '@cendor/sdk';
// Foundry Local exposes an OpenAI-compatible endpoint; point the provider at it (no key needed).
// Start the service and resolve the model id with Microsoft's Foundry Local tooling, then:
const agent = new Agent({
name: 'local',
model: 'qwen2.5-0.5b-instruct-generic-cpu', // the resolved model id, not the catalog alias
provider: 'foundry_local',
baseURL: 'http://localhost:5273', // or set FOUNDRY_LOCAL_ENDPOINT
});
const result = await run(agent, 'Give me one tip for faster cold starts.');
Already running the service yourself? Just pass base_url= (or FOUNDRY_LOCAL_ENDPOINT) and
skip the manager.
Plugs into the stack
Every provider path terminates in an instrumented client, so budgets, audit, redaction and cassette record/replay apply identically across all ten. Provider-level details that aren’t SDK-specific — token counting, price sources, OTel ingestion — live in the library docs: Providers & Integration.
Honest limits
- Ten paths, one shape — but capability varies. Native structured output, token-level streaming, and prompt caching each cover a subset (called out on Agents & the loop); everywhere else degrades gracefully.
- HF/Azure ids need explicit
provider=— there is no reliable inference for arbitrary Hub/deployment names, and guessing wrong would mis-attribute cost. - Deployment-name models start unpriced — register a rate or budgets can’t bind (above).
- TypeScript ships all ten provider paths behind the same seam. Hugging Face / Ollama /
Gemini / Bedrock drive the model today; their end-to-end usage capture in JS lands with the
matching
@cendor/coredetection release — see the parity matrix.