Skip to content

nanobot serve: /v1/chat/completions always returns zero usage tokens #4309

Description

@alx1379

Bug Description

Summary

nanobot serve exposes an OpenAI-compatible /v1/chat/completions endpoint, but every successful response includes hardcoded zero token usage:

"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 }
The agent loop already tracks real usage from provider responses in AgentLoop._last_usage, but the HTTP API layer does not forward it. This breaks any downstream billing/metering integration that relies on the standard OpenAI usage field.


Root cause
In nanobot/api/server.py, _chat_completion_response() hardcodes usage to zero:

"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
handle_chat_completions() calls agent_loop.process_direct(...) and only uses the returned text. It never reads usage from the agent loop afterward.

Meanwhile, AgentLoop already stores provider usage after each run:

# nanobot/agent/loop.py
self._last_usage: dict[str, int] = {}
...
self._last_usage = result.usage
Other parts of nanobot already consume _last_usage (for example /status via nanobot/command/builtin.py), so the data exists internally but is not exposed via the HTTP API.

Suggested fix
After process_direct() completes (non-streaming path), read agent_loop._last_usage
Pass it into _chat_completion_response() (or merge into the final JSON)
Optionally include usage in the streaming path as well (e.g. final SSE chunk or a documented extension)
Add/adjust tests in tests/test_openai_api.py to assert non-zero usage when the mock agent sets _last_usage
Document real usage behavior in docs/openai-api.md
Example sketch:

def _chat_completion_response(content: str, model: str, usage: dict[str, int] | None = None) -> dict[str, Any]:
    usage = usage or {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
    return {
        ...
        "usage": {
            "prompt_tokens": int(usage.get("prompt_tokens", 0)),
            "completion_tokens": int(usage.get("completion_tokens", 0)),
            "total_tokens": int(usage.get("total_tokens", 0)),
        },
    }
And in handle_chat_completions() after a successful non-streaming call:

usage = getattr(agent_loop, "_last_usage", None) or {}
return web.json_response(_chat_completion_response(response_text, model_name, usage=usage))
Impact / use case
We run nanobot instances behind a router that bills users per token (OpenAI-style metering). Because usage is always zero, billing integrations cannot deduct credits or write usage logs unless they patch nanobot locally or estimate tokens heuristically.

Any external system using nanobot serve as an OpenAI-compatible proxy for cost tracking, quotas, or analytics is affected.

Environment
Repo: HKUDS/nanobot (main as of June 2026)
Command: nanobot serve
Endpoint: POST /v1/chat/completions (stream=false)
Related docs: docs/openai-api.md (does not currently mention real usage reporting)
Additional context
OpenAI-compatible API was added in #1362
Internal token tracking exists elsewhere (e.g. /status, JSONL usage recorder), but not in the serve HTTP response
Current API tests validate response shape and session behavior, but do not require real usage values

---
## Labels (if you have triage access)
Suggested: `bug`, `api`, `good first issue` (small, localized fix)
---
## Note on assignment
On public repos you usually **cannot assign** issues to the project unless you are a maintainer or they use an org workflow. Filing the issue on HKUDS/nanobot is enough; maintainers will triage it.
If you want a shorter title for mobile notifications:
> **Serve API returns zero usage despite agent tracking real tokens**

### Steps to Reproduce

Configure a working LLM provider in config.json
Start the API server:
nanobot serve
Send a non-streaming request:
curl -s http://127.0.0.1:8900/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"hi"}],"stream":false}' \
  | jq '.usage'
Actual behavior
{
  "prompt_tokens": 0,
  "completion_tokens": 0,
  "total_tokens": 0
}
This happens even when the assistant reply is non-empty and the underlying LLM call succeeds.


### Expected Behavior

Expected behavior
The response should include real token counts from the completed agent run, matching OpenAI-compatible semantics:

{
  "prompt_tokens": <int>,
  "completion_tokens": <int>,
  "total_tokens": <int>
}
If usage is temporarily unavailable, documenting that behavior would also be helpful; silently returning zeros is misleading for API consumers.

### Relevant Logs

```shell

nanobot Version

v0.2.1

Python Version

3.12

Operating System

Linux

Channel / Platform

Telegram

LLM Provider

OpenAI

Configuration (Optional)

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions