@@ -150,13 +150,15 @@ func (e *AistudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
150150 case wsrelay .MessageTypeStreamChunk :
151151 if len (event .Payload ) > 0 {
152152 appendAPIResponseChunk (ctx , e .cfg , bytes .Clone (event .Payload ))
153- if detail , ok := parseGeminiStreamUsage (event .Payload ); ok {
153+ filtered := filterAistudioUsageMetadata (event .Payload )
154+ if detail , ok := parseGeminiStreamUsage (filtered ); ok {
154155 reporter .publish (ctx , detail )
155156 }
156- }
157- lines := sdktranslator .TranslateStream (ctx , body .toFormat , opts .SourceFormat , req .Model , bytes .Clone (opts .OriginalRequest ), translatedReq , bytes .Clone (event .Payload ), & param )
158- for i := range lines {
159- out <- cliproxyexecutor.StreamChunk {Payload : []byte (lines [i ])}
157+ lines := sdktranslator .TranslateStream (ctx , body .toFormat , opts .SourceFormat , req .Model , bytes .Clone (opts .OriginalRequest ), translatedReq , bytes .Clone (filtered ), & param )
158+ for i := range lines {
159+ out <- cliproxyexecutor.StreamChunk {Payload : []byte (lines [i ])}
160+ }
161+ break
160162 }
161163 case wsrelay .MessageTypeStreamEnd :
162164 return
@@ -281,3 +283,62 @@ func (e *AistudioExecutor) buildEndpoint(model, action, alt string) string {
281283 }
282284 return base
283285}
286+
287+ // filterAistudioUsageMetadata removes usageMetadata from intermediate SSE events so that
288+ // only the terminal chunk retains token statistics.
289+ func filterAistudioUsageMetadata (payload []byte ) []byte {
290+ if len (payload ) == 0 {
291+ return payload
292+ }
293+
294+ lines := bytes .Split (payload , []byte ("\n " ))
295+ modified := false
296+ for idx , line := range lines {
297+ trimmed := bytes .TrimSpace (line )
298+ if len (trimmed ) == 0 || ! bytes .HasPrefix (trimmed , []byte ("data:" )) {
299+ continue
300+ }
301+ dataIdx := bytes .Index (line , []byte ("data:" ))
302+ if dataIdx < 0 {
303+ continue
304+ }
305+ rawJSON := bytes .TrimSpace (line [dataIdx + 5 :])
306+ cleaned , changed := stripUsageMetadataFromJSON (rawJSON )
307+ if ! changed {
308+ continue
309+ }
310+ var rebuilt []byte
311+ rebuilt = append (rebuilt , line [:dataIdx ]... )
312+ rebuilt = append (rebuilt , []byte ("data:" )... )
313+ if len (cleaned ) > 0 {
314+ rebuilt = append (rebuilt , ' ' )
315+ rebuilt = append (rebuilt , cleaned ... )
316+ }
317+ lines [idx ] = rebuilt
318+ modified = true
319+ }
320+ if ! modified {
321+ return payload
322+ }
323+ return bytes .Join (lines , []byte ("\n " ))
324+ }
325+
326+ // stripUsageMetadataFromJSON drops usageMetadata when no finishReason is present.
327+ func stripUsageMetadataFromJSON (rawJSON []byte ) ([]byte , bool ) {
328+ jsonBytes := bytes .TrimSpace (rawJSON )
329+ if len (jsonBytes ) == 0 || ! gjson .ValidBytes (jsonBytes ) {
330+ return rawJSON , false
331+ }
332+ finishReason := gjson .GetBytes (jsonBytes , "candidates.0.finishReason" )
333+ if finishReason .Exists () && finishReason .String () != "" {
334+ return rawJSON , false
335+ }
336+ if ! gjson .GetBytes (jsonBytes , "usageMetadata" ).Exists () {
337+ return rawJSON , false
338+ }
339+ cleaned , err := sjson .DeleteBytes (jsonBytes , "usageMetadata" )
340+ if err != nil {
341+ return rawJSON , false
342+ }
343+ return cleaned , true
344+ }
0 commit comments