11"use client" ;
2+ import { ApiPath , Alibaba , ALIBABA_BASE_URL } from "@/app/constant" ;
23import {
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" ;
1011import {
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" ;
2419import { getClientConfig } from "@/app/config/client" ;
25- import { getMessageTextContent } from "@/app/utils" ;
20+ import {
21+ getMessageTextContent ,
22+ getMessageTextContentWithoutThinking ,
23+ getTimeoutMSByModel ,
24+ } from "@/app/utils" ;
2625import { fetch } from "@/app/utils/stream" ;
2726
2827export 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 ) ;
0 commit comments