diff --git a/__tests__/api/chat/route.test.ts b/__tests__/api/chat/route.test.ts index 1ccfc201..c0cf0a68 100644 --- a/__tests__/api/chat/route.test.ts +++ b/__tests__/api/chat/route.test.ts @@ -41,7 +41,8 @@ vi.mock('@sentry/nextjs', () => ({ })); vi.mock('@ai-sdk/google', () => ({ - createGoogleGenerativeAI: () => + // AI SDK v7 renamed the provider factory from createGoogleGenerativeAI to createGoogle. + createGoogle: () => Object.assign( vi.fn(() => 'gemini-model'), { @@ -50,10 +51,13 @@ vi.mock('@ai-sdk/google', () => ({ ), })); +// AI SDK v7 returns the stream through createUIMessageStreamResponse({ stream: toUIMessageStream(...) }) +// rather than result.toUIMessageStreamResponse(). Mock the surface the route actually calls. vi.mock('ai', () => ({ convertToModelMessages: vi.fn((messages) => messages), + toUIMessageStream: vi.fn(() => 'ui-message-stream'), + createUIMessageStreamResponse: vi.fn(() => new Response('stream', { status: 200 })), streamText: vi.fn(() => ({ - toUIMessageStreamResponse: vi.fn(() => new Response('stream', { status: 200 })), totalUsage: Promise.resolve({}), request: Promise.resolve({ body: null }), })), diff --git a/__tests__/lib/ai/utils.test.ts b/__tests__/lib/ai/utils.test.ts index 6ad14a5c..29b2742e 100644 --- a/__tests__/lib/ai/utils.test.ts +++ b/__tests__/lib/ai/utils.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { extractDocumentIdFromFilename, extractRelevantSources } from '@/lib/ai/utils'; +import { + extractDocumentIdFromFilename, + extractRelevantSources, + extractRequestMetadata, +} from '@/lib/ai/utils'; describe('ai/utils', () => { describe('extractRelevantSources', () => { @@ -97,4 +101,65 @@ describe('ai/utils', () => { ).toBe('550E8400-E29B-41D4-A716-446655440000'); }); }); + describe('extractRequestMetadata', () => { + // AI SDK v7 hands back an already-parsed object. This is the shape the + // chat route actually receives, and `request.body` is typed `unknown`, so + // only a test can catch a regression here. + it('should read generationConfig and systemInstruction from an object body', () => { + expect( + extractRequestMetadata({ + generationConfig: { temperature: 0.4, topP: 0.9 }, + systemInstruction: { parts: [{ text: 'You are a helpful assistant.' }] }, + contents: [{ role: 'user', parts: [{ text: 'hi' }] }], + }) + ).toEqual({ + generationConfig: '{"temperature":0.4,"topP":0.9}', + systemPrompt: 'You are a helpful assistant.', + }); + }); + + it('should still read a JSON string body (the pre-v7 shape)', () => { + expect( + extractRequestMetadata( + JSON.stringify({ + generationConfig: { temperature: 0.2 }, + systemInstruction: { parts: [{ text: 'Be concise.' }] }, + }) + ) + ).toEqual({ + generationConfig: '{"temperature":0.2}', + systemPrompt: 'Be concise.', + }); + }); + + it('should return nulls when the body carries neither field', () => { + expect(extractRequestMetadata({ contents: [] })).toEqual({ + generationConfig: null, + systemPrompt: null, + }); + }); + + it('should serialize an empty generationConfig rather than dropping it', () => { + // Google sends `generationConfig: {}` when no sampling settings are set. + expect(extractRequestMetadata({ generationConfig: {} })).toEqual({ + generationConfig: '{}', + systemPrompt: null, + }); + }); + + it('should return nulls for unusable bodies', () => { + expect(extractRequestMetadata(undefined)).toEqual({ + generationConfig: null, + systemPrompt: null, + }); + expect(extractRequestMetadata(null)).toEqual({ + generationConfig: null, + systemPrompt: null, + }); + expect(extractRequestMetadata('not json')).toEqual({ + generationConfig: null, + systemPrompt: null, + }); + }); + }); }); diff --git a/app/(logged-in)/org/[slug]/layout.tsx b/app/(logged-in)/org/[slug]/layout.tsx index 81317a72..3efad1de 100644 --- a/app/(logged-in)/org/[slug]/layout.tsx +++ b/app/(logged-in)/org/[slug]/layout.tsx @@ -9,7 +9,7 @@ export default async function OrgLayout({ params, }: { children: React.ReactNode; - params: { slug: string }; + params: Promise<{ slug: string }>; }) { const { slug } = await params; const caller = await createCaller(); diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index a2137140..5f1680a5 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,10 +1,20 @@ -import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { createGoogle } from '@ai-sdk/google'; import type { GroundingMetadata } from '@google/genai'; import * as Sentry from '@sentry/nextjs'; -import { UIMessage, convertToModelMessages, streamText } from 'ai'; +import { + UIMessage, + convertToModelMessages, + createUIMessageStreamResponse, + streamText, + toUIMessageStream, +} from 'ai'; import { and, eq } from 'drizzle-orm'; -import { extractDocumentIdFromFilename, extractRelevantSources } from '@/lib/ai/utils'; +import { + extractDocumentIdFromFilename, + extractRelevantSources, + extractRequestMetadata, +} from '@/lib/ai/utils'; import { ApiResponseHandler } from '@/lib/api/responses'; import { requireOrgAccess, requireUser } from '@/lib/auth/guards'; import { db } from '@/lib/db/drizzle'; @@ -14,7 +24,7 @@ import { getRAGSettings } from '@/lib/services/rag-service'; import { chatRequestSchema } from '@/lib/types/chat'; const DEFAULT_MODEL = 'gemini-2.5-flash'; -const googleGenerativeAIProvider = createGoogleGenerativeAI({ +const googleProvider = createGoogle({ apiKey: process.env.GOOGLE_AI_API_KEY, }); @@ -105,146 +115,136 @@ export async function POST(req: Request) { } = ragSettings ?? {}; const result = streamText({ - model: googleGenerativeAIProvider(DEFAULT_MODEL), - messages: convertToModelMessages(messages), + model: googleProvider(DEFAULT_MODEL), + messages: await convertToModelMessages(messages), tools: { - // @ts-expect-error - Google AI SDK file search tool type incompatibility - file_search: googleGenerativeAIProvider.tools.fileSearch({ + file_search: googleProvider.tools.fileSearch({ fileSearchStoreNames: [orgDocs[0].fileSearchStoreName], }), }, activeTools: ['file_search'], - ...(systemPrompt && { system: systemPrompt }), + // v7 drops request bodies from step results by default; the LLM log needs it + // to record the resolved generationConfig and systemInstruction. + include: { requestBody: true }, + ...(systemPrompt && { instructions: systemPrompt }), ...(maxOutputTokens && { maxOutputTokens }), ...(temperature && { temperature }), ...(topP && { topP }), ...(topK && { topK }), }); - return result.toUIMessageStreamResponse({ - originalMessages: messages as UIMessage[], - messageMetadata: ({ part }) => { - // Extract sources from grounding metadata during streaming - // Sources are automatically attached to message metadata by AI SDK - if (part.type === 'finish-step' && part.providerMetadata?.google?.groundingMetadata) { - const metadata = part.providerMetadata.google.groundingMetadata as GroundingMetadata; - const relevantSources = extractRelevantSources(metadata); - - if (relevantSources.length > 0) { - // Extract document IDs from filenames (format: {documentId}-{originalName}) - // Generate proxy URLs with organizationId for access control - const sourcesWithIds = relevantSources - .map((source) => { - const documentId = extractDocumentIdFromFilename(source.title); - if (!documentId) return null; - - return { - documentId, - title: source.title.replace(`${documentId}-`, ''), // Remove documentId prefix for display - url: `/api/documents/${chatSession.organizationId}/${documentId}`, - }; - }) - .filter((s): s is { documentId: string; title: string; url: string } => s !== null); - - return { sources: sourcesWithIds }; - } - } - - return undefined; - }, - async onFinish({ messages: updatedMessages, finishReason }) { - try { - // Only save NEW messages (not already in the database) - // Use existingMessageCount from DB to determine what's new - // This handles the case where useAIChat optimistically adds messages to local state - const newMessages = updatedMessages.slice(existingMessageCount); - - if (newMessages.length > 0) { - // Messages already have metadata attached by messageMetadata callback - // Just serialize and save to database - const messagesToInsert = newMessages.map((msg) => ({ - chatSessionId, - role: msg.role, - parts: JSON.stringify(msg.parts), // Store UIMessagePart[] array - metadata: msg.metadata ? JSON.stringify(msg.metadata) : null, - })); - - await db.insert(chatMessages).values(messagesToInsert); + return createUIMessageStreamResponse({ + stream: toUIMessageStream({ + stream: result.stream, + originalMessages: messages as UIMessage[], + messageMetadata: ({ part }) => { + // Extract sources from grounding metadata during streaming + // Sources are automatically attached to message metadata by AI SDK + if (part.type === 'finish-step' && part.providerMetadata?.google?.groundingMetadata) { + const metadata = part.providerMetadata.google.groundingMetadata as GroundingMetadata; + const relevantSources = extractRelevantSources(metadata); + + if (relevantSources.length > 0) { + // Extract document IDs from filenames (format: {documentId}-{originalName}) + // Generate proxy URLs with organizationId for access control + const sourcesWithIds = relevantSources + .map((source) => { + const documentId = extractDocumentIdFromFilename(source.title); + if (!documentId) return null; + + return { + documentId, + title: source.title.replace(`${documentId}-`, ''), // Remove documentId prefix for display + url: `/api/documents/${chatSession.organizationId}/${documentId}`, + }; + }) + .filter((s): s is { documentId: string; title: string; url: string } => s !== null); + + return { sources: sourcesWithIds }; + } } - // Update session timestamp - await db - .update(chatSessions) - .set({ updatedAt: new Date() }) - .where(eq(chatSessions.id, chatSessionId)); - - const usage = await result.totalUsage; - const request = await result.request; - - let generationConfig: string | null = null; - let actualSystemPrompt: string | null = null; - + return undefined; + }, + async onEnd({ messages: updatedMessages, finishReason }) { try { - if (typeof request.body === 'string') { - const requestBody = JSON.parse(request.body); - if (requestBody.generationConfig) { - generationConfig = JSON.stringify(requestBody.generationConfig); - } - // Extract systemInstruction text from request and store in systemPrompt field - if (requestBody.systemInstruction?.parts?.[0]?.text) { - actualSystemPrompt = requestBody.systemInstruction.parts[0].text; - } + // Only save NEW messages (not already in the database) + // Use existingMessageCount from DB to determine what's new + // This handles the case where useAIChat optimistically adds messages to local state + const newMessages = updatedMessages.slice(existingMessageCount); + + if (newMessages.length > 0) { + // Messages already have metadata attached by messageMetadata callback + // Just serialize and save to database + const messagesToInsert = newMessages.map((msg) => ({ + chatSessionId, + role: msg.role, + parts: JSON.stringify(msg.parts), // Store UIMessagePart[] array + metadata: msg.metadata ? JSON.stringify(msg.metadata) : null, + })); + + await db.insert(chatMessages).values(messagesToInsert); } - } catch (error) { - console.error('Error parsing request body:', error); - } - // Log LLM call - const responseTime = Date.now() - startTime; - const lastUserMsg = updatedMessages.filter((m) => m.role === 'user').pop(); - const lastAssistantMsg = updatedMessages.filter((m) => m.role === 'assistant').pop(); - - await llmLogsService.createLlmLog({ - endpoint: 'chat', - model: DEFAULT_MODEL, - systemPrompt: actualSystemPrompt, - userPrompt: lastUserMsg ? JSON.stringify(lastUserMsg.parts) : null, - response: lastAssistantMsg ? JSON.stringify(lastAssistantMsg.parts) : null, - tokensUsed: usage.totalTokens ?? null, - promptTokens: usage.inputTokens ?? null, - completionTokens: usage.outputTokens ?? null, - reasoningTokens: usage.reasoningTokens ?? null, - cachedInputTokens: usage.cachedInputTokens ?? null, - responseTimeMs: responseTime, - finishReason: finishReason, - generationConfig, - userId: user.id, - organizationId: chatSession.organizationId, - chatSessionId: chatSession.id, - }); - } catch (error) { - console.error('Error saving messages or logging:', error); - - // Log to Sentry for monitoring and alerting - Sentry.captureException(error, { - tags: { - feature: 'chat', - operation: 'save_messages', - chatSessionId: chatSession.id, + // Update session timestamp + await db + .update(chatSessions) + .set({ updatedAt: new Date() }) + .where(eq(chatSessions.id, chatSessionId)); + + const usage = await result.usage; + const { request } = await result.finalStep; + const { generationConfig, systemPrompt: actualSystemPrompt } = extractRequestMetadata( + request.body + ); + + // Log LLM call + const responseTime = Date.now() - startTime; + const lastUserMsg = updatedMessages.filter((m) => m.role === 'user').pop(); + const lastAssistantMsg = updatedMessages.filter((m) => m.role === 'assistant').pop(); + + await llmLogsService.createLlmLog({ + endpoint: 'chat', + model: DEFAULT_MODEL, + systemPrompt: actualSystemPrompt, + userPrompt: lastUserMsg ? JSON.stringify(lastUserMsg.parts) : null, + response: lastAssistantMsg ? JSON.stringify(lastAssistantMsg.parts) : null, + tokensUsed: usage.totalTokens ?? null, + promptTokens: usage.inputTokens ?? null, + completionTokens: usage.outputTokens ?? null, + reasoningTokens: usage.outputTokenDetails.reasoningTokens ?? null, + cachedInputTokens: usage.inputTokenDetails.cacheReadTokens ?? null, + responseTimeMs: responseTime, + finishReason: finishReason ?? null, + generationConfig, userId: user.id, organizationId: chatSession.organizationId, - }, - contexts: { - chat: { + chatSessionId: chatSession.id, + }); + } catch (error) { + console.error('Error saving messages or logging:', error); + + // Log to Sentry for monitoring and alerting + Sentry.captureException(error, { + tags: { + feature: 'chat', + operation: 'save_messages', chatSessionId: chatSession.id, - messageCount: updatedMessages.length, - finishReason, + userId: user.id, + organizationId: chatSession.organizationId, + }, + contexts: { + chat: { + chatSessionId: chatSession.id, + messageCount: updatedMessages.length, + finishReason, + }, }, - }, - }); + }); - // Don't throw - streaming already completed successfully - } - }, + // Don't throw - streaming already completed successfully + } + }, + }), }); } diff --git a/bun.lock b/bun.lock index 0ef244b5..e04051be 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,8 @@ "": { "name": "kosuke-template", "dependencies": { - "@ai-sdk/google": "^2.0.47", - "@ai-sdk/react": "^2.0.115", + "@ai-sdk/google": "^4.0.64", + "@ai-sdk/react": "^4.0.96", "@aws-sdk/client-s3": "^3.962.0", "@aws-sdk/s3-request-presigner": "^3.962.0", "@dnd-kit/core": "^6.3.1", @@ -50,7 +50,7 @@ "@trpc/react-query": "^11.8.1", "@trpc/server": "^11.8.1", "@types/pg": "^8.16.0", - "ai": "^5.0.108", + "ai": "^7.0.93", "better-auth": "^1.4.10", "bullmq": "^5.66.4", "class-variance-authority": "^0.7.1", @@ -128,15 +128,17 @@ "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.75", "", { "dependencies": { "@ai-sdk/provider": "4.0.10", "@ai-sdk/provider-utils": "5.0.36", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HOnhw3oXtBnboBF7kAKipjToCN6+5w6EuukiUKiULcxBitS+vbHRRv9IUBcq+Z1wkXcJjfywKrt5r++I6OYyXg=="], - "@ai-sdk/google": ["@ai-sdk/google@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-grIlvzh+jzMoKNOnn5Xe/8fdYiJOs0ThMVetsGzqflvMkUNF3B83t5i0kf4XqiM8MwTJ8gkdOA4VeQOZKR7TkA=="], + "@ai-sdk/google": ["@ai-sdk/google@4.0.64", "", { "dependencies": { "@ai-sdk/provider": "4.0.10", "@ai-sdk/provider-utils": "5.0.36" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-D8+74xfXkzPi5qCe7iRzQRH/1WW805gBrXfwz7KKEBa0SMY2TENdsTrJnVqFZaT7UYow1Ls6qroaklLd9veU9w=="], - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + "@ai-sdk/mcp": ["@ai-sdk/mcp@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "4.0.10", "@ai-sdk/provider-utils": "5.0.36", "cross-spawn": "^7.0.6", "pkce-challenge": "^5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ROukI/LfoPHf5F4gi1GKQebK5658fR1EG3kt0P0vzrfSy1FAGQ5FyKr9j2lbkXAV9X3kfpf9LJdif7w67XDtgw=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="], + "@ai-sdk/provider": ["@ai-sdk/provider@4.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-fX2ENAc7iDpZ+Wp4+Rk06Usn/Ys7dI9uAkGv0jlF6XVrW13NkRWx5Ou+U6lIM2E1fTLkCs16GGrUAaVvroag7A=="], - "@ai-sdk/react": ["@ai-sdk/react@2.0.115", "", { "dependencies": { "@ai-sdk/provider-utils": "3.0.19", "ai": "5.0.113", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1", "zod": "^3.25.76 || ^4.1.8" }, "optionalPeers": ["zod"] }, "sha512-Etu7gWSEi2dmXss1PoR5CAZGwGShXsF9+Pon1eRO6EmatjYaBMhq1CfHPyYhGzWrint8jJIK2VaAhiMef29qZw=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.36", "", { "dependencies": { "@ai-sdk/provider": "4.0.10", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MFXBn6XDyf37PNQAge/HTatPJE8Vmg/g/w4WPtjSV53jq8FKAzoaN5+43hsdQa9bqgN+/13jxug9ChvsG+godQ=="], + + "@ai-sdk/react": ["@ai-sdk/react@4.0.96", "", { "dependencies": { "@ai-sdk/mcp": "2.0.45", "@ai-sdk/provider": "4.0.10", "@ai-sdk/provider-utils": "5.0.36", "ai": "7.0.93", "swr": "^2.4.1", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-m4823/q+obDgJot9+UOtVFij3DJIEYdv7x/AC4rc6KCMfRP4GR030DJyKjhtzC9xfDmTGttv3tMpqbknh6uUXA=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -1042,7 +1044,7 @@ "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], @@ -1294,7 +1296,7 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], @@ -1344,6 +1346,8 @@ "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + "@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="], + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], @@ -1360,7 +1364,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@5.0.108", "", { "dependencies": { "@ai-sdk/gateway": "2.0.18", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Jex3Lb7V41NNpuqJHKgrwoU6BCLHdI1Pg4qb4GJH4jRIDRXUBySJErHjyN4oTCwbiYCeb/8II9EnqSRPq9EifA=="], + "ai": ["ai@7.0.93", "", { "dependencies": { "@ai-sdk/gateway": "4.0.75", "@ai-sdk/provider": "4.0.10", "@ai-sdk/provider-utils": "5.0.36" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CJss6zb9mlltk/mCr8qom20NBnqEQxVawkqwtT62tCwsxilZpXfHNRMRwcS3XRpzdP1kEluVuDBUayYWEEd95g=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -1866,7 +1870,7 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], "exceljs": ["exceljs@4.4.0", "", { "dependencies": { "archiver": "^5.0.0", "dayjs": "^1.8.34", "fast-csv": "^4.3.1", "jszip": "^3.10.1", "readable-stream": "^3.6.0", "saxes": "^5.0.1", "tmp": "^0.2.0", "unzipper": "^0.10.11", "uuid": "^8.3.0" } }, "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg=="], @@ -2638,6 +2642,8 @@ "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], @@ -2986,7 +2992,7 @@ "svix": ["svix@1.76.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "@types/node": "^22.7.5", "es6-promise": "^4.2.8", "fast-sha256": "^1.3.0", "url-parse": "^1.5.10", "uuid": "^10.0.0" } }, "sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw=="], - "swr": ["swr@2.3.8", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w=="], + "swr": ["swr@2.5.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BRw55e8r0B7SpDN20CAzoQAHl7y1yP7/Zt7oqUjMv0vSt2u2Xnkm88Ws+VypbV9BXHQVuSuyVq7zMjO16wSExw=="], "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], @@ -3082,6 +3088,8 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -3238,10 +3246,6 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="], - - "@ai-sdk/react/ai": ["ai@5.0.113", "", { "dependencies": { "@ai-sdk/gateway": "2.0.21", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-26vivpSO/mzZj0k1Si2IpsFspp26ttQICHRySQiMrtWcRd5mnJMX2a8sG28vmZ38C+JUn1cWmfZrsLMxkSMw9g=="], - "@antfu/install-pkg/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -3284,6 +3288,8 @@ "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + "@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@better-auth/core/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -3440,8 +3446,6 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="], - "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-keywords/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -3676,8 +3680,6 @@ "zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="], - "@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BwV7DU/lAm3Xn6iyyvZdWgVxgLu3SNXzl5y57gMvkW4nGhAOV5269IrJzQwGt03bb107sa6H6uJwWxc77zXoGA=="], - "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], "@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], diff --git a/lib/ai/utils.ts b/lib/ai/utils.ts index 7c2a781d..1fce7dc9 100644 --- a/lib/ai/utils.ts +++ b/lib/ai/utils.ts @@ -61,3 +61,46 @@ export function extractDocumentIdFromFilename(filename: string): string | null { const match = filename.match(uuidPattern); return match ? match[1] : null; } + +/** + * Extract the generation config and resolved system instruction from the + * request body the AI SDK recorded for a step. + * + * AI SDK v7 exposes `request.body` as an already-parsed object (typed + * `unknown`); v5 exposed it as a JSON string. Both are handled so the LLM log + * keeps its observability data either way — the string form silently produced + * nulls after the upgrade, and `unknown` means the compiler cannot flag it. + * + * Requires `include: { requestBody: true }` on the call; v7 omits request + * bodies from step results by default. + */ +export function extractRequestMetadata(body: unknown): { + generationConfig: string | null; + systemPrompt: string | null; +} { + let parsed: unknown = body; + + if (typeof parsed === 'string') { + try { + parsed = JSON.parse(parsed); + } catch { + return { generationConfig: null, systemPrompt: null }; + } + } + + if (typeof parsed !== 'object' || parsed === null) { + return { generationConfig: null, systemPrompt: null }; + } + + const { generationConfig, systemInstruction } = parsed as { + generationConfig?: unknown; + systemInstruction?: { parts?: { text?: unknown }[] }; + }; + + const systemPromptText = systemInstruction?.parts?.[0]?.text; + + return { + generationConfig: generationConfig ? JSON.stringify(generationConfig) : null, + systemPrompt: typeof systemPromptText === 'string' ? systemPromptText : null, + }; +} diff --git a/package.json b/package.json index 50f89e50..43959cdb 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "knip": "knip" }, "dependencies": { - "@ai-sdk/google": "^2.0.47", - "@ai-sdk/react": "^2.0.115", + "@ai-sdk/google": "^4.0.64", + "@ai-sdk/react": "^4.0.96", "@aws-sdk/client-s3": "^3.962.0", "@aws-sdk/s3-request-presigner": "^3.962.0", "@dnd-kit/core": "^6.3.1", @@ -82,7 +82,7 @@ "@trpc/react-query": "^11.8.1", "@trpc/server": "^11.8.1", "@types/pg": "^8.16.0", - "ai": "^5.0.108", + "ai": "^7.0.93", "better-auth": "^1.4.10", "bullmq": "^5.66.4", "class-variance-authority": "^0.7.1",