Skip to content

Commit 97fedf9

Browse files
authored
feat (providers/gateway): include description and pricing info in model list (#7081)
## Background Vercel AI Gateway customers would like programmatic access to pricing info and brief description for the models in the model library. ## Summary Add input/output token cost for the model entries produced by `getAvailableModels` in the Gateway provider. ## Verification Updated unit tests and tested manually with debug output in an example script. ## Future Work We aren't using the `specification` data in `GatewayLanguageModelEntry` type. I looked at removing it as part of this change as dead code/data, but as we've exported the specification type in the provider's `index.ts`, removing it wholesale would be a breaking change. Leaving further work here for the future.
1 parent 82fc049 commit 97fedf9

File tree

5 files changed

+157
-7
lines changed

5 files changed

+157
-7
lines changed

.changeset/gentle-hairs-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/gateway': patch
3+
---
4+
5+
feat (providers/gateway): include description and pricing info in model list

packages/gateway/src/gateway-fetch-metadata.test.ts

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,28 @@ describe('GatewayFetchMetadata', () => {
2828
const mockModelEntry = {
2929
id: 'model-1',
3030
name: 'Model One',
31+
description: 'A test model',
32+
pricing: {
33+
input: '0.000001',
34+
output: '0.000002',
35+
},
3136
specification: {
32-
specificationVersion: 'v2',
37+
specificationVersion: 'v2' as const,
3338
provider: 'test-provider',
3439
modelId: 'model-1',
3540
},
3641
};
3742

43+
const mockModelEntryWithoutPricing = {
44+
id: 'model-2',
45+
name: 'Model Two',
46+
specification: {
47+
specificationVersion: 'v2' as const,
48+
provider: 'test-provider',
49+
modelId: 'model-2',
50+
},
51+
};
52+
3853
const server = createTestServer({
3954
'https://api.example.com/*': {
4055
response: {
@@ -59,6 +74,77 @@ describe('GatewayFetchMetadata', () => {
5974
});
6075
});
6176

77+
it('should handle models with pricing information', async () => {
78+
server.urls['https://api.example.com/*'].response = {
79+
type: 'json-value',
80+
body: {
81+
models: [mockModelEntry],
82+
},
83+
};
84+
85+
const metadata = createBasicMetadataFetcher();
86+
const result = await metadata.getAvailableModels();
87+
88+
expect(result.models[0]).toEqual(mockModelEntry);
89+
expect(result.models[0].pricing).toEqual({
90+
input: '0.000001',
91+
output: '0.000002',
92+
});
93+
});
94+
95+
it('should handle models without pricing information', async () => {
96+
server.urls['https://api.example.com/*'].response = {
97+
type: 'json-value',
98+
body: {
99+
models: [mockModelEntryWithoutPricing],
100+
},
101+
};
102+
103+
const metadata = createBasicMetadataFetcher();
104+
const result = await metadata.getAvailableModels();
105+
106+
expect(result.models[0]).toEqual(mockModelEntryWithoutPricing);
107+
expect(result.models[0].pricing).toBeUndefined();
108+
});
109+
110+
it('should handle mixed models with and without pricing', async () => {
111+
server.urls['https://api.example.com/*'].response = {
112+
type: 'json-value',
113+
body: {
114+
models: [mockModelEntry, mockModelEntryWithoutPricing],
115+
},
116+
};
117+
118+
const metadata = createBasicMetadataFetcher();
119+
const result = await metadata.getAvailableModels();
120+
121+
expect(result.models).toHaveLength(2);
122+
expect(result.models[0].pricing).toEqual({
123+
input: '0.000001',
124+
output: '0.000002',
125+
});
126+
expect(result.models[1].pricing).toBeUndefined();
127+
});
128+
129+
it('should handle models with description', async () => {
130+
const modelWithDescription = {
131+
...mockModelEntry,
132+
description: 'A powerful language model',
133+
};
134+
135+
server.urls['https://api.example.com/*'].response = {
136+
type: 'json-value',
137+
body: {
138+
models: [modelWithDescription],
139+
},
140+
};
141+
142+
const metadata = createBasicMetadataFetcher();
143+
const result = await metadata.getAvailableModels();
144+
145+
expect(result.models[0].description).toBe('A powerful language model');
146+
});
147+
62148
it('should pass headers correctly', async () => {
63149
const metadata = createBasicMetadataFetcher({
64150
headers: () => ({
@@ -159,6 +245,34 @@ describe('GatewayFetchMetadata', () => {
159245

160246
await expect(metadata.getAvailableModels()).rejects.toThrow();
161247
});
248+
249+
it('should reject models with invalid pricing format', async () => {
250+
server.urls['https://api.example.com/*'].response = {
251+
type: 'json-value',
252+
body: {
253+
models: [
254+
{
255+
id: 'model-1',
256+
name: 'Model One',
257+
pricing: {
258+
input: 123, // Should be string, not number
259+
output: '0.000002',
260+
},
261+
specification: {
262+
specificationVersion: 'v2',
263+
provider: 'test-provider',
264+
modelId: 'model-1',
265+
},
266+
},
267+
],
268+
},
269+
};
270+
271+
const metadata = createBasicMetadataFetcher();
272+
273+
await expect(metadata.getAvailableModels()).rejects.toThrow();
274+
});
275+
162276
it('should not double-wrap existing Gateway errors', async () => {
163277
// Create a Gateway error and verify it doesn't get wrapped
164278
const existingError = new GatewayAuthenticationError({
@@ -260,8 +374,13 @@ describe('GatewayFetchMetadata', () => {
260374
const customModelEntry = {
261375
id: 'custom-model-1',
262376
name: 'Custom Model One',
377+
description: 'Custom model description',
378+
pricing: {
379+
input: '0.000005',
380+
output: '0.000010',
381+
},
263382
specification: {
264-
specificationVersion: 'v2',
383+
specificationVersion: 'v2' as const,
265384
provider: 'custom-provider',
266385
modelId: 'custom-model-1',
267386
},

packages/gateway/src/gateway-fetch-metadata.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,16 @@ const gatewayLanguageModelSpecificationSchema = z.object({
4646
modelId: z.string(),
4747
});
4848

49+
const gatewayLanguageModelPricingSchema = z.object({
50+
input: z.string(),
51+
output: z.string(),
52+
});
53+
4954
const gatewayLanguageModelEntrySchema = z.object({
5055
id: z.string(),
5156
name: z.string(),
57+
description: z.string().nullish(),
58+
pricing: gatewayLanguageModelPricingSchema.nullish(),
5259
specification: gatewayLanguageModelSpecificationSchema,
5360
});
5461

packages/gateway/src/gateway-language-model.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,18 +149,18 @@ export class GatewayLanguageModel implements LanguageModelV2 {
149149
}
150150

151151
/**
152-
* Encodes image parts in the prompt to base64. Mutates the passed options
153-
* instance directly to avoid copying the image data.
152+
* Encodes file parts in the prompt to base64. Mutates the passed options
153+
* instance directly to avoid copying the file data.
154154
* @param options - The options to encode.
155-
* @returns The options with the image parts encoded.
155+
* @returns The options with the file parts encoded.
156156
*/
157157
private maybeEncodeFileParts(options: LanguageModelV2CallOptions) {
158158
for (const message of options.prompt) {
159159
for (const part of message.content) {
160160
if (this.isFilePart(part)) {
161161
const filePart = part as LanguageModelV2FilePart;
162-
// If the image part is a URL it will get cleanly converted to a string.
163-
// If it's a binary image attachment we convert it to a data url.
162+
// If the file part is a URL it will get cleanly converted to a string.
163+
// If it's a binary file attachment we convert it to a data url.
164164
// In either case, server-side we should only ever see URLs as strings.
165165
if (filePart.data instanceof Uint8Array) {
166166
const buffer = Uint8Array.from(filePart.data);

packages/gateway/src/gateway-model-entry.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ export interface GatewayLanguageModelEntry {
1212
*/
1313
name: string;
1414

15+
/**
16+
* Optional description of the model.
17+
*/
18+
description?: string | null;
19+
20+
/**
21+
* Optional pricing information for the model.
22+
*/
23+
pricing?: {
24+
/**
25+
* Cost per input token in USD.
26+
*/
27+
input: string;
28+
/**
29+
* Cost per output token in USD.
30+
*/
31+
output: string;
32+
} | null;
33+
1534
/**
1635
* Additional AI SDK language model specifications for the model.
1736
*/

0 commit comments

Comments
 (0)