@@ -1112,36 +1112,45 @@ def test_frontmatter_without_sources_line_gets_one_inserted(self, tmp_path):
11121112 assert "[[summaries/new-doc]]" in text
11131113
11141114
1115+ def _mock_response (content , finish_reason : str = "stop" ) -> MagicMock :
1116+ """Build a fake, already-complete LLM response (single-chunk stream).
1117+
1118+ ``_llm_call``/``_llm_call_async`` now call ``litellm.completion``/
1119+ ``acompletion`` with ``stream=True`` and merge the resulting chunks back
1120+ into one response (see ``_merge_stream_chunks``). Exposing ``.message``
1121+ (rather than the ``.delta`` a genuine stream chunk carries) tells
1122+ ``_merge_stream_chunks`` this single chunk *is* the final response, so it
1123+ is used as-is without needing to fake LiteLLM's internal delta format.
1124+ """
1125+ mock_resp = MagicMock ()
1126+ mock_resp .choices = [MagicMock ()]
1127+ mock_resp .choices [0 ].message .content = content
1128+ mock_resp .choices [0 ].finish_reason = finish_reason
1129+ mock_resp .usage = MagicMock (prompt_tokens = 100 , completion_tokens = 50 )
1130+ mock_resp .usage .prompt_tokens_details = None
1131+ return mock_resp
1132+
1133+
11151134def _mock_completion (responses : list [str ]):
1116- """Create a mock for litellm.completion that returns responses in order ."""
1135+ """Create a mock for litellm.completion returning a single-chunk stream ."""
11171136 call_count = {"n" : 0 }
11181137
11191138 def side_effect (* args , ** kwargs ):
11201139 idx = min (call_count ["n" ], len (responses ) - 1 )
11211140 call_count ["n" ] += 1
1122- mock_resp = MagicMock ()
1123- mock_resp .choices = [MagicMock ()]
1124- mock_resp .choices [0 ].message .content = responses [idx ]
1125- mock_resp .usage = MagicMock (prompt_tokens = 100 , completion_tokens = 50 )
1126- mock_resp .usage .prompt_tokens_details = None
1127- return mock_resp
1141+ return [_mock_response (responses [idx ])]
11281142
11291143 return side_effect
11301144
11311145
11321146def _mock_acompletion (responses : list [str ]):
1133- """Create an async mock for litellm.acompletion."""
1147+ """Create an async mock for litellm.acompletion returning a single-chunk stream ."""
11341148 call_count = {"n" : 0 }
11351149
11361150 async def side_effect (* args , ** kwargs ):
11371151 idx = min (call_count ["n" ], len (responses ) - 1 )
11381152 call_count ["n" ] += 1
1139- mock_resp = MagicMock ()
1140- mock_resp .choices = [MagicMock ()]
1141- mock_resp .choices [0 ].message .content = responses [idx ]
1142- mock_resp .usage = MagicMock (prompt_tokens = 100 , completion_tokens = 50 )
1143- mock_resp .usage .prompt_tokens_details = None
1144- return mock_resp
1153+ return [_mock_response (responses [idx ])]
11451154
11461155 return side_effect
11471156
@@ -1342,15 +1351,7 @@ def sync_side_effect(*args, **kwargs):
13421351 sync_call_count ["n" ] += 1
13431352 if idx == 2 : # the summary-rewrite call
13441353 raise RuntimeError ("simulated API failure" )
1345- mock_resp = MagicMock ()
1346- mock_resp .choices = [MagicMock ()]
1347- mock_resp .choices [0 ].message .content = [
1348- summary_response ,
1349- plan_response ,
1350- ][idx ]
1351- mock_resp .usage = MagicMock (prompt_tokens = 1 , completion_tokens = 1 )
1352- mock_resp .usage .prompt_tokens_details = None
1353- return mock_resp
1354+ return [_mock_response ([summary_response , plan_response ][idx ])]
13541355
13551356 with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
13561357 mock_litellm .completion = MagicMock (side_effect = sync_side_effect )
@@ -1507,21 +1508,11 @@ async def test_short_doc_marks_doc_and_summary(self, tmp_path):
15071508 def sync_side_effect (* args , ** kwargs ):
15081509 captured_sync_calls .append (kwargs ["messages" ])
15091510 idx = min (len (captured_sync_calls ) - 1 , len (sync_responses ) - 1 )
1510- mock_resp = MagicMock ()
1511- mock_resp .choices = [MagicMock ()]
1512- mock_resp .choices [0 ].message .content = sync_responses [idx ]
1513- mock_resp .usage = MagicMock (prompt_tokens = 1 , completion_tokens = 1 )
1514- mock_resp .usage .prompt_tokens_details = None
1515- return mock_resp
1511+ return [_mock_response (sync_responses [idx ])]
15161512
15171513 async def async_side_effect (* args , ** kwargs ):
15181514 captured_async_calls .append (kwargs ["messages" ])
1519- mock_resp = MagicMock ()
1520- mock_resp .choices = [MagicMock ()]
1521- mock_resp .choices [0 ].message .content = concept_response
1522- mock_resp .usage = MagicMock (prompt_tokens = 1 , completion_tokens = 1 )
1523- mock_resp .usage .prompt_tokens_details = None
1524- return mock_resp
1515+ return [_mock_response (concept_response )]
15251516
15261517 with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
15271518 mock_litellm .completion = MagicMock (side_effect = sync_side_effect )
@@ -1586,15 +1577,9 @@ async def test_long_doc_marks_doc_message(self, tmp_path):
15861577
15871578 def sync_side_effect (* args , ** kwargs ):
15881579 captured .append (kwargs ["messages" ])
1589- mock_resp = MagicMock ()
1590- mock_resp .choices = [MagicMock ()]
15911580 # First call: overview (plain text); second: plan (JSON).
1592- mock_resp .choices [0 ].message .content = (
1593- "Overview text" if len (captured ) == 1 else plan_response
1594- )
1595- mock_resp .usage = MagicMock (prompt_tokens = 1 , completion_tokens = 1 )
1596- mock_resp .usage .prompt_tokens_details = None
1597- return mock_resp
1581+ content = "Overview text" if len (captured ) == 1 else plan_response
1582+ return [_mock_response (content )]
15981583
15991584 with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
16001585 mock_litellm .completion = MagicMock (side_effect = sync_side_effect )
@@ -1726,16 +1711,9 @@ async def test_create_and_update_flow(self, tmp_path):
17261711 async def ordered_acompletion (* args , ** kwargs ):
17271712 idx = call_order ["n" ]
17281713 call_order ["n" ] += 1
1729- mock_resp = MagicMock ()
1730- mock_resp .choices = [MagicMock ()]
17311714 # create tasks come first, then update tasks
1732- if idx == 0 :
1733- mock_resp .choices [0 ].message .content = create_page_response
1734- else :
1735- mock_resp .choices [0 ].message .content = update_page_response
1736- mock_resp .usage = MagicMock (prompt_tokens = 100 , completion_tokens = 50 )
1737- mock_resp .usage .prompt_tokens_details = None
1738- return mock_resp
1715+ content = create_page_response if idx == 0 else update_page_response
1716+ return [_mock_response (content )]
17391717
17401718 with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
17411719 mock_litellm .completion = MagicMock (side_effect = _mock_completion ([plan_response ]))
@@ -1823,13 +1801,7 @@ async def test_truncated_update_preserves_existing_page(self, tmp_path):
18231801 )
18241802
18251803 async def truncated_acompletion (* args , ** kwargs ):
1826- mock_resp = MagicMock ()
1827- mock_resp .choices = [MagicMock ()]
1828- mock_resp .choices [0 ].message .content = truncated_page
1829- mock_resp .choices [0 ].finish_reason = "length"
1830- mock_resp .usage = MagicMock (prompt_tokens = 100 , completion_tokens = 50 )
1831- mock_resp .usage .prompt_tokens_details = None
1832- return mock_resp
1804+ return [_mock_response (truncated_page , finish_reason = "length" )]
18331805
18341806 with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
18351807 mock_litellm .completion = MagicMock (side_effect = _mock_completion ([plan_response ]))
@@ -1859,13 +1831,7 @@ async def test_truncated_create_skips_partial_page(self, tmp_path):
18591831 truncated_page = json .dumps ({"brief" : "x" , "content" : "# Ghost\n \n Partial" })
18601832
18611833 async def truncated_acompletion (* args , ** kwargs ):
1862- mock_resp = MagicMock ()
1863- mock_resp .choices = [MagicMock ()]
1864- mock_resp .choices [0 ].message .content = truncated_page
1865- mock_resp .choices [0 ].finish_reason = "length"
1866- mock_resp .usage = MagicMock (prompt_tokens = 100 , completion_tokens = 50 )
1867- mock_resp .usage .prompt_tokens_details = None
1868- return mock_resp
1834+ return [_mock_response (truncated_page , finish_reason = "length" )]
18691835
18701836 with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
18711837 mock_litellm .completion = MagicMock (side_effect = _mock_completion ([plan_response ]))
@@ -1928,13 +1894,7 @@ async def test_truncated_entity_update_preserves_existing_page(self, tmp_path):
19281894 )
19291895
19301896 async def truncated_acompletion (* args , ** kwargs ):
1931- mock_resp = MagicMock ()
1932- mock_resp .choices = [MagicMock ()]
1933- mock_resp .choices [0 ].message .content = truncated_page
1934- mock_resp .choices [0 ].finish_reason = "length"
1935- mock_resp .usage = MagicMock (prompt_tokens = 100 , completion_tokens = 50 )
1936- mock_resp .usage .prompt_tokens_details = None
1937- return mock_resp
1897+ return [_mock_response (truncated_page , finish_reason = "length" )]
19381898
19391899 with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
19401900 mock_litellm .completion = MagicMock (side_effect = _mock_completion ([plan_response ]))
0 commit comments