Tinybird Forward is live! See the Tinybird Forward docs to learn more. To migrate, see Migrate from Classic.
Instrument LLM calls from Vercel AI SDK¶
Vercel AI SDK is a powerful tool for building AI applications. It's a popular choice for many developers and organizations.
To start instrumenting LLM calls with the Vercel AI SDK and Tinybird, first create a data source with this schema:
SCHEMA > `model` LowCardinality(String) `json:$.model` DEFAULT 'unknown', `messages` Array(Map(String, String)) `json:$.messages[:]` DEFAULT [], `user` String `json:$.user` DEFAULT 'unknown', `start_time` DateTime `json:$.start_time` DEFAULT now(), `end_time` DateTime `json:$.end_time` DEFAULT now(), `id` String `json:$.id` DEFAULT '', `stream` Boolean `json:$.stream` DEFAULT false, `call_type` LowCardinality(String) `json:$.call_type` DEFAULT 'unknown', `provider` LowCardinality(String) `json:$.provider` DEFAULT 'unknown', `api_key` String `json:$.api_key` DEFAULT '', `log_event_type` LowCardinality(String) `json:$.log_event_type` DEFAULT 'unknown', `llm_api_duration_ms` Float32 `json:$.llm_api_duration_ms` DEFAULT 0, `cache_hit` Boolean `json:$.cache_hit` DEFAULT false, `response_status` LowCardinality(String) `json:$.standard_logging_object_status` DEFAULT 'unknown', `response_time` Float32 `json:$.standard_logging_object_response_time` DEFAULT 0, `proxy_metadata` String `json:$.proxy_metadata` DEFAULT '', `organization` String `json:$.proxy_metadata.organization` DEFAULT '', `environment` String `json:$.proxy_metadata.environment` DEFAULT '', `project` String `json:$.proxy_metadata.project` DEFAULT '', `chat_id` String `json:$.proxy_metadata.chat_id` DEFAULT '', `response` String `json:$.response` DEFAULT '', `response_id` String `json:$.response.id`, `response_object` String `json:$.response.object` DEFAULT 'unknown', `response_choices` Array(String) `json:$.response.choices[:]` DEFAULT [], `completion_tokens` UInt16 `json:$.response.usage.completion_tokens` DEFAULT 0, `prompt_tokens` UInt16 `json:$.response.usage.prompt_tokens` DEFAULT 0, `total_tokens` UInt16 `json:$.response.usage.total_tokens` DEFAULT 0, `cost` Float32 `json:$.cost` DEFAULT 0, `exception` String `json:$.exception` DEFAULT '', `traceback` String `json:$.traceback` DEFAULT '', `duration` Float32 `json:$.duration` DEFAULT 0 ENGINE MergeTree ENGINE_SORTING_KEY start_time, organization, project, model ENGINE_PARTITION_KEY toYYYYMM(start_time)
Use a wrapper around the LLM provider you use, this is an example using OpenAI:
const openai = createOpenAI({ apiKey: apiKey }); const wrappedOpenAI = wrapModelWithTinybird( openai('gpt-3.5-turbo'), process.env.NEXT_PUBLIC_TINYBIRD_API_URL!, process.env.TINYBIRD_TOKEN!, { event: 'search_filter', environment: process.env.NODE_ENV, project: 'ai-analytics', organization: 'your-org', } );
Implement the wrapper in your app:
import type { LanguageModelV1 } from '@ai-sdk/provider'; type TinybirdConfig = { event?: string; organization?: string; project?: string; environment?: string; user?: string; chatId?: string; }; export function wrapModelWithTinybird( model: LanguageModelV1, tinybirdHost: string, tinybirdToken: string, config: TinybirdConfig = {} ) { const originalDoGenerate = model.doGenerate; const originalDoStream = model.doStream; const logToTinybird = async ( messageId: string, startTime: Date, status: 'success' | 'error', // eslint-disable-next-line @typescript-eslint/no-explicit-any args: any[], result?: { text?: string; usage?: { promptTokens?: number; completionTokens?: number } }, error?: Error ) => { const endTime = new Date(); const duration = endTime.getTime() - startTime.getTime(); const event = { start_time: startTime.toISOString(), end_time: endTime.toISOString(), message_id: messageId, model: model.modelId || 'unknown', provider: 'openai', duration, llm_api_duration_ms: duration, response: status === 'success' ? { id: messageId, object: 'chat.completion', usage: { prompt_tokens: result?.usage?.promptTokens || 0, completion_tokens: result?.usage?.completionTokens || 0, total_tokens: (result?.usage?.promptTokens || 0) + (result?.usage?.completionTokens || 0), }, choices: [{ message: { content: result?.text ?? '' } }], } : undefined, messages: args[0]?.prompt ? [{ role: 'user', content: args[0].prompt }].map(m => ({ role: String(m.role), content: String(m.content) })) : [], proxy_metadata: { organization: config.organization || '', project: config.project || '', environment: config.environment || '', chat_id: config.chatId || '', }, user: config.user || 'unknown', standard_logging_object_status: status, standard_logging_object_response_time: duration, log_event_type: config.event || 'chat_completion', id: messageId, call_type: 'completion', cache_hit: false, ...(status === 'error' && { exception: error?.message || 'Unknown error', traceback: error?.stack || '', }), }; // Send to Tinybird fetch(`${tinybirdHost}/v0/events?name=llm_events`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${tinybirdToken}`, }, body: JSON.stringify(event), }).catch(console.error); }; model.doGenerate = async function (...args) { const startTime = new Date(); const messageId = crypto.randomUUID(); try { const result = await originalDoGenerate.apply(this, args); await logToTinybird(messageId, startTime, 'success', args, result); return result; } catch (error) { await logToTinybird(messageId, startTime, 'error', args, undefined, error as Error); throw error; } }; model.doStream = async function (...args) { const startTime = new Date(); const messageId = crypto.randomUUID(); try { const result = await originalDoStream.apply(this, args); await logToTinybird(messageId, startTime, 'success', args, { text: '', usage: { promptTokens: 0, completionTokens: 0 } }); return result; } catch (error) { await logToTinybird(messageId, startTime, 'error', args, undefined, error as Error); throw error; } }; return model; }
AI analytics template¶
Use the AI Analytics template to bootstrap a multi-tenant, user-facing AI analytics dashboard and LLM cost calculator for your AI models. You can fork it and make it your own.