Skip to content
Merged
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
17 changes: 16 additions & 1 deletion apps/sim/app/api/billing/update-cost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { userStats } from '@sim/db/schema'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { logModelUsage } from '@/lib/billing/core/usage-log'
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { checkInternalApiKey } from '@/lib/copilot/utils'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
Expand All @@ -14,6 +15,9 @@ const logger = createLogger('BillingUpdateCostAPI')
const UpdateCostSchema = z.object({
userId: z.string().min(1, 'User ID is required'),
cost: z.number().min(0, 'Cost must be a non-negative number'),
model: z.string().min(1, 'Model is required'),
inputTokens: z.number().min(0).default(0),
outputTokens: z.number().min(0).default(0),
})

/**
Expand Down Expand Up @@ -71,11 +75,12 @@ export async function POST(req: NextRequest) {
)
}

const { userId, cost } = validation.data
const { userId, cost, model, inputTokens, outputTokens } = validation.data

logger.info(`[${requestId}] Processing cost update`, {
userId,
cost,
model,
})

// Check if user stats record exists (same as ExecutionLogger)
Expand Down Expand Up @@ -107,6 +112,16 @@ export async function POST(req: NextRequest) {
addedCost: cost,
})

// Log usage for complete audit trail
await logModelUsage({
userId,
source: 'copilot',
model,
inputTokens,
outputTokens,
cost,
})

// Check if user has hit overage threshold and bill incrementally
await checkAndBillOverageThreshold(userId)

Expand Down
105 changes: 105 additions & 0 deletions apps/sim/app/api/users/me/usage-logs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('UsageLogsAPI')

const QuerySchema = z.object({
source: z.enum(['workflow', 'wand', 'copilot']).optional(),
workspaceId: z.string().optional(),
period: z.enum(['1d', '7d', '30d', 'all']).optional().default('30d'),
limit: z.coerce.number().min(1).max(100).optional().default(50),
cursor: z.string().optional(),
})

/**
* GET /api/users/me/usage-logs
* Get usage logs for the authenticated user
*/
export async function GET(req: NextRequest) {
try {
const auth = await checkHybridAuth(req, { requireWorkflowId: false })

if (!auth.success || !auth.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const userId = auth.userId

const { searchParams } = new URL(req.url)
const queryParams = {
source: searchParams.get('source') || undefined,
workspaceId: searchParams.get('workspaceId') || undefined,
period: searchParams.get('period') || '30d',
limit: searchParams.get('limit') || '50',
cursor: searchParams.get('cursor') || undefined,
}

const validation = QuerySchema.safeParse(queryParams)

if (!validation.success) {
return NextResponse.json(
{
error: 'Invalid query parameters',
details: validation.error.issues,
},
{ status: 400 }
)
}

const { source, workspaceId, period, limit, cursor } = validation.data

let startDate: Date | undefined
const endDate = new Date()

if (period !== 'all') {
startDate = new Date()
switch (period) {
case '1d':
startDate.setDate(startDate.getDate() - 1)
break
case '7d':
startDate.setDate(startDate.getDate() - 7)
break
case '30d':
startDate.setDate(startDate.getDate() - 30)
break
}
}

const result = await getUserUsageLogs(userId, {
source: source as UsageLogSource | undefined,
workspaceId,
startDate,
endDate,
limit,
cursor,
})

logger.debug('Retrieved usage logs', {
userId,
source,
period,
logCount: result.logs.length,
hasMore: result.pagination.hasMore,
})

return NextResponse.json({
success: true,
...result,
})
} catch (error) {
logger.error('Failed to get usage logs', {
error: error instanceof Error ? error.message : String(error),
})

return NextResponse.json(
{
error: 'Failed to retrieve usage logs',
},
{ status: 500 }
)
}
}
15 changes: 14 additions & 1 deletion apps/sim/app/api/wand/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import OpenAI, { AzureOpenAI } from 'openai'
import { getSession } from '@/lib/auth'
import { logModelUsage } from '@/lib/billing/core/usage-log'
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { env } from '@/lib/core/config/env'
import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/feature-flags'
Expand Down Expand Up @@ -88,7 +89,7 @@ async function updateUserStatsForWand(

try {
const [workflowRecord] = await db
.select({ userId: workflow.userId })
.select({ userId: workflow.userId, workspaceId: workflow.workspaceId })
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)
Expand All @@ -101,6 +102,7 @@ async function updateUserStatsForWand(
}

const userId = workflowRecord.userId
const workspaceId = workflowRecord.workspaceId
const totalTokens = usage.total_tokens || 0
const promptTokens = usage.prompt_tokens || 0
const completionTokens = usage.completion_tokens || 0
Expand Down Expand Up @@ -137,6 +139,17 @@ async function updateUserStatsForWand(
costAdded: costToStore,
})

await logModelUsage({
userId,
source: 'wand',
model: modelName,
inputTokens: promptTokens,
outputTokens: completionTokens,
cost: costToStore,
workspaceId: workspaceId ?? undefined,
workflowId,
})

await checkAndBillOverageThreshold(userId)
} catch (error) {
logger.error(`[${requestId}] Failed to update user stats for wand usage`, error)
Expand Down
Loading
Loading