refactor(chat): migrate to new AI SDK packages

- Add @ai-sdk/mcp and @ai-sdk/react dependencies
- Switch from @ai-sdk/openai to @openrouter/ai-sdk-provider
- Use useChat hook instead of custom useChatSubmission
- Update completion API with convertToModelMessages and stepCountIs
- Simplify stream handling using toUIMessageStreamResponse
- Improve cookie handling in CookieAwareTransport with getSetCookie support
This commit is contained in:
Junaid
2026-02-09 08:44:05 +05:00
parent 447d700f54
commit d9ec9cbf38
5 changed files with 484 additions and 398 deletions
+79 -134
View File
@@ -1,5 +1,6 @@
import { streamText, experimental_createMCPClient } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { convertToModelMessages, streamText, stepCountIs } from 'ai';
import { createMCPClient } from '@ai-sdk/mcp';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { getBaseUrl } from '@/features/dashboard/lib/getBaseUrl';
import { StreamableHTTPClientTransport, StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
@@ -10,137 +11,107 @@ class CookieAwareTransport extends StreamableHTTPClientTransport {
constructor(url: URL, opts?: StreamableHTTPClientTransportOptions, cookies?: string) {
super(url, opts);
this.originalFetch = global.fetch;
// Set initial cookies if provided
if (cookies) {
this.cookies = [cookies];
}
// Override global fetch to include cookies
global.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
init = init || {};
const headers = new Headers(init.headers);
if (this.cookies.length > 0) {
headers.set('Cookie', this.cookies.join('; '));
}
init.headers = headers;
const response = await this.originalFetch(input, init);
// Store any new cookies from response
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
const newCookies = setCookieHeader.split(',').map(cookie => cookie.trim());
this.cookies = [...this.cookies, ...newCookies];
if (typeof response.headers.getSetCookie === 'function') {
const setCookies = response.headers.getSetCookie();
if (setCookies.length > 0) {
this.cookies = [...this.cookies, ...setCookies];
}
} else {
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
this.cookies = [...this.cookies, setCookieHeader];
}
}
return response;
};
}
async close(): Promise<void> {
// Restore original fetch
global.fetch = this.originalFetch;
this.cookies = [];
await super.close();
}
}
// OpenRouter configuration - will be set from request body
export async function POST(req: Request) {
let mcpClient: any = null;
let dataHasChanged = false;
try {
const body = await req.json();
let messages = body.messages || [];
const prompt = body.prompt || body.messages?.[body.messages.length - 1]?.content || '';
// Trim messages if conversation is too long (keep system context by preserving recent messages)
const MAX_MESSAGES = 20; // Keep last 20 messages for context
const MAX_MESSAGES = 20;
if (messages.length > MAX_MESSAGES) {
messages = messages.slice(-MAX_MESSAGES);
}
// Require API key to be provided in request
if (!body.useLocalKeys || !body.apiKey) {
return new Response(JSON.stringify({
return new Response(JSON.stringify({
error: 'API key is required',
details: 'API key must be provided in request body'
}), {
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const openrouterConfig = {
apiKey: body.apiKey,
baseURL: 'https://openrouter.ai/api/v1',
};
const apiKey = body.apiKey;
const baseURL = 'https://openrouter.ai/api/v1';
// Get dynamic base URL
const baseUrl = await getBaseUrl();
const mcpEndpoint = `${baseUrl}/api/mcp-transport/http`;
const cookie = req.headers.get('cookie') || '';
// Create MCP client
const transport = new CookieAwareTransport(
new URL(mcpEndpoint),
{},
cookie
);
mcpClient = await experimental_createMCPClient({
transport,
});
const aiTools = await mcpClient.tools();
// Create OpenRouter client with current configuration
const openrouter = createOpenAI(openrouterConfig);
// Require model to be provided in request
if (!body.model) {
return new Response(JSON.stringify({
return new Response(JSON.stringify({
error: 'Model is required',
details: 'Model must be provided in request body'
}), {
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const model = body.model;
const maxTokens = body.maxTokens ? parseInt(body.maxTokens) : undefined;
// Debug logging
console.log('Starting completion request:', {
model,
maxTokens,
hasApiKey: !!openrouterConfig.apiKey,
apiKeyPrefix: openrouterConfig.apiKey?.substring(0, 10) + '...'
hasApiKey: !!apiKey,
apiKeyPrefix: apiKey?.substring(0, 10) + '...'
});
// Test the API key with a simple request first to catch auth errors early
try {
const testResponse = await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Authorization': `Bearer ${openrouterConfig.apiKey}`,
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
});
if (!testResponse.ok) {
const errorText = await testResponse.text();
console.log('API key validation failed:', errorText);
let errorMessage = 'Invalid API key';
try {
const errorJson = JSON.parse(errorText);
@@ -148,26 +119,34 @@ export async function POST(req: Request) {
} catch {
// Failed to parse error, use default message
}
return new Response(JSON.stringify({
return new Response(JSON.stringify({
error: 'Authentication Error',
details: errorMessage
}), {
}), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
} catch (validationError) {
console.error('API key validation error:', validationError);
return new Response(JSON.stringify({
return new Response(JSON.stringify({
error: 'Authentication Error',
details: 'Failed to validate API key'
}), {
}), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const baseUrl = await getBaseUrl();
const mcpEndpoint = `${baseUrl}/api/mcp-transport/http`;
const cookie = req.headers.get('cookie') || '';
const transport = new CookieAwareTransport(new URL(mcpEndpoint), {}, cookie);
mcpClient = await createMCPClient({ transport });
const aiTools = await mcpClient.tools();
const systemInstructions = `You're an expert at converting natural language to GraphQL queries for our KeystoneJS API.
YOUR EXPERTISE:
@@ -247,14 +226,19 @@ EXAMPLES:
Always complete the full workflow and return actual data, not just schema discovery. The system works with any model type dynamically.`;
const openrouter = createOpenRouter({ apiKey, baseURL });
const inputMessages = messages.length > 0
? messages
: [{ id: `prompt-${Date.now()}`, role: 'user', parts: [{ type: 'text', text: prompt }] }];
const streamTextConfig: any = {
model: openrouter(model),
tools: aiTools,
messages: messages.length > 0 ? messages : [{ role: 'user', content: prompt }],
messages: await convertToModelMessages(inputMessages),
system: systemInstructions,
maxSteps: 10,
onStepFinish: async (step: { toolCalls?: any[]; toolResults?: any[]; finishReason?: string; usage?: any; text?: string; }) => {
// Track if any CRUD operations were called
stopWhen: stepCountIs(10),
onStepFinish: async (step: { toolCalls?: any[] }) => {
if (step.toolCalls && step.toolCalls.length > 0) {
for (const toolCall of step.toolCalls) {
if (['createData', 'updateData', 'deleteData'].includes(toolCall.toolName)) {
@@ -265,13 +249,8 @@ Always complete the full workflow and return actual data, not just schema discov
}
}
},
onFinish: async (result: { text: string; finishReason: string; usage: any; response: any }) => {
console.log('Completion finished successfully');
// Send data change notification through the stream
if (dataHasChanged) {
console.log('Sending data change notification');
// We'll append this as a special message at the end
}
onFinish: async () => {
console.log('Completion finished successfully', { dataHasChanged });
await mcpClient.close();
},
onError: async (error: unknown) => {
@@ -279,70 +258,36 @@ Always complete the full workflow and return actual data, not just schema discov
await mcpClient.close();
},
};
// Add maxTokens only if specified
if (maxTokens) {
streamTextConfig.maxTokens = maxTokens;
streamTextConfig.maxOutputTokens = maxTokens;
}
const response = streamText(streamTextConfig);
// Create a custom stream that includes our data change notification
const stream = response.toDataStream();
const reader = stream.getReader();
return new Response(
new ReadableStream({
async start(controller) {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// Before ending the stream, send data change notification if needed
if (dataHasChanged) {
console.log('Sending data change notification through stream');
const dataChangeMessage = `9:{"dataHasChanged":true}\n`;
controller.enqueue(new TextEncoder().encode(dataChangeMessage));
}
controller.close();
break;
}
controller.enqueue(value);
}
} catch (error) {
controller.error(error);
}
}
}),
{
headers: {
'Content-Type': 'text/plain; charset=utf-8',
}
}
);
const result = streamText(streamTextConfig);
return result.toUIMessageStreamResponse({
originalMessages: inputMessages,
onError: (error) => error instanceof Error ? error.message : String(error),
});
} catch (error) {
// Clean up MCP client if it was created
if (mcpClient) {
try {
await mcpClient.close();
} catch (closeError) {}
} catch {}
}
// Log the full error for debugging
console.error('Completion API Error:', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
details: error
});
return new Response(JSON.stringify({
return new Response(JSON.stringify({
error: 'Internal Server Error',
details: error instanceof Error ? error.message : String(error)
}), {
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
}
@@ -1,6 +1,9 @@
"use client";
import { useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useChat, type UIMessage } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkBreaks from "remark-breaks";
@@ -10,36 +13,17 @@ import {
ChatContainerScrollAnchor,
} from "./chat-container";
import { ScrollButton } from "./scroll-button";
import {
ArrowUp,
X,
} from "lucide-react";
import { ArrowUp, X } from "lucide-react";
// UI Components
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ModeSplitButton } from "./mode-split-button";
import { useSidebarWithSide } from "@/components/ui/sidebar";
import { useChatMode } from "../DashboardLayout";
import { useAiConfig } from "../../hooks/use-ai-config";
import { useChatSubmission } from "../../hooks/use-chat-submission";
import { ChatEmptyState } from "./chat-empty-state";
import { getSharedKeys } from "../../actions/ai-chat";
// Chat mode types
type ChatMode = "sidebar" | "chatbox";
// Types
interface Message {
id: string;
content: string;
isUser: boolean;
timestamp: Date;
}
// Compact Chat Message for Sidebar
function ChatMessage({
isUser,
children,
@@ -48,22 +32,7 @@ function ChatMessage({
children: React.ReactNode;
}) {
return (
<div
className={`text-base flex items-center gap-2 ${
isUser ? "justify-end" : ""
}`}
>
{/* {isUser ? (
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-rose-500 to-indigo-600 shadow-sm order-1 flex-shrink-0" />
) : (
<img
className="rounded-full border border-black/[0.08] shadow-sm flex-shrink-0"
src="https://raw.githubusercontent.com/origin-space/origin-images/refs/heads/main/exp2/user-01_i5l7tp.png"
alt="AI"
width={24}
height={24}
/>
)} */}
<div className={`text-base flex items-center gap-2 ${isUser ? "justify-end" : ""}`}>
<div
className={cn(
"max-w-[calc(100%-2rem)] break-words overflow-hidden",
@@ -78,27 +47,103 @@ function ChatMessage({
);
}
// This component is no longer used - replaced with ChatUnactivatedState
const CRUD_TOOLS = new Set(["createData", "updateData", "deleteData"]);
function getMessageText(message: UIMessage) {
return message.parts
?.filter((part: any) => part.type === "text")
.map((part: any) => part.text)
.join("")
.trim();
}
function responseHasCrudTool(messages: UIMessage[]) {
return messages.some((message: any) =>
message.role === "assistant" &&
message.parts?.some((part: any) => {
if (part.type === "dynamic-tool") {
return CRUD_TOOLS.has(part.toolName);
}
if (typeof part.type === "string" && part.type.startsWith("tool-")) {
return CRUD_TOOLS.has(part.type.slice(5));
}
if (part.type === "tool-invocation" && part.toolInvocation?.toolName) {
return CRUD_TOOLS.has(part.toolInvocation.toolName);
}
return false;
})
);
}
// Main Sidebar Chat Component
export function AiChatSidebar() {
const { toggleSidebar } = useSidebarWithSide("right");
const { messages, setMessages, loading, setLoading, sending, setSending, user } = useChatMode();
const { user } = useChatMode();
const { config: aiConfig } = useAiConfig();
const queryClient = useQueryClient();
const [input, setInput] = useState("");
const { config: aiConfig, setConfig: setAiConfig } = useAiConfig();
const { handleSubmit: submitChat } = useChatSubmission({
messages,
setMessages,
setLoading,
setSending,
const { messages, sendMessage, status, error } = useChat({
transport: new DefaultChatTransport({
api: "/api/completion",
credentials: "include",
}),
onFinish: ({ messages }) => {
if (responseHasCrudTool(messages as UIMessage[])) {
queryClient.invalidateQueries();
}
},
});
const isLoading = status === "submitted" || status === "streaming";
const handleSubmit = async () => {
if (!input.trim()) return;
const currentInput = input;
const text = input.trim();
if (!text || isLoading) return;
setInput("");
await submitChat(currentInput);
try {
if (aiConfig.keyMode === "local") {
if (!aiConfig.localKeys?.apiKey || !aiConfig.localKeys?.model) {
throw new Error("Local API key and model are required. Please configure them in settings.");
}
await sendMessage(
{ text },
{
body: {
useLocalKeys: true,
apiKey: aiConfig.localKeys.apiKey,
model: aiConfig.localKeys.model,
maxTokens: parseInt(aiConfig.localKeys.maxTokens || "4000", 10),
},
}
);
return;
}
const keysResult = await getSharedKeys();
if (!keysResult.success || !keysResult.keys) {
throw new Error(keysResult.error || "Shared API keys are not configured.");
}
await sendMessage(
{ text },
{
body: {
useLocalKeys: true,
apiKey: keysResult.keys.apiKey,
model: keysResult.keys.model,
maxTokens: keysResult.keys.maxTokens,
},
}
);
} catch (submitError) {
setInput(text);
console.error("Chat submit error:", submitError);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -108,131 +153,100 @@ export function AiChatSidebar() {
}
};
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex h-16 shrink-0 items-center justify-between px-4 border-b">
<h3 className="font-medium text-muted-foreground">AI Assistant</h3>
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="h-8 w-8"
>
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="h-8 w-8">
<X className="h-4 w-4" />
</Button>
</div>
{/* Messages */}
<ChatContainerRoot className="flex-1 pt-3 px-3 relative">
<ChatContainerContent className="space-y-3">
{messages.length === 0 ? (
<ChatEmptyState userName={user?.name} />
) : (
messages.map((message) => (
<ChatMessage key={message.id} isUser={message.isUser}>
{message.isUser ? (
<p className="whitespace-pre-wrap break-words">
{message.content}
</p>
<ChatContainerContent className="space-y-3">
{messages.length === 0 ? (
<ChatEmptyState userName={user?.name} />
) : (
messages.map((message) => {
const text = getMessageText(message as UIMessage);
const isUser = message.role === "user";
return (
<ChatMessage key={message.id} isUser={isUser}>
{isUser ? (
<p className="whitespace-pre-wrap break-words">{text}</p>
) : text ? (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={{
p: ({ children }) => <div className="mb-1 last:mb-0 break-words">{children}</div>,
ul: ({ children }) => <ul className="mb-1 last:mb-0 pl-2">{children}</ul>,
ol: ({ children }) => <ol className="mb-1 last:mb-0 pl-2">{children}</ol>,
li: ({ children }) => <li className="mb-0.5">{children}</li>,
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
code: ({ children, ...props }) =>
(props as any).inline ? (
<code className="bg-muted px-1 rounded font-mono break-all">{children}</code>
) : (
<pre className="bg-muted border rounded p-2 overflow-x-auto">
<code className="font-mono break-all">{children}</code>
</pre>
),
pre: ({ children }) => <div className="mb-1 last:mb-0">{children}</div>,
}}
>
{text}
</ReactMarkdown>
) : (
<>
{message.content ? (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={{
p: ({ children }) => (
<div className="mb-1 last:mb-0 break-words">
{children}
</div>
),
ul: ({ children }) => (
<ul className="mb-1 last:mb-0 pl-2">{children}</ul>
),
ol: ({ children }) => (
<ol className="mb-1 last:mb-0 pl-2">{children}</ol>
),
li: ({ children }) => (
<li className="mb-0.5">{children}</li>
),
strong: ({ children }) => (
<strong className="font-semibold">{children}</strong>
),
code: ({ children, ...props }) => {
if ((props as any).inline) {
return (
<code className="bg-muted px-1 rounded font-mono break-all">
{children}
</code>
);
}
return (
<pre className="bg-muted border rounded p-2 overflow-x-auto">
<code className="font-mono break-all">
{children}
</code>
</pre>
);
},
pre: ({ children }) => (
<div className="mb-1 last:mb-0">{children}</div>
),
}}
>
{message.content}
</ReactMarkdown>
) : (
<div className="flex items-center gap-1 text-muted-foreground">
<span className="animate-pulse">Thinking...</span>
</div>
)}
</>
<div className="flex items-center gap-1 text-muted-foreground">
<span className="animate-pulse">Thinking...</span>
</div>
)}
</ChatMessage>
))
)}
<ChatContainerScrollAnchor />
</ChatContainerContent>
{/* PromptKit Scroll Button */}
{messages.length > 0 && (
<div className="absolute bottom-4 right-4">
<ScrollButton />
</div>
);
})
)}
</ChatContainerRoot>
{/* Input Area */}
<div className="shadow bg-background border border-transparent ring-1 ring-foreground/10 mx-3 mb-3 space-y-3 rounded-lg p-3">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask me anything..."
className="w-full text-base bg-transparent border-0 resize-none focus:outline-none placeholder:text-muted-foreground min-h-[40px] break-words"
disabled={sending || loading}
rows={1}
/>
{error && (
<div className="text-sm text-destructive px-1">Error: {error.message}</div>
)}
<div className="flex justify-between">
<div className="flex gap-2">
<ModeSplitButton
disabled={sending || loading}
/>
</div>
<ChatContainerScrollAnchor />
</ChatContainerContent>
<Button
size="icon"
className="size-8 rounded-2xl bg-foreground text-background hover:bg-foreground/90"
onClick={handleSubmit}
disabled={sending || loading || !input.trim()}
>
<ArrowUp strokeWidth={3} />
</Button>
{messages.length > 0 && (
<div className="absolute bottom-4 right-4">
<ScrollButton />
</div>
)}
</ChatContainerRoot>
<div className="shadow bg-background border border-transparent ring-1 ring-foreground/10 mx-3 mb-3 space-y-3 rounded-lg p-3">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask me anything..."
className="w-full text-base bg-transparent border-0 resize-none focus:outline-none placeholder:text-muted-foreground min-h-[40px] break-words"
disabled={isLoading}
rows={1}
/>
<div className="flex justify-between">
<div className="flex gap-2">
<ModeSplitButton disabled={isLoading} />
</div>
<Button
size="icon"
className="size-8 rounded-2xl bg-foreground text-background hover:bg-foreground/90"
onClick={handleSubmit}
disabled={isLoading || !input.trim()}
>
<ArrowUp strokeWidth={3} />
</Button>
</div>
</div>
</div>
);
}
@@ -36,10 +36,11 @@ export function AIModelSelector({ disabled = false }: AIModelSelectorProps) {
const { config, setConfig } = useAiConfig();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [inputValue, setInputValue] = useState("");
const currentModel =
config.keyMode === "local"
? config.localKeys?.model || "openai/gpt-4o-mini"
: process.env.NEXT_PUBLIC_OPENROUTER_MODEL || "openai/gpt-4o-mini";
: null;
const handleModelChange = (modelSlug: string) => {
if (config.keyMode === "local") {
@@ -74,8 +75,13 @@ export function AIModelSelector({ disabled = false }: AIModelSelectorProps) {
setIsDropdownOpen(false);
};
const selectedModel = POPULAR_MODELS.find((m) => m.slug === currentModel);
const displayText = selectedModel?.name || currentModel;
const selectedModel = currentModel
? POPULAR_MODELS.find((m) => m.slug === currentModel)
: null;
const displayText =
config.keyMode === "env"
? "Custom"
: selectedModel?.name || "Custom";
const selector = (
<Popover open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
+222 -103
View File
@@ -8,7 +8,8 @@
"name": "openship",
"version": "3.0.0",
"dependencies": {
"@ai-sdk/openai": "^1.3.23",
"@ai-sdk/mcp": "^1.0.13",
"@ai-sdk/react": "^3.0.51",
"@graphql-tools/schema": "^10.0.23",
"@hapi/iron": "^7.0.1",
"@keystone-6/auth": "^8.1.0",
@@ -16,6 +17,7 @@
"@keystone-6/document-renderer": "^1.1.2",
"@keystone-6/fields-document": "^9.1.1",
"@modelcontextprotocol/sdk": "^1.17.1",
"@openrouter/ai-sdk-provider": "^2.0.2",
"@prisma/client": "6.5.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
@@ -51,7 +53,7 @@
"@tanstack/react-table": "^8.21.2",
"@types/nodemailer": "^6.4.17",
"@types/sortablejs": "^1.15.8",
"ai": "^4.3.17",
"ai": "^6.0.49",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -124,26 +126,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/@ai-sdk/openai": {
"version": "1.3.24",
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.24.tgz",
"integrity": "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==",
"node_modules/@ai-sdk/gateway": {
"version": "3.0.39",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.39.tgz",
"integrity": "sha512-SeCZBAdDNbWpVUXiYgOAqis22p5MEYfrjRw0hiBa5hM+7sDGYQpMinUjkM8kbPXMkY+AhKLrHleBl+SuqpzlgA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"@ai-sdk/provider-utils": "2.2.8"
"@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.14",
"@vercel/oidc": "3.1.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.0.0"
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/provider": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz",
"integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==",
"node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
"integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
@@ -152,31 +155,77 @@
"node": ">=18"
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz",
"integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==",
"node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.14.tgz",
"integrity": "sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"nanoid": "^3.3.8",
"secure-json-parse": "^2.7.0"
"@ai-sdk/provider": "3.0.8",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.23.8"
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/mcp": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-1.0.19.tgz",
"integrity": "sha512-RJ5I9IU0MNOFMJXTNh+pjGM8EeUNxT5FZHADoW/x2HhkMvZzpbx6ZKXvLzJ6/+uJBe50PHj4ZNxObeihH0JdoA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.14",
"pkce-challenge": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/mcp/node_modules/@ai-sdk/provider": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
"integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/mcp/node_modules/@ai-sdk/provider-utils": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.14.tgz",
"integrity": "sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/react": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz",
"integrity": "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==",
"version": "3.0.79",
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.79.tgz",
"integrity": "sha512-s/Y+/sISlsEX5Zo/by0jwOyA6vnQ7+CldpRYGv5hMmgnarZ1m5B6myw3Y1Bc2xnozUy+wrmwA6HttlmR4xOOEg==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider-utils": "2.2.8",
"@ai-sdk/ui-utils": "1.2.11",
"@ai-sdk/provider-utils": "4.0.14",
"ai": "6.0.77",
"swr": "^2.2.5",
"throttleit": "2.1.0"
},
@@ -184,30 +233,36 @@
"node": ">=18"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
"react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1"
}
},
"node_modules/@ai-sdk/ui-utils": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz",
"integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==",
"node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
"integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"@ai-sdk/provider-utils": "2.2.8",
"zod-to-json-schema": "^3.24.1"
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.14.tgz",
"integrity": "sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.23.8"
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@alloc/quick-lru": {
@@ -4982,6 +5037,19 @@
"node": ">=12.4.0"
}
},
"node_modules/@openrouter/ai-sdk-provider": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.1.1.tgz",
"integrity": "sha512-UypPbVnSExxmG/4Zg0usRiit3auvQVrjUXSyEhm0sZ9GQnW/d8p/bKgCk2neh1W5YyRSo7PNQvCrAEBHZnqQkQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"ai": "^6.0.0",
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -7896,6 +7964,12 @@
"node": ">=18.0.0"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -8134,6 +8208,66 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
@@ -8513,12 +8647,6 @@
"@types/ms": "*"
}
},
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -9359,6 +9487,15 @@
"win32"
]
},
"node_modules/@vercel/oidc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
"integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==",
"license": "Apache-2.0",
"engines": {
"node": ">= 20"
}
},
"node_modules/@whatwg-node/events": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz",
@@ -9524,29 +9661,51 @@
}
},
"node_modules/ai": {
"version": "4.3.19",
"resolved": "https://registry.npmjs.org/ai/-/ai-4.3.19.tgz",
"integrity": "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==",
"version": "6.0.77",
"resolved": "https://registry.npmjs.org/ai/-/ai-6.0.77.tgz",
"integrity": "sha512-tyyhrRpCRFVlivdNIFLK8cexSBB2jwTqO0z1qJQagk+UxZ+MW8h5V8xsvvb+xdKDY482Y8KAm0mr7TDnPKvvlw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"@ai-sdk/provider-utils": "2.2.8",
"@ai-sdk/react": "1.2.12",
"@ai-sdk/ui-utils": "1.2.11",
"@opentelemetry/api": "1.9.0",
"jsondiffpatch": "0.6.0"
"@ai-sdk/gateway": "3.0.39",
"@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.14",
"@opentelemetry/api": "1.9.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"zod": "^3.23.8"
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/ai/node_modules/@ai-sdk/provider": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
"integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
"engines": {
"node": ">=18"
}
},
"node_modules/ai/node_modules/@ai-sdk/provider-utils": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.14.tgz",
"integrity": "sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/ajv": {
@@ -11186,12 +11345,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
"license": "Apache-2.0"
},
"node_modules/direction": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz",
@@ -11841,6 +11994,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -14299,35 +14453,6 @@
"node": ">=6"
}
},
"node_modules/jsondiffpatch": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
"integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
"license": "MIT",
"dependencies": {
"@types/diff-match-patch": "^1.0.36",
"chalk": "^5.3.0",
"diff-match-patch": "^1.0.5"
},
"bin": {
"jsondiffpatch": "bin/jsondiffpatch.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/jsondiffpatch/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
@@ -19300,12 +19425,6 @@
"compute-scroll-into-view": "^3.0.2"
}
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+4 -2
View File
@@ -11,7 +11,8 @@
"migrate": "prisma migrate deploy"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.23",
"@ai-sdk/mcp": "^1.0.13",
"@ai-sdk/react": "^3.0.51",
"@graphql-tools/schema": "^10.0.23",
"@hapi/iron": "^7.0.1",
"@keystone-6/auth": "^8.1.0",
@@ -19,6 +20,7 @@
"@keystone-6/document-renderer": "^1.1.2",
"@keystone-6/fields-document": "^9.1.1",
"@modelcontextprotocol/sdk": "^1.17.1",
"@openrouter/ai-sdk-provider": "^2.0.2",
"@prisma/client": "6.5.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
@@ -54,7 +56,7 @@
"@tanstack/react-table": "^8.21.2",
"@types/nodemailer": "^6.4.17",
"@types/sortablejs": "^1.15.8",
"ai": "^4.3.17",
"ai": "^6.0.49",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",