mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
feat: restore Langfuse observability integration (#103)
- Add lib/langfuse.ts with client, trace input/output, telemetry config - Add instrumentation.ts for OpenTelemetry setup with Langfuse span processor - Add /api/log-save endpoint for logging diagram saves - Add /api/log-feedback endpoint for thumbs up/down feedback - Update chat route with sessionId tracking and telemetry - Add feedback buttons (thumbs up/down) to chat messages - Add sessionId tracking throughout the app - Update env.example with Langfuse configuration - Add @langfuse/client, @langfuse/otel, @langfuse/tracing, @opentelemetry/sdk-trace-node
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai';
|
||||
import { getAIModel } from '@/lib/ai-providers';
|
||||
import { findCachedResponse } from '@/lib/cached-responses';
|
||||
import { setTraceInput, setTraceOutput, getTelemetryConfig, wrapWithObserve } from '@/lib/langfuse';
|
||||
import { getSystemPrompt } from '@/lib/system-prompts';
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -61,7 +62,27 @@ function createCachedStreamResponse(xml: string): Response {
|
||||
|
||||
// Inner handler function
|
||||
async function handleChatRequest(req: Request): Promise<Response> {
|
||||
const { messages, xml } = await req.json();
|
||||
const { messages, xml, sessionId } = await req.json();
|
||||
|
||||
// Get user IP for Langfuse tracking
|
||||
const forwardedFor = req.headers.get('x-forwarded-for');
|
||||
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
|
||||
|
||||
// Validate sessionId for Langfuse (must be string, max 200 chars)
|
||||
const validSessionId = sessionId && typeof sessionId === 'string' && sessionId.length <= 200
|
||||
? sessionId
|
||||
: undefined;
|
||||
|
||||
// Extract user input text for Langfuse trace
|
||||
const currentMessage = messages[messages.length - 1];
|
||||
const userInputText = currentMessage?.parts?.find((p: any) => p.type === 'text')?.text || '';
|
||||
|
||||
// Update Langfuse trace with input, session, and user
|
||||
setTraceInput({
|
||||
input: userInputText,
|
||||
sessionId: validSessionId,
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
// === FILE VALIDATION START ===
|
||||
const fileValidation = validateFileParts(messages);
|
||||
@@ -191,9 +212,19 @@ ${lastMessageText}
|
||||
messages: allMessages,
|
||||
...(providerOptions && { providerOptions }),
|
||||
...(headers && { headers }),
|
||||
onFinish: ({ usage, providerMetadata }) => {
|
||||
console.log('[Cache] providerMetadata:', JSON.stringify(providerMetadata, null, 2));
|
||||
// Langfuse telemetry config (returns undefined if not configured)
|
||||
...(getTelemetryConfig({ sessionId: validSessionId, userId }) && {
|
||||
experimental_telemetry: getTelemetryConfig({ sessionId: validSessionId, userId }),
|
||||
}),
|
||||
onFinish: ({ text, usage, providerMetadata }) => {
|
||||
console.log('[Cache] Full providerMetadata:', JSON.stringify(providerMetadata, null, 2));
|
||||
console.log('[Cache] Usage:', JSON.stringify(usage, null, 2));
|
||||
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
|
||||
// AI SDK uses inputTokens/outputTokens, Langfuse expects promptTokens/completionTokens
|
||||
setTraceOutput(text, {
|
||||
promptTokens: usage?.inputTokens,
|
||||
completionTokens: usage?.outputTokens,
|
||||
});
|
||||
},
|
||||
tools: {
|
||||
// Client-side tool that will be executed on the client
|
||||
@@ -260,7 +291,8 @@ IMPORTANT: Keep edits concise:
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// Wrap handler with error handling
|
||||
async function safeHandler(req: Request): Promise<Response> {
|
||||
try {
|
||||
return await handleChatRequest(req);
|
||||
} catch (error) {
|
||||
@@ -268,3 +300,10 @@ export async function POST(req: Request) {
|
||||
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap with Langfuse observe (if configured)
|
||||
const observedHandler = wrapWithObserve(safeHandler);
|
||||
|
||||
export async function POST(req: Request) {
|
||||
return observedHandler(req);
|
||||
}
|
||||
|
||||
103
app/api/log-feedback/route.ts
Normal file
103
app/api/log-feedback/route.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { getLangfuseClient } from '@/lib/langfuse';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { z } from 'zod';
|
||||
|
||||
const feedbackSchema = z.object({
|
||||
messageId: z.string().min(1).max(200),
|
||||
feedback: z.enum(['good', 'bad']),
|
||||
sessionId: z.string().min(1).max(200).optional(),
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const langfuse = getLangfuseClient();
|
||||
if (!langfuse) {
|
||||
return Response.json({ success: true, logged: false });
|
||||
}
|
||||
|
||||
// Validate input
|
||||
let data;
|
||||
try {
|
||||
data = feedbackSchema.parse(await req.json());
|
||||
} catch {
|
||||
return Response.json({ success: false, error: 'Invalid input' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { messageId, feedback, sessionId } = data;
|
||||
|
||||
// Get user IP for tracking
|
||||
const forwardedFor = req.headers.get('x-forwarded-for');
|
||||
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
|
||||
|
||||
try {
|
||||
// Find the most recent chat trace for this session to attach the score to
|
||||
const tracesResponse = await langfuse.api.trace.list({
|
||||
sessionId,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const traces = tracesResponse.data || [];
|
||||
const latestTrace = traces[0];
|
||||
|
||||
if (!latestTrace) {
|
||||
// No trace found for this session - create a standalone feedback trace
|
||||
const traceId = randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
await langfuse.api.ingestion.batch({
|
||||
batch: [
|
||||
{
|
||||
type: 'trace-create',
|
||||
id: randomUUID(),
|
||||
timestamp,
|
||||
body: {
|
||||
id: traceId,
|
||||
name: 'user-feedback',
|
||||
sessionId,
|
||||
userId,
|
||||
input: { messageId, feedback },
|
||||
metadata: { source: 'feedback-button', note: 'standalone - no chat trace found' },
|
||||
timestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'score-create',
|
||||
id: randomUUID(),
|
||||
timestamp,
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId,
|
||||
name: 'user-feedback',
|
||||
value: feedback === 'good' ? 1 : 0,
|
||||
comment: `User gave ${feedback} feedback`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
// Attach score to the existing chat trace
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
await langfuse.api.ingestion.batch({
|
||||
batch: [
|
||||
{
|
||||
type: 'score-create',
|
||||
id: randomUUID(),
|
||||
timestamp,
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: latestTrace.id,
|
||||
name: 'user-feedback',
|
||||
value: feedback === 'good' ? 1 : 0,
|
||||
comment: `User gave ${feedback} feedback`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({ success: true, logged: true });
|
||||
} catch (error) {
|
||||
console.error('Langfuse feedback error:', error);
|
||||
return Response.json({ success: false, error: 'Failed to log feedback' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
65
app/api/log-save/route.ts
Normal file
65
app/api/log-save/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getLangfuseClient } from '@/lib/langfuse';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { z } from 'zod';
|
||||
|
||||
const saveSchema = z.object({
|
||||
filename: z.string().min(1).max(255),
|
||||
format: z.enum(['drawio', 'png', 'svg']),
|
||||
sessionId: z.string().min(1).max(200).optional(),
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const langfuse = getLangfuseClient();
|
||||
if (!langfuse) {
|
||||
return Response.json({ success: true, logged: false });
|
||||
}
|
||||
|
||||
// Validate input
|
||||
let data;
|
||||
try {
|
||||
data = saveSchema.parse(await req.json());
|
||||
} catch {
|
||||
return Response.json({ success: false, error: 'Invalid input' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { filename, format, sessionId } = data;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Find the most recent chat trace for this session to attach the save flag
|
||||
const tracesResponse = await langfuse.api.trace.list({
|
||||
sessionId,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const traces = tracesResponse.data || [];
|
||||
const latestTrace = traces[0];
|
||||
|
||||
if (latestTrace) {
|
||||
// Add a score to the existing trace to flag that user saved
|
||||
await langfuse.api.ingestion.batch({
|
||||
batch: [
|
||||
{
|
||||
type: 'score-create',
|
||||
id: randomUUID(),
|
||||
timestamp,
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: latestTrace.id,
|
||||
name: 'diagram-saved',
|
||||
value: 1,
|
||||
comment: `User saved diagram as ${filename}.${format}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
// If no trace found, skip logging (user hasn't chatted yet)
|
||||
|
||||
return Response.json({ success: true, logged: !!latestTrace });
|
||||
} catch (error) {
|
||||
console.error('Langfuse save error:', error);
|
||||
return Response.json({ success: false, error: 'Failed to log save' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,7 @@ interface ChatInputProps {
|
||||
onFileChange?: (files: File[]) => void;
|
||||
showHistory?: boolean;
|
||||
onToggleHistory?: (show: boolean) => void;
|
||||
sessionId?: string;
|
||||
error?: Error | null;
|
||||
}
|
||||
|
||||
@@ -108,6 +109,7 @@ export function ChatInput({
|
||||
onFileChange = () => {},
|
||||
showHistory = false,
|
||||
onToggleHistory = () => {},
|
||||
sessionId,
|
||||
error = null,
|
||||
}: ChatInputProps) {
|
||||
const { diagramHistory, saveDiagramToFile } = useDiagram();
|
||||
@@ -325,7 +327,7 @@ export function ChatInput({
|
||||
open={showSaveDialog}
|
||||
onOpenChange={setShowSaveDialog}
|
||||
onSave={(filename, format) =>
|
||||
saveDiagramToFile(filename, format)
|
||||
saveDiagramToFile(filename, format, sessionId)
|
||||
}
|
||||
defaultFilename={`diagram-${new Date()
|
||||
.toISOString()
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import ExamplePanel from "./chat-example-panel";
|
||||
import { UIMessage } from "ai";
|
||||
import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils";
|
||||
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, RotateCcw, Pencil } from "lucide-react";
|
||||
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, ThumbsUp, ThumbsDown, RotateCcw, Pencil } from "lucide-react";
|
||||
import { CodeBlock } from "./code-block";
|
||||
|
||||
interface EditPair {
|
||||
@@ -67,6 +67,7 @@ interface ChatMessageDisplayProps {
|
||||
error?: Error | null;
|
||||
setInput: (input: string) => void;
|
||||
setFiles: (files: File[]) => void;
|
||||
sessionId?: string;
|
||||
onRegenerate?: (messageIndex: number) => void;
|
||||
onEditMessage?: (messageIndex: number, newText: string) => void;
|
||||
}
|
||||
@@ -76,6 +77,7 @@ export function ChatMessageDisplay({
|
||||
error,
|
||||
setInput,
|
||||
setFiles,
|
||||
sessionId,
|
||||
onRegenerate,
|
||||
onEditMessage,
|
||||
}: ChatMessageDisplayProps) {
|
||||
@@ -88,6 +90,7 @@ export function ChatMessageDisplay({
|
||||
);
|
||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||
const [copyFailedMessageId, setCopyFailedMessageId] = useState<string | null>(null);
|
||||
const [feedback, setFeedback] = useState<Record<string, "good" | "bad">>({});
|
||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
||||
const [editText, setEditText] = useState<string>("");
|
||||
|
||||
@@ -103,6 +106,34 @@ export function ChatMessageDisplay({
|
||||
}
|
||||
};
|
||||
|
||||
const submitFeedback = async (messageId: string, value: "good" | "bad") => {
|
||||
// Toggle off if already selected
|
||||
if (feedback[messageId] === value) {
|
||||
setFeedback((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[messageId];
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setFeedback((prev) => ({ ...prev, [messageId]: value }));
|
||||
|
||||
try {
|
||||
await fetch("/api/log-feedback", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
messageId,
|
||||
feedback: value,
|
||||
sessionId,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to log feedback:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisplayChart = useCallback(
|
||||
(xml: string) => {
|
||||
const currentXml = xml || "";
|
||||
@@ -436,6 +467,32 @@ export function ChatMessageDisplay({
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{/* Divider */}
|
||||
<div className="w-px h-4 bg-border mx-1" />
|
||||
{/* Thumbs up */}
|
||||
<button
|
||||
onClick={() => submitFeedback(message.id, "good")}
|
||||
className={`p-1.5 rounded-lg transition-colors ${
|
||||
feedback[message.id] === "good"
|
||||
? "text-green-600 bg-green-100"
|
||||
: "text-muted-foreground/60 hover:text-green-600 hover:bg-green-50"
|
||||
}`}
|
||||
title="Good response"
|
||||
>
|
||||
<ThumbsUp className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{/* Thumbs down */}
|
||||
<button
|
||||
onClick={() => submitFeedback(message.id, "bad")}
|
||||
className={`p-1.5 rounded-lg transition-colors ${
|
||||
feedback[message.id] === "bad"
|
||||
? "text-red-600 bg-red-100"
|
||||
: "text-muted-foreground/60 hover:text-red-600 hover:bg-red-50"
|
||||
}`}
|
||||
title="Bad response"
|
||||
>
|
||||
<ThumbsDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,9 @@ export default function ChatPanel({
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
// Generate a unique session ID for Langfuse tracing
|
||||
const [sessionId, setSessionId] = useState(() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
||||
|
||||
// Store XML snapshots for each user message (keyed by message index)
|
||||
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map());
|
||||
|
||||
@@ -217,6 +220,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
{
|
||||
body: {
|
||||
xml: chartXml,
|
||||
sessionId,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -297,6 +301,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
{
|
||||
body: {
|
||||
xml: savedXml,
|
||||
sessionId,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -353,6 +358,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
{
|
||||
body: {
|
||||
xml: savedXml,
|
||||
sessionId,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -440,6 +446,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
error={error}
|
||||
setInput={setInput}
|
||||
setFiles={handleFileChange}
|
||||
sessionId={sessionId}
|
||||
onRegenerate={handleRegenerate}
|
||||
onEditMessage={handleEditMessage}
|
||||
/>
|
||||
@@ -455,12 +462,14 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
onClearChat={() => {
|
||||
setMessages([]);
|
||||
clearDiagram();
|
||||
setSessionId(`session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
||||
xmlSnapshotsRef.current.clear();
|
||||
}}
|
||||
files={files}
|
||||
onFileChange={handleFileChange}
|
||||
showHistory={showHistory}
|
||||
onToggleHistory={setShowHistory}
|
||||
sessionId={sessionId}
|
||||
error={error}
|
||||
/>
|
||||
</footer>
|
||||
|
||||
@@ -16,7 +16,7 @@ interface DiagramContextType {
|
||||
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
||||
handleDiagramExport: (data: any) => void;
|
||||
clearDiagram: () => void;
|
||||
saveDiagramToFile: (filename: string, format: ExportFormat) => void;
|
||||
saveDiagramToFile: (filename: string, format: ExportFormat, sessionId?: string) => void;
|
||||
}
|
||||
|
||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
||||
@@ -107,7 +107,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
setDiagramHistory([]);
|
||||
};
|
||||
|
||||
const saveDiagramToFile = (filename: string, format: ExportFormat) => {
|
||||
const saveDiagramToFile = (filename: string, format: ExportFormat, sessionId?: string) => {
|
||||
if (!drawioRef.current) {
|
||||
console.warn("Draw.io editor not ready");
|
||||
return;
|
||||
@@ -145,6 +145,9 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
extension = ".svg";
|
||||
}
|
||||
|
||||
// Log save event to Langfuse (flags the trace)
|
||||
logSaveToLangfuse(filename, format, sessionId);
|
||||
|
||||
// Handle download
|
||||
let url: string;
|
||||
if (typeof fileContent === "string" && fileContent.startsWith("data:")) {
|
||||
@@ -174,6 +177,19 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
drawioRef.current.exportDiagram({ format: drawioFormat });
|
||||
};
|
||||
|
||||
// Log save event to Langfuse (just flags the trace, doesn't send content)
|
||||
const logSaveToLangfuse = async (filename: string, format: string, sessionId?: string) => {
|
||||
try {
|
||||
await fetch("/api/log-save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, format, sessionId }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to log save to Langfuse:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DiagramContext.Provider
|
||||
value={{
|
||||
|
||||
@@ -41,3 +41,9 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
# DeepSeek Configuration
|
||||
# DEEPSEEK_API_KEY=sk-...
|
||||
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # Optional: Custom endpoint
|
||||
|
||||
# Langfuse Observability (Optional)
|
||||
# Enable LLM tracing and analytics - https://langfuse.com
|
||||
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
||||
# LANGFUSE_SECRET_KEY=sk-lf-...
|
||||
# LANGFUSE_BASEURL=https://cloud.langfuse.com # EU region, use https://us.cloud.langfuse.com for US
|
||||
|
||||
35
instrumentation.ts
Normal file
35
instrumentation.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
||||
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
||||
|
||||
export function register() {
|
||||
// Skip telemetry if Langfuse env vars are not configured
|
||||
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
||||
console.warn('[Langfuse] Environment variables not configured - telemetry disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const langfuseSpanProcessor = new LangfuseSpanProcessor({
|
||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
||||
baseUrl: process.env.LANGFUSE_BASEURL,
|
||||
// Filter out Next.js HTTP request spans so AI SDK spans become root traces
|
||||
shouldExportSpan: ({ otelSpan }) => {
|
||||
const spanName = otelSpan.name;
|
||||
// Skip Next.js HTTP infrastructure spans
|
||||
if (spanName.startsWith('POST /') ||
|
||||
spanName.startsWith('GET /') ||
|
||||
spanName.includes('BaseServer') ||
|
||||
spanName.includes('handleRequest')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const tracerProvider = new NodeTracerProvider({
|
||||
spanProcessors: [langfuseSpanProcessor],
|
||||
});
|
||||
|
||||
// Register globally so AI SDK's telemetry also uses this processor
|
||||
tracerProvider.register();
|
||||
}
|
||||
95
lib/langfuse.ts
Normal file
95
lib/langfuse.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { observe, updateActiveTrace } from '@langfuse/tracing';
|
||||
import { LangfuseClient } from '@langfuse/client';
|
||||
import * as api from '@opentelemetry/api';
|
||||
|
||||
// Singleton LangfuseClient instance for direct API calls
|
||||
let langfuseClient: LangfuseClient | null = null;
|
||||
|
||||
export function getLangfuseClient(): LangfuseClient | null {
|
||||
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!langfuseClient) {
|
||||
langfuseClient = new LangfuseClient({
|
||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
||||
baseUrl: process.env.LANGFUSE_BASEURL,
|
||||
});
|
||||
}
|
||||
|
||||
return langfuseClient;
|
||||
}
|
||||
|
||||
// Check if Langfuse is configured
|
||||
export function isLangfuseEnabled(): boolean {
|
||||
return !!process.env.LANGFUSE_PUBLIC_KEY;
|
||||
}
|
||||
|
||||
// Update trace with input data at the start of request
|
||||
export function setTraceInput(params: {
|
||||
input: string;
|
||||
sessionId?: string;
|
||||
userId?: string;
|
||||
}) {
|
||||
if (!isLangfuseEnabled()) return;
|
||||
|
||||
updateActiveTrace({
|
||||
name: 'chat',
|
||||
input: params.input,
|
||||
sessionId: params.sessionId,
|
||||
userId: params.userId,
|
||||
});
|
||||
}
|
||||
|
||||
// Update trace with output and end the span
|
||||
export function setTraceOutput(output: string, usage?: { promptTokens?: number; completionTokens?: number }) {
|
||||
if (!isLangfuseEnabled()) return;
|
||||
|
||||
updateActiveTrace({ output });
|
||||
|
||||
const activeSpan = api.trace.getActiveSpan();
|
||||
if (activeSpan) {
|
||||
// Manually set usage attributes since AI SDK Bedrock streaming doesn't provide them
|
||||
if (usage?.promptTokens) {
|
||||
activeSpan.setAttribute('ai.usage.promptTokens', usage.promptTokens);
|
||||
activeSpan.setAttribute('gen_ai.usage.input_tokens', usage.promptTokens);
|
||||
}
|
||||
if (usage?.completionTokens) {
|
||||
activeSpan.setAttribute('ai.usage.completionTokens', usage.completionTokens);
|
||||
activeSpan.setAttribute('gen_ai.usage.output_tokens', usage.completionTokens);
|
||||
}
|
||||
activeSpan.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Get telemetry config for streamText
|
||||
export function getTelemetryConfig(params: {
|
||||
sessionId?: string;
|
||||
userId?: string;
|
||||
}) {
|
||||
if (!isLangfuseEnabled()) return undefined;
|
||||
|
||||
return {
|
||||
isEnabled: true,
|
||||
// Disable automatic input recording to avoid uploading large base64 images to Langfuse media
|
||||
// User text input is recorded manually via setTraceInput
|
||||
recordInputs: false,
|
||||
recordOutputs: true,
|
||||
metadata: {
|
||||
sessionId: params.sessionId,
|
||||
userId: params.userId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap a handler with Langfuse observe
|
||||
export function wrapWithObserve<T>(
|
||||
handler: (req: Request) => Promise<T>
|
||||
): (req: Request) => Promise<T> {
|
||||
if (!isLangfuseEnabled()) {
|
||||
return handler;
|
||||
}
|
||||
|
||||
return observe(handler, { name: 'chat', endOnExit: false });
|
||||
}
|
||||
370
package-lock.json
generated
370
package-lock.json
generated
@@ -17,8 +17,12 @@
|
||||
"@ai-sdk/openai": "^2.0.19",
|
||||
"@ai-sdk/react": "^2.0.22",
|
||||
"@aws-sdk/credential-providers": "^3.943.0",
|
||||
"@langfuse/client": "^4.4.9",
|
||||
"@langfuse/otel": "^4.4.4",
|
||||
"@langfuse/tracing": "^4.4.9",
|
||||
"@next/third-parties": "^16.0.6",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
@@ -2338,6 +2342,62 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@langfuse/client": {
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@langfuse/client/-/client-4.4.9.tgz",
|
||||
"integrity": "sha512-Y7bU70tMx/lYOU/A7NGvXXVZoL3AiFigbf7EwS5PVFc0xd34eRUmvwdLwEtuK7CnYTyxIZTzVVP2KEaicWCYZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langfuse/core": "^4.4.9",
|
||||
"@langfuse/tracing": "^4.4.9",
|
||||
"mustache": "^4.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@langfuse/core": {
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@langfuse/core/-/core-4.4.9.tgz",
|
||||
"integrity": "sha512-9Hz/eH6dkOP8E/FLt1fsAQR8RE/TF8Ag/39GmY8JjN1o/Tl/MFJfK2QvqRGrkjDkIkMJGOSD+iQmV2pYm4upDA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@langfuse/otel": {
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@langfuse/otel/-/otel-4.4.9.tgz",
|
||||
"integrity": "sha512-T9tSucDC3cvdeUB8fTGXm6ZZcC4kOEreJuM/C66nHQ26+DWtx2AN75JWuadc2E+VU/K+BjTb5XAkBceWYtvXjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langfuse/core": "^4.4.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^2.0.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": ">=0.202.0 <1.0.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@langfuse/tracing": {
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@langfuse/tracing/-/tracing-4.4.9.tgz",
|
||||
"integrity": "sha512-if+G/v9NsyTKj40KKX96bRSdMXwyDbVL4GJQvmwQ9SxvGYF+d99pGFB7L6QOeCd1KBHMdmDe733ncmvCnSHJ9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langfuse/core": "^4.4.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -2606,6 +2666,273 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api-logs": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
|
||||
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/context-async-hooks": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz",
|
||||
"integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/core": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
|
||||
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/exporter-trace-otlp-http": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.208.0.tgz",
|
||||
"integrity": "sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/otlp-exporter-base": "0.208.0",
|
||||
"@opentelemetry/otlp-transformer": "0.208.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-exporter-base": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
|
||||
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/otlp-transformer": "0.208.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-transformer": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
|
||||
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/sdk-logs": "0.208.0",
|
||||
"@opentelemetry/sdk-metrics": "2.2.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.2.0",
|
||||
"protobufjs": "^7.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-logs": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
|
||||
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-metrics": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
|
||||
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-base": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
|
||||
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-node": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.2.0.tgz",
|
||||
"integrity": "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/context-async-hooks": "2.2.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/semantic-conventions": {
|
||||
"version": "1.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
|
||||
"integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||
@@ -4620,7 +4947,6 @@
|
||||
"version": "20.17.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@@ -8326,6 +8652,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/longest-streak": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
||||
@@ -9209,6 +9542,15 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -9718,6 +10060,31 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -10942,7 +11309,6 @@
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
|
||||
@@ -18,8 +18,12 @@
|
||||
"@ai-sdk/openai": "^2.0.19",
|
||||
"@ai-sdk/react": "^2.0.22",
|
||||
"@aws-sdk/credential-providers": "^3.943.0",
|
||||
"@langfuse/client": "^4.4.9",
|
||||
"@langfuse/otel": "^4.4.4",
|
||||
"@langfuse/tracing": "^4.4.9",
|
||||
"@next/third-parties": "^16.0.6",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
|
||||
Reference in New Issue
Block a user