core/agent+cli: separate last-request usage from billed turn totals

Tool-loop rounds re-send history, so summed prompt tokens are billing
not context size. Track last_request_usage and label turn/session as
billed; clarify /status and end-of-turn lines.
This commit is contained in:
2026-07-15 11:04:41 +08:00
parent 58041c6c87
commit a61b536243
7 changed files with 82 additions and 8 deletions
+39
View File
@@ -230,12 +230,51 @@ async def test_chat_agent_accumulates_session_usage() -> None:
agent = ChatAgent(client, model="m", stream=False)
_ = [e async for e in agent.run("one")]
assert agent.last_turn_usage.total_tokens == 6
assert agent.last_request_usage.total_tokens == 6
assert agent.last_turn_rounds == 1
assert agent.session_usage.total_tokens == 6
_ = [e async for e in agent.run("two")]
assert agent.last_turn_usage.total_tokens == 10
assert agent.last_request_usage.total_tokens == 10
assert agent.session_usage.total_tokens == 16
async def test_chat_agent_turn_usage_sums_tool_rounds() -> None:
"""Multi-round tool loop: turn usage is billing sum; last_request is final call."""
@tool
def ping() -> str:
return "pong"
registry = ToolRegistry([ping])
client = ScriptedClient(
[
_response(
AssistantChatMessage(
content="",
tool_calls=[
AssistantFunctionToolCall(
id="1",
function=AssistantFunctionTool(name="ping", arguments="{}"),
)
],
),
usage={"prompt_tokens": 100, "completion_tokens": 5, "total_tokens": 105},
),
_response(
AssistantChatMessage(content="done"),
usage={"prompt_tokens": 200, "completion_tokens": 3, "total_tokens": 203},
),
]
)
agent = ChatAgent(client, model="m", tools=registry, stream=False)
_ = [e async for e in agent.run("go")]
assert agent.last_turn_rounds == 2
assert agent.last_request_usage.prompt_tokens == 200
assert agent.last_turn_usage.prompt_tokens == 300
assert agent.last_turn_usage.total_tokens == 308
async def test_stream_yields_deltas_incrementally() -> None:
"""Text deltas are yielded as chunks arrive, not only after the full stream."""
+2
View File
@@ -97,3 +97,5 @@ def test_format_line_marks_estimate() -> None:
assert "(est)" in line
mixed = TokenUsage(prompt_tokens=1, completion_tokens=0, total_tokens=1, source="mixed").format_line()
assert "(api+est)" in mixed
billed = TokenUsage(prompt_tokens=1, completion_tokens=0, total_tokens=1).format_line(billed=True)
assert billed.startswith("billed tokens")