Skip to content
Draft
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
18 changes: 9 additions & 9 deletions app/api/analyses/[id]/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { generateText } from 'ai'
import { getCreditBalance, deductCredits, CREDITS } from '@/lib/credits'
import { getCurrentUser } from '@/lib/auth'

const model = 'openai/gpt-4-turbo'

Expand All @@ -21,18 +22,17 @@ interface AppSuggestion {

export async function POST(request: NextRequest) {
try {
const { analysisId, selectedRepos, userId } = (await request.json()) as {
analysisId: string
selectedRepos: SelectedRepository[]
userId: string
const user = await getCurrentUser()
if (!user) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
}

// Check credit balance before proceeding
if (!userId) {
return NextResponse.json({ error: 'User ID required' }, { status: 401 })
const { analysisId, selectedRepos } = (await request.json()) as {
analysisId: string
selectedRepos: SelectedRepository[]
}

const currentBalance = await getCreditBalance(userId)
const currentBalance = await getCreditBalance(user.id)
if (currentBalance < CREDITS.ANALYSIS_COST) {
return NextResponse.json(
{
Expand Down Expand Up @@ -95,7 +95,7 @@ Return as JSON array of app suggestions. Focus on practical, buildable applicati
}

// Deduct credits for successful analysis
const deductResult = await deductCredits(userId, CREDITS.ANALYSIS_COST, 'analysis', {
const deductResult = await deductCredits(user.id, CREDITS.ANALYSIS_COST, 'analysis', {
analysisId,
selectedRepos: selectedRepos.map((r) => r.name),
})
Expand Down
2 changes: 1 addition & 1 deletion app/api/auth/vercel/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function GET(request: NextRequest) {
// Save to DB
const db = getDb()
await db`
UPDATE users
UPDATE user_auth
SET vercel_access_token = ${access_token},
vercel_team_id = ${team_id ?? null},
updated_at = NOW()
Expand Down
2 changes: 1 addition & 1 deletion app/api/auth/vercel/disconnect/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function POST() {

const db = getDb()
await db`
UPDATE users
UPDATE user_auth
SET vercel_access_token = NULL,
vercel_team_id = NULL,
updated_at = NOW()
Expand Down
50 changes: 39 additions & 11 deletions app/api/build-app/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest } from 'next/server'
import { generateWithGateway } from '@/lib/ai-gateway'
import { generateWithGatewayDetailed } from '@/lib/ai-gateway'
import { getCurrentUser } from '@/lib/auth'
import { getSubscriptionByGithubId, upsertSubscription, type AppBlueprint } from '@/lib/queries'
import { hasProAccess } from '@/lib/pro-access'
Expand All @@ -15,6 +15,10 @@ interface BuildAppRequest {
>
}

function isTruncatedFinishReason(finishReason: string | null | undefined): boolean {
return finishReason === 'length' || finishReason === 'max_tokens'
}

/** Generate a single file's content using Claude */
async function generateSingleFile(
blueprint: BuildAppRequest['blueprint'],
Expand Down Expand Up @@ -46,12 +50,22 @@ Write the FULL, working implementation for this file.
Return ONLY the raw file content — no markdown fences, no explanation, no preamble.
Just the file content itself, ready to save.`

return generateWithGateway({
const result = await generateWithGatewayDetailed({
feature: 'build-app',
userId,
maxOutputTokens: 4096,
messages: [{ role: 'user', content: prompt }],
})

if (isTruncatedFinishReason(result.finishReason)) {
throw new Error(`AI output was truncated while generating ${filePath}`)
}

if (!result.text.trim()) {
throw new Error(`AI returned empty content for ${filePath}`)
}

return result.text
}

/** Build the list of all files to generate */
Expand Down Expand Up @@ -127,8 +141,8 @@ async function pushFileToGitHub(
)

if (!res.ok) {
const err = (await res.json()) as { message?: string }
console.warn(`[build-app] Failed to push ${path}: ${err.message}`)
const err = (await res.json().catch(() => ({}))) as { message?: string }
throw new Error(err.message ?? `Failed to push ${path} to GitHub`)
}
}

Expand Down Expand Up @@ -189,8 +203,8 @@ async function pushFileToGitLab(
)

if (!res.ok) {
const err = (await res.json()) as { message?: string }
console.warn(`[build-app] Failed to push ${path} to GitLab: ${err.message}`)
const err = (await res.json().catch(() => ({}))) as { message?: string }
throw new Error(err.message ?? `Failed to push ${path} to GitLab`)
}
}

Expand Down Expand Up @@ -285,20 +299,34 @@ export async function POST(request: NextRequest) {
content = await generateSingleFile(blueprint, path, purpose, user.id)
} catch (e) {
console.warn(`[build-app] Failed to generate ${path}:`, e)
content = `# Error generating ${path}\n# ${e instanceof Error ? e.message : String(e)}\n`
send({
step: 'error',
message: `Could not generate ${path}: ${e instanceof Error ? e.message : String(e)}`,
})
return
}

// Push to platform
if (platform === 'github') {
await pushFileToGitHub(accessToken, user.github_username, cleanRepoName, path, content)
} else if (gitlabProjectId !== null) {
await pushFileToGitLab(accessToken, gitlabProjectId, gitlabBranch, path, content)
try {
if (platform === 'github') {
await pushFileToGitHub(accessToken, user.github_username, cleanRepoName, path, content)
} else if (gitlabProjectId !== null) {
await pushFileToGitLab(accessToken, gitlabProjectId, gitlabBranch, path, content)
}
} catch (e) {
console.warn(`[build-app] Failed to push ${path}:`, e)
send({
step: 'error',
message: `Could not push ${path}: ${e instanceof Error ? e.message : String(e)}`,
})
return
}

pushed++
send({
step: 'pushing',
message: `Pushed ${pushed}/${total} files`,
repoUrl,
current: pushed,
total,
path,
Expand Down
51 changes: 27 additions & 24 deletions app/api/generate-scaffold/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: aiConfigErrorMessage() }, { status: 503 })
}

const { appName, description, technologies, existingFiles, missingFiles, userId } = await request.json()
const { appName, description, technologies, existingFiles, missingFiles } = await request.json()
const user = await getCurrentUser()
if (!user) {
return NextResponse.json({ error: 'Sign in with GitHub to generate scaffolds.' }, { status: 401 })
Expand All @@ -32,19 +32,17 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
}

if (userId) {
const currentBalance = await getCreditBalance(userId)
if (currentBalance < CREDITS.SCAFFOLD_COST) {
return NextResponse.json(
{
error: 'Insufficient credits',
required: CREDITS.SCAFFOLD_COST,
available: currentBalance,
message: 'Upgrade to Pro to get unlimited scaffold generation with 3,000 monthly credits.',
},
{ status: 402 },
)
}
const currentBalance = await getCreditBalance(user.id)
if (currentBalance < CREDITS.SCAFFOLD_COST) {
return NextResponse.json(
{
error: 'Insufficient credits',
required: CREDITS.SCAFFOLD_COST,
available: currentBalance,
message: 'Upgrade to Pro to get unlimited scaffold generation with 3,000 monthly credits.',
},
{ status: 402 },
)
}

const raw = await generateWithGateway({
Expand Down Expand Up @@ -119,23 +117,28 @@ Example structure:
throw new Error(`Failed to parse scaffold: ${e instanceof Error ? e.message : 'Invalid JSON'}`)
}

let creditsUsed = 0
if (userId) {
const deductResult = await deductCredits(userId, CREDITS.SCAFFOLD_COST, 'scaffold', {
appName,
technologies,
})
const deductResult = await deductCredits(user.id, CREDITS.SCAFFOLD_COST, 'scaffold', {
appName,
technologies,
})

if (deductResult.success) {
creditsUsed = CREDITS.SCAFFOLD_COST
}
if (!deductResult.success) {
return NextResponse.json(
{
error: 'Insufficient credits',
required: CREDITS.SCAFFOLD_COST,
available: await getCreditBalance(user.id),
message: deductResult.error,
},
{ status: 402 },
)
}

return NextResponse.json({
success: true,
scaffold,
appName,
creditsUsed,
creditsUsed: CREDITS.SCAFFOLD_COST,
})
} catch (error) {
console.error('[scaffold] Generation error:', error)
Expand Down
38 changes: 29 additions & 9 deletions lib/ai-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ export function gatewayProviderOptions(userId?: string, feature?: AiGatewayFeatu
} as const
}

export interface GenerateWithGatewayParams {
system?: string
messages: Array<{ role: 'user' | 'assistant'; content: string }>
maxOutputTokens?: number
temperature?: number
userId?: string
feature: AiGatewayFeature
}

export interface GenerateWithGatewayResult {
text: string
finishReason?: string | null
}

export function getAnthropicClient(): Anthropic {
const gatewayKey = process.env.AI_GATEWAY_API_KEY?.trim()
const oidc = process.env.VERCEL_OIDC_TOKEN?.trim()
Expand All @@ -87,14 +101,9 @@ export function getAnthropicClient(): Anthropic {
return new Anthropic({ apiKey: directKey })
}

export async function generateWithGateway(params: {
system?: string
messages: Array<{ role: 'user' | 'assistant'; content: string }>
maxOutputTokens?: number
temperature?: number
userId?: string
feature: AiGatewayFeature
}): Promise<string> {
export async function generateWithGatewayDetailed(
params: GenerateWithGatewayParams,
): Promise<GenerateWithGatewayResult> {
// When we have a direct Anthropic key (no gateway auth), use the SDK client directly
// to avoid routing through Vercel AI Gateway which requires paid gateway credits.
if (!usesGatewayAuth() && directAnthropicKey()) {
Expand All @@ -107,7 +116,10 @@ export async function generateWithGateway(params: {
messages: params.messages,
})
const block = response.content[0]
return block.type === 'text' ? block.text : ''
return {
text: block.type === 'text' ? block.text : '',
finishReason: response.stop_reason,
}
}

// Otherwise route through Vercel AI Gateway
Expand All @@ -125,6 +137,14 @@ export async function generateWithGateway(params: {
...gatewayProviderOptions(params.userId, params.feature),
})

return {
text: result.text,
finishReason: result.finishReason,
}
}

export async function generateWithGateway(params: GenerateWithGatewayParams): Promise<string> {
const result = await generateWithGatewayDetailed(params)
return result.text
}

Expand Down
Loading
Loading