Nomearod Claude Opus 4.6 (1M context) commited on
Commit
cafa02c
·
1 Parent(s): b8e3368

fix: eliminate redundant stream_complete() call in run_stream

Browse files

The tool-use loop's final complete() already produces the synthesis
when it returns with no tool_calls. Previously, run_stream discarded
that response and made a second stream_complete() call — doubling
the API cost for tool-using requests.

Now always emit response.content from the loop's final complete() as
a single chunk. This means /ask/stream no longer incrementally streams
tokens for the synthesis, but it eliminates the extra LLM call and
makes cost accounting exact (total_cost now covers all LLM calls).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

agent_bench/agents/orchestrator.py CHANGED
@@ -200,7 +200,6 @@ class Orchestrator:
200
  total_cost = 0.0
201
 
202
  # Step 1: Run tool-use loop normally (non-streamed)
203
- used_tools = False
204
  for _ in range(self.max_iterations):
205
  response = await self.provider.complete(
206
  messages, tools=tools, temperature=self.temperature
@@ -209,7 +208,6 @@ class Orchestrator:
209
  if not response.tool_calls:
210
  break
211
 
212
- used_tools = True
213
  messages.append(
214
  Message(
215
  role=Role.ASSISTANT,
@@ -235,17 +233,10 @@ class Orchestrator:
235
  sources=[{"source": s} for s in dict.fromkeys(all_sources)],
236
  )
237
 
238
- # Step 3: Stream the final synthesis
239
- if used_tools:
240
- # Tools were used — need a fresh streaming call to synthesize
241
- async for chunk in self.provider.stream_complete(
242
- messages, temperature=self.temperature
243
- ):
244
- yield StreamEvent(type="chunk", content=chunk)
245
- else:
246
- # No tools needed — response already has the answer, emit it
247
- # without a redundant second LLM call
248
- yield StreamEvent(type="chunk", content=response.content)
249
 
250
  yield StreamEvent(
251
  type="done",
 
200
  total_cost = 0.0
201
 
202
  # Step 1: Run tool-use loop normally (non-streamed)
 
203
  for _ in range(self.max_iterations):
204
  response = await self.provider.complete(
205
  messages, tools=tools, temperature=self.temperature
 
208
  if not response.tool_calls:
209
  break
210
 
 
211
  messages.append(
212
  Message(
213
  role=Role.ASSISTANT,
 
233
  sources=[{"source": s} for s in dict.fromkeys(all_sources)],
234
  )
235
 
236
+ # Step 3: Emit the final answer as a single chunk.
237
+ # The loop's last complete() already produced the synthesis — reuse it
238
+ # instead of making a redundant stream_complete() call.
239
+ yield StreamEvent(type="chunk", content=response.content)
 
 
 
 
 
 
 
240
 
241
  yield StreamEvent(
242
  type="done",
tests/test_serving.py CHANGED
@@ -400,8 +400,8 @@ class TestStreaming:
400
  assert "FastAPI" in full_text # MockProvider mentions FastAPI
401
 
402
  @pytest.mark.asyncio
403
- async def test_stream_mock_provider_3_chunks(self, test_app):
404
- """MockProvider yields exactly 3 deterministic chunks."""
405
  import json as json_mod
406
 
407
  async with AsyncClient(
@@ -417,7 +417,8 @@ class TestStreaming:
417
  if line.startswith("data: ")
418
  and json_mod.loads(line[6:])["type"] == "chunk"
419
  ]
420
- assert len(chunks) == 3
 
421
 
422
  @pytest.mark.asyncio
423
  async def test_non_streaming_unchanged(self, test_app):
 
400
  assert "FastAPI" in full_text # MockProvider mentions FastAPI
401
 
402
  @pytest.mark.asyncio
403
+ async def test_stream_emits_single_answer_chunk(self, test_app):
404
+ """Stream emits the complete answer as a single chunk (no redundant LLM call)."""
405
  import json as json_mod
406
 
407
  async with AsyncClient(
 
417
  if line.startswith("data: ")
418
  and json_mod.loads(line[6:])["type"] == "chunk"
419
  ]
420
+ assert len(chunks) == 1
421
+ assert len(chunks[0]["content"]) > 0
422
 
423
  @pytest.mark.asyncio
424
  async def test_non_streaming_unchanged(self, test_app):