Fix AI stream by returning stream.value directly from Server Actions.
createStreamableValue must be created and returned in the action itself; wrapping in { data } or a helper return caused production RSC serialization errors.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+60
-61
@@ -3,6 +3,8 @@ import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createStreamableValue } from "ai/rsc";
|
||||
import { ERROR_PREFIX } from "@/lib/constant";
|
||||
|
||||
export type AIStream = ReturnType<typeof createStreamableValue<string>>;
|
||||
|
||||
function getOpenAiBaseUrl(): string {
|
||||
return (
|
||||
process.env.OPENAI_BASE_URL ??
|
||||
@@ -14,77 +16,74 @@ function getOpenAiBaseUrl(): string {
|
||||
const model =
|
||||
process.env.OPENAI_MODEL ?? "huihui_ai/gemma-4-abliterated:e4b";
|
||||
|
||||
export async function streamAIResponse(
|
||||
system: string,
|
||||
user: string,
|
||||
): Promise<{ data?: ReturnType<typeof createStreamableValue<string>>["value"]; error?: string }> {
|
||||
export function assertOpenAiKey(): string {
|
||||
const apiKey = process.env.OPENAI_API_KEY?.trim();
|
||||
if (!apiKey) {
|
||||
return { error: "未配置 OPENAI_API_KEY,请在 .env.local 或 Docker env_file 中设置" };
|
||||
throw new Error(
|
||||
"未配置 OPENAI_API_KEY,请在 .env.local 或 Docker env_file 中设置",
|
||||
);
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
/** 在 Server Action 内 createStreamableValue 后调用,不可从子函数返回 stream.value */
|
||||
export function pumpAIStream(
|
||||
stream: AIStream,
|
||||
system: string,
|
||||
user: string,
|
||||
): void {
|
||||
const openai = createOpenAI({
|
||||
apiKey,
|
||||
apiKey: assertOpenAiKey(),
|
||||
baseURL: getOpenAiBaseUrl(),
|
||||
});
|
||||
|
||||
const stream = createStreamableValue<string>();
|
||||
const { fullStream } = streamText({
|
||||
temperature: 0.5,
|
||||
model: openai(model),
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: user },
|
||||
],
|
||||
maxRetries: 0,
|
||||
});
|
||||
|
||||
try {
|
||||
const { fullStream } = streamText({
|
||||
temperature: 0.5,
|
||||
model: openai(model),
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: user },
|
||||
],
|
||||
maxRetries: 0,
|
||||
});
|
||||
let buffer = "";
|
||||
let done = false;
|
||||
const intervalId = setInterval(() => {
|
||||
if (done && buffer.length === 0) {
|
||||
clearInterval(intervalId);
|
||||
stream.done();
|
||||
return;
|
||||
}
|
||||
if (buffer.length <= 6) {
|
||||
stream.update(buffer);
|
||||
buffer = "";
|
||||
} else {
|
||||
const chunk = buffer.slice(0, 6);
|
||||
buffer = buffer.slice(6);
|
||||
stream.update(chunk);
|
||||
}
|
||||
}, 60);
|
||||
|
||||
let buffer = "";
|
||||
let done = false;
|
||||
const intervalId = setInterval(() => {
|
||||
if (done && buffer.length === 0) {
|
||||
clearInterval(intervalId);
|
||||
stream.done();
|
||||
return;
|
||||
}
|
||||
if (buffer.length <= 6) {
|
||||
stream.update(buffer);
|
||||
buffer = "";
|
||||
} else {
|
||||
const chunk = buffer.slice(0, 6);
|
||||
buffer = buffer.slice(6);
|
||||
stream.update(chunk);
|
||||
}
|
||||
}, 60);
|
||||
|
||||
(async () => {
|
||||
for await (const part of fullStream) {
|
||||
switch (part.type) {
|
||||
case "text-delta":
|
||||
buffer += part.textDelta;
|
||||
break;
|
||||
case "error": {
|
||||
const err = part.error as { message?: string };
|
||||
stream.update(ERROR_PREFIX + (err.message ?? String(part.error)));
|
||||
break;
|
||||
}
|
||||
(async () => {
|
||||
for await (const part of fullStream) {
|
||||
switch (part.type) {
|
||||
case "text-delta":
|
||||
buffer += part.textDelta;
|
||||
break;
|
||||
case "error": {
|
||||
const err = part.error as { message?: string };
|
||||
stream.update(ERROR_PREFIX + (err.message ?? String(part.error)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
})()
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
stream.update(ERROR_PREFIX + message);
|
||||
})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
|
||||
return { data: stream.value };
|
||||
} catch (err) {
|
||||
stream.done();
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { error: message };
|
||||
}
|
||||
}
|
||||
})()
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
stream.update(ERROR_PREFIX + message);
|
||||
})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user