mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
* v0 * v1 * Basic ss tes * Ss tests * Stuff * Add mcp * mcp v1 * Improvement * Fix * BROKEN * Checkpoint * Streaming * Fix abort * Things are broken * Streaming seems to work but copilot is dumb * Fix edge issue * LUAAAA * Fix stream buffer * Fix lint * Checkpoint * Initial temp state, in the middle of a refactor * Initial test shows diff store still working * Tool refactor * First cleanup pass complete - untested * Continued cleanup * Refactor * Refactor complete - no testing yet * Fix - cursor makes me sad * Fix mcp * Clean up mcp * Updated mcp * Add respond to subagents * Fix definitions * Add tools * Add tools * Add copilot mcp tracking * Fix lint * Fix mcp * Fix * Updates * Clean up mcp * Fix copilot mcp tool names to be sim prefixed * Add opus 4.6 * Fix discovery tool * Fix * Remove logs * Fix go side tool rendering * Update docs * Fix hydration * Fix tool call resolution * Fix * Fix lint * Fix superagent and autoallow integrations * Fix always allow * Update block * Remove plan docs * Fix hardcoded ff * Fix dropped provider * Fix lint * Fix tests * Fix dead messages array * Fix discovery * Fix run workflow * Fix run block * Fix run from block in copilot * Fix lint * Fix skip and mtb * Fix typing * Fix tool call * Bump api version * Fix bun lock * Nuke bad files
174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
import { db } from '@sim/db'
|
|
import { userStats } from '@sim/db/schema'
|
|
import { createLogger } from '@sim/logger'
|
|
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'
|
|
import { generateRequestId } from '@/lib/core/utils/request'
|
|
|
|
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),
|
|
source: z.enum(['copilot', 'mcp_copilot']).default('copilot'),
|
|
})
|
|
|
|
/**
|
|
* POST /api/billing/update-cost
|
|
* Update user cost with a pre-calculated cost value (internal API key auth required)
|
|
*/
|
|
export async function POST(req: NextRequest) {
|
|
const requestId = generateRequestId()
|
|
const startTime = Date.now()
|
|
|
|
try {
|
|
logger.info(`[${requestId}] Update cost request started`)
|
|
|
|
if (!isBillingEnabled) {
|
|
logger.debug(`[${requestId}] Billing is disabled, skipping cost update`)
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Billing disabled, cost update skipped',
|
|
data: {
|
|
billingEnabled: false,
|
|
processedAt: new Date().toISOString(),
|
|
requestId,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Check authentication (internal API key)
|
|
const authResult = checkInternalApiKey(req)
|
|
if (!authResult.success) {
|
|
logger.warn(`[${requestId}] Authentication failed: ${authResult.error}`)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: authResult.error || 'Authentication failed',
|
|
},
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
const body = await req.json()
|
|
const validation = UpdateCostSchema.safeParse(body)
|
|
|
|
if (!validation.success) {
|
|
logger.warn(`[${requestId}] Invalid request body`, {
|
|
errors: validation.error.issues,
|
|
body,
|
|
})
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Invalid request body',
|
|
details: validation.error.issues,
|
|
},
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const { userId, cost, model, inputTokens, outputTokens, source } = validation.data
|
|
const isMcp = source === 'mcp_copilot'
|
|
|
|
logger.info(`[${requestId}] Processing cost update`, {
|
|
userId,
|
|
cost,
|
|
model,
|
|
source,
|
|
})
|
|
|
|
// Check if user stats record exists (same as ExecutionLogger)
|
|
const userStatsRecords = await db.select().from(userStats).where(eq(userStats.userId, userId))
|
|
|
|
if (userStatsRecords.length === 0) {
|
|
logger.error(
|
|
`[${requestId}] User stats record not found - should be created during onboarding`,
|
|
{
|
|
userId,
|
|
}
|
|
)
|
|
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
|
|
}
|
|
|
|
const updateFields: Record<string, unknown> = {
|
|
totalCost: sql`total_cost + ${cost}`,
|
|
currentPeriodCost: sql`current_period_cost + ${cost}`,
|
|
totalCopilotCost: sql`total_copilot_cost + ${cost}`,
|
|
currentPeriodCopilotCost: sql`current_period_copilot_cost + ${cost}`,
|
|
totalCopilotCalls: sql`total_copilot_calls + 1`,
|
|
lastActive: new Date(),
|
|
}
|
|
|
|
// Also increment MCP-specific counters when source is mcp_copilot
|
|
if (isMcp) {
|
|
updateFields.totalMcpCopilotCost = sql`total_mcp_copilot_cost + ${cost}`
|
|
updateFields.currentPeriodMcpCopilotCost = sql`current_period_mcp_copilot_cost + ${cost}`
|
|
}
|
|
|
|
await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))
|
|
|
|
logger.info(`[${requestId}] Updated user stats record`, {
|
|
userId,
|
|
addedCost: cost,
|
|
source,
|
|
})
|
|
|
|
// Log usage for complete audit trail
|
|
await logModelUsage({
|
|
userId,
|
|
source: isMcp ? 'mcp_copilot' : 'copilot',
|
|
model,
|
|
inputTokens,
|
|
outputTokens,
|
|
cost,
|
|
})
|
|
|
|
// Check if user has hit overage threshold and bill incrementally
|
|
await checkAndBillOverageThreshold(userId)
|
|
|
|
const duration = Date.now() - startTime
|
|
|
|
logger.info(`[${requestId}] Cost update completed successfully`, {
|
|
userId,
|
|
duration,
|
|
cost,
|
|
})
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: {
|
|
userId,
|
|
cost,
|
|
processedAt: new Date().toISOString(),
|
|
requestId,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime
|
|
|
|
logger.error(`[${requestId}] Cost update failed`, {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
duration,
|
|
})
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Internal server error',
|
|
requestId,
|
|
},
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|