@@ -2,8 +2,13 @@ package openai
22
33import (
44 "context"
5+ "encoding/base64"
6+ "fmt"
57
8+ "github.com/mudler/LocalAI/core/backend"
9+ "github.com/mudler/LocalAI/core/config"
610 "github.com/mudler/LocalAI/core/http/endpoints/openai/types"
11+ "github.com/mudler/LocalAI/core/schema"
712 "github.com/mudler/LocalAI/pkg/reasoning"
813)
914
@@ -84,3 +89,142 @@ func (s *speechStreamer) finish() (content string, audio []byte, err error) {
8489 }
8590 return s .extractor .CleanedContent (), s .audio , s .err
8691}
92+
93+ // streamLLMResponse drives a streamed, plain-content (no tools) realtime reply.
94+ // It announces the assistant item before tokens arrive, feeds the LLM token
95+ // callback through a speechStreamer (transcript deltas + sentence-piped TTS),
96+ // then emits the terminal events. It returns true when it has fully handled the
97+ // response so the caller can return; callers must only invoke it for turns with
98+ // no tools and an audio modality (see triggerResponseAtTurn).
99+ func streamLLMResponse (ctx context.Context , session * Session , conv * Conversation , t Transport , responseID string , history schema.Messages , images []string , llmCfg * config.ModelConfig ) bool {
100+ // Announce the assistant item up front so streamed deltas target a known item.
101+ item := types.MessageItemUnion {
102+ Assistant : & types.MessageItemAssistant {
103+ ID : generateItemID (),
104+ Status : types .ItemStatusInProgress ,
105+ Content : []types.MessageContentOutput {{Type : types .MessageContentTypeOutputAudio }},
106+ },
107+ }
108+ conv .Lock .Lock ()
109+ conv .Items = append (conv .Items , & item )
110+ conv .Lock .Unlock ()
111+
112+ sendEvent (t , types.ResponseOutputItemAddedEvent {
113+ ServerEventBase : types.ServerEventBase {},
114+ ResponseID : responseID ,
115+ OutputIndex : 0 ,
116+ Item : item ,
117+ })
118+ sendEvent (t , types.ResponseContentPartAddedEvent {
119+ ServerEventBase : types.ServerEventBase {},
120+ ResponseID : responseID ,
121+ ItemID : item .Assistant .ID ,
122+ OutputIndex : 0 ,
123+ ContentIndex : 0 ,
124+ Part : item .Assistant .Content [0 ],
125+ })
126+
127+ cancel := func () {
128+ conv .Lock .Lock ()
129+ for i := len (conv .Items ) - 1 ; i >= 0 ; i -- {
130+ if conv .Items [i ].Assistant != nil && conv .Items [i ].Assistant .ID == item .Assistant .ID {
131+ conv .Items = append (conv .Items [:i ], conv .Items [i + 1 :]... )
132+ break
133+ }
134+ }
135+ conv .Lock .Unlock ()
136+ sendEvent (t , types.ResponseDoneEvent {
137+ ServerEventBase : types.ServerEventBase {},
138+ Response : types.Response {ID : responseID , Object : "realtime.response" , Status : types .ResponseStatusCancelled },
139+ })
140+ }
141+
142+ var template string
143+ if llmCfg .TemplateConfig .UseTokenizerTemplate {
144+ template = llmCfg .GetModelTemplate ()
145+ } else {
146+ template = llmCfg .TemplateConfig .Chat
147+ }
148+ thinkingStartToken := reasoning .DetectThinkingStartToken (template , & llmCfg .ReasoningConfig )
149+
150+ streamer := newSpeechStreamer (ctx , t , session , responseID , item .Assistant .ID , thinkingStartToken , llmCfg .ReasoningConfig )
151+ cb := func (token string , _ backend.TokenUsage ) bool {
152+ if ctx .Err () != nil {
153+ return false
154+ }
155+ streamer .onToken (token )
156+ return true
157+ }
158+
159+ predFunc , err := session .ModelInterface .Predict (ctx , history , images , nil , nil , cb , nil , nil , nil , nil , nil )
160+ if err != nil {
161+ sendError (t , "inference_failed" , fmt .Sprintf ("backend error: %v" , err ), "" , item .Assistant .ID )
162+ return true
163+ }
164+ if _ , err := predFunc (); err != nil {
165+ if ctx .Err () != nil {
166+ cancel ()
167+ return true
168+ }
169+ sendError (t , "prediction_failed" , fmt .Sprintf ("backend error: %v" , err ), "" , item .Assistant .ID )
170+ return true
171+ }
172+ if ctx .Err () != nil {
173+ cancel ()
174+ return true
175+ }
176+
177+ content , audio , err := streamer .finish ()
178+ if err != nil {
179+ sendError (t , "tts_error" , fmt .Sprintf ("TTS generation failed: %v" , err ), "" , item .Assistant .ID )
180+ return true
181+ }
182+
183+ _ , isWebRTC := t .(* WebRTCTransport )
184+
185+ sendEvent (t , types.ResponseOutputAudioTranscriptDoneEvent {
186+ ServerEventBase : types.ServerEventBase {},
187+ ResponseID : responseID ,
188+ ItemID : item .Assistant .ID ,
189+ OutputIndex : 0 ,
190+ ContentIndex : 0 ,
191+ Transcript : content ,
192+ })
193+ if ! isWebRTC {
194+ sendEvent (t , types.ResponseOutputAudioDoneEvent {
195+ ServerEventBase : types.ServerEventBase {},
196+ ResponseID : responseID ,
197+ ItemID : item .Assistant .ID ,
198+ OutputIndex : 0 ,
199+ ContentIndex : 0 ,
200+ })
201+ }
202+
203+ conv .Lock .Lock ()
204+ item .Assistant .Status = types .ItemStatusCompleted
205+ item .Assistant .Content [0 ].Transcript = content
206+ if ! isWebRTC {
207+ item .Assistant .Content [0 ].Audio = base64 .StdEncoding .EncodeToString (audio )
208+ }
209+ conv .Lock .Unlock ()
210+
211+ sendEvent (t , types.ResponseContentPartDoneEvent {
212+ ServerEventBase : types.ServerEventBase {},
213+ ResponseID : responseID ,
214+ ItemID : item .Assistant .ID ,
215+ OutputIndex : 0 ,
216+ ContentIndex : 0 ,
217+ Part : item .Assistant .Content [0 ],
218+ })
219+ sendEvent (t , types.ResponseOutputItemDoneEvent {
220+ ServerEventBase : types.ServerEventBase {},
221+ ResponseID : responseID ,
222+ OutputIndex : 0 ,
223+ Item : item ,
224+ })
225+ sendEvent (t , types.ResponseDoneEvent {
226+ ServerEventBase : types.ServerEventBase {},
227+ Response : types.Response {ID : responseID , Object : "realtime.response" , Status : types .ResponseStatusCompleted },
228+ })
229+ return true
230+ }
0 commit comments