















@@ -1028,6 +1028,136 @@ describe("openai transport stream", () => {
10281028}
10291029});
103010301031+it("refuses ModelStudio chat streams with no user or assistant payload turns", async () => {
1032+const model = {
1033+id: "qwen-coder-plus",
1034+name: "qwen-coder-plus",
1035+api: "openai-completions",
1036+provider: "qwen",
1037+baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
1038+reasoning: false,
1039+input: ["text"],
1040+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1041+contextWindow: 4096,
1042+maxTokens: 256,
1043+} satisfies Model<"openai-completions">;
1044+const stream = createOpenAICompletionsTransportStreamFn()(
1045+model,
1046+{
1047+systemPrompt: "runtime-only system prompt",
1048+messages: [],
1049+tools: [],
1050+} as never,
1051+{ apiKey: "test-key" } as never,
1052+);
1053+1054+let errorPayload: Record<string, unknown> | undefined;
1055+for await (const event of stream as AsyncIterable<{
1056+type: string;
1057+error?: Record<string, unknown>;
1058+}>) {
1059+if (event.type === "error") {
1060+errorPayload = event.error;
1061+}
1062+}
1063+1064+expect(errorPayload).toMatchObject({ stopReason: "error" });
1065+expect(String(errorPayload?.errorMessage)).toContain(
1066+"contains no non-empty user or assistant messages",
1067+);
1068+expect(String(errorPayload?.errorMessage)).toContain("system/tool-only request");
1069+});
1070+1071+it("allows generic OpenAI-compatible chat streams without the ModelStudio turn guard", async () => {
1072+let capturedRoles: string[] | undefined;
1073+const server = createServer((req, res) => {
1074+let body = "";
1075+req.setEncoding("utf8");
1076+req.on("data", (chunk) => {
1077+body += chunk;
1078+});
1079+req.on("end", () => {
1080+const parsed = JSON.parse(body) as { messages?: Array<{ role?: string }> };
1081+capturedRoles = parsed.messages?.map((message) => message.role ?? "");
1082+res.writeHead(200, {
1083+"content-type": "text/event-stream; charset=utf-8",
1084+"cache-control": "no-cache",
1085+connection: "keep-alive",
1086+});
1087+const created = Math.floor(Date.now() / 1000);
1088+res.write(
1089+`data: ${JSON.stringify({
1090+ id: "chatcmpl-system-only",
1091+ object: "chat.completion.chunk",
1092+ created,
1093+ model: "generic-openai-compatible",
1094+ choices: [
1095+ {
1096+ index: 0,
1097+ delta: { role: "assistant", content: "OK" },
1098+ finish_reason: null,
1099+ },
1100+ ],
1101+ })}\n\n`,
1102+);
1103+res.write(
1104+`data: ${JSON.stringify({
1105+ id: "chatcmpl-system-only",
1106+ object: "chat.completion.chunk",
1107+ created,
1108+ model: "generic-openai-compatible",
1109+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
1110+ })}\n\n`,
1111+);
1112+res.write("data: [DONE]\n\n");
1113+res.end();
1114+});
1115+});
1116+1117+await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
1118+try {
1119+const address = server.address();
1120+if (!address || typeof address === "string") {
1121+throw new Error("Missing loopback server address");
1122+}
1123+const model = {
1124+id: "generic-openai-compatible",
1125+name: "Generic OpenAI Compatible",
1126+api: "openai-completions",
1127+provider: "custom-openai-compatible",
1128+baseUrl: `http://127.0.0.1:${address.port}/v1`,
1129+reasoning: false,
1130+input: ["text"],
1131+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1132+contextWindow: 4096,
1133+maxTokens: 256,
1134+} satisfies Model<"openai-completions">;
1135+const stream = createOpenAICompletionsTransportStreamFn()(
1136+model,
1137+{
1138+systemPrompt: "runtime-only system prompt",
1139+messages: [],
1140+tools: [],
1141+} as never,
1142+{ apiKey: "test-key" } as never,
1143+);
1144+1145+let doneReason: string | undefined;
1146+for await (const event of stream as AsyncIterable<{ type: string; reason?: string }>) {
1147+if (event.type === "done") {
1148+doneReason = event.reason;
1149+}
1150+}
1151+1152+expect(capturedRoles).toEqual(["system"]);
1153+expect(doneReason).toBe("stop");
1154+} finally {
1155+await new Promise<void>((resolve, reject) => {
1156+server.close((error) => (error ? reject(error) : resolve()));
1157+});
1158+}
1159+});
1160+10311161it("parses JSON chat completions returned to streaming requests", async () => {
10321162let capturedStreamFlag: unknown;
10331163const server = createServer((req, res) => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。