diff --git a/llama_stack/ui/app/api/auth/[...nextauth]/route.ts b/llama_stack/ui/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 000000000..7b38c1bb4
--- /dev/null
+++ b/llama_stack/ui/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,6 @@
+import NextAuth from "next-auth";
+import { authOptions } from "@/lib/auth";
+
+const handler = NextAuth(authOptions);
+
+export { handler as GET, handler as POST };
diff --git a/llama_stack/ui/app/auth/signin/page.tsx b/llama_stack/ui/app/auth/signin/page.tsx
new file mode 100644
index 000000000..c9510fd6b
--- /dev/null
+++ b/llama_stack/ui/app/auth/signin/page.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { signIn, signOut, useSession } from "next-auth/react";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Copy, Check, Home, Github } from "lucide-react";
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+
+export default function SignInPage() {
+ const { data: session, status } = useSession();
+ const [copied, setCopied] = useState(false);
+ const router = useRouter();
+
+ const handleCopyToken = async () => {
+ if (session?.accessToken) {
+ await navigator.clipboard.writeText(session.accessToken);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ if (status === "loading") {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ Authentication
+
+ {session
+ ? "You are successfully authenticated!"
+ : "Sign in with GitHub to use your access token as an API key"}
+
+
+
+ {!session ? (
+ {
+ console.log("Signing in with GitHub...");
+ signIn("github", { callbackUrl: "/auth/signin" }).catch(
+ (error) => {
+ console.error("Sign in error:", error);
+ },
+ );
+ }}
+ className="w-full"
+ variant="default"
+ >
+
+ Sign in with GitHub
+
+ ) : (
+
+
+ Signed in as {session.user?.email}
+
+
+ {session.accessToken && (
+
+
+ GitHub Access Token:
+
+
+
+ {session.accessToken}
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+ This GitHub token will be used as your API key for
+ authenticated Llama Stack requests.
+
+
+ )}
+
+
+ router.push("/")} className="flex-1">
+
+ Go to Dashboard
+
+ signOut()}
+ variant="outline"
+ className="flex-1"
+ >
+ Sign out
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/llama_stack/ui/app/layout.tsx b/llama_stack/ui/app/layout.tsx
index ed8a6cd5d..19fb18c36 100644
--- a/llama_stack/ui/app/layout.tsx
+++ b/llama_stack/ui/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/ui/theme-provider";
+import { SessionProvider } from "@/components/providers/session-provider";
import { Geist, Geist_Mono } from "next/font/google";
import { ModeToggle } from "@/components/ui/mode-toggle";
import "./globals.css";
@@ -21,34 +22,38 @@ export const metadata: Metadata = {
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/layout/app-sidebar";
+import { SignInButton } from "@/components/ui/sign-in-button";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
-
-
-
-
- {/* Header with aligned elements */}
-
-
-
+
+
+
+
+
+ {/* Header with aligned elements */}
+
-
-
-
-
-
-
{children}
-
-
-
+
{children}
+
+
+
+
);
diff --git a/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx b/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx
index e6feef363..82aa3496e 100644
--- a/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx
+++ b/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx
@@ -4,11 +4,12 @@ import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { ChatCompletion } from "@/lib/types";
import { ChatCompletionDetailView } from "@/components/chat-completions/chat-completion-detail";
-import { client } from "@/lib/client";
+import { useAuthClient } from "@/hooks/use-auth-client";
export default function ChatCompletionDetailPage() {
const params = useParams();
const id = params.id as string;
+ const client = useAuthClient();
const [completionDetail, setCompletionDetail] =
useState
(null);
@@ -45,7 +46,7 @@ export default function ChatCompletionDetailPage() {
};
fetchCompletionDetail();
- }, [id]);
+ }, [id, client]);
return (
(
null,
@@ -109,7 +110,7 @@ export default function ResponseDetailPage() {
};
fetchResponseDetail();
- }, [id]);
+ }, [id, client]);
return (
({
}),
}));
+// Mock next-auth
+jest.mock("next-auth/react", () => ({
+ useSession: () => ({
+ status: "authenticated",
+ data: { accessToken: "mock-token" },
+ }),
+}));
+
// Mock helper functions
jest.mock("@/lib/truncate-text");
jest.mock("@/lib/format-message-content");
-// Mock the client
-jest.mock("@/lib/client", () => ({
- client: {
- chat: {
- completions: {
- list: jest.fn(),
- },
+// Mock the auth client hook
+const mockClient = {
+ chat: {
+ completions: {
+ list: jest.fn(),
},
},
+};
+
+jest.mock("@/hooks/use-auth-client", () => ({
+ useAuthClient: () => mockClient,
}));
// Mock the usePagination hook
const mockLoadMore = jest.fn();
-jest.mock("@/hooks/usePagination", () => ({
+jest.mock("@/hooks/use-pagination", () => ({
usePagination: jest.fn(() => ({
data: [],
status: "idle",
@@ -47,7 +57,7 @@ import {
} from "@/lib/format-message-content";
// Import the mocked hook
-import { usePagination } from "@/hooks/usePagination";
+import { usePagination } from "@/hooks/use-pagination";
const mockedUsePagination = usePagination as jest.MockedFunction<
typeof usePagination
>;
diff --git a/llama_stack/ui/components/chat-completions/chat-completions-table.tsx b/llama_stack/ui/components/chat-completions/chat-completions-table.tsx
index d5838d877..65f6c71af 100644
--- a/llama_stack/ui/components/chat-completions/chat-completions-table.tsx
+++ b/llama_stack/ui/components/chat-completions/chat-completions-table.tsx
@@ -10,8 +10,7 @@ import {
extractTextFromContentPart,
extractDisplayableText,
} from "@/lib/format-message-content";
-import { usePagination } from "@/hooks/usePagination";
-import { client } from "@/lib/client";
+import { usePagination } from "@/hooks/use-pagination";
interface ChatCompletionsTableProps {
/** Optional pagination configuration */
@@ -32,12 +31,15 @@ function formatChatCompletionToRow(completion: ChatCompletion): LogTableRow {
export function ChatCompletionsTable({
paginationOptions,
}: ChatCompletionsTableProps) {
- const fetchFunction = async (params: {
- after?: string;
- limit: number;
- model?: string;
- order?: string;
- }) => {
+ const fetchFunction = async (
+ client: ReturnType,
+ params: {
+ after?: string;
+ limit: number;
+ model?: string;
+ order?: string;
+ },
+ ) => {
const response = await client.chat.completions.list({
after: params.after,
limit: params.limit,
diff --git a/llama_stack/ui/components/logs/logs-table-scroll.test.tsx b/llama_stack/ui/components/logs/logs-table-scroll.test.tsx
index 0a143cc16..a5c3fde46 100644
--- a/llama_stack/ui/components/logs/logs-table-scroll.test.tsx
+++ b/llama_stack/ui/components/logs/logs-table-scroll.test.tsx
@@ -12,7 +12,7 @@ jest.mock("next/navigation", () => ({
}));
// Mock the useInfiniteScroll hook
-jest.mock("@/hooks/useInfiniteScroll", () => ({
+jest.mock("@/hooks/use-infinite-scroll", () => ({
useInfiniteScroll: jest.fn((onLoadMore, options) => {
const ref = React.useRef(null);
diff --git a/llama_stack/ui/components/logs/logs-table.tsx b/llama_stack/ui/components/logs/logs-table.tsx
index 72924cea8..3d4e609c7 100644
--- a/llama_stack/ui/components/logs/logs-table.tsx
+++ b/llama_stack/ui/components/logs/logs-table.tsx
@@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
import { useRef } from "react";
import { truncateText } from "@/lib/truncate-text";
import { PaginationStatus } from "@/lib/types";
-import { useInfiniteScroll } from "@/hooks/useInfiniteScroll";
+import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
import {
Table,
TableBody,
diff --git a/llama_stack/ui/components/providers/session-provider.tsx b/llama_stack/ui/components/providers/session-provider.tsx
new file mode 100644
index 000000000..e194e5f0a
--- /dev/null
+++ b/llama_stack/ui/components/providers/session-provider.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
+
+export function SessionProvider({ children }: { children: React.ReactNode }) {
+ return {children} ;
+}
diff --git a/llama_stack/ui/components/responses/responses-table.test.tsx b/llama_stack/ui/components/responses/responses-table.test.tsx
index 2286f10d5..0338b9151 100644
--- a/llama_stack/ui/components/responses/responses-table.test.tsx
+++ b/llama_stack/ui/components/responses/responses-table.test.tsx
@@ -12,21 +12,31 @@ jest.mock("next/navigation", () => ({
}),
}));
+// Mock next-auth
+jest.mock("next-auth/react", () => ({
+ useSession: () => ({
+ status: "authenticated",
+ data: { accessToken: "mock-token" },
+ }),
+}));
+
// Mock helper functions
jest.mock("@/lib/truncate-text");
-// Mock the client
-jest.mock("@/lib/client", () => ({
- client: {
- responses: {
- list: jest.fn(),
- },
+// Mock the auth client hook
+const mockClient = {
+ responses: {
+ list: jest.fn(),
},
+};
+
+jest.mock("@/hooks/use-auth-client", () => ({
+ useAuthClient: () => mockClient,
}));
// Mock the usePagination hook
const mockLoadMore = jest.fn();
-jest.mock("@/hooks/usePagination", () => ({
+jest.mock("@/hooks/use-pagination", () => ({
usePagination: jest.fn(() => ({
data: [],
status: "idle",
@@ -40,7 +50,7 @@ jest.mock("@/hooks/usePagination", () => ({
import { truncateText as originalTruncateText } from "@/lib/truncate-text";
// Import the mocked hook
-import { usePagination } from "@/hooks/usePagination";
+import { usePagination } from "@/hooks/use-pagination";
const mockedUsePagination = usePagination as jest.MockedFunction<
typeof usePagination
>;
diff --git a/llama_stack/ui/components/responses/responses-table.tsx b/llama_stack/ui/components/responses/responses-table.tsx
index 116a2adbe..a3e8c0c15 100644
--- a/llama_stack/ui/components/responses/responses-table.tsx
+++ b/llama_stack/ui/components/responses/responses-table.tsx
@@ -6,8 +6,7 @@ import {
UsePaginationOptions,
} from "@/lib/types";
import { LogsTable, LogTableRow } from "@/components/logs/logs-table";
-import { usePagination } from "@/hooks/usePagination";
-import { client } from "@/lib/client";
+import { usePagination } from "@/hooks/use-pagination";
import type { ResponseListResponse } from "llama-stack-client/resources/responses/responses";
import {
isMessageInput,
@@ -125,12 +124,15 @@ function formatResponseToRow(response: OpenAIResponse): LogTableRow {
}
export function ResponsesTable({ paginationOptions }: ResponsesTableProps) {
- const fetchFunction = async (params: {
- after?: string;
- limit: number;
- model?: string;
- order?: string;
- }) => {
+ const fetchFunction = async (
+ client: ReturnType,
+ params: {
+ after?: string;
+ limit: number;
+ model?: string;
+ order?: string;
+ },
+ ) => {
const response = await client.responses.list({
after: params.after,
limit: params.limit,
diff --git a/llama_stack/ui/components/ui/sign-in-button.tsx b/llama_stack/ui/components/ui/sign-in-button.tsx
new file mode 100644
index 000000000..caa15772f
--- /dev/null
+++ b/llama_stack/ui/components/ui/sign-in-button.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { User } from "lucide-react";
+import Link from "next/link";
+import { useSession } from "next-auth/react";
+import { Button } from "./button";
+
+export function SignInButton() {
+ const { data: session, status } = useSession();
+
+ return (
+
+
+
+
+ {status === "loading"
+ ? "Loading..."
+ : session
+ ? session.user?.email || "Signed In"
+ : "Sign In"}
+
+
+
+ );
+}
diff --git a/llama_stack/ui/hooks/use-auth-client.ts b/llama_stack/ui/hooks/use-auth-client.ts
new file mode 100644
index 000000000..94ae769f4
--- /dev/null
+++ b/llama_stack/ui/hooks/use-auth-client.ts
@@ -0,0 +1,24 @@
+import { useSession } from "next-auth/react";
+import { useMemo } from "react";
+import LlamaStackClient from "llama-stack-client";
+
+export function useAuthClient() {
+ const { data: session } = useSession();
+
+ const client = useMemo(() => {
+ const clientHostname =
+ typeof window !== "undefined" ? window.location.origin : "";
+
+ const options: any = {
+ baseURL: `${clientHostname}/api`,
+ };
+
+ if (session?.accessToken) {
+ options.apiKey = session.accessToken;
+ }
+
+ return new LlamaStackClient(options);
+ }, [session?.accessToken]);
+
+ return client;
+}
diff --git a/llama_stack/ui/hooks/useInfiniteScroll.ts b/llama_stack/ui/hooks/use-infinite-scroll.ts
similarity index 100%
rename from llama_stack/ui/hooks/useInfiniteScroll.ts
rename to llama_stack/ui/hooks/use-infinite-scroll.ts
diff --git a/llama_stack/ui/hooks/usePagination.ts b/llama_stack/ui/hooks/use-pagination.ts
similarity index 65%
rename from llama_stack/ui/hooks/usePagination.ts
rename to llama_stack/ui/hooks/use-pagination.ts
index 55a8d4ac8..58847ece5 100644
--- a/llama_stack/ui/hooks/usePagination.ts
+++ b/llama_stack/ui/hooks/use-pagination.ts
@@ -2,6 +2,9 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { PaginationStatus, UsePaginationOptions } from "@/lib/types";
+import { useSession } from "next-auth/react";
+import { useAuthClient } from "@/hooks/use-auth-client";
+import { useRouter } from "next/navigation";
interface PaginationState {
data: T[];
@@ -28,13 +31,18 @@ export interface PaginationReturn {
}
interface UsePaginationParams extends UsePaginationOptions {
- fetchFunction: (params: {
- after?: string;
- limit: number;
- model?: string;
- order?: string;
- }) => Promise>;
+ fetchFunction: (
+ client: ReturnType,
+ params: {
+ after?: string;
+ limit: number;
+ model?: string;
+ order?: string;
+ },
+ ) => Promise>;
errorMessagePrefix: string;
+ enabled?: boolean;
+ useAuth?: boolean;
}
export function usePagination({
@@ -43,7 +51,12 @@ export function usePagination({
order = "desc",
fetchFunction,
errorMessagePrefix,
+ enabled = true,
+ useAuth = true,
}: UsePaginationParams): PaginationReturn {
+ const { status: sessionStatus } = useSession();
+ const client = useAuthClient();
+ const router = useRouter();
const [state, setState] = useState>({
data: [],
status: "loading",
@@ -74,7 +87,7 @@ export function usePagination({
error: null,
}));
- const response = await fetchFunction({
+ const response = await fetchFunction(client, {
after: after || undefined,
limit: fetchLimit,
...(model && { model }),
@@ -91,6 +104,17 @@ export function usePagination({
status: "idle",
}));
} catch (err) {
+ // Check if it's a 401 unauthorized error
+ if (
+ err &&
+ typeof err === "object" &&
+ "status" in err &&
+ err.status === 401
+ ) {
+ router.push("/auth/signin");
+ return;
+ }
+
const errorMessage = isInitialLoad
? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.`
: `Failed to load more ${errorMessagePrefix}. Please try again.`;
@@ -107,7 +131,7 @@ export function usePagination({
}));
}
},
- [limit, model, order, fetchFunction, errorMessagePrefix],
+ [limit, model, order, fetchFunction, errorMessagePrefix, client, router],
);
/**
@@ -120,17 +144,28 @@ export function usePagination({
}
}, [fetchData]);
- // Auto-load initial data on mount
+ // Auto-load initial data on mount when enabled
useEffect(() => {
- if (!hasFetchedInitialData.current) {
+ // If using auth, wait for session to load
+ const isAuthReady = !useAuth || sessionStatus !== "loading";
+ const shouldFetch = enabled && isAuthReady;
+
+ if (shouldFetch && !hasFetchedInitialData.current) {
hasFetchedInitialData.current = true;
fetchData();
+ } else if (!shouldFetch) {
+ // Reset the flag when disabled so it can fetch when re-enabled
+ hasFetchedInitialData.current = false;
}
- }, [fetchData]);
+ }, [fetchData, enabled, useAuth, sessionStatus]);
+
+ // Override status if we're waiting for auth
+ const effectiveStatus =
+ useAuth && sessionStatus === "loading" ? "loading" : state.status;
return {
data: state.data,
- status: state.status,
+ status: effectiveStatus,
hasMore: state.hasMore,
error: state.error,
loadMore,
diff --git a/llama_stack/ui/instrumentation.ts b/llama_stack/ui/instrumentation.ts
new file mode 100644
index 000000000..903528443
--- /dev/null
+++ b/llama_stack/ui/instrumentation.ts
@@ -0,0 +1,11 @@
+/**
+ * Next.js Instrumentation
+ * This file is used for initializing monitoring, tracing, or other observability tools.
+ * It runs once when the server starts, before any application code.
+ *
+ * Learn more: https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
+ */
+
+export async function register() {
+ await import("./lib/config-validator");
+}
diff --git a/llama_stack/ui/lib/auth.ts b/llama_stack/ui/lib/auth.ts
new file mode 100644
index 000000000..bc18b168b
--- /dev/null
+++ b/llama_stack/ui/lib/auth.ts
@@ -0,0 +1,38 @@
+import { NextAuthOptions } from "next-auth";
+import GithubProvider from "next-auth/providers/github";
+
+export const authOptions: NextAuthOptions = {
+ providers: [
+ GithubProvider({
+ clientId: process.env.GITHUB_CLIENT_ID!,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
+ authorization: {
+ params: {
+ scope: "read:user user:email",
+ },
+ },
+ }),
+ ],
+ debug: process.env.NODE_ENV === "development",
+ callbacks: {
+ async jwt({ token, account }) {
+ // Persist the OAuth access_token to the token right after signin
+ if (account) {
+ token.accessToken = account.access_token;
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ // Send properties to the client, like an access_token from a provider.
+ session.accessToken = token.accessToken as string;
+ return session;
+ },
+ },
+ pages: {
+ signIn: "/auth/signin",
+ error: "/auth/signin", // Redirect errors to our custom page
+ },
+ session: {
+ strategy: "jwt",
+ },
+};
diff --git a/llama_stack/ui/lib/client.ts b/llama_stack/ui/lib/client.ts
deleted file mode 100644
index 8492496e2..000000000
--- a/llama_stack/ui/lib/client.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import LlamaStackClient from "llama-stack-client";
-
-export const client = new LlamaStackClient({
- baseURL:
- typeof window !== "undefined" ? `${window.location.origin}/api` : "/api",
-});
diff --git a/llama_stack/ui/lib/config-validator.ts b/llama_stack/ui/lib/config-validator.ts
new file mode 100644
index 000000000..19f4397b8
--- /dev/null
+++ b/llama_stack/ui/lib/config-validator.ts
@@ -0,0 +1,56 @@
+/**
+ * Validates environment configuration for the application
+ * This is called during server initialization
+ */
+export function validateServerConfig() {
+ if (process.env.NODE_ENV === "development") {
+ console.log("š Starting Llama Stack UI Server...");
+
+ // Check optional configurations
+ const optionalConfigs = {
+ NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:8322",
+ LLAMA_STACK_BACKEND_URL:
+ process.env.LLAMA_STACK_BACKEND_URL || "http://localhost:8321",
+ LLAMA_STACK_UI_PORT: process.env.LLAMA_STACK_UI_PORT || "8322",
+ GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
+ GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
+ };
+
+ console.log("\nš Configuration:");
+ console.log(` - NextAuth URL: ${optionalConfigs.NEXTAUTH_URL}`);
+ console.log(` - Backend URL: ${optionalConfigs.LLAMA_STACK_BACKEND_URL}`);
+ console.log(` - UI Port: ${optionalConfigs.LLAMA_STACK_UI_PORT}`);
+
+ // Check GitHub OAuth configuration
+ if (
+ !optionalConfigs.GITHUB_CLIENT_ID ||
+ !optionalConfigs.GITHUB_CLIENT_SECRET
+ ) {
+ console.log(
+ "\nš GitHub OAuth not configured (authentication features disabled)",
+ );
+ console.log(" To enable GitHub OAuth:");
+ console.log(" 1. Go to https://github.com/settings/applications/new");
+ console.log(
+ " 2. Set Application name: Llama Stack UI (or your preferred name)",
+ );
+ console.log(" 3. Set Homepage URL: http://localhost:8322");
+ console.log(
+ " 4. Set Authorization callback URL: http://localhost:8322/api/auth/callback/github",
+ );
+ console.log(
+ " 5. Create the app and copy the Client ID and Client Secret",
+ );
+ console.log(" 6. Add them to your .env.local file:");
+ console.log(" GITHUB_CLIENT_ID=your_client_id");
+ console.log(" GITHUB_CLIENT_SECRET=your_client_secret");
+ } else {
+ console.log(" - GitHub OAuth: ā
Configured");
+ }
+
+ console.log("");
+ }
+}
+
+// Call this function when the module is imported
+validateServerConfig();
diff --git a/llama_stack/ui/package-lock.json b/llama_stack/ui/package-lock.json
index 4c4620ac2..8fd5fb56c 100644
--- a/llama_stack/ui/package-lock.json
+++ b/llama_stack/ui/package-lock.json
@@ -18,6 +18,7 @@
"llama-stack-client": "0.2.13",
"lucide-react": "^0.510.0",
"next": "15.3.3",
+ "next-auth": "^4.24.11",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -548,7 +549,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -2423,6 +2423,15 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@panva/hkdf": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
+ "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/@pkgr/core": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz",
@@ -5279,7 +5288,6 @@
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -9036,6 +9044,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "4.15.9",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -9949,6 +9966,38 @@
}
}
},
+ "node_modules/next-auth": {
+ "version": "4.24.11",
+ "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz",
+ "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==",
+ "license": "ISC",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@panva/hkdf": "^1.0.2",
+ "cookie": "^0.7.0",
+ "jose": "^4.15.5",
+ "oauth": "^0.9.15",
+ "openid-client": "^5.4.0",
+ "preact": "^10.6.3",
+ "preact-render-to-string": "^5.1.19",
+ "uuid": "^8.3.2"
+ },
+ "peerDependencies": {
+ "@auth/core": "0.34.2",
+ "next": "^12.2.5 || ^13 || ^14 || ^15",
+ "nodemailer": "^6.6.5",
+ "react": "^17.0.2 || ^18 || ^19",
+ "react-dom": "^17.0.2 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@auth/core": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
@@ -10071,6 +10120,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/oauth": {
+ "version": "0.9.15",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
+ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
+ "license": "MIT"
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -10081,6 +10136,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -10194,6 +10258,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/oidc-token-hash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
+ "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -10233,6 +10306,39 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/openid-client": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
+ "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
+ "license": "MIT",
+ "dependencies": {
+ "jose": "^4.15.9",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/openid-client/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/openid-client/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -10560,6 +10666,34 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/preact": {
+ "version": "10.26.9",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz",
+ "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/preact-render-to-string": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
+ "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
+ "license": "MIT",
+ "dependencies": {
+ "pretty-format": "^3.8.0"
+ },
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
+ "node_modules/preact-render-to-string/node_modules/pretty-format": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
+ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
+ "license": "MIT"
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -12409,6 +12543,15 @@
}
}
},
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
diff --git a/llama_stack/ui/package.json b/llama_stack/ui/package.json
index 43a5c2ac1..9524ce0a5 100644
--- a/llama_stack/ui/package.json
+++ b/llama_stack/ui/package.json
@@ -23,6 +23,7 @@
"llama-stack-client": "0.2.13",
"lucide-react": "^0.510.0",
"next": "15.3.3",
+ "next-auth": "^4.24.11",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/llama_stack/ui/types/next-auth.d.ts b/llama_stack/ui/types/next-auth.d.ts
new file mode 100644
index 000000000..4ed718132
--- /dev/null
+++ b/llama_stack/ui/types/next-auth.d.ts
@@ -0,0 +1,7 @@
+import NextAuth from "next-auth";
+
+declare module "next-auth" {
+ interface Session {
+ accessToken?: string;
+ }
+}