Providers
A Provider owns the network boundary — the agent hands it a normalized request (history, enabled tools, scope contexts, system) and receives a normalized response back.
Built-in
Security
Never embed API keys in the client
The provider runs in the user's browser — anything you put into baseURL, fetch headers, or environment-substituted strings ships in your bundle to every user.
Treat your LLM API key like a database password: keep it on a server, point baseURL at your own proxy, and let the proxy attach auth on the way out.
Request & Response
The agent hands the provider two normalized shapes — ProviderRequest going in, ProviderResponse coming back. The provider's job is the translation between these and the LLM's wire format.
ProviderRequest
Input to runChat and runChatStream.
type ProviderRequest = {
messages: ChatMessage[];
tools: Tool[];
contexts: ScopeContext[];
system?: string;
};messages— conversation history snapshot.tools— tools enabled for this turn.contexts—ScopeContext[]available for this turn.system— base system prompt.
ProviderResponse
Return value of runChat. Everything is optional — fill in what the upstream API gave you.
type ProviderResponse = {
content?: string;
reasoning?: string;
calls?: AssistantToolCall[];
metadata?: Record<string, unknown>;
finishReason?: FinishReason;
};content— final assistant text.reasoning— reasoning / thinking trace when the model emits one.calls— tool calls the LLM requested ({ id, name, arguments }).metadata— provider-specific data that must survive history persistence (e.g., signed thinking blocks).finishReason—'stop' | 'tool_calls' | 'length'.
Custom Provider
Extend AbstractProvider for a sync-only API, or StreamableProvider if the backend supports streaming.
ProviderConfig
Shape accepted by every provider's constructor:
type ProviderConfig = {
baseURL: string;
fetch?: typeof fetch;
};baseURL— URL the provider POSTs to.fetch— optional wrapper aroundglobalThis.fetch. See Custom Fetch.
AbstractProvider
Sync
Implement runChat — receives the prepared ProviderRequest, returns a ProviderResponse.
import {
AbstractProvider,
Ag2bProviderRequestError,
type ProviderRequest,
type ProviderResponse,
} from '@ag2b/core';
export class MyProvider extends AbstractProvider {
protected async runChat(
request: ProviderRequest,
signal?: AbortSignal
): Promise<ProviderResponse> {
const res = await this.fetch(this.baseURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(toWireFormat(request)),
signal,
});
if (!res.ok) {
throw new Ag2bProviderRequestError(
`Request failed with status ${res.status}`,
res.status,
await res.text()
);
}
return fromWireFormat(await res.json());
}
}StreamableProvider
Sync
Stream
Extends AbstractProvider. Implement runChat and runChatStream — an async generator that yields ProviderStreamChunk events as the server streams.
import { StreamableProvider, type ProviderRequest, type ProviderStreamChunk } from '@ag2b/core';
export class MyStreamingProvider extends StreamableProvider {
protected async runChat(
request: ProviderRequest,
signal?: AbortSignal
): Promise<ProviderResponse> { /* … */ }
protected async *runChatStream(
request: ProviderRequest,
signal?: AbortSignal
): AsyncGenerator<ProviderStreamChunk> {
// parse the stream and yield ProviderStreamChunk events
}
}Stream Events
runChatStream yields one of four event shapes. Yield as soon as each fragment arrives — the agent forwards them onward without buffering.
provider_content_delta — a chunk of assistant text.
type ProviderContentDelta = {
type: 'provider_content_delta';
delta: string;
};provider_reasoning_delta — a chunk of reasoning / thinking text. Keep on its own channel — consumers render reasoning separately from content.
type ProviderReasoningDelta = {
type: 'provider_reasoning_delta';
delta: string;
};provider_tool_call_delta — incremental tool call. First chunk for an index carries id and name, later chunks carry argument JSON deltas to concatenate.
type ProviderToolCallDelta = {
type: 'provider_tool_call_delta';
index: number;
id?: string;
name?: string;
argumentsDelta: string;
};provider_stream_done — final event, yielded exactly once when the upstream stream completes.
type ProviderStreamDone = {
type: 'provider_stream_done';
finishReason: FinishReason;
metadata?: Record<string, unknown>;
};Preparing Request
Providers can transform the ProviderRequest before runChat / runChatStream sees it. Override prepareRequest to shape the request for your provider's wire format — rewrite messages, inject provider-specific fields, or re-encode scope contexts.
protected prepareRequest(request: ProviderRequest): ProviderRequest {
return {
...request,
system: customEncode(request.system, request.contexts),
};
}The default implementation inlines each scope context into messages / system.
prepareRequest must be a pure transformation — no I/O, no side effects.
Calling super.prepareRequest is optional — an override owns the full transformation.
Custom Fetch
Every provider accepts a fetch option via ProviderConfig.
Forwarding a short-lived access token is the recommended pattern — see Security. Your proxy at baseURL validates the token, checks per-user permissions, then attaches the real LLM API key before forwarding upstream. The user's browser never sees the key.
import { Agent, OpenAiProvider } from '@ag2b/core';
const authedFetch: typeof fetch = (input, init) =>
fetch(input, {
...init,
headers: {
...init?.headers,
Authorization: `Bearer ${getAccessToken()}`,
},
});
const agent = new Agent({
provider: new OpenAiProvider({
baseURL: '/api/llm',
model: 'gpt-4o',
fetch: authedFetch,
}),
});The wrapper receives the same (input, init) arguments as globalThis.fetch and must return a Response. Forward init so the agent's AbortSignal still cancels in-flight requests.
Runtime Errors
Custom providers should throw these so callers can catch at a stable boundary.
Ag2bProviderRequestError
Thrown when the provider returns a non-OK HTTP status. Carries .status and the raw response .body.
Ag2bProviderResponseError
Thrown when the response payload is malformed — no choices, unparseable tool arguments, missing stream body, mid-stream error events.