🎄 merry christmas: ai sdk v6 beta + tool approval (#1361)
This commit is contained in:
parent
6e5b883cf2
commit
4d3ba8d9fe
21 changed files with 429 additions and 202 deletions
|
|
@ -13,9 +13,7 @@ import {
|
|||
type ResumableStreamContext,
|
||||
} from "resumable-stream";
|
||||
import { auth, type UserType } from "@/app/(auth)/auth";
|
||||
import type { VisibilityType } from "@/components/visibility-selector";
|
||||
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
||||
import type { ChatModel } from "@/lib/ai/models";
|
||||
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
|
||||
import { getLanguageModel } from "@/lib/ai/providers";
|
||||
import { createDocument } from "@/lib/ai/tools/create-document";
|
||||
|
|
@ -32,6 +30,7 @@ import {
|
|||
saveChat,
|
||||
saveMessages,
|
||||
updateChatTitleById,
|
||||
updateMessage,
|
||||
} from "@/lib/db/queries";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
|
|
@ -75,17 +74,8 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
id,
|
||||
message,
|
||||
selectedChatModel,
|
||||
selectedVisibilityType,
|
||||
}: {
|
||||
id: string;
|
||||
message: ChatMessage;
|
||||
selectedChatModel: ChatModel["id"];
|
||||
selectedVisibilityType: VisibilityType;
|
||||
} = requestBody;
|
||||
const { id, message, messages, selectedChatModel, selectedVisibilityType } =
|
||||
requestBody;
|
||||
|
||||
const session = await auth();
|
||||
|
||||
|
|
@ -104,6 +94,9 @@ export async function POST(request: Request) {
|
|||
return new ChatSDKError("rate_limit:chat").toResponse();
|
||||
}
|
||||
|
||||
// Check if this is a tool approval flow (all messages sent)
|
||||
const isToolApprovalFlow = Boolean(messages);
|
||||
|
||||
const chat = await getChatById({ id });
|
||||
let messagesFromDb: DBMessage[] = [];
|
||||
let titlePromise: Promise<string> | null = null;
|
||||
|
|
@ -112,9 +105,11 @@ export async function POST(request: Request) {
|
|||
if (chat.userId !== session.user.id) {
|
||||
return new ChatSDKError("forbidden:chat").toResponse();
|
||||
}
|
||||
// Only fetch messages if chat already exists
|
||||
messagesFromDb = await getMessagesByChatId({ id });
|
||||
} else {
|
||||
// Only fetch messages if chat already exists and not tool approval
|
||||
if (!isToolApprovalFlow) {
|
||||
messagesFromDb = await getMessagesByChatId({ id });
|
||||
}
|
||||
} else if (message?.role === "user") {
|
||||
// Save chat immediately with placeholder title
|
||||
await saveChat({
|
||||
id,
|
||||
|
|
@ -127,7 +122,10 @@ export async function POST(request: Request) {
|
|||
titlePromise = generateTitleFromUserMessage({ message });
|
||||
}
|
||||
|
||||
const uiMessages = [...convertToUIMessages(messagesFromDb), message];
|
||||
// Use all messages for tool approval, otherwise DB messages + new message
|
||||
const uiMessages = isToolApprovalFlow
|
||||
? (messages as ChatMessage[])
|
||||
: [...convertToUIMessages(messagesFromDb), message as ChatMessage];
|
||||
|
||||
const { longitude, latitude, city, country } = geolocation(request);
|
||||
|
||||
|
|
@ -138,24 +136,29 @@ export async function POST(request: Request) {
|
|||
country,
|
||||
};
|
||||
|
||||
await saveMessages({
|
||||
messages: [
|
||||
{
|
||||
chatId: id,
|
||||
id: message.id,
|
||||
role: "user",
|
||||
parts: message.parts,
|
||||
attachments: [],
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
// Only save user messages to the database (not tool approval responses)
|
||||
if (message?.role === "user") {
|
||||
await saveMessages({
|
||||
messages: [
|
||||
{
|
||||
chatId: id,
|
||||
id: message.id,
|
||||
role: "user",
|
||||
parts: message.parts,
|
||||
attachments: [],
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const streamId = generateUUID();
|
||||
await createStreamId({ streamId, chatId: id });
|
||||
|
||||
const stream = createUIMessageStream({
|
||||
execute: ({ writer: dataStream }) => {
|
||||
// Pass original messages for tool approval continuation
|
||||
originalMessages: isToolApprovalFlow ? uiMessages : undefined,
|
||||
execute: async ({ writer: dataStream }) => {
|
||||
// Handle title generation in parallel
|
||||
if (titlePromise) {
|
||||
titlePromise.then((title) => {
|
||||
|
|
@ -171,7 +174,7 @@ export async function POST(request: Request) {
|
|||
const result = streamText({
|
||||
model: getLanguageModel(selectedChatModel),
|
||||
system: systemPrompt({ selectedChatModel, requestHints }),
|
||||
messages: convertToModelMessages(uiMessages),
|
||||
messages: await convertToModelMessages(uiMessages),
|
||||
stopWhen: stepCountIs(5),
|
||||
experimental_activeTools: isReasoningModel
|
||||
? []
|
||||
|
|
@ -215,32 +218,67 @@ export async function POST(request: Request) {
|
|||
);
|
||||
},
|
||||
generateId: generateUUID,
|
||||
onFinish: async ({ messages }) => {
|
||||
await saveMessages({
|
||||
messages: messages.map((currentMessage) => ({
|
||||
id: currentMessage.id,
|
||||
role: currentMessage.role,
|
||||
parts: currentMessage.parts,
|
||||
createdAt: new Date(),
|
||||
attachments: [],
|
||||
chatId: id,
|
||||
})),
|
||||
});
|
||||
onFinish: async ({ messages: finishedMessages }) => {
|
||||
if (isToolApprovalFlow) {
|
||||
// For tool approval, update existing messages (tool state changed) and save new ones
|
||||
for (const finishedMsg of finishedMessages) {
|
||||
const existingMsg = uiMessages.find((m) => m.id === finishedMsg.id);
|
||||
if (existingMsg) {
|
||||
// Update existing message with new parts (tool state changed)
|
||||
await updateMessage({
|
||||
id: finishedMsg.id,
|
||||
parts: finishedMsg.parts,
|
||||
});
|
||||
} else {
|
||||
// Save new message
|
||||
await saveMessages({
|
||||
messages: [
|
||||
{
|
||||
id: finishedMsg.id,
|
||||
role: finishedMsg.role,
|
||||
parts: finishedMsg.parts,
|
||||
createdAt: new Date(),
|
||||
attachments: [],
|
||||
chatId: id,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (finishedMessages.length > 0) {
|
||||
// Normal flow - save all finished messages
|
||||
await saveMessages({
|
||||
messages: finishedMessages.map((currentMessage) => ({
|
||||
id: currentMessage.id,
|
||||
role: currentMessage.role,
|
||||
parts: currentMessage.parts,
|
||||
createdAt: new Date(),
|
||||
attachments: [],
|
||||
chatId: id,
|
||||
})),
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
return "Oops, an error occurred!";
|
||||
},
|
||||
});
|
||||
|
||||
// const streamContext = getStreamContext();
|
||||
const streamContext = getStreamContext();
|
||||
|
||||
// if (streamContext) {
|
||||
// return new Response(
|
||||
// await streamContext.resumableStream(streamId, () =>
|
||||
// stream.pipeThrough(new JsonToSseTransformStream())
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
if (streamContext) {
|
||||
try {
|
||||
const resumableStream = await streamContext.resumableStream(
|
||||
streamId,
|
||||
() => stream.pipeThrough(new JsonToSseTransformStream())
|
||||
);
|
||||
if (resumableStream) {
|
||||
return new Response(resumableStream);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create resumable stream:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(stream.pipeThrough(new JsonToSseTransformStream()));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,24 @@ const filePartSchema = z.object({
|
|||
|
||||
const partSchema = z.union([textPartSchema, filePartSchema]);
|
||||
|
||||
const userMessageSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.enum(["user"]),
|
||||
parts: z.array(partSchema),
|
||||
});
|
||||
|
||||
// For tool approval flows, we accept all messages (more permissive schema)
|
||||
const messageSchema = z.object({
|
||||
id: z.string(),
|
||||
role: z.string(),
|
||||
parts: z.array(z.any()),
|
||||
});
|
||||
|
||||
export const postRequestBodySchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
message: z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.enum(["user"]),
|
||||
parts: z.array(partSchema),
|
||||
}),
|
||||
// Either a single new message or all messages (for tool approvals)
|
||||
message: userMessageSchema.optional(),
|
||||
messages: z.array(messageSchema).optional(),
|
||||
selectedChatModel: z.string(),
|
||||
selectedVisibilityType: z.enum(["public", "private"]),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@ export const ConfirmationRequest = ({ children }: ConfirmationRequestProps) => {
|
|||
const { state } = useConfirmation();
|
||||
|
||||
// Only show when approval is requested
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
if (state !== "approval-requested") {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -117,9 +116,7 @@ export const ConfirmationAccepted = ({
|
|||
// Only show when approved and in response states
|
||||
if (
|
||||
!approval?.approved ||
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
(state !== "approval-responded" &&
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
state !== "output-denied" &&
|
||||
state !== "output-available")
|
||||
) {
|
||||
|
|
@ -141,9 +138,7 @@ export const ConfirmationRejected = ({
|
|||
// Only show when rejected and in response states
|
||||
if (
|
||||
approval?.approved !== false ||
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
(state !== "approval-responded" &&
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
state !== "output-denied" &&
|
||||
state !== "output-available")
|
||||
) {
|
||||
|
|
@ -162,7 +157,6 @@ export const ConfirmationActions = ({
|
|||
const { state } = useConfirmation();
|
||||
|
||||
// Only show when approval is requested
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
if (state !== "approval-requested") {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ const getStatusBadge = (status: ToolUIPart["state"]) => {
|
|||
const labels: Record<ToolUIPart["state"], string> = {
|
||||
"input-streaming": "Pending",
|
||||
"input-available": "Running",
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
"approval-requested": "Awaiting Approval",
|
||||
"approval-responded": "Responded",
|
||||
"output-available": "Completed",
|
||||
|
|
@ -51,7 +50,6 @@ const getStatusBadge = (status: ToolUIPart["state"]) => {
|
|||
const icons: Record<ToolUIPart["state"], ReactNode> = {
|
||||
"input-streaming": <CircleIcon className="size-4" />,
|
||||
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
||||
// @ts-expect-error state only available in AI SDK v6
|
||||
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
|
||||
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
|
||||
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type { UIArtifact } from "./artifact";
|
|||
import { PreviewMessage, ThinkingMessage } from "./message";
|
||||
|
||||
type ArtifactMessagesProps = {
|
||||
addToolApprovalResponse: UseChatHelpers<ChatMessage>["addToolApprovalResponse"];
|
||||
chatId: string;
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
votes: Vote[] | undefined;
|
||||
|
|
@ -20,6 +21,7 @@ type ArtifactMessagesProps = {
|
|||
};
|
||||
|
||||
function PureArtifactMessages({
|
||||
addToolApprovalResponse,
|
||||
chatId,
|
||||
status,
|
||||
votes,
|
||||
|
|
@ -45,6 +47,7 @@ function PureArtifactMessages({
|
|||
>
|
||||
{messages.map((message, index) => (
|
||||
<PreviewMessage
|
||||
addToolApprovalResponse={addToolApprovalResponse}
|
||||
chatId={chatId}
|
||||
isLoading={status === "streaming" && index === messages.length - 1}
|
||||
isReadonly={isReadonly}
|
||||
|
|
@ -64,7 +67,12 @@ function PureArtifactMessages({
|
|||
))}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{status === "submitted" && <ThinkingMessage key="thinking" />}
|
||||
{status === "submitted" &&
|
||||
!messages.some((msg) =>
|
||||
msg.parts?.some(
|
||||
(part) => "state" in part && part.state === "approval-responded"
|
||||
)
|
||||
) && <ThinkingMessage key="thinking" />}
|
||||
</AnimatePresence>
|
||||
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export type UIArtifact = {
|
|||
};
|
||||
|
||||
function PureArtifact({
|
||||
addToolApprovalResponse,
|
||||
chatId,
|
||||
input,
|
||||
setInput,
|
||||
|
|
@ -69,6 +70,7 @@ function PureArtifact({
|
|||
selectedVisibilityType,
|
||||
selectedModelId,
|
||||
}: {
|
||||
addToolApprovalResponse: UseChatHelpers<ChatMessage>["addToolApprovalResponse"];
|
||||
chatId: string;
|
||||
input: string;
|
||||
setInput: Dispatch<SetStateAction<string>>;
|
||||
|
|
@ -320,6 +322,7 @@ function PureArtifact({
|
|||
|
||||
<div className="flex h-full flex-col items-center justify-between">
|
||||
<ArtifactMessages
|
||||
addToolApprovalResponse={addToolApprovalResponse}
|
||||
artifactStatus={artifact.status}
|
||||
chatId={chatId}
|
||||
isReadonly={isReadonly}
|
||||
|
|
|
|||
|
|
@ -85,19 +85,54 @@ export function Chat({
|
|||
stop,
|
||||
regenerate,
|
||||
resumeStream,
|
||||
addToolApprovalResponse,
|
||||
} = useChat<ChatMessage>({
|
||||
id,
|
||||
messages: initialMessages,
|
||||
experimental_throttle: 100,
|
||||
generateId: generateUUID,
|
||||
// Auto-continue after tool approval (only for APPROVED tools)
|
||||
// Denied tools don't need server continuation - state is saved on next user message
|
||||
sendAutomaticallyWhen: ({ messages: currentMessages }) => {
|
||||
const lastMessage = currentMessages.at(-1);
|
||||
// Only continue if a tool was APPROVED (not denied)
|
||||
const shouldContinue =
|
||||
lastMessage?.parts?.some(
|
||||
(part) =>
|
||||
"state" in part &&
|
||||
part.state === "approval-responded" &&
|
||||
"approval" in part &&
|
||||
(part.approval as { approved?: boolean })?.approved === true
|
||||
) ?? false;
|
||||
return shouldContinue;
|
||||
},
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/chat",
|
||||
fetch: fetchWithErrorHandlers,
|
||||
prepareSendMessagesRequest(request) {
|
||||
const lastMessage = request.messages.at(-1);
|
||||
|
||||
// Check if this is a tool approval continuation:
|
||||
// - Last message is NOT a user message (meaning no new user input)
|
||||
// - OR any message has tool parts that were responded to (approved or denied)
|
||||
const isToolApprovalContinuation =
|
||||
lastMessage?.role !== "user" ||
|
||||
request.messages.some((msg) =>
|
||||
msg.parts?.some((part) => {
|
||||
const state = (part as { state?: string }).state;
|
||||
return (
|
||||
state === "approval-responded" || state === "output-denied"
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
body: {
|
||||
id: request.id,
|
||||
message: request.messages.at(-1),
|
||||
// Send all messages for tool approval continuation, otherwise just the last user message
|
||||
...(isToolApprovalContinuation
|
||||
? { messages: request.messages }
|
||||
: { message: lastMessage }),
|
||||
selectedChatModel: currentModelIdRef.current,
|
||||
selectedVisibilityType: visibilityType,
|
||||
...request.body,
|
||||
|
|
@ -170,6 +205,7 @@ export function Chat({
|
|||
/>
|
||||
|
||||
<Messages
|
||||
addToolApprovalResponse={addToolApprovalResponse}
|
||||
chatId={id}
|
||||
isArtifactVisible={isArtifactVisible}
|
||||
isReadonly={isReadonly}
|
||||
|
|
@ -203,6 +239,7 @@ export function Chat({
|
|||
</div>
|
||||
|
||||
<Artifact
|
||||
addToolApprovalResponse={addToolApprovalResponse}
|
||||
attachments={attachments}
|
||||
chatId={id}
|
||||
input={input}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export function DocumentPreview({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full cursor-pointer">
|
||||
<div className="relative w-full max-w-[450px] cursor-pointer">
|
||||
<HitboxLayer
|
||||
hitboxRef={hitboxRef}
|
||||
result={result}
|
||||
|
|
@ -119,7 +119,7 @@ export function DocumentPreview({
|
|||
}
|
||||
|
||||
const LoadingSkeleton = ({ artifactKind }: { artifactKind: ArtifactKind }) => (
|
||||
<div className="w-full">
|
||||
<div className="w-full max-w-[450px]">
|
||||
<div className="flex h-[57px] flex-row items-center justify-between gap-2 rounded-t-2xl border border-b-0 p-4 dark:border-zinc-700 dark:bg-muted">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<div className="text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -35,19 +35,25 @@ export type ToolHeaderProps = {
|
|||
};
|
||||
|
||||
const getStatusBadge = (status: ToolUIPart["state"]) => {
|
||||
const labels = {
|
||||
const labels: Record<ToolUIPart["state"], string> = {
|
||||
"input-streaming": "Pending",
|
||||
"input-available": "Running",
|
||||
"approval-requested": "Pending",
|
||||
"approval-responded": "Approved",
|
||||
"output-available": "Completed",
|
||||
"output-error": "Error",
|
||||
} as const;
|
||||
"output-denied": "Denied",
|
||||
};
|
||||
|
||||
const icons = {
|
||||
const icons: Record<ToolUIPart["state"], ReactNode> = {
|
||||
"input-streaming": <CircleIcon className="size-4" />,
|
||||
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
||||
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
|
||||
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
|
||||
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
||||
"output-error": <XCircleIcon className="size-4 text-red-600" />,
|
||||
} as const;
|
||||
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<Badge
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { PreviewAttachment } from "./preview-attachment";
|
|||
import { Weather } from "./weather";
|
||||
|
||||
const PurePreviewMessage = ({
|
||||
addToolApprovalResponse,
|
||||
chatId,
|
||||
message,
|
||||
vote,
|
||||
|
|
@ -34,6 +35,7 @@ const PurePreviewMessage = ({
|
|||
isReadonly,
|
||||
requiresScrollPadding: _requiresScrollPadding,
|
||||
}: {
|
||||
addToolApprovalResponse: UseChatHelpers<ChatMessage>["addToolApprovalResponse"];
|
||||
chatId: string;
|
||||
message: ChatMessage;
|
||||
vote: Vote | undefined;
|
||||
|
|
@ -76,9 +78,10 @@ const PurePreviewMessage = ({
|
|||
),
|
||||
"w-full":
|
||||
(message.role === "assistant" &&
|
||||
message.parts?.some(
|
||||
(message.parts?.some(
|
||||
(p) => p.type === "text" && p.text?.trim()
|
||||
)) ||
|
||||
) ||
|
||||
message.parts?.some((p) => p.type.startsWith("tool-")))) ||
|
||||
mode === "edit",
|
||||
"max-w-[calc(100%-2.5rem)] sm:max-w-[min(fit-content,80%)]":
|
||||
message.role === "user" && mode !== "edit",
|
||||
|
|
@ -122,7 +125,7 @@ const PurePreviewMessage = ({
|
|||
<div key={key}>
|
||||
<MessageContent
|
||||
className={cn({
|
||||
"w-fit break-words rounded-2xl px-3 py-2 text-right text-white":
|
||||
"wrap-break-word w-fit rounded-2xl px-3 py-2 text-right text-white":
|
||||
message.role === "user",
|
||||
"bg-transparent px-0 py-0 text-left":
|
||||
message.role === "assistant",
|
||||
|
|
@ -163,22 +166,95 @@ const PurePreviewMessage = ({
|
|||
|
||||
if (type === "tool-getWeather") {
|
||||
const { toolCallId, state } = part;
|
||||
const approvalId = (part as { approval?: { id: string } })
|
||||
.approval?.id;
|
||||
const isDenied =
|
||||
state === "output-denied" ||
|
||||
(state === "approval-responded" &&
|
||||
(part as { approval?: { approved?: boolean } }).approval
|
||||
?.approved === false);
|
||||
const widthClass = "w-[min(100%,450px)]";
|
||||
|
||||
if (state === "output-available") {
|
||||
return (
|
||||
<div className={widthClass} key={toolCallId}>
|
||||
<Weather weatherAtLocation={part.output} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDenied) {
|
||||
return (
|
||||
<div className={widthClass} key={toolCallId}>
|
||||
<Tool className="w-full" defaultOpen={true}>
|
||||
<ToolHeader
|
||||
state="output-denied"
|
||||
type="tool-getWeather"
|
||||
/>
|
||||
<ToolContent>
|
||||
<div className="px-4 py-3 text-muted-foreground text-sm">
|
||||
Weather lookup was denied.
|
||||
</div>
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === "approval-responded") {
|
||||
return (
|
||||
<div className={widthClass} key={toolCallId}>
|
||||
<Tool className="w-full" defaultOpen={true}>
|
||||
<ToolHeader state={state} type="tool-getWeather" />
|
||||
<ToolContent>
|
||||
<ToolInput input={part.input} />
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tool defaultOpen={true} key={toolCallId}>
|
||||
<ToolHeader state={state} type="tool-getWeather" />
|
||||
<ToolContent>
|
||||
{state === "input-available" && (
|
||||
<ToolInput input={part.input} />
|
||||
)}
|
||||
{state === "output-available" && (
|
||||
<ToolOutput
|
||||
errorText={undefined}
|
||||
output={<Weather weatherAtLocation={part.output} />}
|
||||
/>
|
||||
)}
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
<div className={widthClass} key={toolCallId}>
|
||||
<Tool className="w-full" defaultOpen={true}>
|
||||
<ToolHeader state={state} type="tool-getWeather" />
|
||||
<ToolContent>
|
||||
{(state === "input-available" ||
|
||||
state === "approval-requested") && (
|
||||
<ToolInput input={part.input} />
|
||||
)}
|
||||
{state === "approval-requested" && approvalId && (
|
||||
<div className="flex items-center justify-end gap-2 border-t px-4 py-3">
|
||||
<button
|
||||
className="rounded-md px-3 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={() => {
|
||||
addToolApprovalResponse({
|
||||
id: approvalId,
|
||||
approved: false,
|
||||
reason: "User denied weather lookup",
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Deny
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-sm transition-colors hover:bg-primary/90"
|
||||
onClick={() => {
|
||||
addToolApprovalResponse({
|
||||
id: approvalId,
|
||||
approved: true,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Allow
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -285,22 +361,15 @@ const PurePreviewMessage = ({
|
|||
export const PreviewMessage = memo(
|
||||
PurePreviewMessage,
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.isLoading !== nextProps.isLoading) {
|
||||
return false;
|
||||
if (
|
||||
prevProps.isLoading === nextProps.isLoading &&
|
||||
prevProps.message.id === nextProps.message.id &&
|
||||
prevProps.requiresScrollPadding === nextProps.requiresScrollPadding &&
|
||||
equal(prevProps.message.parts, nextProps.message.parts) &&
|
||||
equal(prevProps.vote, nextProps.vote)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (prevProps.message.id !== nextProps.message.id) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.requiresScrollPadding !== nextProps.requiresScrollPadding) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.message.parts, nextProps.message.parts)) {
|
||||
return false;
|
||||
}
|
||||
if (!equal(prevProps.vote, nextProps.vote)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Greeting } from "./greeting";
|
|||
import { PreviewMessage, ThinkingMessage } from "./message";
|
||||
|
||||
type MessagesProps = {
|
||||
addToolApprovalResponse: UseChatHelpers<ChatMessage>["addToolApprovalResponse"];
|
||||
chatId: string;
|
||||
status: UseChatHelpers<ChatMessage>["status"];
|
||||
votes: Vote[] | undefined;
|
||||
|
|
@ -22,6 +23,7 @@ type MessagesProps = {
|
|||
};
|
||||
|
||||
function PureMessages({
|
||||
addToolApprovalResponse,
|
||||
chatId,
|
||||
status,
|
||||
votes,
|
||||
|
|
@ -54,6 +56,7 @@ function PureMessages({
|
|||
|
||||
{messages.map((message, index) => (
|
||||
<PreviewMessage
|
||||
addToolApprovalResponse={addToolApprovalResponse}
|
||||
chatId={chatId}
|
||||
isLoading={
|
||||
status === "streaming" && messages.length - 1 === index
|
||||
|
|
@ -74,7 +77,12 @@ function PureMessages({
|
|||
/>
|
||||
))}
|
||||
|
||||
{status === "submitted" && <ThinkingMessage />}
|
||||
{status === "submitted" &&
|
||||
!messages.some((msg) =>
|
||||
msg.parts?.some(
|
||||
(part) => "state" in part && part.state === "approval-responded"
|
||||
)
|
||||
) && <ThinkingMessage />}
|
||||
|
||||
<div
|
||||
className="min-h-[24px] min-w-[24px] shrink-0"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const entitlementsByUserType: Record<UserType, Entitlements> = {
|
|||
* For users without an account
|
||||
*/
|
||||
guest: {
|
||||
maxMessagesPerDay: 10,
|
||||
maxMessagesPerDay: 20,
|
||||
},
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ const mockResponses: Record<string, string> = {
|
|||
greeting: "Hello! How can I help you today?",
|
||||
};
|
||||
|
||||
const mockUsage = {
|
||||
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
|
||||
outputTokens: { total: 20, text: 20, reasoning: 0 },
|
||||
};
|
||||
|
||||
function getResponseForPrompt(prompt: unknown): string {
|
||||
const promptStr = JSON.stringify(prompt).toLowerCase();
|
||||
|
||||
|
|
@ -25,17 +30,14 @@ function getResponseForPrompt(prompt: unknown): string {
|
|||
|
||||
const createMockModel = (): LanguageModel => {
|
||||
return {
|
||||
specificationVersion: "v2",
|
||||
specificationVersion: "v3",
|
||||
provider: "mock",
|
||||
modelId: "mock-model",
|
||||
defaultObjectGenerationMode: "tool",
|
||||
supportedUrls: [],
|
||||
supportsImageUrls: false,
|
||||
supportsStructuredOutputs: false,
|
||||
supportedUrls: {},
|
||||
doGenerate: async ({ prompt }: { prompt: unknown }) => ({
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
||||
usage: mockUsage,
|
||||
content: [{ type: "text", text: getResponseForPrompt(prompt) }],
|
||||
warnings: [],
|
||||
}),
|
||||
|
|
@ -46,24 +48,26 @@ const createMockModel = (): LanguageModel => {
|
|||
return {
|
||||
stream: new ReadableStream({
|
||||
async start(controller) {
|
||||
controller.enqueue({ type: "text-start", id: "t1" });
|
||||
for (const word of words) {
|
||||
controller.enqueue({
|
||||
type: "text-delta",
|
||||
textDelta: `${word} `,
|
||||
id: "t1",
|
||||
delta: `${word} `,
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
}
|
||||
controller.enqueue({ type: "text-end", id: "t1" });
|
||||
controller.enqueue({
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20 },
|
||||
usage: mockUsage,
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
};
|
||||
},
|
||||
} as unknown as LanguageModel;
|
||||
|
|
@ -71,17 +75,14 @@ const createMockModel = (): LanguageModel => {
|
|||
|
||||
const createMockReasoningModel = (): LanguageModel => {
|
||||
return {
|
||||
specificationVersion: "v2",
|
||||
specificationVersion: "v3",
|
||||
provider: "mock",
|
||||
modelId: "mock-reasoning-model",
|
||||
defaultObjectGenerationMode: "tool",
|
||||
supportedUrls: [],
|
||||
supportsImageUrls: false,
|
||||
supportsStructuredOutputs: false,
|
||||
supportedUrls: {},
|
||||
doGenerate: async () => ({
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
||||
usage: mockUsage,
|
||||
content: [{ type: "text", text: "This is a reasoned response." }],
|
||||
reasoning: [
|
||||
{ type: "text", text: "Let me think through this step by step..." },
|
||||
|
|
@ -91,62 +92,77 @@ const createMockReasoningModel = (): LanguageModel => {
|
|||
doStream: () => ({
|
||||
stream: new ReadableStream({
|
||||
async start(controller) {
|
||||
controller.enqueue({ type: "reasoning-start", id: "r1" });
|
||||
controller.enqueue({
|
||||
type: "reasoning",
|
||||
textDelta: "Let me think through this step by step... ",
|
||||
type: "reasoning-delta",
|
||||
id: "r1",
|
||||
delta: "Let me think through this step by step... ",
|
||||
});
|
||||
controller.enqueue({ type: "reasoning-end", id: "r1" });
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
controller.enqueue({ type: "text-start", id: "t1" });
|
||||
controller.enqueue({
|
||||
type: "text-delta",
|
||||
textDelta: "This is a reasoned response.",
|
||||
id: "t1",
|
||||
delta: "This is a reasoned response.",
|
||||
});
|
||||
controller.enqueue({ type: "text-end", id: "t1" });
|
||||
controller.enqueue({
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20 },
|
||||
usage: mockUsage,
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
}),
|
||||
} as unknown as LanguageModel;
|
||||
};
|
||||
|
||||
const createMockTitleModel = (): LanguageModel => {
|
||||
return {
|
||||
specificationVersion: "v2",
|
||||
specificationVersion: "v3",
|
||||
provider: "mock",
|
||||
modelId: "mock-title-model",
|
||||
defaultObjectGenerationMode: "tool",
|
||||
supportedUrls: [],
|
||||
supportsImageUrls: false,
|
||||
supportsStructuredOutputs: false,
|
||||
supportedUrls: {},
|
||||
doGenerate: async () => ({
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 5, outputTokens: 5, totalTokens: 10 },
|
||||
usage: {
|
||||
inputTokens: { total: 5, noCache: 5, cacheRead: 0, cacheWrite: 0 },
|
||||
outputTokens: { total: 5, text: 5, reasoning: 0 },
|
||||
},
|
||||
content: [{ type: "text", text: "Test Conversation" }],
|
||||
warnings: [],
|
||||
}),
|
||||
doStream: () => ({
|
||||
stream: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue({ type: "text-start", id: "t1" });
|
||||
controller.enqueue({
|
||||
type: "text-delta",
|
||||
textDelta: "Test Conversation",
|
||||
id: "t1",
|
||||
delta: "Test Conversation",
|
||||
});
|
||||
controller.enqueue({ type: "text-end", id: "t1" });
|
||||
controller.enqueue({
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 5, outputTokens: 5 },
|
||||
usage: {
|
||||
inputTokens: {
|
||||
total: 5,
|
||||
noCache: 5,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
outputTokens: { total: 5, text: 5, reasoning: 0 },
|
||||
},
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
}),
|
||||
} as unknown as LanguageModel;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { simulateReadableStream } from "ai";
|
||||
import { MockLanguageModelV2 } from "ai/test";
|
||||
import { MockLanguageModelV3 } from "ai/test";
|
||||
import { getResponseChunksByPrompt } from "@/tests/prompts/utils";
|
||||
|
||||
export const chatModel = new MockLanguageModelV2({
|
||||
const mockUsage = {
|
||||
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
|
||||
outputTokens: { total: 20, text: 20, reasoning: 0 },
|
||||
};
|
||||
|
||||
export const chatModel = new MockLanguageModelV3({
|
||||
doGenerate: async () => ({
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
||||
usage: mockUsage,
|
||||
content: [{ type: "text", text: "Hello, world!" }],
|
||||
warnings: [],
|
||||
}),
|
||||
|
|
@ -16,15 +20,13 @@ export const chatModel = new MockLanguageModelV2({
|
|||
initialDelayInMs: 1000,
|
||||
chunks: getResponseChunksByPrompt(prompt),
|
||||
}),
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
}),
|
||||
});
|
||||
|
||||
export const reasoningModel = new MockLanguageModelV2({
|
||||
export const reasoningModel = new MockLanguageModelV3({
|
||||
doGenerate: async () => ({
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
||||
usage: mockUsage,
|
||||
content: [{ type: "text", text: "Hello, world!" }],
|
||||
warnings: [],
|
||||
}),
|
||||
|
|
@ -34,15 +36,13 @@ export const reasoningModel = new MockLanguageModelV2({
|
|||
initialDelayInMs: 1000,
|
||||
chunks: getResponseChunksByPrompt(prompt, true),
|
||||
}),
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
}),
|
||||
});
|
||||
|
||||
export const titleModel = new MockLanguageModelV2({
|
||||
export const titleModel = new MockLanguageModelV3({
|
||||
doGenerate: async () => ({
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
||||
usage: mockUsage,
|
||||
content: [{ type: "text", text: "This is a test title" }],
|
||||
warnings: [],
|
||||
}),
|
||||
|
|
@ -57,19 +57,17 @@ export const titleModel = new MockLanguageModelV2({
|
|||
{
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13 },
|
||||
usage: mockUsage,
|
||||
},
|
||||
],
|
||||
}),
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
}),
|
||||
});
|
||||
|
||||
export const artifactModel = new MockLanguageModelV2({
|
||||
export const artifactModel = new MockLanguageModelV3({
|
||||
doGenerate: async () => ({
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
finishReason: "stop",
|
||||
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
||||
usage: mockUsage,
|
||||
content: [{ type: "text", text: "Hello, world!" }],
|
||||
warnings: [],
|
||||
}),
|
||||
|
|
@ -79,6 +77,5 @@ export const artifactModel = new MockLanguageModelV2({
|
|||
initialDelayInMs: 100,
|
||||
chunks: getResponseChunksByPrompt(prompt),
|
||||
}),
|
||||
rawCall: { rawPrompt: null, rawSettings: {} },
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export const getWeather = tool({
|
|||
.describe("City name (e.g., 'San Francisco', 'New York', 'London')")
|
||||
.optional(),
|
||||
}),
|
||||
needsApproval: true,
|
||||
execute: async (input) => {
|
||||
let latitude: number;
|
||||
let longitude: number;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ config({
|
|||
|
||||
const runMigrate = async () => {
|
||||
if (!process.env.POSTGRES_URL) {
|
||||
throw new Error("POSTGRES_URL is not defined");
|
||||
console.log("⏭️ POSTGRES_URL not defined, skipping migrations");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const connection = postgres(process.env.POSTGRES_URL, { max: 1 });
|
||||
|
|
|
|||
|
|
@ -250,6 +250,20 @@ export async function saveMessages({ messages }: { messages: DBMessage[] }) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function updateMessage({
|
||||
id,
|
||||
parts,
|
||||
}: {
|
||||
id: string;
|
||||
parts: DBMessage["parts"];
|
||||
}) {
|
||||
try {
|
||||
return await db.update(message).set({ parts }).where(eq(message.id, id));
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError("bad_request:database", "Failed to update message");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMessagesByChatId({ id }: { id: string }) {
|
||||
try {
|
||||
return await db
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type {
|
||||
CoreAssistantMessage,
|
||||
CoreToolMessage,
|
||||
AssistantModelMessage,
|
||||
ToolModelMessage,
|
||||
UIMessage,
|
||||
UIMessagePart,
|
||||
} from 'ai';
|
||||
|
|
@ -63,7 +63,7 @@ export function generateUUID(): string {
|
|||
});
|
||||
}
|
||||
|
||||
type ResponseMessageWithoutId = CoreToolMessage | CoreAssistantMessage;
|
||||
type ResponseMessageWithoutId = ToolModelMessage | AssistantModelMessage;
|
||||
type ResponseMessage = ResponseMessageWithoutId & { id: string };
|
||||
|
||||
export function getMostRecentUserMessage(messages: UIMessage[]) {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@
|
|||
"test": "export PLAYWRIGHT=True && pnpm exec playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/gateway": "^2.0.18",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/react": "2.0.109",
|
||||
"@ai-sdk/gateway": "2.0.0-beta.85",
|
||||
"@ai-sdk/provider": "3.0.0-beta.27",
|
||||
"@ai-sdk/react": "3.0.0-beta.162",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/state": "^6.5.0",
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
"@vercel/functions": "^2.0.0",
|
||||
"@vercel/otel": "^1.12.0",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "5.0.108",
|
||||
"ai": "6.0.0-beta.159",
|
||||
"bcrypt-ts": "^5.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"classnames": "^2.5.1",
|
||||
|
|
|
|||
80
pnpm-lock.yaml
generated
80
pnpm-lock.yaml
generated
|
|
@ -9,14 +9,14 @@ importers:
|
|||
.:
|
||||
dependencies:
|
||||
'@ai-sdk/gateway':
|
||||
specifier: ^2.0.18
|
||||
version: 2.0.18(zod@3.25.76)
|
||||
specifier: 2.0.0-beta.85
|
||||
version: 2.0.0-beta.85(zod@3.25.76)
|
||||
'@ai-sdk/provider':
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
specifier: 3.0.0-beta.27
|
||||
version: 3.0.0-beta.27
|
||||
'@ai-sdk/react':
|
||||
specifier: 2.0.109
|
||||
version: 2.0.109(react@19.0.1)(zod@3.25.76)
|
||||
specifier: 3.0.0-beta.162
|
||||
version: 3.0.0-beta.162(react@19.0.1)(zod@3.25.76)
|
||||
'@codemirror/lang-javascript':
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.3
|
||||
|
|
@ -96,8 +96,8 @@ importers:
|
|||
specifier: ^12.10.0
|
||||
version: 12.10.0(@types/react@18.3.18)(react-dom@19.0.1(react@19.0.1))(react@19.0.1)
|
||||
ai:
|
||||
specifier: 5.0.108
|
||||
version: 5.0.108(zod@3.25.76)
|
||||
specifier: 6.0.0-beta.159
|
||||
version: 6.0.0-beta.159(zod@3.25.76)
|
||||
bcrypt-ts:
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.3
|
||||
|
|
@ -303,31 +303,27 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@ai-sdk/gateway@2.0.18':
|
||||
resolution: {integrity: sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==}
|
||||
'@ai-sdk/gateway@2.0.0-beta.85':
|
||||
resolution: {integrity: sha512-1LFCTwweCe1KWyBR/v64zbvNJbAu4JPooa+0JVUclcb90RYQH2FDIQCbxN4H6J8is4yaTkHW5tbLNjHpOkxZuA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.18':
|
||||
resolution: {integrity: sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==}
|
||||
'@ai-sdk/provider-utils@4.0.0-beta.53':
|
||||
resolution: {integrity: sha512-83/aNTnKfurb4jdaOSfh1KgxY27SuWZbecc3bUfiUxLMtAMBLusgWMaaSbfl2VHufAoGLXIuRYRq4lXlNCdXzw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
|
||||
'@ai-sdk/provider@3.0.0-beta.27':
|
||||
resolution: {integrity: sha512-g/H1lyBQa5TAolD0t9uW552z0dwo2evMPxE9gu22zkEUvQSkOl8F1C0Jg31sUPTn9xmKnDK+hjWsAsZnOFE9mQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@2.0.109':
|
||||
resolution: {integrity: sha512-5qM8KuN7bv7E+g6BXkSAYLFjwIfMSTKOA1prjg1zEShJXJyLSc+Yqkd3EfGibm75b7nJAqJNShurDmR/IlQqFQ==}
|
||||
'@ai-sdk/react@3.0.0-beta.162':
|
||||
resolution: {integrity: sha512-vgJPUbHJ+y5Ebs1KmCnnjdebP7dsgW4uBEtPvmJJv/5JW7K4nizEFVOoxb2FF/mU7KbbE8qiprnM/XPRgC/znQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
|
|
@ -2179,8 +2175,8 @@ packages:
|
|||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
|
@ -2579,8 +2575,8 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ai@5.0.108:
|
||||
resolution: {integrity: sha512-Jex3Lb7V41NNpuqJHKgrwoU6BCLHdI1Pg4qb4GJH4jRIDRXUBySJErHjyN4oTCwbiYCeb/8II9EnqSRPq9EifA==}
|
||||
ai@6.0.0-beta.159:
|
||||
resolution: {integrity: sha512-iwyz0iycu0Tu1of9GLGwiXvNgW6dB+hnFE0dzWubTQFKz0C8nB8gjMwRTiNhNXO6HuN9+uokg9DO7HYM2CRExw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
|
@ -4534,33 +4530,33 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@ai-sdk/gateway@2.0.18(zod@3.25.76)':
|
||||
'@ai-sdk/gateway@2.0.0-beta.85(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@3.25.76)
|
||||
'@ai-sdk/provider': 3.0.0-beta.27
|
||||
'@ai-sdk/provider-utils': 4.0.0-beta.53(zod@3.25.76)
|
||||
'@vercel/oidc': 3.0.5
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.18(zod@3.25.76)':
|
||||
'@ai-sdk/provider-utils@4.0.0-beta.53(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@standard-schema/spec': 1.0.0
|
||||
'@ai-sdk/provider': 3.0.0-beta.27
|
||||
'@standard-schema/spec': 1.1.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
'@ai-sdk/provider@3.0.0-beta.27':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@2.0.109(react@19.0.1)(zod@3.25.76)':
|
||||
'@ai-sdk/react@3.0.0-beta.162(react@19.0.1)(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@3.25.76)
|
||||
ai: 5.0.108(zod@3.25.76)
|
||||
'@ai-sdk/provider-utils': 4.0.0-beta.53(zod@3.25.76)
|
||||
ai: 6.0.0-beta.159(zod@3.25.76)
|
||||
react: 19.0.1
|
||||
swr: 2.3.3(react@19.0.1)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
|
|
@ -6166,7 +6162,7 @@ snapshots:
|
|||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
|
|
@ -6576,11 +6572,11 @@ snapshots:
|
|||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
ai@5.0.108(zod@3.25.76):
|
||||
ai@6.0.0-beta.159(zod@3.25.76):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 2.0.18(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@3.25.76)
|
||||
'@ai-sdk/gateway': 2.0.0-beta.85(zod@3.25.76)
|
||||
'@ai-sdk/provider': 3.0.0-beta.27
|
||||
'@ai-sdk/provider-utils': 4.0.0-beta.53(zod@3.25.76)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
zod: 3.25.76
|
||||
|
||||
|
|
|
|||
30
tests/prompts/utils.ts
Normal file
30
tests/prompts/utils.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { LanguageModelV3StreamPart } from "@ai-sdk/provider";
|
||||
|
||||
const mockUsage = {
|
||||
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
|
||||
outputTokens: { total: 20, text: 20, reasoning: 0 },
|
||||
};
|
||||
|
||||
export function getResponseChunksByPrompt(
|
||||
_prompt: unknown,
|
||||
includeReasoning = false
|
||||
): LanguageModelV3StreamPart[] {
|
||||
const chunks: LanguageModelV3StreamPart[] = [];
|
||||
|
||||
if (includeReasoning) {
|
||||
chunks.push(
|
||||
{ type: "reasoning-start", id: "r1" },
|
||||
{ type: "reasoning-delta", id: "r1", delta: "Let me think about this." },
|
||||
{ type: "reasoning-end", id: "r1" }
|
||||
);
|
||||
}
|
||||
|
||||
chunks.push(
|
||||
{ type: "text-start", id: "t1" },
|
||||
{ type: "text-delta", id: "t1", delta: "Hello, world!" },
|
||||
{ type: "text-end", id: "t1" },
|
||||
{ type: "finish", finishReason: "stop", usage: mockUsage }
|
||||
);
|
||||
|
||||
return chunks;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue