{"repo": "ashishlekhyani/gitscope", "files": {"src/app/api/ai/commit-message/route.ts": {"8037b038ec1a60fd55c264eb464684f056dc9865": {"t": 1785745577.1117463, "n": 5093, "h": "0505095f425316f3", "c": "/**\n * POST /api/ai/commit-message\n * ============================\n * Generate conventional commit messages from a diff or description.\n * Supports: Conventional Commits, Angular, Semantic, or free-form styles.\n */\n\nimport { getServerSession } from \"next-auth\";\nimport { authOptions } from \"@/lib/auth\";\nimport { resolveAiPlanFromSessionDb } from \"@/lib/ai-plan\";\nimport { getUserBYOKKeys } from \"@/lib/byok\";\nimport { consumeUsageBudget } from \"@/lib/ai-usage\";\nimport { callAI } from \"@/lib/ai-providers\";\nimport { callHuggingFace } from \"@/lib/hf-inference\";\n\nexport const maxDuration = 20;\n\ninterface CommitMsgBody {\n diff?: string;\n description?: string;\n changedFiles?: string[];\n style?: \"conventional\" | \"angular\" | \"semantic\" | \"free\";\n scope?: string;\n count?: number; // how many alternatives to generate (1-5)\n}\n\nconst COMMIT_SYSTEM = `You are a commit message expert. Generate precise, conventional commit messages that explain WHY the change was made, not just what changed.\n\nConventional Commits format:\n(): \n\n[optional body: more detail in imperative mood]\n\n[optional footer: BREAKING CHANGE, Closes #issue]\n\nTypes: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert\n\nRules:\n- Subject line: 50 chars max, imperative mood (\"add\" not \"added\"), no period at end\n- Body: wrapped at 72 chars, explains motivation and context\n- Be specific: \"fix: handle null user in auth middleware\" not \"fix: bug fix\"\n\nRespond with valid JSON:\n{\n \"messages\": [\n {\n \"subject\": \"feat(auth): add OAuth2 PKCE flow for SPA clients\",\n \"body\": \"Implements PKCE (Proof Key for Code Exchange) extension...\",\n \"footer\": \"Closes #142\",\n \"type\": \"feat\",\n \"scope\": \"auth\",\n \"breaking\": false\n }\n ]\n}`;\n\nexport async function POST(req: Request) {\n try {\n const session = await getServerSession(authOptions);\n if (!session?.user?.id) return Response.json({ error: \"Unauthorized\" }, { status: 401 });\n\n const body = (await req.json()) as CommitMsgBody;\n const { diff, description, changedFiles, style = \"conventional\", scope, count = 3 } = body;\n\n if (!diff && !description) {\n return Response.json({ error: \"Either diff or description is required\" }, { status: 400 });\n }\n\n const plan = await resolveAiPlanFromSessionDb(session);\n const budget = await consumeUsageBudget({ userId: session.user.id, feature: \"repo-analysis\", plan, limit: plan === \"free\" ? 10 : 100 });\n if (!budget.allowed) return Response.json({ error: \"Usage limit reached\" }, { status: 429 });\n\n const byokKeys = await getUserBYOKKeys(session.user.id);\n\n const userPrompt = `Generate ${count} ${style} commit message alternative(s) for this change.\n${scope ? `Suggested scope: ${scope}` : \"\"}\n${changedFiles?.length ? `Changed files: ${changedFiles.slice(0, 10).join(\", \")}` : \"\"}\n\n${description ? `## Developer's description\\n${description}\\n` : \"\"}\n${diff ? `## Diff (first 3000 chars)\\n\\`\\`\\`diff\\n${diff.slice(0, 3000)}\\n\\`\\`\\`` : \"\"}\n\nReturn the JSON schema with ${count} message option(s). Make each option meaningfully different.`;\n\n const hasAnyKey = byokKeys.anthropic || byokKeys.openai || byokKeys.gemini ||\n byokKeys.groq || byokKeys.deepseek || byokKeys.mistral || byokKeys.cerebras ||\n process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY ||\n process.env.GROQ_API_KEY || process.env.DEEPSEEK_API_KEY || process.env.CEREBRAS_API_KEY;\n\n let rawResponse: string;\n let modelUsed = \"huggingface\";\n let providerUsed = \"huggingface\";\n\n if (hasAnyKey) {\n const result = await callAI({\n plan: plan as \"free\" | \"developer\",\n systemPrompt: COMMIT_SYSTEM,\n userPrompt,\n maxTokens: 1200,\n byokKeys,\n });\n rawResponse = result?.text ?? \"\";\n modelUsed = result?.model ?? \"unknown\";\n providerUsed = result?.provider ?? \"unknown\";\n } else {\n const result = await callHuggingFace({\n tier: \"balanced\",\n messages: [{ role: \"system\", content: COMMIT_SYSTEM }, { role: \"user\", content: userPrompt }],\n maxNewTokens: 600,\n temperature: 0.5,\n apiKey: byokKeys.huggingface ?? undefined,\n });\n rawResponse = result?.text ?? \"\";\n modelUsed = result?.model ?? \"huggingface\";\n }\n\n try {\n const jsonMatch = rawResponse.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) throw new Error(\"No JSON\");\n const parsed = JSON.parse(jsonMatch[0]);\n return Response.json({ ...parsed, model: modelUsed, provider: providerUsed });\n } catch {\n // Fallback: parse the text as a simple message\n const firstLine = rawResponse.split(\"\\n\")[0]?.trim() ?? \"chore: update code\";\n return Response.json({\n messages: [{ subject: firstLine, body: \"\", footer: \"\", type: \"chore\", breaking: false }],\n model: modelUsed,\n provider: providerUsed,\n });\n }\n } catch (err) {\n console.error(\"[Commit Message]\", err);\n return Response.json({ error: \"Failed to generate commit message\" }, { status: 500 });\n }\n}\n"}, "adcbc2ef805fc84ac9de1ed8147bd3031efc8d9f": {"t": 1785754395.7817972, "n": 5093, "h": "0505095f425316f3", "c": "/**\n * POST /api/ai/commit-message\n * ============================\n * Generate conventional commit messages from a diff or description.\n * Supports: Conventional Commits, Angular, Semantic, or free-form styles.\n */\n\nimport { getServerSession } from \"next-auth\";\nimport { authOptions } from \"@/lib/auth\";\nimport { resolveAiPlanFromSessionDb } from \"@/lib/ai-plan\";\nimport { getUserBYOKKeys } from \"@/lib/byok\";\nimport { consumeUsageBudget } from \"@/lib/ai-usage\";\nimport { callAI } from \"@/lib/ai-providers\";\nimport { callHuggingFace } from \"@/lib/hf-inference\";\n\nexport const maxDuration = 20;\n\ninterface CommitMsgBody {\n diff?: string;\n description?: string;\n changedFiles?: string[];\n style?: \"conventional\" | \"angular\" | \"semantic\" | \"free\";\n scope?: string;\n count?: number; // how many alternatives to generate (1-5)\n}\n\nconst COMMIT_SYSTEM = `You are a commit message expert. Generate precise, conventional commit messages that explain WHY the change was made, not just what changed.\n\nConventional Commits format:\n(): \n\n[optional body: more detail in imperative mood]\n\n[optional footer: BREAKING CHANGE, Closes #issue]\n\nTypes: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert\n\nRules:\n- Subject line: 50 chars max, imperative mood (\"add\" not \"added\"), no period at end\n- Body: wrapped at 72 chars, explains motivation and context\n- Be specific: \"fix: handle null user in auth middleware\" not \"fix: bug fix\"\n\nRespond with valid JSON:\n{\n \"messages\": [\n {\n \"subject\": \"feat(auth): add OAuth2 PKCE flow for SPA clients\",\n \"body\": \"Implements PKCE (Proof Key for Code Exchange) extension...\",\n \"footer\": \"Closes #142\",\n \"type\": \"feat\",\n \"scope\": \"auth\",\n \"breaking\": false\n }\n ]\n}`;\n\nexport async function POST(req: Request) {\n try {\n const session = await getServerSession(authOptions);\n if (!session?.user?.id) return Response.json({ error: \"Unauthorized\" }, { status: 401 });\n\n const body = (await req.json()) as CommitMsgBody;\n const { diff, description, changedFiles, style = \"conventional\", scope, count = 3 } = body;\n\n if (!diff && !description) {\n return Response.json({ error: \"Either diff or description is required\" }, { status: 400 });\n }\n\n const plan = await resolveAiPlanFromSessionDb(session);\n const budget = await consumeUsageBudget({ userId: session.user.id, feature: \"repo-analysis\", plan, limit: plan === \"free\" ? 10 : 100 });\n if (!budget.allowed) return Response.json({ error: \"Usage limit reached\" }, { status: 429 });\n\n const byokKeys = await getUserBYOKKeys(session.user.id);\n\n const userPrompt = `Generate ${count} ${style} commit message alternative(s) for this change.\n${scope ? `Suggested scope: ${scope}` : \"\"}\n${changedFiles?.length ? `Changed files: ${changedFiles.slice(0, 10).join(\", \")}` : \"\"}\n\n${description ? `## Developer's description\\n${description}\\n` : \"\"}\n${diff ? `## Diff (first 3000 chars)\\n\\`\\`\\`diff\\n${diff.slice(0, 3000)}\\n\\`\\`\\`` : \"\"}\n\nReturn the JSON schema with ${count} message option(s). Make each option meaningfully different.`;\n\n const hasAnyKey = byokKeys.anthropic || byokKeys.openai || byokKeys.gemini ||\n byokKeys.groq || byokKeys.deepseek || byokKeys.mistral || byokKeys.cerebras ||\n process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY ||\n process.env.GROQ_API_KEY || process.env.DEEPSEEK_API_KEY || process.env.CEREBRAS_API_KEY;\n\n let rawResponse: string;\n let modelUsed = \"huggingface\";\n let providerUsed = \"huggingface\";\n\n if (hasAnyKey) {\n const result = await callAI({\n plan: plan as \"free\" | \"developer\",\n systemPrompt: COMMIT_SYSTEM,\n userPrompt,\n maxTokens: 1200,\n byokKeys,\n });\n rawResponse = result?.text ?? \"\";\n modelUsed = result?.model ?? \"unknown\";\n providerUsed = result?.provider ?? \"unknown\";\n } else {\n const result = await callHuggingFace({\n tier: \"balanced\",\n messages: [{ role: \"system\", content: COMMIT_SYSTEM }, { role: \"user\", content: userPrompt }],\n maxNewTokens: 600,\n temperature: 0.5,\n apiKey: byokKeys.huggingface ?? undefined,\n });\n rawResponse = result?.text ?? \"\";\n modelUsed = result?.model ?? \"huggingface\";\n }\n\n try {\n const jsonMatch = rawResponse.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) throw new Error(\"No JSON\");\n const parsed = JSON.parse(jsonMatch[0]);\n return Response.json({ ...parsed, model: modelUsed, provider: providerUsed });\n } catch {\n // Fallback: parse the text as a simple message\n const firstLine = rawResponse.split(\"\\n\")[0]?.trim() ?? \"chore: update code\";\n return Response.json({\n messages: [{ subject: firstLine, body: \"\", footer: \"\", type: \"chore\", breaking: false }],\n model: modelUsed,\n provider: providerUsed,\n });\n }\n } catch (err) {\n console.error(\"[Commit Message]\", err);\n return Response.json({ error: \"Failed to generate commit message\" }, { status: 500 });\n }\n}\n"}, "55db0478feb74c77dd60619764b73140f8d0abb8": {"t": 1785754397.4058456, "n": 5042, "h": "0409520aa97cfe52", "c": "/**\n * POST /api/ai/commit-message\n * ============================\n * Generate conventional commit messages from a diff or description.\n * Supports: Conventional Commits, Angular, Semantic, or free-form styles.\n */\n\nimport { getServerSession } from \"next-auth\";\nimport { authOptions } from \"@/lib/auth\";\nimport { resolveAiPlanFromSessionDb } from \"@/lib/ai-plan\";\nimport { getUserBYOKKeys } from \"@/lib/byok\";\nimport { consumeUsageBudget } from \"@/lib/ai-usage\";\nimport { callAI } from \"@/lib/ai-providers\";\nimport { callHuggingFace } from \"@/lib/hf-inference\";\n\nexport const maxDuration = 20;\n\ninterface CommitMsgBody {\n diff?: string;\n description?: string;\n changedFiles?: string[];\n style?: \"conventional\" | \"angular\" | \"semantic\" | \"free\";\n scope?: string;\n count?: number; // how many alternatives to generate (1-5)\n}\n\nconst COMMIT_SYSTEM = `You are a commit message expert. Generate precise, conventional commit messages that explain WHY the change was made, not just what changed.\n\nConventional Commits format:\n(): \n\n[optional body: more detail in imperative mood]\n\n[optional footer: BREAKING CHANGE, Closes #issue]\n\nTypes: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert\n\nRules:\n- Subject line: 50 chars max, imperative mood (\"add\" not \"added\"), no period at end\n- Body: wrapped at 72 chars, explains motivation and context\n- Be specific: \"fix: handle null user in auth middleware\" not \"fix: bug fix\"\n\nRespond with valid JSON:\n{\n \"messages\": [\n {\n \"subject\": \"feat(auth): add OAuth2 PKCE flow for SPA clients\",\n \"body\": \"Implements PKCE (Proof Key for Code Exchange) extension...\",\n \"footer\": \"Closes #142\",\n \"type\": \"feat\",\n \"scope\": \"auth\",\n \"breaking\": false\n }\n ]\n}`;\n\nexport async function POST(req: Request) {\n try {\n const session = await getServerSession(authOptions);\n if (!session?.user?.id) return Response.json({ error: \"Unauthorized\" }, { status: 401 });\n\n const body = (await req.json()) as CommitMsgBody;\n const { diff, description, changedFiles, style = \"conventional\", scope, count = 3 } = body;\n\n if (!diff && !description) {\n return Response.json({ error: \"Either diff or description is required\" }, { status: 400 });\n }\n\n const plan = await resolveAiPlanFromSessionDb(session);\n const budget = await consumeUsageBudget({ userId: session.user.id, feature: \"repo-analysis\", plan, limit: plan === \"free\" ? 10 : 100 });\n if (!budget.allowed) return Response.json({ error: \"Usage limit reached\" }, { status: 429 });\n\n const byokKeys = await getUserBYOKKeys(session.user.id);\n\n const userPrompt = `Generate ${count} ${style} commit message alternative(s) for this change.\n${scope ? `Suggested scope: ${scope}` : \"\"}\n${changedFiles?.length ? `Changed files: ${changedFiles.slice(0, 10).join(\", \")}` : \"\"}\n\n${description ? `## Developer's description\\n${description}\\n` : \"\"}\n${diff ? `## Diff (first 3000 chars)\\n\\`\\`\\`diff\\n${diff.slice(0, 3000)}\\n\\`\\`\\`` : \"\"}\n\nReturn the JSON schema with ${count} message option(s). Make each option meaningfully different.`;\n\n const hasAnyKey = byokKeys.anthropic || byokKeys.openai || byokKeys.gemini ||\n byokKeys.groq || byokKeys.deepseek || byokKeys.mistral || byokKeys.cerebras ||\n process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY ||\n process.env.GROQ_API_KEY || process.env.DEEPSEEK_API_KEY || process.env.CEREBRAS_API_KEY;\n\n let rawResponse: string;\n let modelUsed = \"huggingface\";\n let providerUsed = \"huggingface\";\n\n if (hasAnyKey) {\n const result = await callAI({\n plan: plan as \"free\" | \"developer\",\n systemPrompt: COMMIT_SYSTEM,\n userPrompt,\n maxTokens: 1200,\n byokKeys,\n });\n rawResponse = result?.text ?? \"\";\n modelUsed = result?.model ?? \"unknown\";\n providerUsed = result?.provider ?? \"unknown\";\n } else {\n const result = await callHuggingFace({\n tier: \"balanced\",\n messages: [{ role: \"system\", content: COMMIT_SYSTEM }, { role: \"user\", content: userPrompt }],\n maxNewTokens: 600,\n temperature: 0.5,\n });\n rawResponse = result?.text ?? \"\";\n modelUsed = result?.model ?? \"huggingface\";\n }\n\n try {\n const jsonMatch = rawResponse.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) throw new Error(\"No JSON\");\n const parsed = JSON.parse(jsonMatch[0]);\n return Response.json({ ...parsed, model: modelUsed, provider: providerUsed });\n } catch {\n // Fallback: parse the text as a simple message\n const firstLine = rawResponse.split(\"\\n\")[0]?.trim() ?? \"chore: update code\";\n return Response.json({\n messages: [{ subject: firstLine, body: \"\", footer: \"\", type: \"chore\", breaking: false }],\n model: modelUsed,\n provider: providerUsed,\n });\n }\n } catch (err) {\n console.error(\"[Commit Message]\", err);\n return Response.json({ error: \"Failed to generate commit message\" }, { status: 500 });\n }\n}\n"}}}}