Repository navigation
Description
Before you start
- I searched existing issues and this is not a duplicate.
- I can reproduce this on the latest main.
Problem summary
MindIE tool-enabled async streaming drops token usage from the full response when building simulated chunks.
Affected area(s)
Agents / LangGraph (model adapter)
What happened?
With tools bound, ainvoke() reports 100 input and 20 output tokens, while the aggregated astream() message has usage_metadata=None. Both calls request a non-streaming HTTP completion, so the usage is already available to the adapter. The text and tool call survive.
Expected behavior
The aggregated streaming message should preserve the same usage as ainvoke(). MindIE's _astream explicitly simulates chunks from _agenerate for tool-enabled requests; changing delivery should not discard the underlying response usage. This metadata is consumed by token budget accounting in deerflow/agents/middlewares/token_budget_middleware.py.
Steps to reproduce
From backend/, run the following script in the backend environment. It uses httpx.MockTransport; no server, network request, or real API key is needed.
import asyncio
import json
import sys
from pathlib import Path
import httpx
ROOT = Path.cwd()
sys.path[:0] = [str(ROOT / 'packages/harness'), str(ROOT / 'packages/extension-api')]
from deerflow.models.mindie_provider import MindIEChatModel
async def main():
response = {'id': 'chatcmpl-offline', 'object': 'chat.completion', 'created': 0, 'model': 'mindie-test', 'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'Looking up the weather.', 'tool_calls': [{'id':'call_1','type':'function','function':{'name':'weather','arguments':'{}'}}]}, 'finish_reason': 'tool_calls'}], 'usage': {'prompt_tokens': 100, 'completion_tokens': 20, 'total_tokens': 120}}
calls = []
def handler(request):
calls.append(json.loads(request.content))
return httpx.Response(200, json=response)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
model = MindIEChatModel(model='mindie-test', api_key='offline-dummy', http_async_client=client)
bound = model.bind_tools([{'name': 'weather', 'description': 'Get weather', 'parameters': {'type': 'object', 'properties': {}}}])
result = await bound.ainvoke('What is the weather?')
chunks = [chunk async for chunk in bound.astream('What is the weather?')]
merged = chunks[0]
for chunk in chunks[1:]: merged += chunk
print('ainvoke usage:', result.usage_metadata)
print('astream usage:', merged.usage_metadata)
print('astream content/tools:', merged.content, len(merged.tool_calls))
print('wire streaming:', [call['stream'] for call in calls])
asyncio.run(main())Relevant logs
ainvoke usage: {'input_tokens': 100, 'output_tokens': 20, 'total_tokens': 120, 'input_token_details': {}, 'output_token_details': {}}
astream usage: None
astream content/tools: Looking up the weather. 1
wire streaming: [False, False]
How are you running DeerFlow?
Other: isolated adapter reproduction against the repository source, with dependencies pinned to uv.lock (langchain-core 1.4.9, langchain-openai 1.2.1, openai 2.32.0). No running Gateway or live MindIE endpoint.
Operating system
Windows
Platform details
x86_64, PowerShell
Python version
3.12.2
Node.js version / pnpm version
Not applicable (backend-only reproduction).
uv version
0.12.9
Git state
branch: main
commit: 0f7d870
Support bundle summary
Not applicable: this is an offline adapter unit reproduction, not a deployed stack or setup failure.
Additional context
Root cause: backend/packages/harness/deerflow/models/mindie_provider.py:247-254 constructs AIMessageChunk objects without copying msg.usage_metadata in either the text or empty-content branch.
I can submit a fix preserving usage on exactly one chunk per response, with regression tests for text, empty content, and tool calls. Usage must not be repeated on every text chunk because aggregation sums it.
Related searches: #1602 / #1610 / #1611 concern requesting native streaming usage; this path already has usage in a non-streaming response. #2874 and #2925 concern downstream accounting/cache tracking rather than this provider conversion.
AI assistance: Codex helped audit the code, build and run the offline reproduction, and prepare this report.
Activity
Coder-xiaosuo commentedon Sep 5, 2026
Hello, I'm interested in fixing this issue.
Root cause summary:
I traced the loss to MindIEChatModel._astream in mindie_provider.py. When a request carries tools, MindIE drops choices under stream=True, so the model falls back to _agenerate and simulates streaming by yielding AIMessageChunks. However, none of the simulated chunks carry usage_metadata from the original _agenerate response. Since add_ai_message_chunks only produces metadata if at least one chunk has it, the final merged message ends with usage_metadata=None.
Proposed fix:
Attach msg.usage_metadata to the last yielded chunk (tool-call chunk or final text chunk) to mimic OpenAI's terminal-usage-frame convention, ensuring it's added to exactly one chunk to avoid double counting.
Test plan:
I've prepared tests covering text-only, text+tool-call, and tool-call-only branches – both for reproduction and post-fix verification.
Please assign this issue to me. I'll submit a PR with the fix and tests. Thanks!
fix(models): restore usage_metadata in MindIE tool-mode simulated str…