Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silly-bottles-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ai": patch
---

feat(ai): refresh `customProvider` and `createProviderRegistry` to support file and skill upload abstractions
108 changes: 108 additions & 0 deletions content/docs/03-ai-sdk-core/45-provider-management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,38 @@ export const myProvider = customProvider({
});
```

### Example: files and skills interfaces

You can attach a provider's `files` or `skills` interface to your custom provider. This allows you to use `uploadFile` and `uploadSkill` through the same provider abstraction.

```ts
import { anthropic } from '@ai-sdk/anthropic';
import { openai } from '@ai-sdk/openai';
import { customProvider, uploadFile, uploadSkill } from 'ai';

// custom provider with files interface:
const myOpenAI = customProvider({
languageModels: {
'gpt-4o-mini': openai.responses('gpt-4o-mini'),
},
files: openai.files(),
Copy link
Copy Markdown
Collaborator

@lgrammel lgrammel Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idea for future work: what if we had a files composite that can upload to several providers at once?

});

// custom provider with skills interface:
const myAnthropic = customProvider({
languageModels: {
sonnet: anthropic('claude-sonnet-4-5'),
},
skills: anthropic.skills(),
});

// usage:
await uploadFile({ api: myOpenAI.files!(), data: fileData, filename: 'image.png' });
await uploadSkill({ api: myAnthropic.skills!(), files: skillFiles, displayTitle: 'My Skill' });
```

If no `files` or `skills` option is set but a `fallbackProvider` is configured, the custom provider will inherit those interfaces from the fallback.

## Provider Registry

You can create a [provider registry](/docs/reference/ai-sdk-core/provider-registry) with multiple providers and models using `createProviderRegistry`.
Expand Down Expand Up @@ -221,6 +253,82 @@ const { image } = await generateImage({
});
```

### Example: Use video models

You can access video models by using the `videoModel` method on the registry.
The provider id will become the prefix of the model id: `providerId:modelId`.

```ts highlight={"5"}
import { experimental_generateVideo } from 'ai';
import { fal } from '@ai-sdk/fal';
import { createProviderRegistry } from 'ai';

const registry = createProviderRegistry({ fal });

const { videos } = await experimental_generateVideo({
model: registry.videoModel('fal:luma-dream-machine/ray-2'),
prompt: 'A cat walking on a beach at sunset',
});
```

### Example: Use files interface

You can access a provider's files interface by calling `registry.files(providerId)`.
This is useful when you want to upload files through a provider in the registry before referencing them in model requests.

```ts highlight={"7,8"}
import { openai } from '@ai-sdk/openai';
import { createProviderRegistry, customProvider, generateText, uploadFile } from 'ai';

const registry = createProviderRegistry({
openai: customProvider({
languageModels: { 'gpt-4o-mini': openai.responses('gpt-4o-mini') },
files: openai.files(),
}),
});

const { providerReference } = await uploadFile({
api: registry.files('openai'),
data: fileData,
filename: 'image.png',
});

const { text } = await generateText({
model: registry.languageModel('openai:gpt-4o-mini'),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Describe what you see in this image.' },
{ type: 'image', image: providerReference },
],
},
],
});
```

### Example: Use skills interface

You can access a provider's skills interface by calling `registry.skills(providerId)`.

```ts highlight={"8"}
import { anthropic } from '@ai-sdk/anthropic';
import { createProviderRegistry, customProvider, uploadSkill } from 'ai';

const registry = createProviderRegistry({
anthropic: customProvider({
languageModels: { sonnet: anthropic('claude-sonnet-4-5') },
skills: anthropic.skills(),
}),
});

await uploadSkill({
api: registry.skills('anthropic'),
files: skillFiles,
displayTitle: 'My Skill',
});
```

## Combining Custom Providers, Provider Registry, and Middleware

The central idea of provider management is to set up a file that contains all the providers and models you want to use.
Expand Down
10 changes: 3 additions & 7 deletions examples/ai-e2e-next/app/api/chat/upload-file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@ import { xai } from '@ai-sdk/xai';
import {
consumeStream,
convertToModelMessages,
createProviderRegistry,
streamText,
UIMessage,
} from 'ai';

export const maxDuration = 60;

const providerMap = {
anthropic: (modelId: string) => anthropic(modelId),
google: (modelId: string) => google(modelId),
openai: (modelId: string) => openai.responses(modelId),
xai: (modelId: string) => xai(modelId),
};
const registry = createProviderRegistry({ anthropic, google, openai, xai });

export async function POST(req: Request) {
const {
Expand All @@ -29,7 +25,7 @@ export async function POST(req: Request) {
providerId: 'anthropic' | 'google' | 'openai' | 'xai';
} = await req.json();

const model = providerMap[providerId](modelId);
const model = registry.languageModel(`${providerId}:${modelId}`);

const result = streamText({
model,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { fal } from '@ai-sdk/fal';
import { customProvider, experimental_generateVideo } from 'ai';
import { presentVideos } from '../lib/present-video';
import { run } from '../lib/run';
import { withSpinner } from '../lib/spinner';

const myProvider = customProvider({
videoModels: {
'luma-ray-2': fal.video('luma-dream-machine/ray-2'),
},
});

run(async () => {
const { videos } = await withSpinner('Generating video...', () =>
experimental_generateVideo({
model: myProvider.videoModel('luma-ray-2'),
prompt: 'A cat walking on a beach at sunset',
aspectRatio: '16:9',
duration: 5,
}),
);

await presentVideos(videos);
});
20 changes: 20 additions & 0 deletions examples/ai-functions/src/registry/generate-video-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { fal } from '@ai-sdk/fal';
import { createProviderRegistry, experimental_generateVideo } from 'ai';
import { presentVideos } from '../lib/present-video';
import { run } from '../lib/run';
import { withSpinner } from '../lib/spinner';

const registry = createProviderRegistry({ fal });

run(async () => {
const { videos } = await withSpinner('Generating video...', () =>
experimental_generateVideo({
model: registry.videoModel('fal:luma-dream-machine/ray-2'),
prompt: 'A cat walking on a beach at sunset',
aspectRatio: '16:9',
duration: 5,
}),
);

await presentVideos(videos);
});
38 changes: 38 additions & 0 deletions examples/ai-functions/src/registry/upload-file-custom-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { openai } from '@ai-sdk/openai';
import { customProvider, generateText, uploadFile } from 'ai';
import fs from 'node:fs';
import { run } from '../lib/run';

const myProvider = customProvider({
languageModels: {
'gpt-4o-mini': openai.responses('gpt-4o-mini'),
},
files: openai.files(),
});

run(async () => {
const { providerReference, mediaType, filename } = await uploadFile({
api: myProvider.files(),
data: fs.readFileSync('./data/comic-cat.png'),
filename: 'comic-cat.png',
});

console.log('Provider reference:', providerReference);
console.log('Media type:', mediaType);
console.log('Filename:', filename);

const result = await generateText({
model: myProvider.languageModel('gpt-4o-mini'),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Describe what you see in this image.' },
{ type: 'image', image: providerReference },
],
},
],
});

console.log(result.text);
});
43 changes: 43 additions & 0 deletions examples/ai-functions/src/registry/upload-file-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { openai } from '@ai-sdk/openai';
import {
createProviderRegistry,
customProvider,
generateText,
uploadFile,
} from 'ai';
import fs from 'node:fs';
import { run } from '../lib/run';

const registry = createProviderRegistry({
openai: customProvider({
languageModels: { 'gpt-4o-mini': openai.responses('gpt-4o-mini') },
files: openai.files(),
}),
});

run(async () => {
const { providerReference, mediaType, filename } = await uploadFile({
api: registry.files('openai'),
data: fs.readFileSync('./data/comic-cat.png'),
filename: 'comic-cat.png',
});

console.log('Provider reference:', providerReference);
console.log('Media type:', mediaType);
console.log('Filename:', filename);

const result = await generateText({
model: registry.languageModel('openai:gpt-4o-mini'),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Describe what you see in this image.' },
{ type: 'image', image: providerReference },
],
},
],
});

console.log(result.text);
});
39 changes: 39 additions & 0 deletions examples/ai-functions/src/registry/upload-skill-custom-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { anthropic } from '@ai-sdk/anthropic';
import { customProvider, uploadSkill } from 'ai';
import { readFileSync } from 'fs';
import { deleteUploadedAnthropicSkill } from '../lib/delete-uploaded-skill';
import { run } from '../lib/run';

const myProvider = customProvider({
languageModels: {
sonnet: anthropic('claude-sonnet-4-5'),
},
skills: anthropic.skills(),
});

run(async () => {
const { providerReference, displayTitle, name, description, latestVersion } =
await uploadSkill({
api: myProvider.skills(),
files: [
{
path: 'island-rescue/SKILL.md',
content: readFileSync('data/island-rescue/SKILL.md'),
},
],
displayTitle: 'Island Rescue Test',
});

console.log('Provider reference:', providerReference);
console.log('Display title:', displayTitle);
console.log('Name:', name);
console.log('Description:', description);
console.log('Latest version:', latestVersion);

try {
await deleteUploadedAnthropicSkill({ providerReference });
console.log('Deleted uploaded Anthropic skill.');
} catch (error) {
console.error('Failed to delete uploaded Anthropic skill:', error);
}
});
32 changes: 32 additions & 0 deletions examples/ai-functions/src/registry/upload-skill-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { uploadSkill } from 'ai';
import { readFileSync } from 'fs';
import { deleteUploadedAnthropicSkill } from '../lib/delete-uploaded-skill';
import { run } from '../lib/run';
import { registry } from './setup-registry';

run(async () => {
const { providerReference, displayTitle, name, description, latestVersion } =
await uploadSkill({
api: registry.skills('anthropic'),
files: [
{
path: 'island-rescue/SKILL.md',
content: readFileSync('data/island-rescue/SKILL.md'),
},
],
displayTitle: 'Island Rescue Test',
});

console.log('Provider reference:', providerReference);
console.log('Display title:', displayTitle);
console.log('Name:', name);
console.log('Description:', description);
console.log('Latest version:', latestVersion);

try {
await deleteUploadedAnthropicSkill({ providerReference });
console.log('Deleted uploaded Anthropic skill.');
} catch (error) {
console.error('Failed to delete uploaded Anthropic skill:', error);
}
});
Loading
Loading