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

ProviderInstall extraResponse normalizationTool 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 standard openai client, 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/core detection. 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:

  1. model is your deployment name, not the underlying model name (Azure keys on deployment).
  2. base_url is the Foundry endpoint/openai/v1/ is appended for you; also read from AZURE_OPENAI_ENDPOINT. No api-version needed (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/core detection release — see the parity matrix.