new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

AI SDK · Providers · all subjects

custom-providers/setup

13 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

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.

Give your agent this brain