core/agent+cli: treat last-request prompt_tokens as context size

context_tokens prefers API usage from the last model call; char est.
is only a pre-call fallback. Clarify billed turn/session totals in UI.
This commit is contained in:
2026-07-15 11:07:52 +08:00
parent a61b536243
commit c9e805f38d
6 changed files with 73 additions and 10 deletions
+24 -1
View File
@@ -4,7 +4,11 @@ from typing import TYPE_CHECKING
from plyngent.lmproto.openai_compatible.model import SystemChatMessage, UserChatMessage
from .budget import DEFAULT_CONTEXT_MAX_TOKENS, DEFAULT_TOOL_RESULT_MAX_CHARS
from .budget import (
DEFAULT_CONTEXT_MAX_TOKENS,
DEFAULT_TOOL_RESULT_MAX_CHARS,
estimate_messages_tokens,
)
from .events import UsageEvent
from .loop import DEFAULT_MAX_ROUNDS, run_chat_loop
from .usage import TokenUsage
@@ -84,6 +88,25 @@ class ChatAgent:
self.last_turn_rounds = 0
self._ensure_system_prompt()
@property
def context_tokens(self) -> int:
"""Best current context size (tokens).
Prefers the last model call's ``prompt_tokens`` (API or per-request
estimate) — that is the real size of the context the model just saw.
Before any call, falls back to a char-based estimate of ``messages``.
"""
if not self.last_request_usage.is_zero():
return self.last_request_usage.prompt_tokens
return estimate_messages_tokens(self.messages)
@property
def context_tokens_source(self) -> str:
"""``api`` / ``estimate`` for :attr:`context_tokens`."""
if not self.last_request_usage.is_zero():
return self.last_request_usage.source
return "estimate"
def _ensure_system_prompt(self) -> None:
"""Prepend system prompt once when configured and history has none."""
if not self.system_prompt:
+8 -5
View File
@@ -54,28 +54,31 @@ _CONTENT_PREVIEW = 200
def _cmd_status(state: ReplState) -> None:
from plyngent.agent.budget import estimate_messages_chars, estimate_messages_tokens
from plyngent.agent.budget import estimate_messages_chars
pending = state.agent.pending_retry_text
pending_disp = "yes" if pending else "no"
ctx_chars = estimate_messages_chars(state.agent.messages)
ctx_tokens = estimate_messages_tokens(state.agent.messages)
ctx_tokens = state.agent.context_tokens
ctx_src = state.agent.context_tokens_source
ctx_budget = state.agent.max_context_tokens
session_u = state.agent.session_usage
last_u = state.agent.last_turn_usage
last_req = state.agent.last_request_usage
last_rounds = state.agent.last_turn_rounds
# API prompt_tokens from the last model call is real context size for that request.
ctx_tag = "api" if ctx_src == "api" else "est"
ctx_tilde = "" if ctx_src == "api" else "~"
click.echo(
f"provider={state.provider_name} model={state.model}\n"
f"session={state.session_id} messages={len(state.agent.messages)} "
f"pending_retry={pending_disp}\n"
f"tools={'on' if state.tools_enabled else 'off'} "
f"rounds={state.max_rounds} stream={'on' if state.agent.stream else 'off'}\n"
f"context_tokens~={ctx_tokens}/{ctx_budget} (est, once) "
f"context_tokens={ctx_tilde}{ctx_tokens}/{ctx_budget} ({ctx_tag}) "
f"context_chars={ctx_chars} "
f"tool_result_max={state.agent.max_tool_result_chars}\n"
f"last_request={last_req.format_line()} "
f"(last model call; ~context size if from API)\n"
f"last_request={last_req.format_line()}\n"
f"usage_last_turn={last_u.format_line(billed=True)} "
f"rounds={last_rounds}\n"
f"usage_session={session_u.format_line(billed=True)}\n"
+11 -2
View File
@@ -94,8 +94,17 @@ def _echo_turn_usage(agent: ChatAgent) -> None:
rounds = agent.last_turn_rounds
parts: list[str] = []
if not agent.last_request_usage.is_zero():
parts.append(f"last_request {agent.last_request_usage.format_line()}")
if not agent.last_turn_usage.is_zero():
# prompt_tokens on the last call ≈ context the model just saw
req = agent.last_request_usage
parts.append(
f"context={req.prompt_tokens} "
f"(prompt+completion={req.prompt_tokens}+{req.completion_tokens}"
f"={req.total_tokens}"
f"{' est' if req.source == 'estimate' else ''})"
)
if not agent.last_turn_usage.is_zero() and (
rounds > 1 or agent.last_turn_usage.total_tokens != agent.last_request_usage.total_tokens
):
label = agent.last_turn_usage.format_line(billed=True)
if rounds > 1:
parts.append(f"turn {label} over {rounds} rounds")