Skip to content

[bug] MindIE tool-enabled async streaming drops token usage #5192

Closed

Description

@BlueX888

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

  1. Coder-xiaosuo commented on Sep 5, 2026

    @Coder-xiaosuo
    Contributor

    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!

  2. added a commit that references this issue on Sep 5, 2026
    27b2b67
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

needs-triageAwaiting maintainer triage

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions