Repository: DiscourseGraphs/discourse-graph
Pull request #102: Move llm-api endpoints to vercel serverless
Author: login: sid597
Created: 2025-03-07T02:54:11Z | Merged: 2025-04-10T19:55:24Z

- [x] gemini
- [x] anthropic
- [x] openai

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added new chat endpoints integrating multiple conversational AI services, enhancing the platform's chat capabilities.
  - Introduced utilities for improved request processing and secure cross-origin interactions.
  - Standardized configurations for language model interactions to ensure consistent and reliable behavior.

- **Chores**
  - Updated website runtime settings for greater reliability, including stricter React mode.
  - Expanded build environment configurations to support additional operational keys.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/website/app/api/llm/anthropic/chat/route.ts b/apps/website/app/api/llm/anthropic/chat/route.ts
new file mode 100644
index 000000000..740758c07
--- /dev/null
+++ b/apps/website/app/api/llm/anthropic/chat/route.ts
@@ -0,0 +1,15 @@
+import { NextRequest } from "next/server";
+import { handleLLMRequest, handleOptionsRequest } from "~/utils/llm/handler";
+import { anthropicConfig } from "~/utils/llm/providers";
+
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+export const runtime = "nodejs";
+
+export const POST = (request: NextRequest): Promise<Response> => {
+  return handleLLMRequest(request, anthropicConfig);
+};
+
+export const OPTIONS = (request: NextRequest): Promise<Response> => {
+  return handleOptionsRequest(request);
+};
diff --git a/apps/website/app/api/llm/gemini/chat/route.ts b/apps/website/app/api/llm/gemini/chat/route.ts
new file mode 100644
index 000000000..8096911b2
--- /dev/null
+++ b/apps/website/app/api/llm/gemini/chat/route.ts
@@ -0,0 +1,15 @@
+import { NextRequest } from "next/server";
+import { handleLLMRequest, handleOptionsRequest } from "~/utils/llm/handler";
+import { geminiConfig } from "~/utils/llm/providers";
+
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+export const runtime = "nodejs";
+
+export const POST = (request: NextRequest): Promise<Response> => {
+  return handleLLMRequest(request, geminiConfig);
+};
+
+export const OPTIONS = (request: NextRequest): Promise<Response> => {
+  return handleOptionsRequest(request);
+};
diff --git a/apps/website/app/api/llm/openai/chat/route.ts b/apps/website/app/api/llm/openai/chat/route.ts
new file mode 100644
index 000000000..49694bffa
--- /dev/null
+++ b/apps/website/app/api/llm/openai/chat/route.ts
@@ -0,0 +1,15 @@
+import { NextRequest } from "next/server";
+import { handleLLMRequest, handleOptionsRequest } from "~/utils/llm/handler";
+import { openaiConfig } from "~/utils/llm/providers";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export const POST = (request: NextRequest): Promise<Response> => {
+  return handleLLMRequest(request, openaiConfig);
+};
+
+export const OPTIONS = (request: NextRequest): Promise<Response> => {
+  return handleOptionsRequest(request);
+};
diff --git a/apps/website/app/types/llm.ts b/apps/website/app/types/llm.ts
new file mode 100644
index 000000000..b461fee47
--- /dev/null
+++ b/apps/website/app/types/llm.ts
@@ -0,0 +1,32 @@
+export type Message = {
+  role: string;
+  content: string;
+};
+
+export type Settings = {
+  model: string;
+  maxTokens: number;
+  temperature: number;
+  safetySettings?: Array<{
+    category: string;
+    threshold: string;
+  }>;
+};
+
+export type RequestBody = {
+  documents: Message[];
+  passphrase?: string;
+  settings: Settings;
+};
+
+export const CONTENT_TYPE_JSON = "application/json";
+export const CONTENT_TYPE_TEXT = "text/plain";
+
+export type LLMProviderConfig = {
+  apiKeyEnvVar: string;
+  apiUrl: string | ((settings: Settings) => string);
+  apiHeaders: (apiKey: string) => Record<string, string>;
+  formatRequestBody: (messages: Message[], settings: Settings) => any;
+  extractResponseText: (responseData: any) => string | null;
+  errorMessagePath: string;
+};
diff --git a/apps/website/app/utils/llm/cors.ts b/apps/website/app/utils/llm/cors.ts
new file mode 100644
index 000000000..edc1ffd0a
--- /dev/null
+++ b/apps/website/app/utils/llm/cors.ts
@@ -0,0 +1,39 @@
+import { NextRequest } from "next/server";
+
+const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+
+const isVercelPreviewUrl = (origin: string): boolean =>
+  origin.includes(".vercel.app") || origin.includes("discourse-graph");
+
+const isAllowedOrigin = (origin: string): boolean =>
+  allowedOrigins.some((allowed) => origin.startsWith(allowed)) ||
+  isVercelPreviewUrl(origin);
+
+export default function cors(req: NextRequest, res: Response) {
+  const origin = req.headers.get("origin");
+  const originIsAllowed = origin && isAllowedOrigin(origin);
+
+  if (req.method === "OPTIONS") {
+    return new Response(null, {
+      status: 204,
+      headers: {
+        ...(originIsAllowed ? { "Access-Control-Allow-Origin": origin } : {}),
+        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
+        "Access-Control-Allow-Headers":
+          "Content-Type, Authorization, x-vercel-protection-bypass",
+        "Access-Control-Max-Age": "86400",
+      },
+    });
+  }
+
+  if (originIsAllowed) {
+    res.headers.set("Access-Control-Allow-Origin", origin as string);
+    res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+    res.headers.set(
+      "Access-Control-Allow-Headers",
+      "Content-Type, Authorization, x-vercel-protection-bypass",
+    );
+  }
+
+  return res;
+}
diff --git a/apps/website/app/utils/llm/handler.ts b/apps/website/app/utils/llm/handler.ts
new file mode 100644
index 000000000..19e28b8b2
--- /dev/null
+++ b/apps/website/app/utils/llm/handler.ts
@@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "./cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  RequestBody,
+  LLMProviderConfig,
+} from "~/types/llm";
+
+function getNestedProperty(obj: any, path: string): any {
+  return path.split(".").reduce((prev, curr) => {
+    return prev && prev[curr] !== undefined ? prev[curr] : undefined;
+  }, obj);
+}
+
+export async function handleLLMRequest(
+  request: NextRequest,
+  config: LLMProviderConfig,
+): Promise<Response> {
+  try {
+    const requestData: RequestBody = await request.json();
+    const { documents: messages, settings } = requestData;
+
+    const apiKey = process.env[config.apiKeyEnvVar];
+
+    if (!apiKey) {
+      console.error(`${config.apiKeyEnvVar} environment variable is not set`);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API key not configured. Please set the ${config.apiKeyEnvVar} environment variable in your Vercel project settings.`,
+          }),
+          {
+            status: 500,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    const body = JSON.stringify(config.formatRequestBody(messages, settings));
+
+    const apiUrl =
+      typeof config.apiUrl === "function"
+        ? config.apiUrl(settings)
+        : config.apiUrl;
+
+    const response = await fetch(apiUrl, {
+      method: "POST",
+      headers: config.apiHeaders(apiKey),
+      body,
+    });
+
+    const responseData = await response.json();
+
+    if (!response.ok) {
+      console.error(`API error:`, responseData);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API error: ${getNestedProperty(responseData, config.errorMessagePath) || "Unknown error"}`,
+          }),
+          {
+            status: response.status,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    const replyText = config.extractResponseText(responseData);
+
+    if (!replyText) {
+      console.error("Invalid response format from API:", responseData);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error:
+              "Invalid response format from API. Check server logs for details.",
+          }),
+          {
+            status: 500,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    return cors(
+      request,
+      new Response(replyText, {
+        headers: { "Content-Type": CONTENT_TYPE_TEXT },
+      }),
+    );
+  } catch (error) {
+    console.error("Error processing request:", error);
+    return cors(
+      request,
+      new Response(
+        JSON.stringify({
+          error: `Internal Server Error: ${error instanceof Error ? error.message : "Unknown error"}`,
+        }),
+        {
+          status: 500,
+          headers: { "Content-Type": CONTENT_TYPE_JSON },
+        },
+      ),
+    );
+  }
+}
+
+export async function handleOptionsRequest(
+  request: NextRequest,
+): Promise<Response> {
+  return cors(request, new Response(null, { status: 204 }));
+}
diff --git a/apps/website/app/utils/llm/providers.ts b/apps/website/app/utils/llm/providers.ts
new file mode 100644
index 000000000..8a94fec14
--- /dev/null
+++ b/apps/website/app/utils/llm/providers.ts
@@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "~/types/llm";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.choices?.[0]?.message?.content,
+  errorMessagePath: "error?.message",
+};
+
+export const geminiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "GEMINI_API_KEY",
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+  apiHeaders: () => ({
+    "Content-Type": "application/json",
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    contents: messages.map((msg) => ({
+      role: msg.role === "user" ? "user" : "model",
+      parts: [{ text: msg.content }],
+    })),
+    generationConfig: {
+      maxOutputTokens: settings.maxTokens,
+      temperature: settings.temperature,
+    },
+    safetySettings: settings.safetySettings,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.candidates?.[0]?.content?.parts?.[0]?.text,
+  errorMessagePath: "error?.message",
+};
+
+export const anthropicConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "ANTHROPIC_API_KEY",
+  apiUrl: "https://api.anthropic.com/v1/messages",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    "x-api-key": apiKey,
+    "anthropic-version": "2023-06-01",
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    max_tokens: settings.maxTokens,
+    messages: messages,
+    temperature: settings.temperature,
+  }),
+  extractResponseText: (responseData: any) => responseData.content?.[0]?.text,
+  errorMessagePath: "error?.message",
+};
diff --git a/apps/website/next.config.ts b/apps/website/next.config.ts
index e9ffa3083..c34e983e8 100644
--- a/apps/website/next.config.ts
+++ b/apps/website/next.config.ts
@@ -1,7 +1,9 @@
 import type { NextConfig } from "next";
 
-const nextConfig: NextConfig = {
-  /* config options here */
+const nextConfig = {
+  reactStrictMode: true,
+  serverRuntimeConfig: {
+    maxDuration: 300,
+  },
 };
-
 export default nextConfig;
diff --git a/turbo.json b/turbo.json
index 4cb713730..45f57fbdd 100644
--- a/turbo.json
+++ b/turbo.json
@@ -3,7 +3,13 @@
   "ui": "tui",
   "tasks": {
     "build": {
-      "passThroughEnv": ["RESEND_API_KEY"],
+      "passThroughEnv": [
+        "RESEND_API_KEY",
+        "OPENAI_API_KEY",
+        "ANTHROPIC_API_KEY",
+        "GEMINI_API_KEY",
+        "BLOB_READ_WRITE_TOKEN"
+      ],
       "dependsOn": ["^build"],
       "inputs": ["$TURBO_DEFAULT$", ".env*"],
       "outputs": [".next/**", "!.next/cache/**", "dist/**"]


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>apps/website/app/api/llm-endpoints/chat-anthropic/route.ts (1)</summary><blockquote>

`82-85`: **Safeguard against missing usage data.**
The code assumes `responseData.usage` always exists. If `usage` is absent from the API response, accessing `usage.input_tokens` could result in a runtime error. Consider adding a check to gracefully handle scenarios where the usage object is missing.  

```diff
- const usage: AnthropicUsage = responseData.usage;
- const inputTokens = usage.input_tokens;
- const outputTokens = usage.output_tokens;
+ const usage: AnthropicUsage | undefined = responseData.usage;
+ const inputTokens = usage?.input_tokens ?? 0;
+ const outputTokens = usage?.output_tokens ?? 0;
```

</blockquote></details>
<details>
<summary>apps/website/app/api/llm-endpoints/chat-openai/route.ts (1)</summary><blockquote>

`46-53`: **Validate optional fields.**
When selectively adding `temperature` and `max_tokens` to the payload, ensure they align with OpenAI’s supported parameter ranges. Additionally, consider validating them to avoid unexpected 400 errors or unbounded generation.

</blockquote></details>
<details>
<summary>apps/website/next.config.ts (4)</summary><blockquote>

`3-3`: **Consider adding explicit type annotation for better type safety.**

The explicit `NextConfig` type annotation has been removed from the declaration. While TypeScript can infer types, explicit typing provides better documentation and type safety.

```diff
-const nextConfig = {
+const nextConfig: NextConfig = {
```

---

`10-12`: **Consider if the long maxDuration is necessary for all endpoints.**

Setting `maxDuration` to 300 seconds (5 minutes) is quite long for serverless functions and may impact costs. Consider if this duration is necessary for all endpoints or if it could be reduced for certain operations.

You might want to:
1. Set different durations for different API routes based on their expected execution time
2. Implement timeout handling to gracefully handle long-running operations
3. Consider optimizing the more time-consuming operations

---

`13-15`: **Ensure environment variables are properly documented.**

The configuration sets the `NEXT_PUBLIC_APP_ENV` environment variable with a default value of "development". Make sure this is documented in your project's README or environment setup guide.

```diff
  env: {
    NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV || "development",
+    // Add any other environment variables needed for the LLM endpoints here
  },
```

---

`3-16`: **Consider adding configuration comments for better documentation.**

The Next.js configuration contains several important settings but lacks comments explaining their purpose. Adding descriptive comments would make the configuration more maintainable.

```diff
const nextConfig = {
+  // Enable additional checks during development
  reactStrictMode: true,
  experimental: {
    serverActions: {
+      // Restrict server actions to specific origins for security
      allowedOrigins: ["localhost:3000", "discoursegraphs.com"],
    },
  },
+  // Configure server runtime for long-running LLM API requests
  serverRuntimeConfig: {
    maxDuration: 300,
  },
+  // Public environment variables accessible by browser code
  env: {
    NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV || "development",
  },
};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0dd45efb1cd10a3c366b645065581421dafdf89a and 331cd109725620cdac1de0a4eaf526820251c0d8.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/website/app/api/llm-endpoints/chat-anthropic/route.ts` (1 hunks)
* `apps/website/app/api/llm-endpoints/chat-gemini/route.ts` (1 hunks)
* `apps/website/app/api/llm-endpoints/chat-openai/route.ts` (1 hunks)
* `apps/website/next.config.ts` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>apps/website/app/api/llm-endpoints/chat-anthropic/route.ts (2)</summary>

`14-18`: **Confirm the role of the passphrase.**
The `passphrase` field in `RequestBody` is neither validated nor used within the function. If you intend to restrict access or perform any secure operation, consider implementing logic to handle this parameter. Otherwise, remove it to avoid confusion.

---

`120-125`: **Good standardized error response.**
The `createErrorResponse` function provides a consistent structure for error handling. This approach is clear and reusable.

</details>
<details>
<summary>apps/website/app/api/llm-endpoints/chat-gemini/route.ts (2)</summary>

`21-24`: **Verify passphrase usage.**
A `passphrase` field is present but never validated or used. If this is intended to gate access, add the necessary authorization checks. Otherwise, consider removing it to reduce complexity.

---

`122-127`: **Error response helper is properly structured.**
The centralized error-handling strategy is straightforward and ensures a uniform error format.

</details>
<details>
<summary>apps/website/app/api/llm-endpoints/chat-openai/route.ts (2)</summary>

`15-18`: **Confirm the intended use of passphrase.**
The `passphrase` property is present but never used. Confirm whether you plan to enforce a secure check or remove this property if it’s unnecessary.

---

`98-103`: **Consistent error response pattern.**
The error-handling approach matches the style used in other endpoints, promoting clarity and standardization.

</details>
<details>
<summary>apps/website/next.config.ts (2)</summary>

`4-4`: **Good addition of strict mode.**

Enabling React strict mode helps identify potential problems in the application during development by performing additional checks and warnings.

---

`5-9`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify that the allowed origins list is complete for your environments.**

The `allowedOrigins` configuration restricts which domains can invoke server actions. Ensure this list includes all necessary domains for your production, staging, and development environments.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any other domains or URLs used in the codebase that might need to be added to allowedOrigins
echo "Checking for potential domains that might need to be in allowedOrigins..."
rg -i "https?://[^/\"']+" --no-filename | grep -v "localhost:3000\|discoursegraphs.com" | sort | uniq
```

Length of output: 90371

---



</details>

**Action Required: Verify Allowed Origins List Completeness**

The current configuration in **apps/website/next.config.ts** specifies only `"localhost:3000"` and `"discoursegraphs.com"`. A scan of the codebase has surfaced many external URLs (for example, references such as `http://localhost:3003`, `https://samepage.ngrok.io`, and various third‐party domains). Please double-check and confirm that the list in `allowedOrigins` includes every domain that may legitimately trigger server actions in your production, staging, and development setups. At the same time, ensure that domains referenced in static assets (like embedded YouTube URLs or external documentation links) are not mistakenly added unless they are expected to invoke server actions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T04:42:06Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>apps/website/lib/cors.ts (2)</summary><blockquote>

`3-16`: **Consider refining Vercel preview URL detection.**

The current Vercel preview URL detection is quite permissive - it allows any domain containing ".vercel.app" or "discourse-graph". This might allow unintended origins to access your API.

```diff
function isVercelPreviewUrl(origin: string | null): boolean {
  if (!origin) return false;
-  return origin.includes(".vercel.app") || origin.includes("discourse-graph");
+  return origin.endsWith(".vercel.app") || origin.includes("discourse-graph.vercel.app");
}
```

---

`18-48`: **Add explicit rejection for disallowed origins in non-OPTIONS requests.**

Currently, the CORS middleware only adds headers for allowed origins but doesn't explicitly reject requests from disallowed origins for non-OPTIONS requests.

```diff
export default function cors(req: NextRequest, res: Response) {
  const origin = req.headers.get("origin");

  const originIsAllowed = isAllowedOrigin(origin);

  if (req.method === "OPTIONS") {
    return new Response(null, {
      status: 204,
      headers: {
        ...(originIsAllowed && origin
          ? { "Access-Control-Allow-Origin": origin }
          : {}),
        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
        "Access-Control-Allow-Headers":
          "Content-Type, Authorization, x-vercel-protection-bypass",
        "Access-Control-Max-Age": "86400",
      },
    });
  }

  if (originIsAllowed && origin) {
    res.headers.set("Access-Control-Allow-Origin", origin);
    res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    res.headers.set(
      "Access-Control-Allow-Headers",
      "Content-Type, Authorization, x-vercel-protection-bypass",
    );
+  } else if (origin && !originIsAllowed) {
+    // Explicitly reject requests from disallowed origins
+    return new Response(JSON.stringify({ error: "Origin not allowed" }), {
+      status: 403,
+      headers: { "Content-Type": "application/json" },
+    });
  }

  return res;
}
```

</blockquote></details>
<details>
<summary>apps/website/app/api/llm/anthropic/chat/route.ts (2)</summary><blockquote>

`15-18`: **Remove unused AnthropicUsage type definition.**

The AnthropicUsage type is defined but never used in the code. Consider removing it to keep the codebase clean.

```diff
-type AnthropicUsage = {
-  input_tokens: number;
-  output_tokens: number;
-};
```

---

`30-139`: **Consider adding request validation for better error handling.**

The current implementation lacks input validation beyond JSON parsing. Adding validation for required fields and parameter constraints would prevent unnecessary API calls with invalid data.

```diff
export async function POST(request: NextRequest): Promise<Response> {
  try {
    const requestData: RequestBody = await request.json();
    const { documents: messages, settings } = requestData;
    const { model, maxTokens, temperature } = settings;

+    // Validate required fields
+    if (!messages || !Array.isArray(messages) || messages.length === 0) {
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: "Messages array is required and cannot be empty",
+          }),
+          {
+            status: 400,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          }
+        )
+      );
+    }
+
+    // Validate temperature is between 0 and 1
+    if (temperature < 0 || temperature > 1) {
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: "Temperature must be between 0 and 1",
+          }),
+          {
+            status: 400,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          }
+        )
+      );
+    }

    const apiKey = process.env.ANTHROPIC_API_KEY;
    // Rest of the function remains the same
```

</blockquote></details>
<details>
<summary>apps/website/app/api/llm/gemini/chat/route.ts (4)</summary><blockquote>

`4-7`: **Enhance type safety with a union type for message roles**

The `Message` type uses a generic `string` for the `role` field, but in practice, roles are typically limited to specific values. Consider using a union type for better type safety:

```diff
-type Message = {
-  role: string;
-  content: string;
-};
+type Message = {
+  role: "user" | "assistant" | "system" | "model";
+  content: string;
+};
```

---

`47-50`: **Add input validation before processing the request**

The code doesn't validate the structure or values of the incoming request data. Add validation to ensure the request conforms to the expected format before proceeding with the API call:

```diff
 const requestData: RequestBody = await request.json();
 const { documents: messages, settings } = requestData;
 const { model, maxTokens, temperature } = settings;

+// Validate required fields
+if (!Array.isArray(messages) || messages.length === 0) {
+  return cors(
+    request, 
+    new Response(
+      JSON.stringify({ error: "Invalid request: messages array is required and cannot be empty" }),
+      { status: 400, headers: { "Content-Type": CONTENT_TYPE_JSON } }
+    )
+  );
+}
+
+// Validate settings
+if (!model || typeof maxTokens !== 'number' || typeof temperature !== 'number') {
+  return cors(
+    request,
+    new Response(
+      JSON.stringify({ error: "Invalid request: model, maxTokens, and temperature are required" }),
+      { status: 400, headers: { "Content-Type": CONTENT_TYPE_JSON } }
+    )
+  );
+}
```

---

`1-152`: **Consider refactoring to reduce code duplication across LLM API routes**

The implementation is solid and follows patterns established in the OpenAI and Anthropic API routes. However, there's significant code duplication between these routes. Consider extracting common functionality into shared utilities:

```typescript
// Example shared utility in lib/llm-api-utils.ts
export function createLLMApiHandler({
  envVarName,
  errorMessage,
  apiUrl,
  prepareRequestBody,
  extractResponseText,
}) {
  return async function(request: NextRequest): Promise<Response> {
    try {
      const requestData = await request.json();
      const { documents: messages, settings } = requestData;
      
      const apiKey = process.env[envVarName];
      if (!apiKey) {
        console.error(`${envVarName} environment variable is not set`);
        return cors(
          request,
          new Response(
            JSON.stringify({ error: errorMessage }),
            { status: 500, headers: { "Content-Type": "application/json" } }
          )
        );
      }
      
      // Rest of the implementation...
    } catch (error) {
      // Error handling...
    }
  };
}
```

This would make the code more maintainable when changes are needed across all LLM routes.

---

`108-125`: **Consider more robust response parsing**

The current implementation uses optional chaining to safely navigate the response structure, which is good. However, for more robust error handling, consider adding a specific check for blocked content:

```diff
 const replyText = responseData.candidates?.[0]?.content?.parts?.[0]?.text;

+// Check for blocked content
+if (responseData.promptFeedback?.blockReason) {
+  console.error("Content blocked by Gemini API:", responseData.promptFeedback);
+  return cors(
+    request,
+    new Response(
+      JSON.stringify({
+        error: `Content blocked by Gemini API: ${responseData.promptFeedback.blockReason}`
+      }),
+      {
+        status: 400,
+        headers: { "Content-Type": CONTENT_TYPE_JSON },
+      }
+    )
+  );
+}

 if (!replyText) {
   console.error("Invalid response format from Gemini API:", responseData);
   // ...
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 331cd109725620cdac1de0a4eaf526820251c0d8 and a5f727656bf2955312749423a79114fc2db5f32d.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `apps/website/app/api/llm/anthropic/chat/route.ts` (1 hunks)
* `apps/website/app/api/llm/gemini/chat/route.ts` (1 hunks)
* `apps/website/app/api/llm/openai/chat/route.ts` (1 hunks)
* `apps/website/lib/cors.ts` (1 hunks)
* `turbo.json` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (3)</summary>

<details>
<summary>apps/website/app/api/llm/openai/chat/route.ts (2)</summary><blockquote>

<details>
<summary>apps/website/app/api/llm/anthropic/chat/route.ts (5)</summary>

* `runtime` (30-30)
* `preferredRegion` (31-31)
* `maxDuration` (32-32)
* `POST` (34-139)
* `OPTIONS` (141-143)

</details>
<details>
<summary>apps/website/app/api/llm/gemini/chat/route.ts (5)</summary>

* `runtime` (35-35)
* `preferredRegion` (36-36)
* `maxDuration` (37-37)
* `POST` (46-148)
* `OPTIONS` (150-152)

</details>

</blockquote></details>
<details>
<summary>apps/website/app/api/llm/gemini/chat/route.ts (2)</summary><blockquote>

<details>
<summary>apps/website/app/api/llm/anthropic/chat/route.ts (5)</summary>

* `runtime` (30-30)
* `preferredRegion` (31-31)
* `maxDuration` (32-32)
* `POST` (34-139)
* `OPTIONS` (141-143)

</details>
<details>
<summary>apps/website/app/api/llm/openai/chat/route.ts (5)</summary>

* `runtime` (24-24)
* `preferredRegion` (25-25)
* `maxDuration` (26-26)
* `POST` (28-129)
* `OPTIONS` (131-133)

</details>

</blockquote></details>
<details>
<summary>apps/website/app/api/llm/anthropic/chat/route.ts (2)</summary><blockquote>

<details>
<summary>apps/website/app/api/llm/openai/chat/route.ts (5)</summary>

* `runtime` (24-24)
* `preferredRegion` (25-25)
* `maxDuration` (26-26)
* `POST` (28-129)
* `OPTIONS` (131-133)

</details>
<details>
<summary>apps/website/app/api/llm/gemini/chat/route.ts (5)</summary>

* `runtime` (35-35)
* `preferredRegion` (36-36)
* `maxDuration` (37-37)
* `POST` (46-148)
* `OPTIONS` (150-152)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>turbo.json (1)</summary>

`6-12`: **Well-structured environment variable configuration for Vercel serverless endpoints.**

The PR has properly expanded the `passThroughEnv` array to include all necessary API keys needed for the new serverless endpoints. Adding these variables ensures they'll be accessible during build time.

</details>
<details>
<summary>apps/website/app/api/llm/openai/chat/route.ts (3)</summary>

`24-26`: **Good server runtime configuration.**

The runtime settings are properly configured for serverless functions with appropriate region settings and a reasonable max duration.

---

`131-133`: **CORS preflight request handling implemented correctly.**

The OPTIONS handler is properly implemented to support CORS preflight requests, which is essential for cross-origin API calls.

---

`28-129`: <details>
<summary>✅ Verification successful</summary>

**Fix OpenAI parameter name for token limit.**

The OpenAI API expects `max_tokens` rather than `max_completion_tokens` in the request body.

```diff
 const body = JSON.stringify({
   model: model,
   messages: messages,
   temperature: temperature,
-  max_completion_tokens: maxTokens,
+  max_tokens: maxTokens,
 });
```

---

🌐 Web query:

```web
What parameters does OpenAI chat completion API accept for token limitations?
```

💡 Result:

The OpenAI Chat Completion API provides several parameters and considerations for managing token limitations, which vary depending on the model used. Here are the key parameters and their functionalities:

## Main Token Management Parameters

**`max_tokens`**  
- Defines the maximum number of tokens the model can generate in its response.  
- The sum of input tokens (prompt) and `max_tokens` must not exceed the model's **context window** (e.g., 4,096 tokens for `gpt-3.5-turbo`, 128,000 tokens for `gpt-4o`)[1][3][10].  
- Example: For `gpt-4o`, even though the total context window is 128k tokens, the maximum value for `max_tokens` (output) is **4,096 tokens**[10].

**`model`**  
- The model itself determines:  
  - **Total context window** (input + output tokens).  
  - **Output token limits** (e.g., `gpt-4o` allows 4,096 output tokens despite its 128k total context)[10].  
- Common limits:  
  | Model             | Context Window | Max Output Tokens (`max_tokens`) |
  |-------------------|----------------|-------------------------------|
  | `gpt-3.5-turbo`   | 4,096–16k      | ~4,096                        |
  | `gpt-4`           | 8,192          | ~8,192                        |
  | `gpt-4o`          | 128,000        | 4,096                         |  
  | `gpt-4-32k`       | 32,768         | ~32k                          | [1][3][7][10]  

## Additional Constraints and Errors  
- **Input + Output Limit**: Requests exceeding the model’s total token capacity will fail with errors like:  
  `This model’s maximum context length is 4097 tokens. However, you requested [...] (prompt + max_tokens)`[3][7].  
- **Rate Limits**: Account-level quotas (e.g., 60,000 tokens/minute) apply to all API interactions. Exceeding these results in `429 Too Many Requests` errors[5][6].  

## Best Practices  
1. **Token Reservations**: Leave sufficient tokens for responses. For example, with a 4,096-token model, limit prompts to ~3,000 tokens to allow responses[3][9].  
2. **Streaming**: Use `stream=True` to handle partial responses and monitor `finish_reason="length"` to detect token limits[6][9].  
3. **Token Counting**: Use libraries like `tiktoken` to calculate input tokens and dynamically adjust `max_tokens`[7][8].  

For extended conversations, implement strategies like truncating old messages or summarizing history to stay within limits[2][8].

Citations:

- 1: https://learn.microsoft.com/en-us/answers/questions/2116223/azure-openai-token-limit-issue
- 2: https://devblogs.microsoft.com/surface-duo/android-openai-chatgpt-15/
- 3: https://community.openai.com/t/token-limit-api-for-input/1028748
- 4: https://community.openai.com/t/maximum-token-length-allowed/137151
- 5: https://community.openai.com/t/how-does-the-usage-limit-work/481036
- 6: https://docs.langdock.com/api-endpoints/completion/openai
- 7: https://community.openai.com/t/clarification-for-max-tokens/19576
- 8: https://www.bretcameron.com/blog/three-strategies-to-overcome-open-ai-token-limits
- 9: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/chatgpt
- 10: https://community.openai.com/t/max-tokens-chat-completion-gpt4o/758066

---



</details>

**Action: Update the OpenAI API request parameter.**

After reviewing the latest API details, please change the property from `max_completion_tokens` to `max_tokens` since the OpenAI Chat Completion API expects the token limit to be specified using `max_tokens`.

- **File**: `apps/website/app/api/llm/openai/chat/route.ts`
- **Location**: Lines 28-129
- **Change**:

```diff
 const body = JSON.stringify({
   model: model,
   messages: messages,
   temperature: temperature,
-  max_completion_tokens: maxTokens,
+  max_tokens: maxTokens,
 });
```

</details>
<details>
<summary>apps/website/app/api/llm/anthropic/chat/route.ts (1)</summary>

`141-143`: **CORS preflight request handling implemented correctly.**

The OPTIONS handler is properly implemented to support CORS preflight requests.

</details>
<details>
<summary>apps/website/app/api/llm/gemini/chat/route.ts (2)</summary>

`39-44`: **Verify the role mapping logic matches Gemini's requirements**

The current implementation maps any non-user role to "model". Verify that this is correct for the Gemini API, especially if your application uses other roles like "system" or "assistant".

```diff
-    role: msg.role === "user" ? "user" : "model",
+    // Map common roles to Gemini's expected format
+    role: msg.role === "user" ? "user" : 
+          (msg.role === "assistant" ? "model" : "model"),
```

This ensures that future developers understand the intentional mapping of roles.

---

`71-81`: **Security consideration for API key and safetySettings validation**

Two security considerations in this section:

1. The API key is included in the URL, which might be logged in various places. While this follows Gemini's API design, consider adding a comment noting this as a potential security consideration.

2. The `safetySettings` from the client request are passed directly to the API without validation. If these come from untrusted sources, consider validating them.

```diff
 const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
+// Note: API key in URL is Google's design but may appear in logs

+// Validate safetySettings if needed
+const validatedSafetySettings = settings.safetySettings ? validateSafetySettings(settings.safetySettings) : undefined;

 const body = JSON.stringify({
   contents: convertToGeminiFormat(messages),
   generationConfig: {
     maxOutputTokens: maxTokens,
     temperature: temperature,
   },
-  safetySettings: settings.safetySettings,
+  safetySettings: validatedSafetySettings,
 });
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:26:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>apps/website/next.config.ts (1)</summary><blockquote>

`3-3`: **Consider adding back the type annotation for clarity.**

The AI summary indicates that the type annotation `: NextConfig` was removed from the constant declaration. While TypeScript can infer the type, explicit typing can improve code readability and maintainability.

```diff
- const nextConfig = {
+ const nextConfig: NextConfig = {
```

</blockquote></details>
<details>
<summary>apps/website/lib/llm/types.ts (2)</summary><blockquote>

`3-6`: **Consider using a union type for `role` to improve type safety.**

The `role` property is defined as a simple string, but LLM providers typically expect specific role values (like "user", "assistant", "system"). Using a union type would provide better type safety and code completion.

```diff
export type Message = {
-  role: string;
+  role: "user" | "assistant" | "system" | string;
  content: string;
};
```

---

`18-22`: **Rename `documents` field to `messages` for clarity.**

The field `documents` contains an array of `Message` objects, so naming it `messages` would be more intuitive and consistent with the actual data type it holds.

```diff
export type RequestBody = {
-  documents: Message[];
+  messages: Message[];
  passphrase?: string;
  settings: Settings;
};
```

</blockquote></details>
<details>
<summary>apps/website/lib/llm/providers.ts (2)</summary><blockquote>

`29-32`: **Improve role mapping logic for Gemini.**

The current role mapping is binary (either "user" or "model"), which might not handle other roles like "system" correctly. Consider adding explicit handling for system messages or other special roles.

```diff
  formatRequestBody: (messages: Message[], settings: Settings) => ({
    contents: messages.map((msg) => ({
-      role: msg.role === "user" ? "user" : "model",
+      role: msg.role === "user" ? "user" : 
+           msg.role === "system" ? "system" : "model",
      parts: [{ text: msg.content }],
    })),
```

---

`48-51`: **Consider using a variable for the Anthropic API version.**

The Anthropic API version is hardcoded, which might lead to issues if the API is updated. Using a constant or environment variable would make it easier to update in the future.

```diff
+const ANTHROPIC_API_VERSION = "2023-06-01";

// Later in the config
  apiHeaders: (apiKey: string) => ({
    "Content-Type": "application/json",
    "x-api-key": apiKey,
-    "anthropic-version": "2023-06-01",
+    "anthropic-version": ANTHROPIC_API_VERSION,
  }),
```

</blockquote></details>
<details>
<summary>apps/website/lib/llm/handler.ts (3)</summary><blockquote>

`26-40`: **Use appropriate HTTP status code for missing API keys.**

Using HTTP status code 500 (Internal Server Error) for a missing API key isn't appropriate as it's a configuration issue, not a server error. Consider using 503 (Service Unavailable) instead.

```diff
  if (!apiKey) {
    console.error(`${config.apiKeyEnvVar} environment variable is not set`);
    return cors(
      request,
      new Response(
        JSON.stringify({
          error: `API key not configured. Please set the ${config.apiKeyEnvVar} environment variable in your Vercel project settings.`,
        }),
        {
-          status: 500,
+          status: 503,
          headers: { "Content-Type": CONTENT_TYPE_JSON },
        },
      ),
    );
  }
```

---

`98-105`: **Limit error details in production responses.**

The current error handling exposes full error messages to clients, which could potentially leak sensitive information. Consider limiting error details in production environments while still logging the full error for debugging.

```diff
  catch (error) {
    console.error("Error processing request:", error);
+   const isProduction = process.env.NODE_ENV === 'production';
    return cors(
      request,
      new Response(
        JSON.stringify({
-         error: `Internal Server Error: ${error instanceof Error ? error.message : "Unknown error"}`,
+         error: isProduction ? "Internal Server Error" : `Internal Server Error: ${error instanceof Error ? error.message : "Unknown error"}`,
        }),
```

---

`21-22`: **Update RequestBody field name in destructuring to match earlier recommendation.**

If you implement the earlier suggestion to rename the `documents` field to `messages` in the RequestBody type, you'll need to update this destructuring assignment as well.

```diff
    const requestData: RequestBody = await request.json();
-    const { documents: messages, settings } = requestData;
+    const { messages, settings } = requestData;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a5f727656bf2955312749423a79114fc2db5f32d and 2b99736b0ecc0f4b72ed6fb01be1fc2115a1fa45.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `apps/website/app/api/llm/anthropic/chat/route.ts` (1 hunks)
* `apps/website/app/api/llm/gemini/chat/route.ts` (1 hunks)
* `apps/website/app/api/llm/openai/chat/route.ts` (1 hunks)
* `apps/website/lib/llm/handler.ts` (1 hunks)
* `apps/website/lib/llm/providers.ts` (1 hunks)
* `apps/website/lib/llm/types.ts` (1 hunks)
* `apps/website/next.config.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* apps/website/app/api/llm/openai/chat/route.ts
* apps/website/app/api/llm/anthropic/chat/route.ts
* apps/website/app/api/llm/gemini/chat/route.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>apps/website/lib/llm/providers.ts (1)</summary><blockquote>

<details>
<summary>apps/website/lib/llm/types.ts (3)</summary>

* `LLMProviderConfig` (27-34)
* `Message` (3-6)
* `Settings` (8-16)

</details>

</blockquote></details>
<details>
<summary>apps/website/lib/llm/handler.ts (1)</summary><blockquote>

<details>
<summary>apps/website/lib/llm/types.ts (4)</summary>

* `LLMProviderConfig` (27-34)
* `RequestBody` (18-22)
* `CONTENT_TYPE_JSON` (24-24)
* `CONTENT_TYPE_TEXT` (25-25)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/website/lib/llm/handler.ts</summary>

[error] 63-63: eval() exposes to security risks and performance issues.

See the MDN web docs for more details.
Refactor the code so that it doesn't need to call eval().

(lint/security/noGlobalEval)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>apps/website/next.config.ts (1)</summary>

`3-11`: **Solid configuration for Vercel serverless functions!**

The changes to the Next.js configuration appropriately support the migration of LLM API endpoints to Vercel serverless:

1. `reactStrictMode: true` enables additional development-time checks and warnings that help catch potential issues early.

2. `serverRuntimeConfig.maxDuration: 300` sets a 5-minute timeout for serverless functions, which is appropriate for LLM API calls that might take longer to process.

3. The environment variable configuration provides a good default for `NEXT_PUBLIC_APP_ENV`.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T03:13:59Z
user: login: mdroidian
body: A few changes requested.

Ideally we test this in the Roam app.  You could create a quick command palette function to run it.  But we can consider this another task as well.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-04T01:48:15Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T01:48:39Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T01:48:40Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T01:48:44Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T01:49:09Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T09:07:09Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T09:07:15Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T09:10:14Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T09:10:20Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T09:34:44Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T09:40:38Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T09:40:44Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T10:29:42Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T10:29:53Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>apps/website/app/utils/llm/cors.ts (1)</summary><blockquote>

`3-3`: **Define origins in environment variables for flexibility.**

The hardcoded allowed origins could be moved to environment variables to make the application more configurable across different environments without code changes.

```diff
-const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+const allowedOrigins = process.env.ALLOWED_ORIGINS 
+  ? process.env.ALLOWED_ORIGINS.split(",") 
+  : ["https://roamresearch.com", "http://localhost:3000"];
```

</blockquote></details>
<details>
<summary>apps/website/app/utils/llm/handler.ts (1)</summary><blockquote>

`55-71`: **Enhance error message with more details for debugging.**

The error handling could provide more specific details about the type of API error for better debugging.

```diff
if (!response.ok) {
  console.error(`API error:`, responseData);
  return cors(
    request,
    new Response(
      JSON.stringify({
        error: `API error: ${getNestedProperty(responseData, config.errorMessagePath) || "Unknown error"}`,
+       statusCode: response.status,
+       provider: config.apiKeyEnvVar.split('_')[0].toLowerCase()
      }),
      {
        status: response.status,
        headers: { "Content-Type": CONTENT_TYPE_JSON },
      },
    ),
  );
}
```

</blockquote></details>
<details>
<summary>apps/website/app/types/llm.ts (2)</summary><blockquote>

`1-4`: **Consider using more specific role types.**

The `Message` type's `role` property could be more strictly typed for better type safety and autocomplete support.

```diff
export type Message = {
-  role: string;
+  role: 'user' | 'assistant' | 'system';
  content: string;
};
```

---

`16-20`: **Consider renaming 'documents' field for clarity.**

The `documents` field in `RequestBody` is actually an array of messages. The naming could be more intuitive.

```diff
export type RequestBody = {
-  documents: Message[];
+  messages: Message[];
  passphrase?: string;
  settings: Settings;
};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2b99736b0ecc0f4b72ed6fb01be1fc2115a1fa45 and c2a2c91075fe5eee4bf22d7be8552c70253bfffa.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `apps/website/app/api/llm/anthropic/chat/route.ts` (1 hunks)
* `apps/website/app/api/llm/gemini/chat/route.ts` (1 hunks)
* `apps/website/app/api/llm/openai/chat/route.ts` (1 hunks)
* `apps/website/app/types/llm.ts` (1 hunks)
* `apps/website/app/utils/llm/cors.ts` (1 hunks)
* `apps/website/app/utils/llm/handler.ts` (1 hunks)
* `apps/website/app/utils/llm/providers.ts` (1 hunks)
* `apps/website/next.config.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (4)</summary>

* apps/website/next.config.ts
* apps/website/app/api/llm/openai/chat/route.ts
* apps/website/app/api/llm/anthropic/chat/route.ts
* apps/website/app/api/llm/gemini/chat/route.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>apps/website/app/utils/llm/providers.ts (1)</summary><blockquote>

<details>
<summary>apps/website/app/types/llm.ts (3)</summary>

* `LLMProviderConfig` (25-32)
* `Message` (1-4)
* `Settings` (6-14)

</details>

</blockquote></details>
<details>
<summary>apps/website/app/utils/llm/handler.ts (2)</summary><blockquote>

<details>
<summary>apps/website/app/types/llm.ts (4)</summary>

* `LLMProviderConfig` (25-32)
* `RequestBody` (16-20)
* `CONTENT_TYPE_JSON` (22-22)
* `CONTENT_TYPE_TEXT` (23-23)

</details>
<details>
<summary>apps/website/app/utils/llm/cors.ts (1)</summary>

* `cors` (18-48)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>apps/website/app/utils/llm/cors.ts (3)</summary>

`5-8`: **LGTM - Vercel preview URL detection.**

The `isVercelPreviewUrl` function properly checks for Vercel preview URLs, which is important for testing the application in Vercel's preview environments.

---

`10-16`: **LGTM - Origin validation function.**

The `isAllowedOrigin` function correctly checks if the origin is in the allowed list or is a Vercel preview URL, which is important for CORS security.

---

`18-48`: **LGTM - Well-implemented CORS handling.**

The CORS implementation is secure and handles both preflight (OPTIONS) and regular requests appropriately. It only sets CORS headers for allowed origins and includes the necessary headers for secure cross-origin requests.

</details>
<details>
<summary>apps/website/app/utils/llm/handler.ts (6)</summary>

`10-14`: **LGTM - Utility function for accessing nested properties.**

The `getNestedProperty` function safely accesses nested properties in objects, which is useful for extracting error messages from API responses.

---

`16-40`: **LGTM - API key validation and error handling.**

The initial part of the `handleLLMRequest` function correctly extracts request data and validates the presence of an API key, returning appropriate error responses when keys aren't configured.

---

`73-90`: **LGTM - Handling invalid response formats.**

This section correctly handles cases where the API response doesn't contain the expected text format, returning an appropriate error message.

---

`92-112`: **LGTM - Response and error handling.**

The completion of the `handleLLMRequest` function properly returns the extracted text with correct content type headers and includes comprehensive error handling for exceptions.

---

`115-119`: **LGTM - Options request handler.**

The `handleOptionsRequest` function correctly handles OPTIONS requests for CORS preflight with a 204 status code.

---

`42-53`: <details>
<summary>❓ Verification inconclusive</summary>

**Security concern with API key in URL for Gemini.**

When the API URL is a function (as in the Gemini config), there's potential for exposing the API key in the URL if the function embeds it.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if Gemini config places API key in URL
grep -A 5 "apiUrl.*function" apps/website/app/utils/llm/providers.ts
```

Length of output: 70

---



</details>

**Action Required: Verify API Key Exposure in Gemini Config Function Implementation**

The initial concern was that when `config.apiUrl` is a function (as in the Gemini config), there’s a potential for the API key to be embedded in the URL. Our preliminary grep check in `apps/website/app/utils/llm/providers.ts` did not reveal any evidence of a function embedding the API key in the URL. However, since the verification output was empty, I recommend a manual review of the Gemini configuration to ensure that:
- The function (if present) that computes `apiUrl` does not concatenate or embed the API key into the URL.
- No insecure URL construction is occurring that might expose the API key in network requests.

Please confirm that the Gemini provider’s implementation in `apps/website/app/utils/llm/providers.ts` safely handles the API key.

</details>
<details>
<summary>apps/website/app/types/llm.ts (3)</summary>

`6-14`: **LGTM - Comprehensive settings type.**

The `Settings` type includes all necessary parameters for configuring LLM requests, including model selection, token limits, temperature, and safety settings.

---

`22-23`: **LGTM - Content type constants.**

These content type constants are correctly defined and will ensure consistent content type headers across the application.

---

`25-32`: **LGTM - Well-structured provider configuration type.**

The `LLMProviderConfig` type is well-designed and provides all the necessary configuration options to support different LLM providers with their unique APIs.

</details>
<details>
<summary>apps/website/app/utils/llm/providers.ts (2)</summary>

`3-19`: **LGTM - OpenAI configuration.**

The OpenAI configuration is well-structured and correctly implements the API interface, including authentication, request formatting, and response handling.

---

`44-60`: **LGTM - Anthropic configuration.**

The Anthropic configuration is well-structured and correctly implements the API interface, including appropriate headers with the API version.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T11:22:59Z
user: login: mdroidian
body: Mostly nits.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-05T13:58:10Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T17:52:23Z
user: login: sid597
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T18:16:30Z
user: login: mdroidian
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T18:49:04Z
user: login: mdroidian
body: 👍
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T19:55:16Z

Review comments:
diff_hunk: @@ -0,0 +1,127 @@
+import { NextRequest } from "next/server";
+
+type Message = {
+  role: string;
+  content: string;
+};
+
+type SafetySetting = {
+  category: string;
+  threshold: number;
+};
+
+type Settings = {
+  model: string;
+  maxTokens: number;
+  temperature: number;
+  safetySettings?: SafetySetting[];
+};
+
+type RequestBody = {
+  documents: Message[];
+  passphrase: string;
+  settings: Settings;
+};
+
+type GeminiMessage = {
+  role: string;
+  parts: { text: string }[];
+};
+
+function convertToGeminiFormat(messages: Message[]): GeminiMessage[] {
+  return messages.map(convertMessageToGeminiFormat);
+}
+
+function convertMessageToGeminiFormat(message: Message): GeminiMessage {
+  return {
+    role: message.role === "user" ? "user" : "model",
+    parts: [{ text: message.content }],
+  };
+}
+
+const DEFAULT_MODEL = "gemini-2.0-flash-exp";
+const CONTENT_TYPE_JSON = "application/json";
+const CONTENT_TYPE_TEXT = "text/plain";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export async function POST(request: NextRequest): Promise<Response> {
+  try {
+    const requestData: RequestBody = await request.json();
+    const { documents: messages, settings } = requestData;
+    const { model, maxTokens, temperature } = settings;
+
+    const modelId = model === "gemini" ? DEFAULT_MODEL : model;
+
+    // Get API key from environment variable
+    const apiKey = process.env.GEMINI_API_KEY;
+
+    if (!apiKey) {
+      console.error("GEMINI_API_KEY environment variable is not set");
+      return createErrorResponse(
+        "API key not configured. Please set the GEMINI_API_KEY environment variable in your Vercel project settings.",
+        500,
+      );
+    }
+
+    const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${apiKey}`;
path: apps/website/app/api/llm-endpoints/chat-gemini/route.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid embedding the API key in query parameters.**
Placing the API key as `?key=${apiKey}` may expose it in access logs or leave it more vulnerable. Consider using an `Authorization` header or a secure header-based approach instead.  

```diff
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${apiKey}`;
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent`;
  ...
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": CONTENT_TYPE_JSON,
-     // "key": apiKey,
+     Authorization: `Bearer ${apiKey}`,
    },
    body,
  });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent`;
    // ... other code
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": CONTENT_TYPE_JSON,
        Authorization: `Bearer ${apiKey}`,
      },
      body,
    });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T04:42:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+import { NextRequest } from "next/server";
+
+export type Message = {
+  role: string;
+  content: string;
+};
+
+export type Settings = {
+  model: string;
+  maxTokens: number;
+  temperature: number;
+  safetySettings?: Array<{
+    category: string;
+    threshold: string;
+  }>;
+};
+
+export type RequestBody = {
+  documents: Message[];
+  passphrase?: string;
+  settings: Settings;
+};
+
+export const CONTENT_TYPE_JSON = "application/json";
+export const CONTENT_TYPE_TEXT = "text/plain";
+
+export type LLMProviderConfig = {
+  apiKeyEnvVar: string;
+  apiUrl: string | ((settings: Settings) => string);
+  apiHeaders: (apiKey: string) => Record<string, string>;
+  formatRequestBody: (messages: Message[], settings: Settings) => any;
+  extractResponseText: (responseData: any) => string | null;
+  errorMessagePath: string;
+};
path: apps/website/lib/llm/types.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Replace `any` types with more specific types and avoid using string paths for error extraction.**

Using `any` types reduces type safety. Additionally, the `errorMessagePath` string will likely be used with `eval()` in the handler (as seen in line 63 of handler.ts), which is a security risk.

```diff
export type LLMProviderConfig = {
  apiKeyEnvVar: string;
  apiUrl: string | ((settings: Settings) => string);
  apiHeaders: (apiKey: string) => Record<string, string>;
-  formatRequestBody: (messages: Message[], settings: Settings) => any;
-  extractResponseText: (responseData: any) => string | null;
-  errorMessagePath: string;
+  formatRequestBody: (messages: Message[], settings: Settings) => Record<string, unknown>;
+  extractResponseText: (responseData: Record<string, unknown>) => string | null;
+  extractErrorMessage: (responseData: Record<string, unknown>) => string | null;
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export type LLMProviderConfig = {
  apiKeyEnvVar: string;
  apiUrl: string | ((settings: Settings) => string);
  apiHeaders: (apiKey: string) => Record<string, string>;
  formatRequestBody: (messages: Message[], settings: Settings) => Record<string, unknown>;
  extractResponseText: (responseData: Record<string, unknown>) => string | null;
  extractErrorMessage: (responseData: Record<string, unknown>) => string | null;
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-03T03:13:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
path: apps/website/lib/llm/providers.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix OpenAI parameter name for max tokens.**

The OpenAI API parameter for token limit should be `max_tokens`, not `max_completion_tokens`. This parameter name mismatch could cause the API to ignore your token limit setting.

```diff
  formatRequestBody: (messages: Message[], settings: Settings) => ({
    model: settings.model,
    messages: messages,
    temperature: settings.temperature,
-    max_completion_tokens: settings.maxTokens,
+    max_tokens: settings.maxTokens,
  }),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  formatRequestBody: (messages: Message[], settings: Settings) => ({
    model: settings.model,
    messages: messages,
    temperature: settings.temperature,
    max_tokens: settings.maxTokens,
  }),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-03T03:13:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.choices?.[0]?.message?.content,
+  errorMessagePath: "error?.message",
+};
+
+export const geminiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "GEMINI_API_KEY",
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+  apiHeaders: () => ({
path: apps/website/lib/llm/providers.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove API key from URL to prevent security risks.**

Including the API key directly in the URL is a security risk as it can be exposed in logs, browser history, or error messages. Instead, pass the API key via the headers.

```diff
export const geminiConfig: LLMProviderConfig = {
  apiKeyEnvVar: "GEMINI_API_KEY",
  apiUrl: (settings: Settings) =>
-    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent`,
  apiHeaders: () => ({
    "Content-Type": "application/json",
  }),
```

And update the headers function:

```diff
  apiHeaders: (apiKey: string) => ({
    "Content-Type": "application/json",
+    "x-goog-api-key": apiKey,
  }),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export const geminiConfig: LLMProviderConfig = {
  apiKeyEnvVar: "GEMINI_API_KEY",
  apiUrl: (settings: Settings) =>
    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent`,
  apiHeaders: (apiKey: string) => ({
    "Content-Type": "application/json",
    "x-goog-api-key": apiKey,
  }),
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-03T03:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  Message,
+  RequestBody,
+  Settings,
+  LLMProviderConfig,
+} from "./types";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export async function handleLLMRequest(
+  request: NextRequest,
+  config: LLMProviderConfig,
+): Promise<Response> {
+  try {
+    const requestData: RequestBody = await request.json();
+    const { documents: messages, settings } = requestData;
+
+    const apiKey = process.env[config.apiKeyEnvVar];
+
+    if (!apiKey) {
+      console.error(`${config.apiKeyEnvVar} environment variable is not set`);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API key not configured. Please set the ${config.apiKeyEnvVar} environment variable in your Vercel project settings.`,
+          }),
+          {
+            status: 500,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    const body = JSON.stringify(config.formatRequestBody(messages, settings));
+
+    const apiUrl =
+      typeof config.apiUrl === "function"
+        ? config.apiUrl(settings)
+        : config.apiUrl;
+
+    const response = await fetch(apiUrl, {
+      method: "POST",
+      headers: config.apiHeaders(apiKey),
+      body,
+    });
+
+    const responseData = await response.json();
+
+    if (!response.ok) {
+      console.error(`API error:`, responseData);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API error: ${eval(`responseData.${config.errorMessagePath}`) || "Unknown error"}`,
+          }),
path: apps/website/lib/llm/handler.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove eval() usage to prevent security vulnerabilities.**

Using `eval()` to access nested properties is a serious security risk that can lead to code injection vulnerabilities. Replace it with a safer alternative like a helper function to access nested properties based on a path string.

```diff
+ // Add this helper function at the top of the file
+ function getNestedProperty(obj: any, path: string): any {
+   return path.split('.').reduce((prev, curr) => {
+     return prev && prev[curr] !== undefined ? prev[curr] : undefined;
+   }, obj);
+ }

// Then replace the eval() usage with:
  if (!response.ok) {
    console.error(`API error:`, responseData);
    return cors(
      request,
      new Response(
        JSON.stringify({
-          error: `API error: ${eval(`responseData.${config.errorMessagePath}`) || "Unknown error"}`,
+          error: `API error: ${getNestedProperty(responseData, config.errorMessagePath) || "Unknown error"}`,
        }),
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 63-63: eval() exposes to security risks and performance issues.

See the MDN web docs for more details.
Refactor the code so that it doesn't need to call eval().

(lint/security/noGlobalEval)

</details>

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-03T03:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+import { NextRequest } from "next/server";
+import {
+  handleLLMRequest,
+  handleOptionsRequest,
+} from "../../../../../lib/llm/handler";
path: apps/website/app/api/llm/anthropic/chat/route.ts
user: login: mdroidian
body: instead of `../../....` use `~` to hit root.
created_at: 2025-04-04T00:33:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,18 @@
+import { NextRequest } from "next/server";
+import {
+  handleLLMRequest,
+  handleOptionsRequest,
+} from "../../../../../lib/llm/handler";
+import { anthropicConfig } from "../../../../../lib/llm/providers";
path: apps/website/app/api/llm/anthropic/chat/route.ts
user: login: mdroidian
body: ~
created_at: 2025-04-04T00:33:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,18 @@
+import { NextRequest } from "next/server";
+import {
+  handleLLMRequest,
+  handleOptionsRequest,
+} from "../../../../../lib/llm/handler";
path: apps/website/app/api/llm/gemini/chat/route.ts
user: login: mdroidian
body: ~
created_at: 2025-04-04T00:33:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,18 @@
+import { NextRequest } from "next/server";
+import {
+  handleLLMRequest,
+  handleOptionsRequest,
+} from "../../../../../lib/llm/handler";
+import { geminiConfig } from "../../../../../lib/llm/providers";
path: apps/website/app/api/llm/gemini/chat/route.ts
user: login: mdroidian
body: ~
created_at: 2025-04-04T00:33:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,18 @@
+import { NextRequest } from "next/server";
+import {
+  handleLLMRequest,
+  handleOptionsRequest,
+} from "../../../../../lib/llm/handler";
path: apps/website/app/api/llm/openai/chat/route.ts
user: login: mdroidian
body: ~
created_at: 2025-04-04T00:34:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,18 @@
+import { NextRequest } from "next/server";
+import {
+  handleLLMRequest,
+  handleOptionsRequest,
+} from "../../../../../lib/llm/handler";
+import { openaiConfig } from "../../../../../lib/llm/providers";
+
path: apps/website/app/api/llm/openai/chat/route.ts
user: login: mdroidian
body: ~
created_at: 2025-04-04T00:34:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,117 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
path: apps/website/lib/llm/handler.ts
user: login: mdroidian
body: ~
created_at: 2025-04-04T00:34:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,18 @@
+import { NextRequest } from "next/server";
+import {
+  handleLLMRequest,
+  handleOptionsRequest,
+} from "../../../../../lib/llm/handler";
+import { anthropicConfig } from "../../../../../lib/llm/providers";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export async function POST(request: NextRequest): Promise<Response> {
path: apps/website/app/api/llm/anthropic/chat/route.ts
user: login: mdroidian
body: prefer arrow functions to keep in line with the rest of the code base

I'll add this to both the style guidelines and cursor rules
created_at: 2025-04-04T00:37:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,48 @@
+import { NextRequest } from "next/server";
path: apps/website/lib/cors.ts
user: login: mdroidian
body: We already have a `/utils` folder.  Let's put this file there.
created_at: 2025-04-04T00:46:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,117 @@
+import { NextRequest } from "next/server";
path: apps/website/lib/llm/handler.ts
user: login: mdroidian
body: We already have a `/utils` folder. Let's put this file there.
created_at: 2025-04-04T00:48:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
path: apps/website/lib/llm/providers.ts
user: login: mdroidian
body: We already have a `/utils` folder. Let's put this file there.
created_at: 2025-04-04T00:48:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,34 @@
+import { NextRequest } from "next/server";
path: apps/website/lib/llm/types.ts
user: login: mdroidian
body: We already have a `/types` folder. Let's put this file there.
Also rename it to `llm.ts` or something similar.
created_at: 2025-04-04T00:48:49Z
author_association: MEMBER
diff_hunk: @@ -3,7 +3,13 @@
   "ui": "tui",
   "tasks": {
     "build": {
-      "passThroughEnv": ["RESEND_API_KEY"],
+      "passThroughEnv": [
path: turbo.json
user: login: mdroidian
body: Should we use the recommended `TURBO_PLATFORM_ENV_DISABLED` instead?
https://turbo.build/repo/docs/crafting-your-repository/using-environment-variables#platform-environment-variables

This likely goes in our vercel account somewhere.
created_at: 2025-04-04T00:55:42Z
author_association: MEMBER
diff_hunk: @@ -1,7 +1,12 @@
 import type { NextConfig } from "next";
 
-const nextConfig: NextConfig = {
-  /* config options here */
+const nextConfig = {
+  reactStrictMode: true,
+  serverRuntimeConfig: {
+    maxDuration: 300,
+  },
+  env: {
+    NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV || "development",
path: apps/website/next.config.ts
user: login: mdroidian
body: Where is this being used?
Is this being set to `production` somewhere in deployment?
created_at: 2025-04-04T00:59:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,117 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  RequestBody,
+  LLMProviderConfig,
+} from "./types";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
path: apps/website/lib/llm/handler.ts
user: login: mdroidian
body: are these needed here if they are on the route?
created_at: 2025-04-04T01:06:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  Message,
+  RequestBody,
+  Settings,
+  LLMProviderConfig,
+} from "./types";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export async function handleLLMRequest(
+  request: NextRequest,
+  config: LLMProviderConfig,
+): Promise<Response> {
+  try {
+    const requestData: RequestBody = await request.json();
+    const { documents: messages, settings } = requestData;
+
+    const apiKey = process.env[config.apiKeyEnvVar];
+
+    if (!apiKey) {
+      console.error(`${config.apiKeyEnvVar} environment variable is not set`);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API key not configured. Please set the ${config.apiKeyEnvVar} environment variable in your Vercel project settings.`,
+          }),
+          {
+            status: 500,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    const body = JSON.stringify(config.formatRequestBody(messages, settings));
+
+    const apiUrl =
+      typeof config.apiUrl === "function"
+        ? config.apiUrl(settings)
+        : config.apiUrl;
+
+    const response = await fetch(apiUrl, {
+      method: "POST",
+      headers: config.apiHeaders(apiKey),
+      body,
+    });
+
+    const responseData = await response.json();
+
+    if (!response.ok) {
+      console.error(`API error:`, responseData);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API error: ${eval(`responseData.${config.errorMessagePath}`) || "Unknown error"}`,
+          }),
path: apps/website/lib/llm/handler.ts
user: login: mdroidian
body: Let's do this
created_at: 2025-04-04T01:15:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
path: apps/website/lib/llm/providers.ts
user: login: mdroidian
body: Is this correct?
created_at: 2025-04-04T01:16:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.choices?.[0]?.message?.content,
+  errorMessagePath: "error?.message",
+};
+
+export const geminiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "GEMINI_API_KEY",
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+  apiHeaders: () => ({
path: apps/website/lib/llm/providers.ts
user: login: mdroidian
body: Let's do this
created_at: 2025-04-04T01:16:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,34 @@
+import { NextRequest } from "next/server";
path: apps/website/lib/llm/types.ts
user: login: mdroidian
body: unused
created_at: 2025-04-04T01:25:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,34 @@
+import { NextRequest } from "next/server";
+
+export type Message = {
+  role: string;
+  content: string;
+};
+
+export type Settings = {
+  model: string;
+  maxTokens: number;
+  temperature: number;
+  safetySettings?: Array<{
+    category: string;
+    threshold: string;
+  }>;
+};
+
+export type RequestBody = {
+  documents: Message[];
+  passphrase?: string;
+  settings: Settings;
+};
+
+export const CONTENT_TYPE_JSON = "application/json";
+export const CONTENT_TYPE_TEXT = "text/plain";
+
+export type LLMProviderConfig = {
+  apiKeyEnvVar: string;
+  apiUrl: string | ((settings: Settings) => string);
+  apiHeaders: (apiKey: string) => Record<string, string>;
+  formatRequestBody: (messages: Message[], settings: Settings) => any;
+  extractResponseText: (responseData: any) => string | null;
+  errorMessagePath: string;
+};
path: apps/website/lib/llm/types.ts
user: login: mdroidian
body: Yes, try to avoid `any` if you can track down what the response will be.
created_at: 2025-04-04T01:41:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  Message,
+  RequestBody,
+  Settings,
+  LLMProviderConfig,
+} from "./types";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export async function handleLLMRequest(
+  request: NextRequest,
+  config: LLMProviderConfig,
+): Promise<Response> {
+  try {
+    const requestData: RequestBody = await request.json();
+    const { documents: messages, settings } = requestData;
+
+    const apiKey = process.env[config.apiKeyEnvVar];
+
+    if (!apiKey) {
+      console.error(`${config.apiKeyEnvVar} environment variable is not set`);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API key not configured. Please set the ${config.apiKeyEnvVar} environment variable in your Vercel project settings.`,
+          }),
+          {
+            status: 500,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    const body = JSON.stringify(config.formatRequestBody(messages, settings));
+
+    const apiUrl =
+      typeof config.apiUrl === "function"
+        ? config.apiUrl(settings)
+        : config.apiUrl;
+
+    const response = await fetch(apiUrl, {
+      method: "POST",
+      headers: config.apiHeaders(apiKey),
+      body,
+    });
+
+    const responseData = await response.json();
+
+    if (!response.ok) {
+      console.error(`API error:`, responseData);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API error: ${eval(`responseData.${config.errorMessagePath}`) || "Unknown error"}`,
+          }),
path: apps/website/lib/llm/handler.ts
user: login: coderabbitai[bot]
body: @mdroidian I'll implement the suggested changes to remove the `eval()` usage for better security:

```typescript
// Add this helper function at the top of the file
function getNestedProperty(obj: any, path: string): any {
  return path.split('.').reduce((prev, curr) => {
    return prev && prev[curr] !== undefined ? prev[curr] : undefined;
  }, obj);
}

// Then replace the eval() usage with:
if (!response.ok) {
  console.error(`API error:`, responseData);
  return cors(
    request,
    new Response(
      JSON.stringify({
        error: `API error: ${getNestedProperty(responseData, config.errorMessagePath) || "Unknown error"}`,
      }),
```

This change eliminates the security vulnerability while maintaining the same functionality in a safer way.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-04T01:48:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
path: apps/website/lib/llm/providers.ts
user: login: coderabbitai[bot]
body: @mdroidian Yes, this is correct. According to the official OpenAI API documentation, the parameter for limiting token count in the Chat Completions API is `max_tokens`, not `max_completion_tokens`. The suggested correction will ensure your token limit settings are properly applied by the OpenAI API.
---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: mdroidian
PR: DiscourseGraphs/discourse-graph#102
File: apps/website/lib/llm/providers.ts:10-15
Timestamp: 2025-04-04T01:48:38.597Z
Learning: OpenAI Chat Completions API uses the parameter name `max_tokens` (not `max_completion_tokens`) to specify the maximum number of tokens to generate in the completion.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-04T01:48:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.choices?.[0]?.message?.content,
+  errorMessagePath: "error?.message",
+};
+
+export const geminiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "GEMINI_API_KEY",
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+  apiHeaders: () => ({
path: apps/website/lib/llm/providers.ts
user: login: coderabbitai[bot]
body: @mdroidian Great! I'll help implement this security fix for the Gemini API configuration.

[plan_code_changes]
In `apps/website/lib/llm/providers.ts`, we'll make the following changes:

1. Remove the API key from the URL in the `apiUrl` function
2. Update the `apiHeaders` function to include the API key in the headers

Here's the implementation:

```typescript
export const geminiConfig: LLMProviderConfig = {
  apiKeyEnvVar: "GEMINI_API_KEY",
  apiUrl: (settings: Settings) =>
    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent`,
  apiHeaders: (apiKey: string) => ({
    "Content-Type": "application/json",
    "x-goog-api-key": apiKey,
  }),
};
```

This change ensures the API key is passed securely through headers rather than being exposed in the URL.
---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: mdroidian
PR: DiscourseGraphs/discourse-graph#102
File: apps/website/lib/llm/providers.ts:21-25
Timestamp: 2025-04-04T01:48:43.104Z
Learning: For Google APIs like Gemini, the API key should be passed in the "x-goog-api-key" header rather than as a query parameter in the URL to prevent security risks like exposure in logs, browser history, or error messages.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-04T01:48:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+import { NextRequest } from "next/server";
+
+export type Message = {
+  role: string;
+  content: string;
+};
+
+export type Settings = {
+  model: string;
+  maxTokens: number;
+  temperature: number;
+  safetySettings?: Array<{
+    category: string;
+    threshold: string;
+  }>;
+};
+
+export type RequestBody = {
+  documents: Message[];
+  passphrase?: string;
+  settings: Settings;
+};
+
+export const CONTENT_TYPE_JSON = "application/json";
+export const CONTENT_TYPE_TEXT = "text/plain";
+
+export type LLMProviderConfig = {
+  apiKeyEnvVar: string;
+  apiUrl: string | ((settings: Settings) => string);
+  apiHeaders: (apiKey: string) => Record<string, string>;
+  formatRequestBody: (messages: Message[], settings: Settings) => any;
+  extractResponseText: (responseData: any) => string | null;
+  errorMessagePath: string;
+};
path: apps/website/lib/llm/types.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Let's examine the providers.ts file to see how LLMProviderConfig is implemented
cat apps/website/lib/llm/providers.ts
```

Length of output: 2161

---

🏁 Script executed:
```
#!/bin/bash
# Let's examine the handler.ts file to see how LLMProviderConfig is used
cat apps/website/lib/llm/handler.ts
```

Length of output: 3076

---



</details>

@mdroidian I've looked at the actual implementation in the providers and handler files. The response structures vary significantly across providers:

- OpenAI: `responseData.choices?.[0]?.message?.content`
- Gemini: `responseData.candidates?.[0]?.content?.parts?.[0]?.text`
- Anthropic: `responseData.content?.[0]?.text`

You can definitely replace `any` with `Record<string, unknown>` for better type safety while maintaining flexibility. Also, as the bot suggested, replacing the string-based `errorMessagePath` with a function-based approach would eliminate the security risk from using `eval()` in the handler.
---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: mdroidian
PR: DiscourseGraphs/discourse-graph#102
File: apps/website/lib/llm/types.ts:27-34
Timestamp: 2025-04-04T01:49:08.750Z
Learning: In the discourse-graph codebase, the LLM providers (OpenAI, Gemini, Anthropic) have different response structures. It's better to use `Record<string, unknown>` instead of `any` for better type safety while maintaining the flexibility to handle different response formats.
```

---

```
Learnt from: mdroidian
PR: DiscourseGraphs/discourse-graph#102
File: apps/website/lib/llm/types.ts:27-34
Timestamp: 2025-04-04T01:49:08.750Z
Learning: In the discourse-graph codebase, using string-based paths like `errorMessagePath` with `eval()` in handlers is a security risk. It's better to use a function-based approach like `extractErrorMessage: (responseData: Record<string, unknown>) => string | null`.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-04T01:49:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
path: apps/website/lib/llm/providers.ts
user: login: sid597
body: ![image](https://github.com/user-attachments/assets/15f6fb05-4a43-48b4-9b4f-6c9a846ef882)

created_at: 2025-04-04T09:07:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
path: apps/website/lib/llm/providers.ts
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-04-04T09:07:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.choices?.[0]?.message?.content,
+  errorMessagePath: "error?.message",
+};
+
+export const geminiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "GEMINI_API_KEY",
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+  apiHeaders: () => ({
path: apps/website/lib/llm/providers.ts
user: login: sid597
body: https://ai.google.dev/gemini-api/docs/text-generation I am using the rest api examples as the reference
created_at: 2025-04-04T09:10:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "./types";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.choices?.[0]?.message?.content,
+  errorMessagePath: "error?.message",
+};
+
+export const geminiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "GEMINI_API_KEY",
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+  apiHeaders: () => ({
path: apps/website/lib/llm/providers.ts
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-04-04T09:10:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  RequestBody,
+  LLMProviderConfig,
+} from "./types";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
path: apps/website/lib/llm/handler.ts
user: login: sid597
body: These are not needed 
created_at: 2025-04-04T09:34:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  Message,
+  RequestBody,
+  Settings,
+  LLMProviderConfig,
+} from "./types";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export async function handleLLMRequest(
+  request: NextRequest,
+  config: LLMProviderConfig,
+): Promise<Response> {
+  try {
+    const requestData: RequestBody = await request.json();
+    const { documents: messages, settings } = requestData;
+
+    const apiKey = process.env[config.apiKeyEnvVar];
+
+    if (!apiKey) {
+      console.error(`${config.apiKeyEnvVar} environment variable is not set`);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API key not configured. Please set the ${config.apiKeyEnvVar} environment variable in your Vercel project settings.`,
+          }),
+          {
+            status: 500,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    const body = JSON.stringify(config.formatRequestBody(messages, settings));
+
+    const apiUrl =
+      typeof config.apiUrl === "function"
+        ? config.apiUrl(settings)
+        : config.apiUrl;
+
+    const response = await fetch(apiUrl, {
+      method: "POST",
+      headers: config.apiHeaders(apiKey),
+      body,
+    });
+
+    const responseData = await response.json();
+
+    if (!response.ok) {
+      console.error(`API error:`, responseData);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API error: ${eval(`responseData.${config.errorMessagePath}`) || "Unknown error"}`,
+          }),
path: apps/website/lib/llm/handler.ts
user: login: sid597
body: @mdroidian lol do you see the eyes emoji by coderabbit 😆 
created_at: 2025-04-04T09:40:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "../../lib/cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  Message,
+  RequestBody,
+  Settings,
+  LLMProviderConfig,
+} from "./types";
+
+export const runtime = "nodejs";
+export const preferredRegion = "auto";
+export const maxDuration = 300;
+
+export async function handleLLMRequest(
+  request: NextRequest,
+  config: LLMProviderConfig,
+): Promise<Response> {
+  try {
+    const requestData: RequestBody = await request.json();
+    const { documents: messages, settings } = requestData;
+
+    const apiKey = process.env[config.apiKeyEnvVar];
+
+    if (!apiKey) {
+      console.error(`${config.apiKeyEnvVar} environment variable is not set`);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API key not configured. Please set the ${config.apiKeyEnvVar} environment variable in your Vercel project settings.`,
+          }),
+          {
+            status: 500,
+            headers: { "Content-Type": CONTENT_TYPE_JSON },
+          },
+        ),
+      );
+    }
+
+    const body = JSON.stringify(config.formatRequestBody(messages, settings));
+
+    const apiUrl =
+      typeof config.apiUrl === "function"
+        ? config.apiUrl(settings)
+        : config.apiUrl;
+
+    const response = await fetch(apiUrl, {
+      method: "POST",
+      headers: config.apiHeaders(apiKey),
+      body,
+    });
+
+    const responseData = await response.json();
+
+    if (!response.ok) {
+      console.error(`API error:`, responseData);
+      return cors(
+        request,
+        new Response(
+          JSON.stringify({
+            error: `API error: ${eval(`responseData.${config.errorMessagePath}`) || "Unknown error"}`,
+          }),
path: apps/website/lib/llm/handler.ts
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-04-04T09:40:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,7 +3,13 @@
   "ui": "tui",
   "tasks": {
     "build": {
-      "passThroughEnv": ["RESEND_API_KEY"],
+      "passThroughEnv": [
path: turbo.json
user: login: sid597
body: I think we should do it in a seperate pr because from my current exploration it seems that we need to create a new build script command in package.json
created_at: 2025-04-04T10:29:42Z
author_association: COLLABORATOR
diff_hunk: @@ -1,7 +1,12 @@
 import type { NextConfig } from "next";
 
-const nextConfig: NextConfig = {
-  /* config options here */
+const nextConfig = {
+  reactStrictMode: true,
+  serverRuntimeConfig: {
+    maxDuration: 300,
+  },
+  env: {
+    NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV || "development",
path: apps/website/next.config.ts
user: login: sid597
body: no, removed
created_at: 2025-04-04T10:29:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "../../types/llm";
+
+export const openaiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "OPENAI_API_KEY",
+  apiUrl: "https://api.openai.com/v1/chat/completions",
+  apiHeaders: (apiKey: string) => ({
+    "Content-Type": "application/json",
+    Authorization: `Bearer ${apiKey}`,
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    model: settings.model,
+    messages: messages,
+    temperature: settings.temperature,
+    max_completion_tokens: settings.maxTokens,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.choices?.[0]?.message?.content,
+  errorMessagePath: "error?.message",
+};
+
+export const geminiConfig: LLMProviderConfig = {
+  apiKeyEnvVar: "GEMINI_API_KEY",
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+  apiHeaders: () => ({
+    "Content-Type": "application/json",
+  }),
+  formatRequestBody: (messages: Message[], settings: Settings) => ({
+    contents: messages.map((msg) => ({
+      role: msg.role === "user" ? "user" : "model",
+      parts: [{ text: msg.content }],
+    })),
+    generationConfig: {
+      maxOutputTokens: settings.maxTokens,
+      temperature: settings.temperature,
+    },
+    safetySettings: settings.safetySettings,
+  }),
+  extractResponseText: (responseData: any) =>
+    responseData.candidates?.[0]?.content?.parts?.[0]?.text,
+  errorMessagePath: "error?.message",
+};
path: apps/website/app/utils/llm/providers.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Security issue: API key embedded in URL.**

The Gemini configuration embeds the API key directly in the URL, which is a security risk as API keys can be exposed in logs, browser history, or referrer headers.

```diff
export const geminiConfig: LLMProviderConfig = {
  apiKeyEnvVar: "GEMINI_API_KEY",
-  apiUrl: (settings: Settings) =>
-    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
-  apiHeaders: () => ({
+  apiUrl: (settings: Settings) =>
+    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent`,
+  apiHeaders: (apiKey: string) => ({
    "Content-Type": "application/json",
+    "x-goog-api-key": apiKey
  }),
  // Rest of the config remains the same
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export const geminiConfig: LLMProviderConfig = {
  apiKeyEnvVar: "GEMINI_API_KEY",
  apiUrl: (settings: Settings) =>
    `https://generativelanguage.googleapis.com/v1beta/models/${settings.model}:generateContent`,
  apiHeaders: (apiKey: string) => ({
    "Content-Type": "application/json",
    "x-goog-api-key": apiKey
  }),
  formatRequestBody: (messages: Message[], settings: Settings) => ({
    contents: messages.map((msg) => ({
      role: msg.role === "user" ? "user" : "model",
      parts: [{ text: msg.content }],
    })),
    generationConfig: {
      maxOutputTokens: settings.maxTokens,
      temperature: settings.temperature,
    },
    safetySettings: settings.safetySettings,
  }),
  extractResponseText: (responseData: any) =>
    responseData.candidates?.[0]?.content?.parts?.[0]?.text,
  errorMessagePath: "error?.message",
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-04T11:22:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+import { NextRequest } from "next/server";
+
+const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+
+function isVercelPreviewUrl(origin: string | null): boolean {
path: apps/website/app/utils/llm/cors.ts
user: login: mdroidian
body: origin is required here
created_at: 2025-04-05T13:44:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,48 @@
+import { NextRequest } from "next/server";
+
+const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+
+function isVercelPreviewUrl(origin: string | null): boolean {
+  if (!origin) return false;
+  return origin.includes(".vercel.app") || origin.includes("discourse-graph");
+}
+
+function isAllowedOrigin(origin: string | null): boolean {
path: apps/website/app/utils/llm/cors.ts
user: login: mdroidian
body: origin is required here
created_at: 2025-04-05T13:44:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "./cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  RequestBody,
+  LLMProviderConfig,
+} from "../../types/llm";
path: apps/website/app/utils/llm/handler.ts
user: login: mdroidian
body: ~
created_at: 2025-04-05T13:46:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { LLMProviderConfig, Message, Settings } from "../../types/llm";
path: apps/website/app/utils/llm/providers.ts
user: login: mdroidian
body: ~
created_at: 2025-04-05T13:46:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,48 @@
+import { NextRequest } from "next/server";
+
+const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+
+function isVercelPreviewUrl(origin: string | null): boolean {
+  if (!origin) return false;
+  return origin.includes(".vercel.app") || origin.includes("discourse-graph");
+}
+
+function isAllowedOrigin(origin: string | null): boolean {
+  if (!origin) return false;
+  return (
+    allowedOrigins.some((allowed) => origin.startsWith(allowed)) ||
+    isVercelPreviewUrl(origin)
+  );
+}
+
+export default function cors(req: NextRequest, res: Response) {
path: apps/website/app/utils/llm/cors.ts
user: login: mdroidian
body: arrow functions
created_at: 2025-04-05T13:48:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+import { NextRequest } from "next/server";
+import { handleLLMRequest, handleOptionsRequest } from "~/utils/llm/handler";
+import { openaiConfig } from "~/utils/llm/providers";
+
+export const runtime = "nodejs";
path: apps/website/app/api/llm/openai/chat/route.ts
user: login: mdroidian
body: why were the function configurations removed from anthropic and gemini?

```
export const preferredRegion = "auto";
export const maxDuration = 300;
export const runtime = "nodejs";
```
created_at: 2025-04-05T13:54:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,48 @@
+import { NextRequest } from "next/server";
+
+const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+
+function isVercelPreviewUrl(origin: string | null): boolean {
+  if (!origin) return false;
+  return origin.includes(".vercel.app") || origin.includes("discourse-graph");
+}
+
+function isAllowedOrigin(origin: string | null): boolean {
+  if (!origin) return false;
+  return (
+    allowedOrigins.some((allowed) => origin.startsWith(allowed)) ||
+    isVercelPreviewUrl(origin)
+  );
+}
+
+export default function cors(req: NextRequest, res: Response) {
+  const origin = req.headers.get("origin");
+
+  const originIsAllowed = isAllowedOrigin(origin);
path: apps/website/app/utils/llm/cors.ts
user: login: mdroidian
body: remove origin from the function params and change this to `origin && isAllowedOrigin(origin)`
created_at: 2025-04-05T13:56:43Z
author_association: MEMBER
diff_hunk: @@ -3,7 +3,13 @@
   "ui": "tui",
   "tasks": {
     "build": {
-      "passThroughEnv": ["RESEND_API_KEY"],
+      "passThroughEnv": [
path: turbo.json
user: login: mdroidian
body: What new build script command would we need to add?
created_at: 2025-04-05T13:57:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import { NextRequest } from "next/server";
+import cors from "./cors";
+import {
+  CONTENT_TYPE_JSON,
+  CONTENT_TYPE_TEXT,
+  RequestBody,
+  LLMProviderConfig,
+} from "../../types/llm";
path: apps/website/app/utils/llm/handler.ts
user: login: sid597
body: oh app is the root here I thought website is 
created_at: 2025-04-07T17:52:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,48 @@
+import { NextRequest } from "next/server";
+
+const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+
+function isVercelPreviewUrl(origin: string | null): boolean {
path: apps/website/app/utils/llm/cors.ts
user: login: sid597
body: I don't know about networking much, this is what cursor says about this:

The origin value can be null because req.headers.get("origin") can return null in several scenarios:
1. If the request doesn't include an Origin header at all (which is common for:

- Direct browser navigation
- Same-origin requests that don't explicitly set the Origin header
- Requests from non-browser clients that don't set this header)


2. When a request comes from a source that doesn't set the Origin header, like:
- Server-to-server API calls
- Curl or Postman requests without the header specified
- Some native mobile apps

The Next.js headers.get() method follows the same behavior as the standard Fetch API's Headers.get(), which returns null if the header doesn't exist in the request.
That's why we need to handle the potential null case, as requests without an Origin header are perfectly valid and will come up in production environments.
created_at: 2025-04-07T18:16:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,48 @@
+import { NextRequest } from "next/server";
+
+const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
+
+function isVercelPreviewUrl(origin: string | null): boolean {
path: apps/website/app/utils/llm/cors.ts
user: login: mdroidian
body: Sure, but how the function is written, it doesn't do anything meaningful will null.  And it is only used in one place, so we can do the check even before using this function.  Same goes with `isAllowedOrigin`
created_at: 2025-04-07T18:49:04Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-03-07T02:54:16Z
body: [vc]: #82gs6iXVRuERoTlfFBJBOJgfAvbVuESHStVyrWI8vWM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJkaXNjb3Vyc2UtZ3JhcGgiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZGlzY291cnNlLWdyYXBoLWdpdC1tb3ZlLWFwaS1lbmRwb2ludHMtNmNhOWU0LWRpc2NvdXJzZS1ncmFwaHMudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZGlzY291cnNlLWdyYXBocy9kaXNjb3Vyc2UtZ3JhcGgvNlVWREF1dW55dDdzVkpTNVhpVjhVUEVqTlBocCIsInByZXZpZXdVcmwiOiJkaXNjb3Vyc2UtZ3JhcGgtZ2l0LW1vdmUtYXBpLWVuZHBvaW50cy02Y2E5ZTQtZGlzY291cnNlLWdyYXBocy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvd2Vic2l0ZS8ifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **discourse-graph** | ✅ Ready ([Inspect](https://vercel.com/discourse-graphs/discourse-graph/6UVDAuunyt7sVJS5XiV8UPEjNPhp)) | [Visit Preview](https://discourse-graph-git-move-api-endpoints-6ca9e4-discourse-graphs.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/discourse-graph-git-move-api-endpoints-6ca9e4-discourse-graphs.vercel.app?via=pr-comment-feedback-link) | Apr 8, 2025 0:43am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-07T02:54:18Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough
The changes update the Next.js configuration, add new API endpoints for Anthropic, Gemini, and OpenAI chat interactions, and enhance the build environment setup. New TypeScript files introduce API routes with exported POST and OPTIONS handlers, along with shared utility functions for request handling and CORS. Additionally, several new types and provider configurations for LLM interactions have been added. The modifications also update build configurations in turbo.json to pass through additional environment variables.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `apps/website/next.config.ts` | Removed type annotation for `nextConfig`; set `reactStrictMode` to `true` and added `serverRuntimeConfig` with `maxDuration: 300`. |
| `apps/website/app/api/llm/anthropic/chat/route.ts`,<br>`apps/website/app/api/llm/gemini/chat/route.ts`,<br>`apps/website/app/api/llm/openai/chat/route.ts` | Introduced new API routes for LLM chat requests with exported POST and OPTIONS methods. The OpenAI route additionally exports `runtime`, `preferredRegion`, and `maxDuration`. |
| `turbo.json` | Updated the build task’s `passThroughEnv` list to include additional environment variables: `"OPENAI_API_KEY"`, `"ANTHROPIC_API_KEY"`, `"GEMINI_API_KEY"`, and `"BLOB_READ_WRITE_TOKEN"`. |
| `apps/website/app/types/llm.ts` | Added new types (`Message`, `Settings`, `RequestBody`, `LLMProviderConfig`) and constants (`CONTENT_TYPE_JSON`, `CONTENT_TYPE_TEXT`) for LLM communication. |
| `apps/website/app/utils/llm/cors.ts` | Added a CORS utility that defines allowed origins and applies CORS headers based on request origin and method. |
| `apps/website/app/utils/llm/handler.ts` | Introduced asynchronous functions `handleLLMRequest` and `handleOptionsRequest` to process API requests and preflight checks with proper error handling. |
| `apps/website/app/utils/llm/providers.ts` | Added provider configurations for OpenAI, Gemini, and Anthropic as `LLMProviderConfig` objects, each outlining API URL, headers, request formatting, and response extraction. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant C as Client
    participant AR as API Route
    participant H as Handler
    participant LP as LLM Provider

    C->>AR: HTTP Request (POST/OPTIONS)
    alt POST Request
        AR->>H: handleLLMRequest(request, config)
        H->>LP: Send formatted API request
        LP-->>H: API response
        H-->>AR: Formulated response
    else OPTIONS Request
        AR->>H: handleOptionsRequest(request)
        H-->>AR: 204 Response with CORS headers
    end
    AR-->>C: Return response with CORS headers
```

## Poem
> I hopped through lines of code so bright,  
> New routes and types gave my heart delight.  
> Configs updated with precision and cheer,  
> API calls now shine crystal clear.  
> From one bunny's trail to another's embrace,  
> Every change makes this garden a happier place!

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=DiscourseGraphs/discourse-graph&utm_content=102):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: mdroidian
created_at: 2025-03-10T21:39:07Z
body: As discussed, let's use the `apps/website` api endpoint for this
author_association: MEMBER
user: login: sid597
created_at: 2025-03-30T04:39:53Z
body: @coderabbitai review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-03-30T04:39:58Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR
user: login: sid597
created_at: 2025-04-03T02:17:15Z
body: @coderabbitai review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-04-03T02:23:42Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR
user: login: sid597
created_at: 2025-04-03T02:58:17Z
body: @coderabbitai review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-04-03T03:10:56Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR
user: login: sid597
created_at: 2025-04-04T11:19:47Z
body: @coderabbitai review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-04-04T11:19:53Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR