Skip to content

Commit f41fe85

Browse files
Add integration with Valyu Deep Search API
1 parent d0b6f37 commit f41fe85

29 files changed

+2564
-168
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ yarn-error.log*
3636
.env
3737
.vercel
3838
.env*.local
39+
.claude
40+
.cursor
41+
.gitignore
42+
.env.local.example
3943

4044
# Playwright
4145
/test-results/
4246
/playwright-report/
4347
/blob-report/
4448
/playwright/*
49+
50+

app/(chat)/api/chat/route.ts

Lines changed: 87 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
convertToModelMessages,
44
createUIMessageStream,
55
JsonToSseTransformStream,
6-
smoothStream,
76
stepCountIs,
87
streamText,
98
} from "ai";
@@ -26,16 +25,17 @@ import { createDocument } from "@/lib/ai/tools/create-document";
2625
import { getWeather } from "@/lib/ai/tools/get-weather";
2726
import { requestSuggestions } from "@/lib/ai/tools/request-suggestions";
2827
import { updateDocument } from "@/lib/ai/tools/update-document";
28+
import { valyuSearch } from "@/lib/ai/tools/valyu-search";
2929
import { isProductionEnvironment } from "@/lib/constants";
3030
import {
31-
createStreamId,
3231
deleteChatById,
3332
getChatById,
3433
getMessageCountByUserId,
3534
getMessagesByChatId,
3635
saveChat,
3736
saveMessages,
3837
updateChatLastContextById,
38+
updateChatTitleById,
3939
} from "@/lib/db/queries";
4040
import type { DBMessage } from "@/lib/db/schema";
4141
import { ChatSDKError } from "@/lib/errors";
@@ -101,13 +101,22 @@ export async function POST(request: Request) {
101101
message,
102102
selectedChatModel,
103103
selectedVisibilityType,
104+
enableValyuSearch,
104105
}: {
105106
id: string;
106107
message: ChatMessage;
107108
selectedChatModel: ChatModel["id"];
108109
selectedVisibilityType: VisibilityType;
110+
enableValyuSearch?: boolean;
109111
} = requestBody;
110112

113+
console.log("🎯 [CHAT API] Request received:", {
114+
chatId: id,
115+
model: selectedChatModel,
116+
enableValyuSearch,
117+
messagePreview: message.parts[0]?.type === "text" ? message.parts[0].text.substring(0, 100) : "non-text",
118+
});
119+
111120
const session = await auth();
112121

113122
if (!session?.user) {
@@ -135,19 +144,56 @@ export async function POST(request: Request) {
135144
// Only fetch messages if chat already exists
136145
messagesFromDb = await getMessagesByChatId({ id });
137146
} else {
138-
const title = await generateTitleFromUserMessage({
139-
message,
140-
});
141-
147+
// For new chats: First save the chat, THEN save the message (foreign key dependency)
142148
await saveChat({
143149
id,
144150
userId: session.user.id,
145-
title,
151+
title: "New Chat", // Temporary title - will be updated in background
146152
visibility: selectedVisibilityType,
147153
});
154+
155+
// Now save the user message (must happen after chat is created)
156+
await saveMessages({
157+
messages: [
158+
{
159+
chatId: id,
160+
id: message.id,
161+
role: "user",
162+
parts: message.parts,
163+
attachments: [],
164+
createdAt: new Date(),
165+
},
166+
],
167+
});
168+
169+
// Generate proper title in the background (non-blocking)
170+
after(async () => {
171+
try {
172+
const title = await generateTitleFromUserMessage({ message });
173+
await updateChatTitleById({ chatId: id, title });
174+
} catch (err) {
175+
console.warn("Failed to generate chat title for chat", id, err);
176+
}
177+
});
148178
// New chat - no need to fetch messages, it's empty
149179
}
150180

181+
// For existing chats, save the user message only
182+
if (chat) {
183+
await saveMessages({
184+
messages: [
185+
{
186+
chatId: id,
187+
id: message.id,
188+
role: "user",
189+
parts: message.parts,
190+
attachments: [],
191+
createdAt: new Date(),
192+
},
193+
],
194+
});
195+
}
196+
151197
const uiMessages = [...convertToUIMessages(messagesFromDb), message];
152198

153199
const { longitude, latitude, city, country } = geolocation(request);
@@ -159,41 +205,32 @@ export async function POST(request: Request) {
159205
country,
160206
};
161207

162-
await saveMessages({
163-
messages: [
164-
{
165-
chatId: id,
166-
id: message.id,
167-
role: "user",
168-
parts: message.parts,
169-
attachments: [],
170-
createdAt: new Date(),
171-
},
172-
],
173-
});
174-
175-
const streamId = generateUUID();
176-
await createStreamId({ streamId, chatId: id });
208+
// Note: Resumable streams are disabled (see commented code at line 289-297)
209+
// so we don't need to create a stream ID in the database
177210

178211
let finalMergedUsage: AppUsage | undefined;
179212

213+
const activeTools =
214+
selectedChatModel === "chat-model-grok"
215+
? []
216+
: [
217+
"getWeather" as const,
218+
"createDocument" as const,
219+
"updateDocument" as const,
220+
"requestSuggestions" as const,
221+
...(enableValyuSearch ? ["valyuSearch" as const] : []),
222+
];
223+
224+
console.log("🔧 [CHAT API] Active tools:", activeTools);
225+
180226
const stream = createUIMessageStream({
181227
execute: ({ writer: dataStream }) => {
182228
const result = streamText({
183229
model: myProvider.languageModel(selectedChatModel),
184-
system: systemPrompt({ selectedChatModel, requestHints }),
230+
system: systemPrompt({ selectedChatModel, requestHints, enableValyuSearch }),
185231
messages: convertToModelMessages(uiMessages),
186232
stopWhen: stepCountIs(5),
187-
experimental_activeTools:
188-
selectedChatModel === "chat-model-reasoning"
189-
? []
190-
: [
191-
"getWeather",
192-
"createDocument",
193-
"updateDocument",
194-
"requestSuggestions",
195-
],
196-
experimental_transform: smoothStream({ chunking: "word" }),
233+
experimental_activeTools: activeTools,
197234
tools: {
198235
getWeather,
199236
createDocument: createDocument({ session, dataStream }),
@@ -202,41 +239,35 @@ export async function POST(request: Request) {
202239
session,
203240
dataStream,
204241
}),
242+
...(enableValyuSearch ? { valyuSearch } : {}),
205243
},
206244
experimental_telemetry: {
207245
isEnabled: isProductionEnvironment,
208246
functionId: "stream-text",
209247
},
210248
onFinish: async ({ usage }) => {
249+
// Helper to write usage data
250+
const writeUsage = (usageData: AppUsage) => {
251+
finalMergedUsage = usageData;
252+
dataStream.write({ type: "data-usage", data: usageData });
253+
};
254+
211255
try {
256+
const modelId = myProvider.languageModel(selectedChatModel).modelId;
212257
const providers = await getTokenlensCatalog();
213-
const modelId =
214-
myProvider.languageModel(selectedChatModel).modelId;
215-
if (!modelId) {
216-
finalMergedUsage = usage;
217-
dataStream.write({
218-
type: "data-usage",
219-
data: finalMergedUsage,
220-
});
221-
return;
222-
}
223258

224-
if (!providers) {
225-
finalMergedUsage = usage;
226-
dataStream.write({
227-
type: "data-usage",
228-
data: finalMergedUsage,
229-
});
259+
// If we can't enrich the usage data, just send the basic usage
260+
if (!modelId || !providers) {
261+
writeUsage(usage);
230262
return;
231263
}
232264

265+
// Enrich usage data with cost information from TokenLens
233266
const summary = getUsage({ modelId, usage, providers });
234-
finalMergedUsage = { ...usage, ...summary, modelId } as AppUsage;
235-
dataStream.write({ type: "data-usage", data: finalMergedUsage });
267+
writeUsage({ ...usage, ...summary, modelId } as AppUsage);
236268
} catch (err) {
237269
console.warn("TokenLens enrichment failed", err);
238-
finalMergedUsage = usage;
239-
dataStream.write({ type: "data-usage", data: finalMergedUsage });
270+
writeUsage(usage);
240271
}
241272
},
242273
});
@@ -245,7 +276,8 @@ export async function POST(request: Request) {
245276

246277
dataStream.merge(
247278
result.toUIMessageStream({
248-
sendReasoning: true,
279+
// Only send reasoning steps for the reasoning model to reduce overhead
280+
sendReasoning: selectedChatModel === "chat-model-grok",
249281
})
250282
);
251283
},
@@ -278,16 +310,7 @@ export async function POST(request: Request) {
278310
},
279311
});
280312

281-
// const streamContext = getStreamContext();
282-
283-
// if (streamContext) {
284-
// return new Response(
285-
// await streamContext.resumableStream(streamId, () =>
286-
// stream.pipeThrough(new JsonToSseTransformStream())
287-
// )
288-
// );
289-
// }
290-
313+
291314
return new Response(stream.pipeThrough(new JsonToSseTransformStream()));
292315
} catch (error) {
293316
const vercelId = request.headers.get("x-vercel-id");

app/(chat)/api/chat/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const postRequestBodySchema = z.object({
2323
}),
2424
selectedChatModel: z.enum(["chat-model", "chat-model-reasoning"]),
2525
selectedVisibilityType: z.enum(["public", "private"]),
26+
enableValyuSearch: z.boolean().optional().default(false),
2627
});
2728

2829
export type PostRequestBody = z.infer<typeof postRequestBodySchema>;

app/(chat)/page.tsx

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { cookies } from "next/headers";
22
import { redirect } from "next/navigation";
33
import { Chat } from "@/components/chat";
44
import { DataStreamHandler } from "@/components/data-stream-handler";
5-
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
5+
import { DEFAULT_CHAT_MODEL, chatModels } from "@/lib/ai/models";
66
import { generateUUID } from "@/lib/utils";
77
import { auth } from "../(auth)/auth";
88

@@ -18,29 +18,18 @@ export default async function Page() {
1818
const cookieStore = await cookies();
1919
const modelIdFromCookie = cookieStore.get("chat-model");
2020

21-
if (!modelIdFromCookie) {
22-
return (
23-
<>
24-
<Chat
25-
autoResume={false}
26-
id={id}
27-
initialChatModel={DEFAULT_CHAT_MODEL}
28-
initialMessages={[]}
29-
initialVisibilityType="private"
30-
isReadonly={false}
31-
key={id}
32-
/>
33-
<DataStreamHandler />
34-
</>
35-
);
36-
}
21+
// Validate that the model from cookie actually exists
22+
const validModelId =
23+
modelIdFromCookie && chatModels.find((m) => m.id === modelIdFromCookie.value)
24+
? modelIdFromCookie.value
25+
: DEFAULT_CHAT_MODEL;
3726

3827
return (
3928
<>
4029
<Chat
4130
autoResume={false}
4231
id={id}
43-
initialChatModel={modelIdFromCookie.value}
32+
initialChatModel={validModelId}
4433
initialMessages={[]}
4534
initialVisibilityType="private"
4635
isReadonly={false}

0 commit comments

Comments
 (0)