Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';

import { VercelAIGatewayProvider } from '../providers/VercelAIGatewayProvider';
import { ProviderType } from '../providers/ProviderType';
import { getModelPrice } from '../services/AccountingService';

describe('VercelAIGatewayProvider', () => {
it('routes Echo-scoped Vercel model IDs to the Vercel AI Gateway API', () => {
const provider = new VercelAIGatewayProvider(
false,
'vercel-ai-gateway/openai/gpt-5-mini'
);

const transformedBody = provider.transformRequestBody({
model: 'vercel-ai-gateway/openai/gpt-5-mini',
});

expect(provider.getType()).toBe(ProviderType.VERCEL_AI_GATEWAY);
expect(provider.getBaseUrl()).toBe('https://ai-gateway.vercel.sh/v1');
expect(transformedBody.model).toBe('openai/gpt-5-mini');
});

it('exposes Vercel AI Gateway pricing under a non-conflicting Echo model ID', () => {
expect(getModelPrice('vercel-ai-gateway/openai/gpt-5-mini')).toMatchObject({
provider: 'Vercel',
model: 'vercel-ai-gateway/openai/gpt-5-mini',
});
});
});
2 changes: 2 additions & 0 deletions packages/app/server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const env = createEnv({
GROQ_API_KEY: z.string().optional(),
XAI_API_KEY: z.string().optional(),
OPENROUTER_API_KEY: z.string().optional(),
AI_GATEWAY_API_KEY: z.string().optional(),
VERCEL_AI_GATEWAY_API_KEY: z.string().optional(),
TAVILY_API_KEY: z.string().optional(),
E2B_API_KEY: z.string().optional(),
GOOGLE_SERVICE_ACCOUNT_KEY_ENCODED: z.string().optional(),
Expand Down
6 changes: 6 additions & 0 deletions packages/app/server/src/providers/ProviderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { OpenAIImageProvider } from './OpenAIImageProvider';
import { OpenAIResponsesProvider } from './OpenAIResponsesProvider';
import { OpenRouterProvider } from './OpenRouterProvider';
import { ProviderType } from './ProviderType';
import { VercelAIGatewayProvider } from './VercelAIGatewayProvider';
import { XAIProvider } from './XAIProvider';
import {
VertexAIProvider,
Expand Down Expand Up @@ -50,6 +51,9 @@ const createChatModelToProviderMapping = (): Record<string, ProviderType> => {
case 'OpenRouter':
mapping[modelConfig.model_id] = ProviderType.OPENROUTER;
break;
case 'Vercel':
mapping[modelConfig.model_id] = ProviderType.VERCEL_AI_GATEWAY;
break;
case 'Groq':
mapping[modelConfig.model_id] = ProviderType.GROQ;
break;
Expand Down Expand Up @@ -180,6 +184,8 @@ export const getProvider = (
return new OpenAIResponsesProvider(stream, model);
case ProviderType.OPENROUTER:
return new OpenRouterProvider(stream, model);
case ProviderType.VERCEL_AI_GATEWAY:
return new VercelAIGatewayProvider(stream, model);
case ProviderType.OPENAI_IMAGES:
return new OpenAIImageProvider(stream, model);
case ProviderType.GEMINI_VEO:
Expand Down
1 change: 1 addition & 0 deletions packages/app/server/src/providers/ProviderType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum ProviderType {
VERTEX_AI = 'VERTEX_AI',
OPENAI_RESPONSES = 'OPENAI_RESPONSES',
OPENROUTER = 'OPENROUTER',
VERCEL_AI_GATEWAY = 'VERCEL_AI_GATEWAY',
OPENAI_IMAGES = 'OPENAI_IMAGES',
OPENAI_VIDEOS = 'OPENAI_VIDEOS',
GROQ = 'GROQ',
Expand Down
34 changes: 34 additions & 0 deletions packages/app/server/src/providers/VercelAIGatewayProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { env } from '../env';
import { GPTProvider } from './GPTProvider';
import { ProviderType } from './ProviderType';

export class VercelAIGatewayProvider extends GPTProvider {
private readonly VERCEL_AI_GATEWAY_BASE_URL =
'https://ai-gateway.vercel.sh/v1';
private readonly MODEL_PREFIX = 'vercel-ai-gateway/';

override getType(): ProviderType {
return ProviderType.VERCEL_AI_GATEWAY;
}

override getBaseUrl(): string {
return this.VERCEL_AI_GATEWAY_BASE_URL;
}

override getApiKey(): string | undefined {
return env.AI_GATEWAY_API_KEY ?? env.VERCEL_AI_GATEWAY_API_KEY;
}

override transformRequestBody(
reqBody: Record<string, unknown>
): Record<string, unknown> {
if (
typeof reqBody.model === 'string' &&
reqBody.model.startsWith(this.MODEL_PREFIX)
) {
reqBody.model = reqBody.model.slice(this.MODEL_PREFIX.length);
}

return reqBody;
}
}
2 changes: 2 additions & 0 deletions packages/app/server/src/services/AccountingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AnthropicModels,
GeminiModels,
OpenRouterModels,
VercelModels,
GroqModels,
OpenAIImageModels,
SupportedOpenAIResponseToolPricing,
Expand All @@ -28,6 +29,7 @@ export const ALL_SUPPORTED_MODELS: SupportedModel[] = [
...AnthropicModels,
...GeminiModels,
...OpenRouterModels,
...VercelModels,
...GroqModels,
...XAIModels,
];
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"update-models:anthropic": "tsx scripts/update-anthropic-models.ts",
"update-models:gemini": "tsx scripts/update-gemini-models.ts",
"update-models:openrouter": "tsx scripts/update-openrouter-models.ts",
"update-models:vercel": "tsx scripts/update-vercel-models.ts",
"update-models:groq": "tsx scripts/update-groq-models.ts",
"update-all-models": "pnpm run update-models:openai && pnpm run update-models:anthropic && pnpm run update-models:gemini && pnpm run update-models:openrouter && pnpm run update-models:groq",
"update-all-models": "pnpm run update-models:openai && pnpm run update-models:anthropic && pnpm run update-models:gemini && pnpm run update-models:openrouter && pnpm run update-models:vercel && pnpm run update-models:groq",
"prepublishOnly": "pnpm run build"
},
"keywords": [
Expand Down
120 changes: 120 additions & 0 deletions packages/sdk/ts/scripts/update-vercel-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env node

import { writeFileSync } from 'fs';
import { join } from 'path';
import { SupportedModel } from './update-models';

interface VercelModel {
id: string;
type?: string;
pricing?: {
input?: string;
output?: string;
};
}

interface VercelModelsResponse {
data: VercelModel[];
}

const ECHO_MODEL_PREFIX = 'vercel-ai-gateway/';

async function fetchVercelModels(): Promise<SupportedModel[]> {
console.log('Fetching models from Vercel AI Gateway...');

const response = await fetch('https://ai-gateway.vercel.sh/v1/models');

if (!response.ok) {
throw new Error(
`Failed to fetch Vercel AI Gateway models: ${response.status} ${response.statusText}`
);
}

const data = (await response.json()) as VercelModelsResponse;
const models: SupportedModel[] = [];

for (const model of data.data) {
if (model.type !== 'language') {
continue;
}

const inputCost = Number(model.pricing?.input);
const outputCost = Number(model.pricing?.output);

if (
Number.isNaN(inputCost) ||
Number.isNaN(outputCost) ||
inputCost === 0 ||
outputCost === 0
) {
console.warn(`Skipping ${model.id} - missing language token pricing`);
continue;
}

models.push({
model_id: `${ECHO_MODEL_PREFIX}${model.id}`,
input_cost_per_token: inputCost,
output_cost_per_token: outputCost,
provider: 'Vercel',
});
}

return models;
}

function generateVercelModelFile(models: SupportedModel[]): string {
const sortedModels = models.sort((a, b) =>
a.model_id.localeCompare(b.model_id)
);

const unionType = sortedModels
.map(model => ` | '${model.model_id}'`)
.join('\n');

const modelObjects = sortedModels
.map(model => {
return ` {
model_id: '${model.model_id}',
input_cost_per_token: ${model.input_cost_per_token},
output_cost_per_token: ${model.output_cost_per_token},
provider: '${model.provider}',
}`;
})
.join(',\n');

return `import { SupportedModel } from '../types';

// Union type of all valid Vercel AI Gateway language model IDs.
export type VercelModel =
${unionType};

export const VercelModels: SupportedModel[] = [
${modelObjects}
];

`;
}

async function updateVercelModels() {
try {
const models = await fetchVercelModels();

if (models.length === 0) {
throw new Error('No compatible Vercel AI Gateway language models found');
}

const fileContent = generateVercelModelFile(models);
const fullPath = join(process.cwd(), 'src/supported-models/chat/vercel.ts');
writeFileSync(fullPath, fileContent, 'utf8');

console.log(`Updated vercel.ts with ${models.length} models`);
} catch (error) {
console.error('Error updating Vercel AI Gateway models:', error);
process.exit(1);
}
}

updateVercelModels().catch(error => {
console.error('Unexpected error:', error);
process.exit(1);
});
2 changes: 2 additions & 0 deletions packages/sdk/ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export { GeminiModels } from './supported-models/chat/gemini';
export type { GeminiModel } from './supported-models/chat/gemini';
export { OpenRouterModels } from './supported-models/chat/openrouter';
export type { OpenRouterModel } from './supported-models/chat/openrouter';
export { VercelModels } from './supported-models/chat/vercel';
export type { VercelModel } from './supported-models/chat/vercel';
export { GroqModels } from './supported-models/chat/groq';
export type { GroqModel } from './supported-models/chat/groq';
export { XAIModels } from './supported-models/chat/xai';
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/ts/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './groq';
export * from './xai';
export * from './openai';
export * from './openrouter';
export * from './vercel';

export function echoFetch(
originalFetch: typeof fetch,
Expand Down
26 changes: 26 additions & 0 deletions packages/sdk/ts/src/providers/vercel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
createOpenAI as createOpenAICompatibleProvider,
OpenAIProvider,
} from '@ai-sdk/openai';
import { ROUTER_BASE_URL } from 'config';
import { EchoConfig } from '../types';
import { validateAppId } from '../utils/validation';
import { echoFetch } from './index';

export function createEchoVercelAIGateway(
{ appId, baseRouterUrl = ROUTER_BASE_URL }: EchoConfig,
getTokenFn: (appId: string) => Promise<string | null>,
onInsufficientFunds?: () => void
): OpenAIProvider {
validateAppId(appId, 'createEchoVercelAIGateway');

return createOpenAICompatibleProvider({
baseURL: baseRouterUrl,
apiKey: 'placeholder_replaced_by_echoFetch',
fetch: echoFetch(
fetch,
async () => await getTokenFn(appId),
onInsufficientFunds
),
});
}
2 changes: 2 additions & 0 deletions packages/sdk/ts/src/resources/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AnthropicModels,
GeminiModels,
OpenRouterModels,
VercelModels,
OpenAIImageModels,
SupportedModel,
SupportedImageModel,
Expand All @@ -26,6 +27,7 @@ export class ModelsResource extends BaseResource {
...AnthropicModels,
...GeminiModels,
...OpenRouterModels,
...VercelModels,
];

return allModels;
Expand Down
Loading