Usage Tracking Examples¶
This document provides examples of implementing usage tracking for the Jodit AI Adapter service.
Basic Usage Tracking¶
import { start, type UsageStats } from 'jodit-ai-adapter';
const { cleanup } = await start({
port: 8082,
onUsage: async (stats: UsageStats) => {
console.log('AI Usage:', {
user: stats.userId,
provider: stats.provider,
model: stats.model,
tokens: stats.totalTokens,
duration: stats.duration + 'ms'
});
}
});
// Call cleanup() to gracefully shut down
Database Integration¶
PostgreSQL Example¶
import { Pool } from 'pg';
import { start } from 'jodit-ai-adapter';
const pool = new Pool({
host: 'localhost',
database: 'myapp',
user: 'user',
password: 'password'
});
await start({
port: 8082,
checkAuthentication: async (apiKey) => {
const result = await pool.query(
'SELECT id FROM users WHERE api_key = $1 AND active = true',
[apiKey]
);
return result.rows[0]?.id || null;
},
onUsage: async (stats) => {
await pool.query(
`INSERT INTO ai_usage
(user_id, provider, model, response_id,
prompt_tokens, completion_tokens, total_tokens,
credits, usd_cost,
duration, timestamp, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
[
stats.userId,
stats.provider,
stats.model,
stats.responseId,
stats.promptTokens || 0,
stats.completionTokens || 0,
stats.totalTokens || 0,
stats.credits.credits,
stats.credits.usdCost,
stats.duration,
new Date(stats.timestamp),
JSON.stringify(stats.metadata)
]
);
}
});
MongoDB Example¶
import { MongoClient } from 'mongodb';
import { start } from 'jodit-ai-adapter';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('myapp');
await start({
port: 8082,
checkAuthentication: async (apiKey) => {
const user = await db.collection('users').findOne({
apiKey,
active: true
});
return user?._id.toString() || null;
},
onUsage: async (stats) => {
await db.collection('ai_usage').insertOne({
userId: stats.userId,
provider: stats.provider,
model: stats.model,
responseId: stats.responseId,
tokens: {
prompt: stats.promptTokens,
completion: stats.completionTokens,
total: stats.totalTokens
},
credits: stats.credits.credits,
usdCost: stats.credits.usdCost,
duration: stats.duration,
timestamp: new Date(stats.timestamp),
metadata: stats.metadata
});
}
});
Credits (Built-in Cost Calculation)¶
Every onUsage callback receives a credits field with a pre-calculated cost breakdown.
No need to maintain your own pricing tables — the adapter does it automatically.
See Credits for the full reference.
import { start } from 'jodit-ai-adapter';
await start({
port: 8082,
onUsage: async (stats) => {
const { credits } = stats;
await db.query(
`INSERT INTO ai_usage (user_id, model, tokens, credits, usd_cost, timestamp)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
stats.userId,
stats.model,
stats.totalTokens,
credits.credits,
credits.usdCost,
new Date(stats.timestamp)
]
);
console.log(`Cost: ${credits.credits} credits ($${credits.usdCost.toFixed(6)}) for ${stats.totalTokens} tokens`);
}
});
Rate Limiting Based on Usage¶
import { start } from 'jodit-ai-adapter';
import Redis from 'ioredis';
const redis = new Redis();
// Limits: max tokens per day
const DAILY_TOKEN_LIMIT = 100_000;
await start({
port: 8082,
checkAuthentication: async (apiKey) => {
const user = await db.users.findByApiKey(apiKey);
if (!user) return null;
// Check daily usage
const today = new Date().toISOString().split('T')[0];
const usageKey = `usage:${user.id}:${today}`;
const todayUsage = await redis.get(usageKey);
if (todayUsage && parseInt(todayUsage) >= DAILY_TOKEN_LIMIT) {
throw new Error('Daily token limit exceeded');
}
return user.id;
},
onUsage: async (stats) => {
// Track daily usage in Redis
const today = new Date().toISOString().split('T')[0];
const usageKey = `usage:${stats.userId}:${today}`;
await redis.incrby(usageKey, stats.totalTokens || 0);
await redis.expire(usageKey, 86400 * 2); // 2 days TTL
// Also save to database
await db.usage.create({
userId: stats.userId,
tokens: stats.totalTokens,
timestamp: new Date(stats.timestamp)
});
}
});
Billing Integration¶
import { start } from 'jodit-ai-adapter';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
await start({
port: 8082,
onUsage: async (stats) => {
const { credits } = stats;
if (credits.credits > 0) {
const user = await db.users.findById(stats.userId);
if (user.stripeCustomerId) {
// Report credits as metered usage
await stripe.subscriptionItems.createUsageRecord(
user.stripeSubscriptionItemId,
{
quantity: credits.credits,
timestamp: Math.floor(stats.timestamp / 1000)
}
);
}
}
await db.usage.create({
userId: stats.userId,
credits: credits.credits,
usdCost: credits.usdCost,
tokens: stats.totalTokens,
timestamp: new Date(stats.timestamp)
});
}
});
Analytics and Reporting¶
import { start } from 'jodit-ai-adapter';
await start({
port: 8082,
onUsage: async (stats) => {
// Send to analytics service
await analytics.track({
userId: stats.userId,
event: 'AI_Request_Completed',
properties: {
provider: stats.provider,
model: stats.model,
tokens: stats.totalTokens,
duration: stats.duration
},
timestamp: new Date(stats.timestamp)
});
// Update real-time metrics (e.g., Prometheus)
metrics.aiRequests.inc({
provider: stats.provider,
model: stats.model,
user: stats.userId
});
metrics.aiTokens.inc({
provider: stats.provider,
model: stats.model
}, stats.totalTokens || 0);
metrics.aiDuration.observe({
provider: stats.provider,
model: stats.model
}, stats.duration / 1000); // Convert to seconds
}
});
Logging Usage¶
import { start } from 'jodit-ai-adapter';
import winston from 'winston';
const usageLogger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({
filename: 'logs/ai-usage.log'
})
]
});
await start({
port: 8082,
onUsage: async (stats) => {
usageLogger.info('AI Usage', {
userId: stats.userId,
provider: stats.provider,
model: stats.model,
tokens: {
prompt: stats.promptTokens,
completion: stats.completionTokens,
total: stats.totalTokens
},
duration: stats.duration,
timestamp: stats.timestamp
});
}
});
Error Handling in Usage Callback¶
import { start } from 'jodit-ai-adapter';
await start({
port: 8082,
onUsage: async (stats) => {
try {
await db.usage.create(stats);
} catch (error) {
// Log error but don't fail the request
console.error('Failed to track usage:', error);
// Optionally: queue for retry
await queue.add('usage-tracking', stats, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
}
}
});
Multi-Tenant Usage Tracking¶
import { start } from 'jodit-ai-adapter';
await start({
port: 8082,
checkAuthentication: async (apiKey) => {
const apiKeyRecord = await db.apiKeys.findOne({
key: apiKey,
active: true
});
if (!apiKeyRecord) return null;
// Return composite ID: organizationId:userId
return `${apiKeyRecord.organizationId}:${apiKeyRecord.userId}`;
},
onUsage: async (stats) => {
const [organizationId, userId] = stats.userId.split(':');
// Track at organization level
await db.orgUsage.increment({
organizationId,
tokens: stats.totalTokens
});
// Track at user level
await db.userUsage.create({
organizationId,
userId,
provider: stats.provider,
model: stats.model,
tokens: stats.totalTokens,
timestamp: new Date(stats.timestamp)
});
}
});
Schema Example (PostgreSQL)¶
CREATE TABLE ai_usage (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
provider VARCHAR(50) NOT NULL,
model VARCHAR(100) NOT NULL,
response_id VARCHAR(255) NOT NULL,
prompt_tokens INTEGER,
completion_tokens INTEGER,
total_tokens INTEGER,
credits INTEGER NOT NULL DEFAULT 0,
usd_cost DECIMAL(12, 8) NOT NULL DEFAULT 0,
duration INTEGER,
timestamp TIMESTAMP NOT NULL,
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_user_timestamp ON ai_usage (user_id, timestamp);
CREATE INDEX idx_provider_model ON ai_usage (provider, model);
-- Query daily usage and cost by user
SELECT
user_id,
DATE(timestamp) as date,
SUM(total_tokens) as total_tokens,
SUM(credits) as total_credits,
SUM(usd_cost) as total_usd_cost,
COUNT(*) as request_count
FROM ai_usage
WHERE timestamp >= NOW() - INTERVAL '30 days'
GROUP BY user_id, DATE(timestamp)
ORDER BY date DESC;