11local json = require (" json" )
22local http_client = require (" http_client" )
3+ local output = require (" output" )
4+
5+ type StreamCallbacks = {
6+ on_content : ((text : string ) -> ())?,
7+ on_tool_call : ((part : any ) -> ())?,
8+ on_thinking : ((text : string ) -> ())?,
9+ on_error : ((error_info : any ) -> ())?,
10+ on_done : ((result : StreamResult ) -> ())?,
11+ }
12+
13+ type StreamInput = {
14+ stream : any ,
15+ metadata : table ?,
16+ }
17+
18+ type StreamResult = {
19+ content : string ,
20+ tool_calls : {any },
21+ finish_reason : string ?,
22+ usage : any ?,
23+ metadata : table ,
24+ }
325
426local client = {
527 _http_client = http_client
@@ -41,9 +63,203 @@ local function parse_error_response(http_response)
4163 return error_info
4264end
4365
66+ function client .process_stream (stream_response : StreamInput , callbacks : StreamCallbacks ? ): (string ?, string ?, StreamResult ?)
67+ if not stream_response or not stream_response .stream then
68+ return nil , " Invalid stream response"
69+ end
70+
71+ callbacks = callbacks or {}
72+ local on_content = callbacks .on_content or function () end
73+ local on_tool_call = callbacks .on_tool_call or function () end
74+ local on_thinking = callbacks .on_thinking or function () end
75+ local on_error = callbacks .on_error or function () end
76+ local on_done = callbacks .on_done or function () end
77+
78+ local full_content = " "
79+ local tool_calls = {}
80+ local finish_reason = nil
81+ local usage = nil
82+ local metadata = stream_response .metadata or {}
83+
84+ while true do
85+ local chunk , err = stream_response .stream :read ()
86+
87+ if err then
88+ on_error ({ message = err })
89+ return nil , err
90+ end
91+
92+ if not chunk then
93+ break
94+ end
95+
96+ if chunk == " " then
97+ goto continue
98+ end
99+
100+ for data_line in chunk :gmatch (' data:%s*(.-)%s*\n ' ) do
101+ if data_line == " " then
102+ goto continue_line
103+ end
104+
105+ local parsed , parse_err = json .decode (data_line )
106+ if parse_err then
107+ goto continue_line
108+ end
109+
110+ if parsed .error then
111+ local error_info = {
112+ message = parsed .error .message ,
113+ code = parsed .error .code ,
114+ status = parsed .error .status
115+ }
116+ on_error (error_info )
117+ return nil , error_info .message , { error = error_info }
118+ end
119+
120+ if parsed .modelVersion then
121+ metadata .model_version = parsed .modelVersion
122+ end
123+ if parsed .responseId then
124+ metadata .response_id = parsed .responseId
125+ end
126+
127+ if parsed .candidates and parsed .candidates [1 ] then
128+ local candidate = parsed .candidates [1 ]
129+
130+ if candidate .content and candidate .content .parts then
131+ for _ , part in ipairs (candidate .content .parts ) do
132+ if part .functionCall then
133+ table.insert (tool_calls , part )
134+ on_tool_call (part )
135+ elseif part .text then
136+ if part .thought == true then
137+ on_thinking (part .text )
138+ else
139+ full_content = full_content .. part .text
140+ on_content (part .text )
141+ end
142+ end
143+ end
144+ end
145+
146+ if candidate .finishReason then
147+ finish_reason = candidate .finishReason
148+ end
149+ end
150+
151+ if parsed .usageMetadata then
152+ usage = parsed .usageMetadata
153+ end
154+
155+ :: continue_line::
156+ end
157+
158+ :: continue::
159+ end
160+
161+ local result : StreamResult = {
162+ content = full_content ,
163+ tool_calls = tool_calls ,
164+ finish_reason = finish_reason ,
165+ usage = usage ,
166+ metadata = metadata
167+ }
168+
169+ on_done (result )
170+ return full_content , nil , result
171+ end
172+
173+ --- Process a streaming response and send chunks via output.streamer.
174+ --- Returns an aggregated Google-like response compatible with map_success_response().
175+ local function handle_stream_response (response , http_options )
176+ local streamer = output .streamer (
177+ http_options .stream_reply_to ,
178+ http_options .stream_topic ,
179+ http_options .stream_buffer_size or 10
180+ )
181+
182+ local full_content = " "
183+ local tool_call_parts = {}
184+ local finish_reason = nil
185+ local usage_metadata = nil
186+ local response_metadata = {}
187+
188+ local _ , stream_err = client .process_stream (
189+ { stream = response .stream , metadata = {} },
190+ {
191+ on_content = function (chunk : string )
192+ full_content = full_content .. chunk
193+ streamer :buffer_content (chunk )
194+ end ,
195+
196+ on_tool_call = function (tool_part : any )
197+ table.insert (tool_call_parts , tool_part )
198+ if tool_part .functionCall then
199+ streamer :send_tool_call (
200+ tool_part .functionCall .name ,
201+ tool_part .functionCall .args or {},
202+ tool_part .functionCall .name
203+ )
204+ end
205+ end ,
206+
207+ on_thinking = function (text : string )
208+ streamer :send_thinking (text )
209+ end ,
210+
211+ on_error = function (error_info : any )
212+ streamer :send_error (" server_error" , error_info .message )
213+ end ,
214+
215+ on_done = function (result : StreamResult )
216+ streamer :flush ()
217+ finish_reason = result .finish_reason
218+ usage_metadata = result .usage
219+ response_metadata = result .metadata
220+ end
221+ }
222+ )
223+
224+ if stream_err then
225+ return nil , {
226+ status_code = 500 ,
227+ message = " Stream processing failed: " .. tostring (stream_err )
228+ }
229+ end
230+
231+ -- Reconstruct Google-like response
232+ local parts = {}
233+ if full_content ~= " " then
234+ table.insert (parts , { text = full_content })
235+ end
236+ for _ , tc_part in ipairs (tool_call_parts ) do
237+ table.insert (parts , tc_part )
238+ end
239+
240+ return {
241+ candidates = {
242+ {
243+ content = { parts = parts , role = " model" },
244+ finishReason = finish_reason
245+ }
246+ },
247+ usageMetadata = usage_metadata ,
248+ modelVersion = response_metadata .model_version ,
249+ responseId = response_metadata .response_id ,
250+ metadata = response_metadata ,
251+ status_code = response .status_code or 200
252+ }
253+ end
254+
44255function client .request (method , url , http_options )
45256 http_options .headers [" Accept" ] = " application/json"
46257
258+ if http_options .stream then
259+ url = url .. " ?alt=sse"
260+ http_options .headers [" Accept" ] = " text/event-stream"
261+ end
262+
47263 local response = nil
48264 local err = nil
49265 if method == " GET" then
@@ -61,10 +277,18 @@ function client.request(method, url, http_options)
61277 end
62278
63279 if response .status_code < 200 or response .status_code >= 300 then
280+ if http_options .stream and response .stream and not response .body then
281+ response .body = response .stream :read ()
282+ end
64283 local parsed_error = parse_error_response (response )
65284 return nil , parsed_error
66285 end
67286
287+ -- Streaming: process stream, send chunks via streamer, return aggregated response
288+ if http_options .stream and response .stream then
289+ return handle_stream_response (response , http_options )
290+ end
291+
68292 local parsed , parse_err = json .decode (response .body )
69293 if parse_err then
70294 local parse_error = {
0 commit comments