Skip to content

Commit 12863f5

Browse files
authored
Merge pull request #6204 from bestsanmao/ali_bytedance_reasoning_content
add 3 type of reasoning_content support (+deepseek-r1@OpenAI @alibaba @bytedance), parse <think></think> from SSE
2 parents 48cd4b1 + cf140d4 commit 12863f5

12 files changed

+318
-294
lines changed

app/client/platforms/alibaba.ts

+107-121
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
2+
import { ApiPath, Alibaba, ALIBABA_BASE_URL } from "@/app/constant";
23
import {
3-
ApiPath,
4-
Alibaba,
5-
ALIBABA_BASE_URL,
6-
REQUEST_TIMEOUT_MS,
7-
} from "@/app/constant";
8-
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
9-
4+
useAccessStore,
5+
useAppConfig,
6+
useChatStore,
7+
ChatMessageTool,
8+
usePluginStore,
9+
} from "@/app/store";
10+
import { streamWithThink } from "@/app/utils/chat";
1011
import {
1112
ChatOptions,
1213
getHeaders,
@@ -15,14 +16,12 @@ import {
1516
SpeechOptions,
1617
MultimodalContent,
1718
} from "../api";
18-
import Locale from "../../locales";
19-
import {
20-
EventStreamContentType,
21-
fetchEventSource,
22-
} from "@fortaine/fetch-event-source";
23-
import { prettyObject } from "@/app/utils/format";
2419
import { getClientConfig } from "@/app/config/client";
25-
import { getMessageTextContent } from "@/app/utils";
20+
import {
21+
getMessageTextContent,
22+
getMessageTextContentWithoutThinking,
23+
getTimeoutMSByModel,
24+
} from "@/app/utils";
2625
import { fetch } from "@/app/utils/stream";
2726

2827
export interface OpenAIListModelResponse {
@@ -92,7 +91,10 @@ export class QwenApi implements LLMApi {
9291
async chat(options: ChatOptions) {
9392
const messages = options.messages.map((v) => ({
9493
role: v.role,
95-
content: getMessageTextContent(v),
94+
content:
95+
v.role === "assistant"
96+
? getMessageTextContentWithoutThinking(v)
97+
: getMessageTextContent(v),
9698
}));
9799

98100
const modelConfig = {
@@ -122,134 +124,118 @@ export class QwenApi implements LLMApi {
122124
options.onController?.(controller);
123125

124126
try {
127+
const headers = {
128+
...getHeaders(),
129+
"X-DashScope-SSE": shouldStream ? "enable" : "disable",
130+
};
131+
125132
const chatPath = this.path(Alibaba.ChatPath);
126133
const chatPayload = {
127134
method: "POST",
128135
body: JSON.stringify(requestPayload),
129136
signal: controller.signal,
130-
headers: {
131-
...getHeaders(),
132-
"X-DashScope-SSE": shouldStream ? "enable" : "disable",
133-
},
137+
headers: headers,
134138
};
135139

136140
// make a fetch request
137141
const requestTimeoutId = setTimeout(
138142
() => controller.abort(),
139-
REQUEST_TIMEOUT_MS,
143+
getTimeoutMSByModel(options.config.model),
140144
);
141145

142146
if (shouldStream) {
143-
let responseText = "";
144-
let remainText = "";
145-
let finished = false;
146-
let responseRes: Response;
147-
148-
// animate response to make it looks smooth
149-
function animateResponseText() {
150-
if (finished || controller.signal.aborted) {
151-
responseText += remainText;
152-
console.log("[Response Animation] finished");
153-
if (responseText?.length === 0) {
154-
options.onError?.(new Error("empty response from server"));
147+
const [tools, funcs] = usePluginStore
148+
.getState()
149+
.getAsTools(
150+
useChatStore.getState().currentSession().mask?.plugin || [],
151+
);
152+
return streamWithThink(
153+
chatPath,
154+
requestPayload,
155+
headers,
156+
tools as any,
157+
funcs,
158+
controller,
159+
// parseSSE
160+
(text: string, runTools: ChatMessageTool[]) => {
161+
// console.log("parseSSE", text, runTools);
162+
const json = JSON.parse(text);
163+
const choices = json.output.choices as Array<{
164+
message: {
165+
content: string | null;
166+
tool_calls: ChatMessageTool[];
167+
reasoning_content: string | null;
168+
};
169+
}>;
170+
171+
if (!choices?.length) return { isThinking: false, content: "" };
172+
173+
const tool_calls = choices[0]?.message?.tool_calls;
174+
if (tool_calls?.length > 0) {
175+
const index = tool_calls[0]?.index;
176+
const id = tool_calls[0]?.id;
177+
const args = tool_calls[0]?.function?.arguments;
178+
if (id) {
179+
runTools.push({
180+
id,
181+
type: tool_calls[0]?.type,
182+
function: {
183+
name: tool_calls[0]?.function?.name as string,
184+
arguments: args,
185+
},
186+
});
187+
} else {
188+
// @ts-ignore
189+
runTools[index]["function"]["arguments"] += args;
190+
}
155191
}
156-
return;
157-
}
158-
159-
if (remainText.length > 0) {
160-
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
161-
const fetchText = remainText.slice(0, fetchCount);
162-
responseText += fetchText;
163-
remainText = remainText.slice(fetchCount);
164-
options.onUpdate?.(responseText, fetchText);
165-
}
166-
167-
requestAnimationFrame(animateResponseText);
168-
}
169-
170-
// start animaion
171-
animateResponseText();
172-
173-
const finish = () => {
174-
if (!finished) {
175-
finished = true;
176-
options.onFinish(responseText + remainText, responseRes);
177-
}
178-
};
179-
180-
controller.signal.onabort = finish;
181-
182-
fetchEventSource(chatPath, {
183-
fetch: fetch as any,
184-
...chatPayload,
185-
async onopen(res) {
186-
clearTimeout(requestTimeoutId);
187-
const contentType = res.headers.get("content-type");
188-
console.log(
189-
"[Alibaba] request response content type: ",
190-
contentType,
191-
);
192-
responseRes = res;
193192

194-
if (contentType?.startsWith("text/plain")) {
195-
responseText = await res.clone().text();
196-
return finish();
197-
}
193+
const reasoning = choices[0]?.message?.reasoning_content;
194+
const content = choices[0]?.message?.content;
198195

196+
// Skip if both content and reasoning_content are empty or null
199197
if (
200-
!res.ok ||
201-
!res.headers
202-
.get("content-type")
203-
?.startsWith(EventStreamContentType) ||
204-
res.status !== 200
198+
(!reasoning || reasoning.length === 0) &&
199+
(!content || content.length === 0)
205200
) {
206-
const responseTexts = [responseText];
207-
let extraInfo = await res.clone().text();
208-
try {
209-
const resJson = await res.clone().json();
210-
extraInfo = prettyObject(resJson);
211-
} catch {}
212-
213-
if (res.status === 401) {
214-
responseTexts.push(Locale.Error.Unauthorized);
215-
}
216-
217-
if (extraInfo) {
218-
responseTexts.push(extraInfo);
219-
}
220-
221-
responseText = responseTexts.join("\n\n");
222-
223-
return finish();
201+
return {
202+
isThinking: false,
203+
content: "",
204+
};
224205
}
225-
},
226-
onmessage(msg) {
227-
if (msg.data === "[DONE]" || finished) {
228-
return finish();
229-
}
230-
const text = msg.data;
231-
try {
232-
const json = JSON.parse(text);
233-
const choices = json.output.choices as Array<{
234-
message: { content: string };
235-
}>;
236-
const delta = choices[0]?.message?.content;
237-
if (delta) {
238-
remainText += delta;
239-
}
240-
} catch (e) {
241-
console.error("[Request] parse error", text, msg);
206+
207+
if (reasoning && reasoning.length > 0) {
208+
return {
209+
isThinking: true,
210+
content: reasoning,
211+
};
212+
} else if (content && content.length > 0) {
213+
return {
214+
isThinking: false,
215+
content: content,
216+
};
242217
}
218+
219+
return {
220+
isThinking: false,
221+
content: "",
222+
};
243223
},
244-
onclose() {
245-
finish();
246-
},
247-
onerror(e) {
248-
options.onError?.(e);
249-
throw e;
224+
// processToolMessage, include tool_calls message and tool call results
225+
(
226+
requestPayload: RequestPayload,
227+
toolCallMessage: any,
228+
toolCallResult: any[],
229+
) => {
230+
requestPayload?.input?.messages?.splice(
231+
requestPayload?.input?.messages?.length,
232+
0,
233+
toolCallMessage,
234+
...toolCallResult,
235+
);
250236
},
251-
openWhenHidden: true,
252-
});
237+
options,
238+
);
253239
} else {
254240
const res = await fetch(chatPath, chatPayload);
255241
clearTimeout(requestTimeoutId);

app/client/platforms/baidu.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
"use client";
2-
import {
3-
ApiPath,
4-
Baidu,
5-
BAIDU_BASE_URL,
6-
REQUEST_TIMEOUT_MS,
7-
} from "@/app/constant";
2+
import { ApiPath, Baidu, BAIDU_BASE_URL } from "@/app/constant";
83
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
94
import { getAccessToken } from "@/app/utils/baidu";
105

@@ -23,7 +18,7 @@ import {
2318
} from "@fortaine/fetch-event-source";
2419
import { prettyObject } from "@/app/utils/format";
2520
import { getClientConfig } from "@/app/config/client";
26-
import { getMessageTextContent } from "@/app/utils";
21+
import { getMessageTextContent, getTimeoutMSByModel } from "@/app/utils";
2722
import { fetch } from "@/app/utils/stream";
2823

2924
export interface OpenAIListModelResponse {
@@ -155,7 +150,7 @@ export class ErnieApi implements LLMApi {
155150
// make a fetch request
156151
const requestTimeoutId = setTimeout(
157152
() => controller.abort(),
158-
REQUEST_TIMEOUT_MS,
153+
getTimeoutMSByModel(options.config.model),
159154
);
160155

161156
if (shouldStream) {

0 commit comments

Comments
 (0)