1919logger = LokiLogger (service_name = "api-tool-calling" )
2020
2121_MAX_ITEMS : int = 500
22- _MAX_RESPONSE_BYTES : int = 50_000
22+ _MAX_RESPONSE_BYTES : int = 150_000
2323
2424
2525def _get_current_model_name () -> str :
@@ -55,7 +55,7 @@ class APIResponseFormatterSignature(dspy.Signature):
5555 message that no results were found for the query.
5656 - If api_response contains an error field or error status, explain the issue to the user
5757 in a friendly, non-technical way.
58- - If the data contains more than 20 items, summarize the key highlights rather than
58+ - If the data contains more than 50 items, summarize the key highlights rather than
5959 listing every item. Always mention the total count when summarizing.
6060 - Output must be clean text — no markdown headers (##), no code blocks (```), no raw
6161 JSON. The answer must be ready for direct display to the user.
@@ -375,15 +375,17 @@ async def stream_forward(
375375
376376 stream_started = False
377377 token_count = 0
378- assembled_answer = ""
378+ accumulated : list [str ] = []
379+ final_prediction : dspy .Prediction | None = None
379380 async for chunk in output_stream :
380381 if isinstance (chunk , dspy .streaming .StreamResponse ):
381382 if chunk .signature_field_name == "formatted_answer" :
382383 stream_started = True
383384 token_count += 1
384- assembled_answer += chunk .chunk
385+ accumulated . append ( chunk .chunk )
385386 yield chunk .chunk
386387 elif isinstance (chunk , dspy .Prediction ):
388+ final_prediction = chunk
387389 # dspy.streamify did not stream individual tokens — yield the
388390 # full answer from the final Prediction as a single frame.
389391 if not stream_started :
@@ -394,13 +396,43 @@ async def stream_forward(
394396 "no StreamResponse tokens — yielding full Prediction answer"
395397 )
396398 stream_started = True
397- assembled_answer = answer
399+ accumulated . append ( answer )
398400 yield answer
399401
402+ assembled_answer = "" .join (accumulated )
403+
400404 if stream_started and token_count > 0 :
401405 logger .debug (
402406 f"APIResponseFormatterModule.stream_forward: streamed { token_count } tokens"
403407 )
408+ # DSPy streaming can drop the last few tokens before EOS.
409+ # The final dspy.Prediction holds the authoritative complete answer.
410+ # Yield any tail that wasn't delivered as StreamResponse chunks.
411+ if final_prediction is not None :
412+ full_answer = getattr (
413+ final_prediction , "formatted_answer" , None
414+ )
415+ if full_answer :
416+ streamed_text = assembled_answer
417+ if full_answer .startswith (streamed_text ) and len (
418+ full_answer
419+ ) > len (streamed_text ):
420+ tail = full_answer [len (streamed_text ) :]
421+ if tail .strip ():
422+ logger .debug (
423+ f"APIResponseFormatterModule.stream_forward: "
424+ f"yielding { len (tail )} missing tail chars from Prediction"
425+ )
426+ assembled_answer += tail
427+ yield tail
428+ elif streamed_text and not full_answer .startswith (
429+ streamed_text
430+ ):
431+ logger .warning (
432+ "APIResponseFormatterModule.stream_forward: "
433+ "streamed output is not a prefix of final Prediction; "
434+ "skipping tail reconciliation"
435+ )
404436
405437 if not stream_started :
406438 # Last-resort fallback: blocking forward() — covers cases where
@@ -532,9 +564,14 @@ def _truncate_if_needed(api_response_str: str) -> str:
532564
533565 encoded = api_response_str .encode ("utf-8" )
534566 if len (encoded ) > _MAX_RESPONSE_BYTES :
535- truncated_str = encoded [:_MAX_RESPONSE_BYTES ].decode (
567+ suffix = "\n [NOTE: Response truncated due to size limit]"
568+ suffix_bytes = len (suffix .encode ("utf-8" ))
569+ truncated_str = encoded [: _MAX_RESPONSE_BYTES - suffix_bytes ].decode (
536570 "utf-8" , errors = "ignore"
537571 )
538- return truncated_str + "\n [NOTE: Response truncated due to size limit]"
572+ logger .warning (
573+ f"API response exceeded byte limit: { len (encoded )} bytes — truncating to { len (truncated_str .encode ('utf-8' ))} bytes"
574+ )
575+ return truncated_str + suffix
539576
540577 return api_response_str
0 commit comments