@@ -79,14 +79,18 @@ function _M.parse_sse_event(event, ctx, state)
7979
8080 local result = { type = " delta" , data = data }
8181
82- -- Extract text content from choices
82+ -- Extract text content and detect tool calls from choices
8383 if type (data .choices ) == " table" and # data .choices > 0 then
8484 local texts = {}
8585 for _ , choice in ipairs (data .choices ) do
86- if type (choice ) == " table"
87- and type (choice .delta ) == " table"
88- and type (choice .delta .content ) == " string" then
89- core .table .insert (texts , choice .delta .content )
86+ if type (choice ) == " table" and type (choice .delta ) == " table" then
87+ if type (choice .delta .content ) == " string" then
88+ core .table .insert (texts , choice .delta .content )
89+ end
90+ if type (choice .delta .tool_calls ) == " table"
91+ and # choice .delta .tool_calls > 0 then
92+ result .has_tool_call = true
93+ end
9094 end
9195 end
9296 if # texts > 0 then
@@ -96,13 +100,20 @@ function _M.parse_sse_event(event, ctx, state)
96100
97101 -- Extract usage (null for non-final chunks; cjson decodes null as userdata)
98102 if type (data .usage ) == " table" then
103+ local u = data .usage
104+ local pd = type (u .prompt_tokens_details ) == " table" and u .prompt_tokens_details
105+ local cd = type (u .completion_tokens_details ) == " table" and u .completion_tokens_details
99106 result .type = " usage"
100107 result .usage = {
101- prompt_tokens = data .usage .prompt_tokens or 0 ,
102- completion_tokens = data .usage .completion_tokens or 0 ,
103- total_tokens = data .usage .total_tokens or 0 ,
108+ prompt_tokens = u .prompt_tokens or 0 ,
109+ completion_tokens = u .completion_tokens or 0 ,
110+ total_tokens = u .total_tokens or 0 ,
111+ cache_read_input_tokens = pd and pd .cached_tokens
112+ or u .prompt_cache_hit_tokens or 0 ,
113+ cache_creation_input_tokens = pd and pd .cache_creation_input_tokens or 0 ,
114+ reasoning_tokens = cd and cd .reasoning_tokens or 0 ,
104115 }
105- result .raw_usage = data . usage
116+ result .raw_usage = u
106117 end
107118
108119 return result
@@ -160,14 +171,53 @@ function _M.extract_usage(res_body)
160171 return nil , nil
161172 end
162173 local raw = res_body .usage
174+ local pdetails = type (raw .prompt_tokens_details ) == " table" and raw .prompt_tokens_details
175+ local cdetails = type (raw .completion_tokens_details ) == " table"
176+ and raw .completion_tokens_details
177+ -- OpenAI uses prompt_tokens_details.cached_tokens; DeepSeek uses prompt_cache_hit_tokens
178+ local cache_read = pdetails and pdetails .cached_tokens or raw .prompt_cache_hit_tokens or 0
163179 return {
164180 prompt_tokens = raw .prompt_tokens or 0 ,
165181 completion_tokens = raw .completion_tokens or 0 ,
166182 total_tokens = raw .total_tokens or (raw .prompt_tokens or 0 ) + (raw .completion_tokens or 0 ),
183+ cache_read_input_tokens = cache_read ,
184+ cache_creation_input_tokens = pdetails and pdetails .cache_creation_input_tokens or 0 ,
185+ reasoning_tokens = cdetails and cdetails .reasoning_tokens or 0 ,
167186 }, raw
168187end
169188
170189
190+ --- Detect whether a non-streaming response contains tool calls.
191+ function _M .has_tool_call (res_body )
192+ if type (res_body ) ~= " table" or type (res_body .choices ) ~= " table" then
193+ return false
194+ end
195+ for _ , choice in ipairs (res_body .choices ) do
196+ if type (choice ) == " table" and type (choice .message ) == " table"
197+ and type (choice .message .tool_calls ) == " table"
198+ and # choice .message .tool_calls > 0 then
199+ return true
200+ end
201+ end
202+ return false
203+ end
204+
205+
206+ --- Extract the end-user identifier from a request body.
207+ function _M .extract_end_user_id (body )
208+ if type (body ) ~= " table" then
209+ return nil
210+ end
211+ if type (body .safety_identifier ) == " string" then
212+ return body .safety_identifier
213+ end
214+ if type (body .user ) == " string" then
215+ return body .user
216+ end
217+ return nil
218+ end
219+
220+
171221--- Extract all text content from a request body for moderation.
172222function _M .extract_request_content (body )
173223 local contents = {}
0 commit comments