On this page
- What belongs inside an adapter
- The adapter sits after context construction
- Request building is more than renaming fields
- Capability validation should happen before the HTTP call
- Authentication belongs at the transport boundary
- The adapter should normalize streams incrementally
- Stream parsing must be stateful even if the API is stateless
- Tool calls need two identities
- The host, not the adapter, authorizes tools
- Reasoning is where weak adapters break
- File handling may require provider-specific lifecycle management
- Usage mapping needs explicit optionality
- Finish reasons need a semantic layer
- Error normalization should preserve two views
- Retry policy should use adapter classification, not adapter loops everywhere
- Keep provider-specific parameters extensible
- Compatible endpoints need defensive adapters
- Adapters should be testable without real providers
- Contract tests catch accidental abstraction leaks
- Adapters should be replaceable
- A practical adapter interface
- Adapter review checklist
- Where BYOKchat fits
- Further reading
A provider adapter is the boundary between your application’s AI model and a provider’s native API.
Its job is not to make every provider identical. Its job is to answer a narrower question:
Given one provider-neutral generation request, how should this specific provider execute it, and how should its native response be translated back into application events?
A good adapter isolates change. When a provider modifies an endpoint, adds a reasoning mode, changes a streaming event, or exposes new usage metadata, most of the application should not care.
What belongs inside an adapter
An adapter typically owns:
base endpoint rules
authentication headers
native request construction
model identifiers
provider-specific settings
stream parsing
content-block translation
tool-call translation
reasoning translation
usage extraction
finish-state mapping
error normalization
request IDs / diagnostics
It should not own:
chat screen layout
conversation database semantics
user permission policy for tools
which chat history to keep
billing UI
project organization
Those belong to higher layers.
The adapter sits after context construction
A healthy flow looks like:
The context manager decides what to send. The adapter decides how this provider represents it.
This matters because context policy should not be duplicated across providers.
Request building is more than renaming fields
Two providers can differ in fundamental structure.
One may expect:
{
"model": "...",
"messages": [...]
}
Another may represent content as nested typed blocks. Another may separate instructions from user content. A compatible endpoint may accept an OpenAI-like body but ignore unsupported parameters.
The adapter must map semantics, not blindly rename keys.
For example:
neutral instruction
→ native system/developer/instruction field
neutral image block
→ provider image block / inline data / file ID
neutral reasoning preference
→ provider-native reasoning parameter if supported
If a semantic concept cannot be represented safely, the adapter should reject or degrade deliberately.
Capability validation should happen before the HTTP call
Suppose the generation request requires:
image input
+
tool calling
+
structured output
If the selected model does not support one of those, the best error occurs before the request is sent.
An adapter can expose a model capability profile:
type ModelCapabilities = {
images: Bool;
tools: Bool;
structuredOutput: Bool;
reasoning: ReasoningCapability;
};
Then request validation can produce an actionable application error:
Selected model does not support image input.
instead of forwarding a provider’s vague 400 response after the user waits.
Authentication belongs at the transport boundary
The adapter knows how the provider authenticates requests.
That may mean:
Authorization: Bearer ...
or a provider-specific key header.
The rest of the generation system should work with a secure credential reference rather than embedding API keys into request-domain objects.
A useful pattern is:
provider connection
→ credential resolver
→ adapter transport configuration
Never make prompts or tool calls responsible for attaching credentials.
See What Happens When You Send an API Key to an AI Provider?.
The adapter should normalize streams incrementally
A provider may stream through SSE, chunked JSON, or another event protocol. Native events can be highly granular.
The adapter should parse those bytes and emit semantic events such as:
generation started
text delta
reasoning delta
citation added
tool call started
tool arguments delta
tool call completed
usage update
response completed
response failed
This lets the UI and orchestrator remain stable.
But do not throw away meaningful provider semantics just to force a tiny common denominator.
If a provider exposes a distinct event the product can use, add a normalized type for that concept.
Stream parsing must be stateful even if the API is stateless
Tool arguments or content blocks may arrive over several events:
{"city":"San
then:
Francisco"}
The adapter needs temporary assembly state.
That state belongs to one in-flight response and should be reset when the generation completes, fails, or is cancelled.
Do not confuse this parser state with durable conversation state.
Tool calls need two identities
Providers often assign a native tool-call ID. Your application should also have its own execution ID.
application tool execution ID
↕
provider native tool-call ID
The native ID may be required when sending the tool result back to the same provider.
The application ID is useful for:
- persistence;
- approval cards;
- retries;
- deduplication;
- cross-provider conversation history;
- diagnostics.
The adapter maps between the two.
See How AI Tool Calling Works.
The host, not the adapter, authorizes tools
The adapter can say:
provider requested tool X with arguments Y
It should not decide:
user has permanently authorized tool X
That is host policy.
The execution flow should be:
This keeps permissions consistent across providers.
Reasoning is where weak adapters break
Provider reasoning features differ significantly.
An adapter may need to translate:
reasoning effort setting
reasoning summary stream
opaque reasoning item
encrypted continuation state
reasoning usage
into separate application concepts.
Do not reduce all reasoning behavior to:
reasoning: Bool
That cannot express whether a model supports configuration, visible summaries, or continuation state.
A provider adapter should preserve native reasoning artifacts only when the application needs them and scope them to the correct provider/model.
File handling may require provider-specific lifecycle management
A neutral attachment may become:
inline image bytes
provider uploaded file ID
extracted text
retrieval index entry
unsupported input
The adapter can own provider-native upload/reference mechanics, while the application owns the local attachment.
If remote file IDs are cached, scope them by connection/provider and treat expiration/deletion as normal.
Do not replace local attachment identity with a remote ID.
Usage mapping needs explicit optionality
Providers report usage differently.
Possible fields include:
input tokens
output tokens
cached input
reasoning usage
tool/search usage
provider-specific billing units
A normalized usage object should preserve what is known without inventing numbers for what is not.
Bad:
missing cache field → assume 0 cached tokens
Better:
cachedInputTokens = unknown
An unknown value and a measured zero are different facts.
Finish reasons need a semantic layer
Providers can use different finish-state names.
The application may care about categories such as:
completed normally
output limit reached
tool call requested
cancelled
safety/refusal
failed
incomplete
Map native values into these categories while preserving the original provider value for diagnostics.
Do not infer completion solely from “stream ended.” A stream can terminate because of network failure.
Error normalization should preserve two views
The product needs a stable category:
enum ProviderErrorCategory {
case authentication
case permission
case rateLimited
case invalidRequest
case modelUnavailable
case contextTooLarge
case transient
case network
case unsupportedCapability
case unknown
}
Diagnostics need native details:
HTTP status
provider error code
safe provider message
request/trace ID
retry-after guidance
Keep both.
If you preserve only the normalized category, support becomes hard. If you expose only raw native errors, UX becomes inconsistent.
Retry policy should use adapter classification, not adapter loops everywhere
The adapter is well positioned to tell the orchestrator:
retryable?
retry-after?
error category?
provider request ID?
But global retry budgets and cancellation should usually live in a shared reliability layer.
This avoids each provider implementing its own unrelated retry loop.
See Designing Reliable AI Retries.
Keep provider-specific parameters extensible
Some users need native controls that do not belong in a universal generation model.
A common approach is:
shared settings
+
provider-specific extension bag/config object
The shared layer owns portable concepts such as temperature where appropriate. The adapter extension owns native-only features.
Avoid two extremes:
- expose every provider parameter globally;
- refuse all native features because they are not universal.
A good adapter architecture can support both portable defaults and provider-native depth.
Compatible endpoints need defensive adapters
An OpenAI-compatible endpoint may:
- omit model metadata;
- accept one endpoint but not another;
- use nonstandard streaming events;
- ignore unsupported parameters;
- support tools but not structured output;
- expose custom headers or authentication.
Treat compatibility as a starting contract, not a guarantee of identical behavior.
The adapter can combine user configuration, discovered metadata, and runtime errors to form a capability profile.
See What Is an OpenAI-Compatible API?.
Adapters should be testable without real providers
Create deterministic fixtures for:
normal text stream
reasoning + text
tool call split across events
usage only in final event
network drop mid-stream
429 with retry guidance
401 invalid key
context-too-long
unknown native event
malformed JSON/SSE
cancel during tool assembly
Then assert normalized output exactly.
A provider adapter is protocol code. Protocol code deserves deterministic tests.
Contract tests catch accidental abstraction leaks
Define invariants that every adapter must satisfy:
one started event before content
no completed event after failure
native errors map to stable categories
credentials never appear in emitted diagnostics
tool calls have application identity
usage unknowns remain unknown
cancellation terminates stream processing
Run the same suite against every adapter.
Provider-specific tests then cover native differences.
Adapters should be replaceable
An adapter boundary is healthy if you can:
remove Provider A implementation
add Provider D implementation
without changing:
- the conversation database;
- chat rendering;
- project storage;
- tool permission rules;
- search/export code.
Some feature UI may change because capabilities differ. Core product semantics should not.
A practical adapter interface
interface ProviderAdapter {
discoverModels(connection): Promise<ModelDescriptor[]>;
capabilities(model): ModelCapabilities;
validate(request): ValidationResult;
makeNativeRequest(request, credential): NativeRequest;
stream(nativeRequest): AsyncIterable<GenerationEvent>;
mapError(error): ProviderError;
}
The exact methods are product-specific, but the responsibilities are intentionally narrow.
Adapter review checklist
Before adding a provider, verify that its adapter:
- contains authentication/request schema details;
- validates unsupported capabilities before sending where possible;
- normalizes streaming incrementally;
- preserves native request IDs and useful diagnostics;
- separates application IDs from provider IDs;
- does not authorize tools itself;
- preserves reasoning state only when needed;
- scopes remote file IDs correctly;
- represents unknown usage as unknown;
- maps finish states explicitly;
- supplies retry guidance without owning unbounded retry behavior;
- supports deterministic fixtures and common contract tests;
- keeps raw provider types out of the core conversation model.
Where BYOKchat fits
A BYOK client with several provider families depends on adapters to make one chat system practical. Each adapter can preserve provider-native streaming, reasoning, tool, file, and model behavior while shared layers continue to own conversations, context, permissions, projects, analytics, and recovery.
That is the real purpose of an adapter: isolate protocol diversity without erasing capability diversity.