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
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 @@ -26,6 +26,7 @@ import {
VertexAIProvider,
PROXY_PASSTHROUGH_ONLY_MODEL as VertexAIProxyPassthroughOnlyModel,
} from './VertexAIProvider';
import { VercelAIGatewayProvider } from './VercelAIGatewayProvider';

/**
* Creates model-to-provider mapping from the model_prices_and_context_window.json file.
Expand Down Expand Up @@ -58,6 +59,9 @@ const createChatModelToProviderMapping = (): Record<string, ProviderType> => {
case 'Xai':
mapping[modelConfig.model_id] = ProviderType.XAI;
break;
case 'VercelAIGateway':
mapping[modelConfig.model_id] = ProviderType.VERCEL_AI_GATEWAY;
break;
// Add other providers as needed
default:
// Skip models with unsupported providers
Expand Down Expand Up @@ -192,6 +196,8 @@ export const getProvider = (
return new GroqProvider(stream, model);
case ProviderType.XAI:
return new XAIProvider(stream, model);
case ProviderType.VERCEL_AI_GATEWAY:
return new VercelAIGatewayProvider(stream, model);
default:
throw new Error(`Unknown provider type: ${type}`);
}
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 @@ -12,4 +12,5 @@ export enum ProviderType {
OPENAI_VIDEOS = 'OPENAI_VIDEOS',
GROQ = 'GROQ',
XAI = 'XAI',
VERCEL_AI_GATEWAY = 'VERCEL_AI_GATEWAY',
}
79 changes: 79 additions & 0 deletions packages/app/server/src/providers/VercelAIGatewayProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { LlmTransactionMetadata, Transaction } from '../types';
import { BaseProvider } from './BaseProvider';
import { ProviderType } from './ProviderType';
import { getCostPerToken } from '../services/AccountingService';
import logger from '../logger';
import { parseSSEGPTFormat, CompletionStateBody } from './GPTProvider';


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

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

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

getApiKey(): string | undefined {
return process.env.AI_GATEWAY_API_KEY;
}

async handleBody(data: string): Promise<Transaction> {
try {
let prompt_tokens = 0;
let completion_tokens = 0;
let total_tokens = 0;
let providerId = 'null';

if (this.getIsStream()) {
// Parse Server-Sent Events (SSE) format for streaming responses
const chunks = parseSSEGPTFormat(data);

for (const chunk of chunks) {
if (chunk.usage && chunk.usage !== null) {
prompt_tokens += chunk.usage.prompt_tokens;
completion_tokens += chunk.usage.completion_tokens;
total_tokens += chunk.usage.total_tokens;
}
providerId = chunk.id || 'null';
}
} else {
// Parse non-streaming response
const parsed = JSON.parse(data) as CompletionStateBody;
prompt_tokens += parsed.usage.prompt_tokens;
completion_tokens += parsed.usage.completion_tokens;
total_tokens += parsed.usage.total_tokens;
providerId = parsed.id || 'null';
}

// Calculate cost based on token usage
const cost = getCostPerToken(
this.getModel(),
prompt_tokens,
completion_tokens
);

const metadata: LlmTransactionMetadata = {
providerId: providerId,
provider: this.getType(),
model: this.getModel(),
inputTokens: prompt_tokens,
outputTokens: completion_tokens,
totalTokens: total_tokens,
};

return {
metadata: metadata,
rawTransactionCost: cost,
status: 'success',
};
} catch (error) {
logger.error(`Error processing Vercel AI Gateway response: ${error}`);
throw error;
}
}
}

112 changes: 112 additions & 0 deletions packages/app/server/src/services/AccountingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
SupportedModel,
SupportedImageModel,
SupportedVideoModel,
SupportedSpeechModel,
SupportedTranscriptionModel,
XAIModels,
VercelAIGatewayModels,
OpenAISpeechModels,
OpenAITranscriptionModels,
} from '@merit-systems/echo-typescript-sdk';

import { Decimal } from '@prisma/client/runtime/library';
Expand All @@ -30,6 +35,7 @@ export const ALL_SUPPORTED_MODELS: SupportedModel[] = [
...OpenRouterModels,
...GroqModels,
...XAIModels,
...VercelAIGatewayModels,
];

// Handle image models separately since they have different pricing structure
Expand All @@ -42,6 +48,14 @@ export const ALL_SUPPORTED_VIDEO_MODELS: SupportedVideoModel[] = [
...OpenAIVideoModels,
];

export const ALL_SUPPORTED_SPEECH_MODELS: SupportedSpeechModel[] = [
...OpenAISpeechModels,
];

export const ALL_SUPPORTED_TRANSCRIPTION_MODELS: SupportedTranscriptionModel[] = [
...OpenAITranscriptionModels,
];

// Create a lookup map for O(1) model price retrieval
const MODEL_PRICE_MAP = new Map<string, SupportedModel>();
ALL_SUPPORTED_MODELS.forEach(model => {
Expand All @@ -60,6 +74,18 @@ ALL_SUPPORTED_VIDEO_MODELS.forEach(model => {
VIDEO_MODEL_MAP.set(model.model_id, model);
});

// Create a separate map for speech models
const SPEECH_MODEL_MAP = new Map<string, SupportedSpeechModel>();
ALL_SUPPORTED_SPEECH_MODELS.forEach(model => {
SPEECH_MODEL_MAP.set(model.model_id, model);
});

// Create a separate map for transcription models
const TRANSCRIPTION_MODEL_MAP = new Map<string, SupportedTranscriptionModel>();
ALL_SUPPORTED_TRANSCRIPTION_MODELS.forEach(model => {
TRANSCRIPTION_MODEL_MAP.set(model.model_id, model);
});

export const getModelPrice = (model: string) => {
const supportedModel = MODEL_PRICE_MAP.get(model);

Expand Down Expand Up @@ -114,6 +140,92 @@ export const isValidVideoModel = (model: string) => {
return VIDEO_MODEL_MAP.has(model);
};

export const isValidSpeechModel = (model: string) => {
return SPEECH_MODEL_MAP.has(model);
};

export const isValidTranscriptionModel = (model: string) => {
return TRANSCRIPTION_MODEL_MAP.has(model);
};

export const getSpeechModelPrice = (
model: string
): SupportedSpeechModel | null => {
const speechModel = SPEECH_MODEL_MAP.get(model);
if (speechModel) {
return speechModel;
}
return null;
};

export const getTranscriptionModelPrice = (
model: string
): SupportedTranscriptionModel | null => {
const transcriptionModel = TRANSCRIPTION_MODEL_MAP.get(model);
if (transcriptionModel) {
return transcriptionModel;
}
return null;
};

/**
* Calculate cost for speech generation (text-to-speech)
* @param model - Speech model ID
* @param characterCount - Number of characters in the input text
* @returns Cost in dollars
*/
export const getSpeechCost = (
model: string,
characterCount: number
): Decimal => {
if (!isValidSpeechModel(model)) {
throw new UnknownModelError(`Invalid speech model: ${model}`);
}

const modelPrice = getSpeechModelPrice(model);
if (!modelPrice) {
throw new Error(`Pricing information not found for speech model: ${model}`);
}

const cost = new Decimal(modelPrice.cost_per_character).mul(characterCount);

if (cost.lessThan(0)) {
throw new Error(`Invalid cost for speech model: ${model}`);
}

return cost;
};

/**
* Calculate cost for transcription (speech-to-text)
* @param model - Transcription model ID
* @param audioSeconds - Duration of audio in seconds
* @returns Cost in dollars
*/
export const getTranscriptionCost = (
model: string,
audioSeconds: number
): Decimal => {
if (!isValidTranscriptionModel(model)) {
throw new UnknownModelError(`Invalid transcription model: ${model}`);
}

const modelPrice = getTranscriptionModelPrice(model);
if (!modelPrice) {
throw new Error(
`Pricing information not found for transcription model: ${model}`
);
}

const cost = new Decimal(modelPrice.cost_per_second).mul(audioSeconds);

if (cost.lessThan(0)) {
throw new Error(`Invalid cost for transcription model: ${model}`);
}

return cost;
};

export const getCostPerToken = (
model: string,
inputTokens: number,
Expand Down
3 changes: 2 additions & 1 deletion packages/app/server/src/services/EchoControlService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export class EchoControlService {

constructor(db: PrismaClient, apiKey: string) {
// Check if the generated Prisma client exists
const generatedPrismaPath = join(__dirname, 'generated', 'prisma');
// Note: __dirname is /src/services/, so we need to go up one level to /src/
const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma');
if (!existsSync(generatedPrismaPath)) {
throw new Error(
`Generated Prisma client not found at ${generatedPrismaPath}. ` +
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/next/src/ai-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './google';
export * from './xai';
export * from './groq';
export * from './openai';
export * from './vercel-ai-gateway';
11 changes: 11 additions & 0 deletions packages/sdk/next/src/ai-providers/vercel-ai-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {
createEchoVercelAIGateway as createEchoVercelAIGatewayBase,
EchoConfig,
} from '@merit-systems/echo-typescript-sdk';
import { getEchoToken } from '../auth/token-manager';


export function createEchoVercelAIGateway(config: EchoConfig) {
return createEchoVercelAIGatewayBase(config, async () => getEchoToken(config));
}

4 changes: 3 additions & 1 deletion packages/sdk/ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"update-models:gemini": "tsx scripts/update-gemini-models.ts",
"update-models:openrouter": "tsx scripts/update-openrouter-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-models:vercel-ai-gateway": "tsx scripts/update-vercel-ai-gateway-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 && pnpm run update-models:vercel-ai-gateway",
"prepublishOnly": "pnpm run build"
},
"keywords": [
Expand Down Expand Up @@ -58,6 +59,7 @@
],
"dependencies": {
"@ai-sdk/anthropic": "2.0.17",
"@ai-sdk/gateway": "^1.0.12",
"@ai-sdk/google": "2.0.14",
"@ai-sdk/groq": "2.0.17",
"@ai-sdk/openai": "2.0.32",
Expand Down
Loading
Loading