-
Notifications
You must be signed in to change notification settings - Fork 41
feat: enhance AI review process and configuration options: #300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ import prisma from "@super/db" | |
| /** | ||
| * POST /api/reviews/trigger | ||
| * Body: { owner, repo, prNumber } | ||
| * Manually queue an AI review for a connected repo (useful when webhooks can't reach localhost). | ||
| * Manually run an AI review for a connected repo (useful when webhooks can't reach localhost). | ||
| */ | ||
| export async function POST(req: NextRequest) { | ||
| try { | ||
|
|
@@ -49,6 +49,7 @@ export async function POST(req: NextRequest) { | |
| const result = await reviewPullRequest(owner, repo, prNumber, { | ||
| userId: session.user.id, | ||
| source: "api_trigger", | ||
| wait: true, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }) | ||
| return NextResponse.json(result) | ||
| } catch (error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,16 @@ import { | |
| } from "@/modules/pull-requests/components/pr-workspace" | ||
| import { toast } from "sonner" | ||
|
|
||
| const STALE_PENDING_MS = 10 * 60 * 1000 | ||
|
|
||
| function isStalePending(review: { | ||
| status: string | ||
| updatedAt?: Date | string | ||
| } | null | undefined) { | ||
| if (review?.status !== "pending" || !review.updatedAt) return false | ||
| return Date.now() - new Date(review.updatedAt).getTime() >= STALE_PENDING_MS | ||
| } | ||
|
|
||
| function hasCompletedReview(review: { | ||
| status: string | ||
| review?: string | ||
|
|
@@ -73,15 +83,12 @@ export default function ReviewDetailPage(props: { | |
|
|
||
| const queueMutation = useMutation({ | ||
| mutationFn: () => queueReview(id), | ||
| onSuccess: (result) => { | ||
| toast.success(result.message || "AI review queued") | ||
| queryClient.setQueryData(["review", id], (prev: unknown) => | ||
| prev && typeof prev === "object" | ||
| ? { ...(prev as object), status: "pending" } | ||
| : prev, | ||
| ) | ||
| queryClient.invalidateQueries({ queryKey: ["review", id] }) | ||
| queryClient.invalidateQueries({ queryKey: ["reviews"] }) | ||
| onSuccess: async (result) => { | ||
| toast.success(result.message || "AI review completed") | ||
| await Promise.all([ | ||
| queryClient.invalidateQueries({ queryKey: ["review", id] }), | ||
| queryClient.invalidateQueries({ queryKey: ["reviews"] }), | ||
| ]) | ||
| }, | ||
| onError: (error) => { | ||
| toast.error( | ||
|
|
@@ -103,15 +110,17 @@ export default function ReviewDetailPage(props: { | |
| // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally once when review first loads | ||
| }, [isLoading, review?.status, review?.prState, review?.review]) | ||
|
|
||
| const stalePending = isStalePending(review) | ||
| const isGenerating = | ||
| queueMutation.isPending || review?.status === "pending" | ||
| queueMutation.isPending || (review?.status === "pending" && !stalePending) | ||
| const completed = hasCompletedReview(review) | ||
| const showGenerate = | ||
| !!review && | ||
| !completed && | ||
| !isGenerating && | ||
| (review.status === "unreviewed" || | ||
| review.status === "failed" || | ||
| stalePending || | ||
|
Comment on lines
120
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A pending review older than ten minutes may still be running because there is no heartbeat. This condition enables another review without checking for an active run, and direct execution does not share Inngest's per-PR concurrency control. Both runs can then update the same database row and sticky GitHub comment, allowing an older run that finishes last to overwrite newer output. Recovery should verify that no run is active or reject results from obsolete runs or head SHAs. |
||
| !review.review?.trim()) | ||
|
|
||
| if (!isLoading && !review) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -153,9 +153,22 @@ export function chatModel( | |||||
| return googleDirect(bareModelId(modelId)) | ||||||
| } | ||||||
|
|
||||||
| export function embeddingProvider(): "vercel" | "merge" | "openai" | null { | ||||||
| const configured = (process.env.EMBEDDING_PROVIDER ?? "").trim().toLowerCase() | ||||||
| if (configured === "vercel" && hasVercelKey()) return "vercel" | ||||||
| if (configured === "merge" && hasMergeKey()) return "merge" | ||||||
| if (configured === "openai" && hasOpenAIKey()) return "openai" | ||||||
|
|
||||||
| // Vercel's free tier currently rejects OpenAI embeddings. Prefer a direct | ||||||
| // key automatically; gateways must be explicitly enabled once paid access | ||||||
| // is available. | ||||||
| if (hasOpenAIKey()) return "openai" | ||||||
| return null | ||||||
| } | ||||||
|
|
||||||
| export function embeddingModel( | ||||||
| modelId: string, | ||||||
| provider: GatewayProviderName = activeGatewayProvider, | ||||||
| provider: "vercel" | "merge" | "openai" = embeddingProvider() ?? "openai", | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| ): EmbeddingModel { | ||||||
| if (provider === "vercel") { | ||||||
| return vercelGateway.embeddingModel(modelId) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -158,14 +158,20 @@ export async function reviewPullRequest( | |||||
| payload.source === "manual" | ||||||
|
|
||||||
| if (options?.wait) { | ||||||
| await runGeneratePrReview(payload) | ||||||
| console.log( | ||||||
| `[reviewPullRequest] completed blocking in-process review for ${owner}/${repo}#${prNumber}`, | ||||||
| ) | ||||||
| return { | ||||||
| success: true, | ||||||
| message: "Review completed", | ||||||
| mode: "blocking" as const, | ||||||
| try { | ||||||
| await runGeneratePrReview(payload) | ||||||
| console.log( | ||||||
| `[reviewPullRequest] completed blocking in-process review for ${owner}/${repo}#${prNumber}`, | ||||||
| ) | ||||||
| return { | ||||||
| success: true, | ||||||
| message: "Review completed", | ||||||
| mode: "blocking" as const, | ||||||
| } | ||||||
| } catch (error) { | ||||||
| const message = error instanceof Error ? error.message : "Unknown Error" | ||||||
| await markReviewFailed(payload, message) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
|
||||||
| throw error | ||||||
|
Comment on lines
+170
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This catch awaits |
||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now blocks the HTTP request until the full LLM pipeline finishes. Unlike the Inngest route, this route doesn't export
maxDuration, so on Vercel the request can hit the function timeout and return a 500/504 even though the review eventually completes (the caller just never sees the result). Consider raisingmaxDurationhere or making the blocking behavior opt-in via a query param.