mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
docs: custom AI chat (#8523)
Co-authored-by: Sandy <github@sandy00.aleeas.com>
This commit is contained in:
@@ -14,4 +14,4 @@ uncompromised
|
||||
myapp
|
||||
Neue
|
||||
CCPA
|
||||
CPRA
|
||||
CPRA
|
||||
@@ -48,3 +48,5 @@ SIEM
|
||||
sess
|
||||
reactivations
|
||||
whsec
|
||||
moonshotai
|
||||
kimi
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: API
|
||||
description: Better Auth API.
|
||||
description: Learn how to call Better Auth API endpoints on the server, pass body, headers, and query parameters, retrieve response headers, and handle errors.
|
||||
---
|
||||
|
||||
When you create a new Better Auth instance, it provides you with an `api` object. This object exposes every endpoint that exists in your Better Auth instance. And you can use this to interact with Better Auth server side.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: CLI
|
||||
description: Built-in CLI for managing your project.
|
||||
description: Learn about the Better Auth CLI commands for generating and migrating database schemas, initializing projects, generating secret keys, and gathering diagnostic info.
|
||||
---
|
||||
|
||||
Better Auth comes with a built-in CLI to help you manage the database schemas, initialize your project, generate a secret key for your application, and gather diagnostic information about your setup.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Client
|
||||
description: Better Auth client library for authentication.
|
||||
description: Learn how to set up the Better Auth client for React, Vue, Svelte, and other frameworks, use hooks, configure fetch options, handle errors, and extend with client plugins.
|
||||
---
|
||||
|
||||
Better Auth offers a client library compatible with popular frontend frameworks like React, Vue, Svelte, and more. This client library includes a set of functions for interacting with the Better Auth server. Each framework's client library is built on top of a core client library that is framework-agnostic, so that all methods and hooks are consistently available across all client libraries.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Cookies
|
||||
description: Learn how cookies are used in Better Auth.
|
||||
description: Learn how Better Auth uses cookies, including cookie prefixes, custom cookie attributes, cross-subdomain sharing, secure cookies, and handling Safari ITP with proxies.
|
||||
---
|
||||
|
||||
Cookies are used to store data such as session tokens, session data, OAuth state, and more. All cookies are signed using the `secret` key provided in the auth options or the `BETTER_AUTH_SECRET` environment variable. If you use [versioned secrets](/docs/reference/options#secrets) for rotation, encrypted cookie data (such as JWE session caches) will automatically use the current key and remain decryptable with previous keys.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Database
|
||||
description: Learn how to use a database with Better Auth.
|
||||
description: Learn about database adapters, migrations, secondary storage with Redis, core schema (user, session, account, verification), custom tables, extending schemas, ID generation, database hooks, and plugin schemas.
|
||||
---
|
||||
|
||||
## Adapters
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Email
|
||||
description: Learn how to use email with Better Auth.
|
||||
description: Learn how to set up email verification, require verified emails for sign-in, auto sign-in after verification, handle post-verification callbacks, and implement password reset emails.
|
||||
---
|
||||
|
||||
Email is a key part of Better Auth, required for all users regardless of their authentication method. Better Auth provides email and password authentication out of the box, and a lot of utilities to help you manage email verification, password reset, and more.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Hooks
|
||||
description: Better Auth Hooks let you customize BetterAuth's behavior
|
||||
description: Learn how to use before and after hooks to customize endpoint behavior, modify requests and responses, handle cookies, throw errors, access auth context, and run background tasks.
|
||||
---
|
||||
|
||||
Hooks in Better Auth let you "hook into" the lifecycle and execute custom logic. They provide a way to customize Better Auth's behavior without writing a full plugin.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: OAuth
|
||||
description: How Better Auth handles OAuth
|
||||
description: Learn how to configure social OAuth providers, sign in and link accounts, request scopes, pass additional data, refresh access tokens, map profiles, and customize provider options.
|
||||
---
|
||||
|
||||
Better Auth comes with built-in support for OAuth 2.0 and OpenID Connect. This allows you to authenticate users via popular OAuth providers like Google, Facebook, GitHub, and more.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Plugins
|
||||
description: Learn how to use plugins with Better Auth.
|
||||
description: Learn how to use and create Better Auth plugins, including defining endpoints, schemas, hooks, middleware, rate limits, trusted origins, and building client plugins with custom actions and atoms.
|
||||
---
|
||||
|
||||
Plugins are a key part of Better Auth, they let you extend the base functionalities. You can use them to add new authentication methods, features, or customize behaviors.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Rate Limit
|
||||
description: How to limit the number of requests a user can make to the server in a given time period.
|
||||
description: Learn how to configure rate limiting in Better Auth, including IP address detection, IPv6 support, custom rate limit windows, storage backends, error handling, and per-endpoint rules.
|
||||
---
|
||||
|
||||
Better Auth includes a built-in rate limiter to help manage traffic and prevent abuse. By default, in production mode, the rate limiter is set to:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Session Management
|
||||
description: Better Auth session management.
|
||||
description: Learn about session management in Better Auth, including session expiration, freshness, cookie caching strategies, secondary storage, stateless sessions, and customizing session responses.
|
||||
---
|
||||
|
||||
Better Auth manages session using a traditional cookie-based session management. The session is stored in a cookie and is sent to the server on every request. The server then verifies the session and returns the user data if the session is valid.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: TypeScript
|
||||
description: Better Auth TypeScript integration.
|
||||
description: Learn about TypeScript configuration for Better Auth, including strict mode, inferring types for sessions and users, defining additional fields, and inferring additional fields on the client.
|
||||
---
|
||||
|
||||
Better Auth is designed to be type-safe. Both the client and server are built with TypeScript, allowing you to easily infer types.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: User & Accounts
|
||||
description: User and account management.
|
||||
description: Learn how to manage users and accounts, including updating user info, changing emails and passwords, deleting users with verification, token encryption, and account linking and unlinking.
|
||||
---
|
||||
|
||||
Beyond authenticating users, Better Auth also provides a set of methods to manage users. This includes, updating user information, changing passwords, and more.
|
||||
|
||||
@@ -35,33 +35,6 @@ Better Auth provides an MCP server so you can use it with any AI model that supp
|
||||
|
||||
<AddToCursor />
|
||||
|
||||
#### CLI Options
|
||||
|
||||
Use the Better Auth CLI to easily add the MCP server to your preferred client:
|
||||
|
||||
<Tabs items={["Cursor", "Claude Code", "Open Code", "Manual"]}>
|
||||
<Tab value="Cursor">
|
||||
```bash title="terminal"
|
||||
npx auth mcp --cursor
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="Claude Code">
|
||||
```bash title="terminal"
|
||||
npx auth mcp --claude-code
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="Open Code">
|
||||
```bash title="terminal"
|
||||
npx auth mcp --open-code
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="Manual">
|
||||
```bash title="terminal"
|
||||
npx auth mcp --manual
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
#### Manual Configuration
|
||||
|
||||
Alternatively, you can manually configure the MCP server for each client:
|
||||
@@ -69,7 +42,7 @@ Alternatively, you can manually configure the MCP server for each client:
|
||||
<Tabs items={["Claude Code", "Open Code", "Manual"]}>
|
||||
<Tab value="Claude Code">
|
||||
```bash title="terminal"
|
||||
claude mcp add --transport http better-auth https://mcp.inkeep.com/better-auth/mcp
|
||||
claude mcp add --transport http better-auth https://context7.com/better-auth/better-auth
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="Open Code">
|
||||
@@ -79,7 +52,7 @@ Alternatively, you can manually configure the MCP server for each client:
|
||||
"mcp": {
|
||||
"better-auth": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.inkeep.com/better-auth/mcp",
|
||||
"url": "https://context7.com/better-auth/better-auth",
|
||||
"enabled": true,
|
||||
}
|
||||
}
|
||||
@@ -90,14 +63,9 @@ Alternatively, you can manually configure the MCP server for each client:
|
||||
```json title="mcp.json"
|
||||
{
|
||||
"better-auth": {
|
||||
"url": "https://mcp.inkeep.com/better-auth/mcp"
|
||||
"url": "https://context7.com/better-auth/better-auth"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Callout>
|
||||
We provide a first‑party MCP, powered by [Inkeep](https://inkeep.com). You can alternatively use [`context7`](https://context7.com/) and other MCP providers.
|
||||
</Callout>
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# GitHub API - community stats
|
||||
GITHUB_TOKEN=
|
||||
|
||||
# Inkeep API - AI chat
|
||||
INKEEP_API_KEY=
|
||||
# Openrouter API - docs AI chat
|
||||
OPENROUTER_API_KEY=
|
||||
|
||||
# Upstash Redis - AI chat rate limiting
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
|
||||
# Typesense - Docs search
|
||||
NEXT_PUBLIC_TYPESENSE_SERVER_URL=
|
||||
@@ -11,3 +15,4 @@ TYPESENSE_ADMIN_API_KEY=
|
||||
|
||||
# Next.js - public URL
|
||||
NEXT_PUBLIC_URL=http://localhost:3000
|
||||
|
||||
|
||||
@@ -1,21 +1 @@
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||
import { convertToModelMessages, streamText } from "ai";
|
||||
|
||||
const inkeep = createOpenAICompatible({
|
||||
name: "inkeep",
|
||||
baseURL: "https://api.inkeep.com/v1",
|
||||
apiKey: process.env.INKEEP_API_KEY,
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages } = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: inkeep.chatModel("inkeep-qa-expert"),
|
||||
messages: await convertToModelMessages(messages, {
|
||||
ignoreIncompleteToolCalls: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
export { POST } from "@/lib/ai-chat/route";
|
||||
|
||||
@@ -183,15 +183,7 @@ function MobileDrawerPanel() {
|
||||
<div className="flex-1 text-left">
|
||||
<DrawerTitle className="text-xs font-medium">AI Chat</DrawerTitle>
|
||||
<DrawerDescription className="text-[10px]">
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://inkeep.com"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="underline hover:text-foreground transition-colors"
|
||||
>
|
||||
Inkeep AI
|
||||
</a>
|
||||
Better Auth docs assistant
|
||||
</DrawerDescription>
|
||||
</div>
|
||||
<DrawerClose
|
||||
@@ -287,15 +279,7 @@ function PanelHeader() {
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">AI Chat</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://inkeep.com"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="underline hover:text-foreground transition-colors"
|
||||
>
|
||||
Inkeep AI
|
||||
</a>
|
||||
Better Auth docs assistant
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -393,6 +377,7 @@ function PanelMessages({ className, ...props }: ComponentProps<"div">) {
|
||||
messageIndex={index}
|
||||
/>
|
||||
))}
|
||||
<ThinkingIndicator />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -558,6 +543,27 @@ function PanelInput({ autoFocus = false }: { autoFocus?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Thinking Indicator ──────────────────────────────────────────────────────
|
||||
|
||||
function ThinkingIndicator() {
|
||||
const { status, messages } = useChatContext();
|
||||
const lastMessage = messages.at(-1);
|
||||
const hasNoText =
|
||||
!lastMessage ||
|
||||
lastMessage.role !== "assistant" ||
|
||||
!lastMessage.parts?.some((p) => p.type === "text" && p.text.length > 0);
|
||||
|
||||
if (status !== "submitted" && !(status === "streaming" && hasNoText))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
<span>Looking through docs...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Message ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const roleName: Record<string, string> = {
|
||||
@@ -587,13 +593,6 @@ function Message({
|
||||
}
|
||||
}
|
||||
|
||||
if (message.role === "assistant") {
|
||||
// Remove Inkeep citation markers like (1), (2) etc.
|
||||
markdown = markdown.replace(/\(\d+\)/g, "");
|
||||
// Remove stray backslashes used as line breaks by Inkeep
|
||||
markdown = markdown.replace(/^\s*\\\s*$/gm, "");
|
||||
}
|
||||
|
||||
// Fix incomplete code blocks
|
||||
const codeBlockCount = (markdown.match(/```/g) || []).length;
|
||||
if (codeBlockCount % 2 !== 0) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
LucideAArrowDown,
|
||||
Mail,
|
||||
Mailbox,
|
||||
Navigation,
|
||||
Phone,
|
||||
ScanFace,
|
||||
Server,
|
||||
@@ -436,6 +437,11 @@ export const contents: Content[] = [
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Dynamic Base URL",
|
||||
href: "/docs/concepts/dynamic-base-url",
|
||||
icon: () => <Navigation className="w-4 h-4 text-current" />,
|
||||
},
|
||||
],
|
||||
Icon: () => (
|
||||
<svg
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { Redis } from "@upstash/redis";
|
||||
|
||||
const redis = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_REST_URL!,
|
||||
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
||||
});
|
||||
|
||||
const ratelimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.slidingWindow(20, "10 m"),
|
||||
prefix: "ai-chat",
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract the real client IP from a Vercel-proxied request.
|
||||
* Vercel appends the connecting IP as the rightmost value in x-forwarded-for.
|
||||
*/
|
||||
export function getClientIP(req: Request): string {
|
||||
const forwarded = req.headers.get("x-forwarded-for");
|
||||
if (forwarded) {
|
||||
const ips = forwarded.split(",").map((ip) => ip.trim());
|
||||
return ips[0] || "unknown";
|
||||
}
|
||||
const realIp = req.headers.get("x-real-ip");
|
||||
if (realIp) return realIp.trim();
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export async function checkRateLimit(ip: string) {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return {
|
||||
success: true,
|
||||
limit: 100,
|
||||
remaining: 100,
|
||||
reset: Date.now() + 1000 * 60 * 10,
|
||||
};
|
||||
}
|
||||
const result = await ratelimit.limit(ip);
|
||||
return {
|
||||
success: result.success,
|
||||
limit: result.limit,
|
||||
remaining: result.remaining,
|
||||
reset: result.reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
||||
import { convertToModelMessages, stepCountIs, streamText, tool } from "ai";
|
||||
import * as z from "zod";
|
||||
import { getLLMText } from "@/lib/llm-text";
|
||||
import { source } from "@/lib/source";
|
||||
import { checkRateLimit, getClientIP } from "./rate-limit";
|
||||
|
||||
const openrouter = createOpenRouter({
|
||||
apiKey: process.env.OPENROUTER_API_KEY,
|
||||
});
|
||||
const chatModel = openrouter.chat("anthropic/claude-sonnet-4");
|
||||
|
||||
function buildDocsIndex(): string {
|
||||
const pages = source.getPages();
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const page of pages) {
|
||||
if (page.slugs[0] === "openapi") continue;
|
||||
const slug = page.slugs.join("/");
|
||||
const desc = page.data.description ? ` - ${page.data.description}` : "";
|
||||
lines.push(`- ${page.data.title} [${slug}]${desc}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
let cachedDocsIndex: string | null = null;
|
||||
function getDocsIndex(): string {
|
||||
if (!cachedDocsIndex) {
|
||||
cachedDocsIndex = buildDocsIndex();
|
||||
}
|
||||
return cachedDocsIndex;
|
||||
}
|
||||
|
||||
type SearchEntry = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
headings: string[];
|
||||
contents: { heading: string | undefined; content: string }[];
|
||||
};
|
||||
|
||||
let cachedSearchIndex: SearchEntry[] | null = null;
|
||||
async function getSearchIndex(): Promise<SearchEntry[]> {
|
||||
if (cachedSearchIndex) return cachedSearchIndex;
|
||||
|
||||
const pages = source.getPages();
|
||||
const entries: SearchEntry[] = [];
|
||||
|
||||
await Promise.all(
|
||||
pages
|
||||
.filter((p) => p.slugs[0] !== "openapi")
|
||||
.map(async (page) => {
|
||||
try {
|
||||
const loaded = await page.data.load();
|
||||
entries.push({
|
||||
slug: page.slugs.join("/"),
|
||||
title: page.data.title,
|
||||
description: page.data.description ?? "",
|
||||
headings: loaded.structuredData.headings.map(
|
||||
(h: { content: string }) => h.content,
|
||||
),
|
||||
contents: loaded.structuredData.contents,
|
||||
});
|
||||
} catch {
|
||||
// skip pages that fail to load
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
cachedSearchIndex = entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
const MAX_SEARCH_RESULTS = 10;
|
||||
const MAX_SNIPPET_LENGTH = 200;
|
||||
|
||||
function searchIndex(entries: SearchEntry[], query: string) {
|
||||
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
if (terms.length === 0) return [];
|
||||
|
||||
const scored: { entry: SearchEntry; score: number; snippets: string[] }[] =
|
||||
[];
|
||||
|
||||
for (const entry of entries) {
|
||||
let score = 0;
|
||||
const snippets: string[] = [];
|
||||
const titleLower = entry.title.toLowerCase();
|
||||
const descLower = entry.description.toLowerCase();
|
||||
|
||||
for (const term of terms) {
|
||||
if (titleLower.includes(term)) score += 10;
|
||||
if (descLower.includes(term)) score += 5;
|
||||
}
|
||||
|
||||
for (const h of entry.headings) {
|
||||
const lower = h.toLowerCase();
|
||||
for (const term of terms) {
|
||||
if (lower.includes(term)) score += 3;
|
||||
}
|
||||
}
|
||||
|
||||
for (const section of entry.contents) {
|
||||
const lower = section.content.toLowerCase();
|
||||
const matches = terms.filter((t) => lower.includes(t));
|
||||
if (matches.length > 0) {
|
||||
score += matches.length * 2;
|
||||
const firstMatch = lower.indexOf(matches[0]);
|
||||
const start = Math.max(0, firstMatch - 60);
|
||||
const snippet = section.content.slice(
|
||||
start,
|
||||
start + MAX_SNIPPET_LENGTH,
|
||||
);
|
||||
const heading = section.heading ? `[${section.heading}] ` : "";
|
||||
snippets.push(`${heading}...${snippet}...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
scored.push({ entry, score, snippets: snippets.slice(0, 3) });
|
||||
}
|
||||
}
|
||||
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return scored.slice(0, MAX_SEARCH_RESULTS);
|
||||
}
|
||||
|
||||
const GITHUB_REPO = "better-auth/better-auth";
|
||||
const GITHUB_API = "https://api.github.com";
|
||||
const MAX_CODE_SEARCH_RESULTS = 8;
|
||||
const MAX_FILE_CONTENT_LENGTH = 12_000;
|
||||
|
||||
type GitHubTextMatch = {
|
||||
fragment: string;
|
||||
matches: { text: string; indices: number[] }[];
|
||||
};
|
||||
|
||||
type GitHubSearchItem = {
|
||||
name: string;
|
||||
path: string;
|
||||
html_url: string;
|
||||
repository: { full_name: string };
|
||||
text_matches?: GitHubTextMatch[];
|
||||
};
|
||||
|
||||
function githubHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/vnd.github.text-match+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
};
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function githubSearchCode(query: string, path?: string) {
|
||||
let q = `${query} repo:${GITHUB_REPO}`;
|
||||
if (path) {
|
||||
q += ` path:${path}`;
|
||||
}
|
||||
|
||||
const url = new URL(`${GITHUB_API}/search/code`);
|
||||
url.searchParams.set("q", q);
|
||||
url.searchParams.set("per_page", String(MAX_CODE_SEARCH_RESULTS));
|
||||
|
||||
const res = await fetch(url.toString(), { headers: githubHeaders() });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
return { error: `GitHub API error (${res.status}): ${text}` };
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
total_count: number;
|
||||
items: GitHubSearchItem[];
|
||||
};
|
||||
|
||||
return {
|
||||
total_count: data.total_count,
|
||||
results: data.items.map((item) => ({
|
||||
path: item.path,
|
||||
url: item.html_url,
|
||||
fragments: (item.text_matches ?? []).map((m) => m.fragment),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function githubGetFileContent(path: string, ref = "canary") {
|
||||
const url = `${GITHUB_API}/repos/${GITHUB_REPO}/contents/${encodeURIComponent(path)}?ref=${ref}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.raw+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
...(process.env.GITHUB_TOKEN
|
||||
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
error: `GitHub API error (${res.status}): file not found or inaccessible`,
|
||||
};
|
||||
}
|
||||
|
||||
let content = await res.text();
|
||||
if (content.length > MAX_FILE_CONTENT_LENGTH) {
|
||||
content = `${content.slice(0, MAX_FILE_CONTENT_LENGTH)}\n\n... (truncated — file exceeds ${MAX_FILE_CONTENT_LENGTH} chars)`;
|
||||
}
|
||||
return { path, content };
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = `You are a helpful documentation assistant for Better Auth, a comprehensive framework-agnostic authentication and authorization framework for TypeScript.
|
||||
|
||||
Your role is to answer questions about Better Auth by referencing the official documentation. You should be accurate, concise, and helpful.
|
||||
|
||||
## Rules
|
||||
- Always look up relevant documentation before answering. Do NOT guess or make up information.
|
||||
- Use searchDocs when you're unsure which page has the answer, then use getDocumentation to read the matching pages.
|
||||
- You may call the tools multiple times to gather context from different pages.
|
||||
- When referencing configuration options, APIs, or code, be precise and use the exact names from the documentation.
|
||||
- If the documentation doesn't cover the user's question, say so honestly rather than guessing.
|
||||
- Format your answers with markdown. Use code blocks for code examples.
|
||||
- Keep answers focused and concise - ideally under 400 words. Don't dump entire pages of documentation; extract only the relevant parts.
|
||||
- Provide short code snippets, not full files. Only show the minimum code needed to illustrate the answer.
|
||||
- When users ask about implementation details, internal behavior, or source code, use searchCode and getFileContent to look up the actual source code from the GitHub repository.
|
||||
|
||||
## Available Documentation Pages
|
||||
The following pages are available. Use the slug (in brackets) with the getDocumentation tool to retrieve the full content of a page.
|
||||
|
||||
`;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const ip = getClientIP(req);
|
||||
const rateLimitResult = await checkRateLimit(ip);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
const retryAfter = Math.ceil((rateLimitResult.reset - Date.now()) / 1000);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Rate limit exceeded. Please try again later.",
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": String(Math.max(retryAfter, 1)),
|
||||
"X-RateLimit-Limit": String(rateLimitResult.limit),
|
||||
"X-RateLimit-Remaining": String(rateLimitResult.remaining),
|
||||
"X-RateLimit-Reset": String(rateLimitResult.reset),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const { messages } = await req.json();
|
||||
const docsIndex = getDocsIndex();
|
||||
|
||||
const system = SYSTEM_PROMPT + docsIndex;
|
||||
|
||||
const result = streamText({
|
||||
model: chatModel,
|
||||
maxOutputTokens: 2048,
|
||||
system,
|
||||
messages: await convertToModelMessages(messages, {
|
||||
ignoreIncompleteToolCalls: true,
|
||||
}),
|
||||
tools: {
|
||||
searchDocs: tool({
|
||||
description:
|
||||
"Search across all Better Auth documentation for specific terms, keywords, or concepts. Returns matching page slugs and snippets. Use this when you're not sure which page contains the answer.",
|
||||
inputSchema: z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
"Search query — keywords or terms to find, e.g. 'session token cookie' or 'organization invite'",
|
||||
),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
const index = await getSearchIndex();
|
||||
const results = searchIndex(index, query);
|
||||
if (results.length === 0) {
|
||||
return { results: [], message: "No matching documentation found." };
|
||||
}
|
||||
return {
|
||||
results: results.map((r) => ({
|
||||
slug: r.entry.slug,
|
||||
title: r.entry.title,
|
||||
description: r.entry.description,
|
||||
snippets: r.snippets,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
getDocumentation: tool({
|
||||
description:
|
||||
"Retrieve the full content of a Better Auth documentation page by its slug. Use this to look up detailed information before answering user questions.",
|
||||
inputSchema: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.describe(
|
||||
"The documentation page slug, e.g. 'plugins/passkey' or 'concepts/session-management'",
|
||||
),
|
||||
}),
|
||||
execute: async ({ slug }) => {
|
||||
const slugParts = slug.split("/");
|
||||
const page = source.getPage(slugParts);
|
||||
if (!page) {
|
||||
return { error: `Documentation page not found for slug: ${slug}` };
|
||||
}
|
||||
try {
|
||||
const content = await getLLMText(page);
|
||||
return { content };
|
||||
} catch {
|
||||
return {
|
||||
error: `Failed to load documentation for: ${slug}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
searchCode: tool({
|
||||
description:
|
||||
"Search the Better Auth source code on GitHub using semantic code search. Returns matching file paths and code fragments. Use this when users ask about implementation details, internal behavior, or want to see actual source code.",
|
||||
inputSchema: z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
"Code search query — function names, class names, keywords, or concepts to find in the source, e.g. 'createSession' or 'hashPassword' or 'organization plugin'",
|
||||
),
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional path filter to narrow search to a directory or file pattern, e.g. 'packages/better-auth/src' or 'packages/cli'",
|
||||
),
|
||||
}),
|
||||
execute: async ({ query, path }) => {
|
||||
return githubSearchCode(query, path);
|
||||
},
|
||||
}),
|
||||
getFileContent: tool({
|
||||
description:
|
||||
"Fetch the content of a specific file from the Better Auth GitHub repository. Use this after searchCode to read the full source of a matching file.",
|
||||
inputSchema: z.object({
|
||||
path: z
|
||||
.string()
|
||||
.describe(
|
||||
"The file path in the repository, e.g. 'packages/better-auth/src/api/routes/session.ts'",
|
||||
),
|
||||
}),
|
||||
execute: async ({ path }) => {
|
||||
return githubGetFileContent(path);
|
||||
},
|
||||
}),
|
||||
},
|
||||
stopWhen: stepCountIs(8),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
"@biomejs/biome": "2.4.4",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@inkeep/ai-sdk-provider": "^0.54.0",
|
||||
"@openrouter/ai-sdk-provider": "^2.2.5",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
@@ -38,9 +39,11 @@
|
||||
"@tsparticles/engine": "^3.9.1",
|
||||
"@tsparticles/react": "^3.0.0",
|
||||
"@tsparticles/slim": "^3.9.1",
|
||||
"@upstash/ratelimit": "^2.0.8",
|
||||
"@upstash/redis": "^1.36.4",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"@vercel/og": "^0.10.1",
|
||||
"ai": "^6.0.104",
|
||||
"ai": "^6.0.116",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
||||
Generated
+156
-318
File diff suppressed because it is too large
Load Diff
+1
-5
@@ -20,11 +20,7 @@
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".next/**", "!.next/cache/**"],
|
||||
"passThroughEnv": [
|
||||
"TYPESENSE_ADMIN_API_KEY",
|
||||
"INKEEP_API_KEY",
|
||||
"GITHUB_TOKEN"
|
||||
]
|
||||
"passThroughEnv": ["TYPESENSE_ADMIN_API_KEY", "GITHUB_TOKEN"]
|
||||
},
|
||||
"clean": {},
|
||||
"format": {
|
||||
|
||||
Reference in New Issue
Block a user