Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

170 lines
5.1 KiB
TypeScript

import { createOpenAI } from "@ai-sdk/openai";
import {
type JSONSchema7,
streamText,
convertToModelMessages,
createUIMessageStream,
JsonToSseTransformStream,
type UIMessage,
tool,
stepCountIs,
zodSchema,
} from "ai";
import { RESUMABLE_STREAM_ID_HEADER } from "assistant-stream/resumable";
import { z } from "zod";
import { getResumableStreamContext } from "@/lib/resumable-context";
export const maxDuration = 60;
export async function POST(req: Request) {
const {
messages,
system,
tools,
}: {
id?: string;
messages: UIMessage[];
system?: string;
tools?: Record<string, { description?: string; parameters: JSONSchema7 }>;
} = await req.json();
const streamId = crypto.randomUUID();
const sourceBody = process.env["OPENAI_API_KEY"]
? await buildOpenAIBody({
messages,
...(system && { system }),
...(tools && { tools }),
})
: buildMockBody({ messages });
const context = await getResumableStreamContext();
const stream = await context.run(streamId, () => sourceBody);
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
[RESUMABLE_STREAM_ID_HEADER]: streamId,
},
});
}
function convertFrontendTools(
tools: Record<string, { description?: string; parameters: JSONSchema7 }>,
): Record<string, ReturnType<typeof tool>> {
return Object.fromEntries(
Object.entries(tools).map(([name, t]) => [
name,
tool({
...(t.description ? { description: t.description } : {}),
inputSchema: t.parameters as never,
}),
]),
);
}
async function buildOpenAIBody({
messages,
system,
tools,
}: {
messages: UIMessage[];
system?: string;
tools?: Record<string, { description?: string; parameters: JSONSchema7 }>;
}): Promise<ReadableStream<Uint8Array>> {
const openai = createOpenAI({
apiKey: process.env["OPENAI_API_KEY"]!,
...(process.env["OPENAI_BASE_URL"] && {
baseURL: process.env["OPENAI_BASE_URL"],
}),
});
const result = streamText({
model: openai("gpt-5.4-nano"),
messages: await convertToModelMessages(messages),
...(system ? { system } : {}),
stopWhen: stepCountIs(10),
tools: {
...convertFrontendTools(tools ?? {}),
get_current_weather: tool({
description: "Get the current weather",
inputSchema: zodSchema(z.object({ city: z.string() })),
execute: async ({ city }) => `The weather in ${city} is sunny`,
}),
slow_count: tool({
description: "Count slowly to N",
inputSchema: zodSchema(
z.object({ to: z.number().int().min(1).max(50) }),
),
execute: async ({ to }) => {
await new Promise((r) => setTimeout(r, to * 200));
return `counted to ${to}`;
},
}),
},
});
const response = result.toUIMessageStreamResponse();
if (!response.body) throw new Error("expected response body");
return response.body;
}
function buildMockBody({
messages,
}: {
messages: UIMessage[];
}): ReadableStream<Uint8Array> {
const lastUserText =
messages
.filter((m) => m.role === "user")
.at(-1)
?.parts.flatMap((p) =>
p.type === "text" ? [(p as { type: "text"; text: string }).text] : [],
)
.join(" ") ?? "your message";
const text =
`(mock response. set OPENAI_API_KEY for the real model)\n\n` +
`you said: ${lastUserText}\n\n` +
`here is a slow stream you can interrupt mid-flight by reloading the page. ` +
`the resumable layer keeps the producer running on the server, so when ` +
`the browser reconnects it continues right where it left off, with no ` +
`wasted tokens and no lost progress. ` +
`lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod ` +
`tempor incididunt ut labore et dolore magna aliqua. ut enim ad minim ` +
`veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea ` +
`commodo consequat. duis aute irure dolor in reprehenderit in voluptate ` +
`velit esse cillum dolore eu fugiat nulla pariatur. excepteur sint ` +
`occaecat cupidatat non proident, sunt in culpa qui officia deserunt ` +
`mollit anim id est laborum.\n\n` +
`done.`;
const messageId = `msg-${crypto.randomUUID()}`;
const textPartId = `text-${crypto.randomUUID()}`;
const tokens = text.match(/\S+\s*|\s+/g) ?? [text];
const uiStream = createUIMessageStream({
execute: async ({ writer }) => {
writer.write({ type: "start", messageId });
writer.write({ type: "start-step" });
writer.write({ type: "text-start", id: textPartId });
for (const tok of tokens) {
await new Promise((r) => setTimeout(r, 250));
writer.write({
type: "text-delta",
id: textPartId,
delta: tok,
});
}
writer.write({ type: "text-end", id: textPartId });
writer.write({ type: "finish-step" });
writer.write({ type: "finish" });
},
});
return uiStream
.pipeThrough(new JsonToSseTransformStream())
.pipeThrough(new TextEncoderStream());
}