Custom provider publication requirement
Custom providers must be published in your own GitHub repository as an NPM package. You are responsible for hosting and maintaining your provider. After publishing, you can submit a PR to the AI SDK repository to add your provider to the Community Providers documentation section, using the OpenRouter provider documentation as a template.
Required npm packages for custom provider implementation
To implement a custom language model provider, install: npm install @ai-sdk/provider @ai-sdk/provider-utils
Custom provider implementation steps overview
Implementing a custom language model provider involves: creating an entry point, adding a language model implementation, mapping the input (prompt, tools, settings), processing the results (generate, streaming, tool calls), and supporting object generation.
Reference implementation recommendation
The best way to get started implementing a custom provider is to use the Mistral provider as a reference implementation at https://github.com/vercel/ai/tree/main/packages/mistral
Provider entry point creation example
import {
generateId,
loadApiKey,
withoutTrailingSlash,
} from '@ai-sdk/provider-utils';
import { ProviderV4 } from '@ai-sdk/provider';
import { CustomChatLanguageModel } from './custom-chat-language-model';
// Define your provider interface extending ProviderV4
interface CustomProvider extends ProviderV4 {
(modelId: string, settings?: CustomChatSettings): CustomChatLanguageModel;
// Add specific methods for different model types
languageModel(
modelId: string,
settings?: CustomChatSettings,
): CustomChatLanguageModel;
}
// Provider settings
interface CustomProviderSettings {
/**
* Base URL for API calls
*/
baseURL?: string;
/**
* API key for authentication
*/
apiKey?: string;
/**
* Custom headers for requests
*/
headers?: Record<string, string>;
}
// Factory function to create provider instance
function createCustom(options: CustomProviderSettings = {}): CustomProvider {
const createChatModel = (
modelId: string,
settings: CustomChatSettings = {},
) =>
new CustomChatLanguageModel(modelId, settings, {
provider: 'custom',
baseURL:
withoutTrailingSlash(options.baseURL) ?? 'https://api.custom.ai/v1',
headers: () => ({
Authorization: `Bearer ${loadApiKey({
apiKey: options.apiKey,
environmentVariableName: 'CUSTOM_API_KEY',
description: 'Custom Provider',
})}`,
...options.headers,
}),
generateId: options.generateId ?? generateId,
});
const provider = function (modelId: string, settings?: CustomChatSettings) {
if (new.target) {
throw new Error(
'The model factory function cannot be called with the new keyword.',
);
}
return createChatModel(modelId, settings);
};
provider.languageModel = createChatModel;
return provider as CustomProvider;
}
// Export default provider instance
const custom = createCustom();
LanguageModelV4 implementation structure
class CustomChatLanguageModel implements LanguageModelV4 {
readonly specificationVersion = 'V4';
readonly provider: string;
readonly modelId: string;
constructor(
modelId: string,
settings: CustomChatSettings,
config: CustomChatConfig,
) {
this.provider = config.provider;
this.modelId = modelId;
// Initialize with settings and config
}
// Convert AI SDK prompt to provider format
private getArgs(options: LanguageModelV4CallOptions) {
const warnings: SharedV4Warning[] = [];
// Map messages to provider format
const messages = this.convertToProviderMessages(options.prompt);
// Handle tools if provided
const tools = options.tools
? this.prepareTools(options.tools, options.toolChoice)
: undefined;
// Build request body
const body = {
model: this.modelId,
messages,
temperature: options.temperature,
max_tokens: options.maxOutputTokens,
stop: options.stopSequences,
tools,
// ... other parameters
};
return { args: body, warnings };
}
async doGenerate(options: LanguageModelV4CallOptions) {
const { args, warnings } = this.getArgs(options);
// Make API call
const response = await postJsonToApi({
url: `${this.config.baseURL}/chat/completions`,
headers: this.config.headers(),
body: args,
abortSignal: options.abortSignal,
});
// Convert provider response to AI SDK format
const content: LanguageModelV4Content[] = [];
// Extract text content
if (response.choices[0].message.content) {
content.push({
type: 'text',
text: response.choices[0].message.content,
});
}
// Extract tool calls
if (response.choices[0].message.tool_calls) {
for (const toolCall of response.choices[0].message.tool_calls) {
content.push({
type: 'tool-call',
toolCallType: 'function',
toolCallId: toolCall.id,
toolName: toolCall.function.name,
args: JSON.stringify(toolCall.function.arguments),
});
}
}
return {
content,
finishReason: this.mapFinishReason(response.choices[0].finish_reason),
usage: {
inputTokens: response.usage?.prompt_tokens,
outputTokens: response.usage?.completion_tokens,
totalTokens: response.usage?.total_tokens,
},
request: { body: args },
response: { body: response },
warnings,
};
}
async doStream(options: LanguageModelV4CallOptions) {
const { args, warnings } = this.getArgs(options);
// Create streaming response
const response = await fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
...this.config.headers(),
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...args, stream: true }),
signal: options.abortSignal,
});
// Transform stream to AI SDK format
const stream = response
.body!.pipeThrough(new TextDecoderStream())
.pipeThrough(this.createParser())
.pipeThrough(this.createTransformer(warnings));
return { stream, warnings };
}
// Supported URL patterns for native file handling
get supportedUrls() {
return {
'image/*': [/^https:\/\/example\.com\/images\/.*/],
};
}
}
Message conversion implementation example
private convertToProviderMessages(prompt: LanguageModelV4Prompt) {
return prompt.map((message) => {
switch (message.role) {
case 'system':
return { role: 'system', content: message.content };
case 'user':
return {
role: 'user',
content: message.content.map((part) => {
switch (part.type) {
case 'text':
return { type: 'text', text: part.text };
case 'file':
return {
type: 'image_url',
image_url: {
url: this.convertFileToUrl(part.data),
},
};
default:
throw new Error(`Unsupported part type: ${part.type}`);
}
}),
};
case 'assistant':
// Handle assistant messages with text, tool calls, etc.
return this.convertAssistantMessage(message);
case 'tool':
// Handle tool results
return this.convertToolMessage(message);
default:
throw new Error(`Unsupported message role: ${message.role}`);
}
});
}
Streaming transformer implementation example
private createTransformer(warnings: SharedV4Warning[]) {
let isFirstChunk = true;
return new TransformStream<ParsedChunk, LanguageModelV4StreamPart>({
async transform(chunk, controller) {
// Send warnings with first chunk
if (isFirstChunk) {
controller.enqueue({ type: 'stream-start', warnings });
isFirstChunk = false;
}
// Handle different chunk types
if (chunk.choices?.[0]?.delta?.content) {
controller.enqueue({
type: 'text',
text: chunk.choices[0].delta.content,
});
}
if (chunk.choices?.[0]?.delta?.tool_calls) {
for (const toolCall of chunk.choices[0].delta.tool_calls) {
controller.enqueue({
type: 'tool-call-delta',
toolCallType: 'function',
toolCallId: toolCall.id,
toolName: toolCall.function.name,
argsTextDelta: toolCall.function.arguments,
});
}
}
// Handle finish reason
if (chunk.choices?.[0]?.finish_reason) {
controller.enqueue({
type: 'finish',
finishReason: this.mapFinishReason(chunk.choices[0].finish_reason),
usage: {
inputTokens: chunk.usage?.prompt_tokens,
outputTokens: chunk.usage?.completion_tokens,
totalTokens: chunk.usage?.total_tokens,
},
});
}
},
});
}
Error handling implementation example
import {
APICallError,
InvalidResponseDataError,
TooManyRequestsError,
} from '@ai-sdk/provider';
private handleError(error: unknown): never {
if (error instanceof Response) {
const status = error.status;
if (status === 429) {
throw new TooManyRequestsError({
cause: error,
retryAfter: this.getRetryAfter(error),
});
}
throw new APICallError({
statusCode: status,
statusText: error.statusText,
cause: error,
isRetryable: status >= 500 && status < 600,
});
}
throw error;
}
Workflow serialization config type requirement
To make a custom provider compatible with Workflow DevKit, the headers field in config must be optional so deserialized models work without pre-configured auth:
type CustomChatConfig = {
provider: string;
baseURL: string;
headers?: () => Record<string, string | undefined>; // must be optional
fetch?: FetchFunction;
generateId?: () => string;
};
Workflow serialization methods implementation
import {
serializeModel,
deserializeModel,
WORKFLOW_SERIALIZE,
WORKFLOW_DESERIALIZE,
} from '@ai-sdk/provider-utils';
class CustomChatLanguageModel implements LanguageModelV4 {
// Note: classId is generated automatically by the workflow SWC compiler at build time
static [WORKFLOW_SERIALIZE](model: CustomChatLanguageModel) {
return serializeModel(model);
}
static [WORKFLOW_DESERIALIZE](options: {
modelId: string;
config: CustomChatConfig;
}) {
return deserializeModel(CustomChatLanguageModel, options);
}
// ... rest of class
}
Workflow serialization behavior
serializeModel() automatically extracts only serializable config properties (strings, numbers, booleans), filtering out functions like headers, fetch, and generateId. Since headers are not serialized, call sites must use optional chaining: headers: combineHeaders(this.config.headers?.(), options.headers) instead of headers: combineHeaders(this.config.headers(), options.headers). The deserialized model will not have auth headers; in a workflow context, authentication is typically handled through environment variables or request-level options available in the step execution environment.
V4 specification resources
Language Model Specification V4 source code is available at https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/v4. Provider utilities are available at https://github.com/vercel/ai/tree/main/packages/provider-utils. V4 types are documented at https://github.com/vercel/ai/tree/main/packages/provider/src/language-model/v4 under @ai-sdk/provider package.