Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions agent/codex_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def _record_codex_app_server_usage(agent, turn) -> dict[str, Any]:
)
return {}

from agent.usage_pricing import CanonicalUsage, estimate_usage_cost
from agent.usage_pricing import CanonicalUsage, estimate_usage_cost, sticky_cost_status

input_tokens = _coerce_usage_int(usage.get("inputTokens"))
cache_read_tokens = _coerce_usage_int(usage.get("cachedInputTokens"))
Expand Down Expand Up @@ -147,7 +147,9 @@ def _record_codex_app_server_usage(agent, turn) -> dict[str, Any]:
)
if cost_result.amount_usd is not None:
agent.session_estimated_cost_usd += float(cost_result.amount_usd)
agent.session_cost_status = cost_result.status
agent.session_cost_status = sticky_cost_status(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the in-memory field sticky, but the later update_token_counts() call still receives raw cost_result.status (current main agent/codex_runtime.py:166), and SessionDB overwrites stored status with that non-null value (hermes_state.py:4690, 4869). Please enforce the same rule at persistence so restarted sessions and /insights retain the guarantee.

agent.session_cost_status, cost_result.status
)
agent.session_cost_source = cost_result.source

if agent._session_db and agent.session_id:
Expand Down
6 changes: 4 additions & 2 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
zai_coding_overload_retry_ceiling,
)
from agent.trajectory import has_incomplete_scratchpad
from agent.usage_pricing import estimate_usage_cost, normalize_usage
from agent.usage_pricing import estimate_usage_cost, normalize_usage, sticky_cost_status
from hermes_constants import PARTIAL_STREAM_STUB_ID
from hermes_logging import set_session_context
from tools.skill_provenance import set_current_write_origin
Expand Down Expand Up @@ -2318,7 +2318,9 @@ def _perform_api_call(next_api_kwargs):
agent.session_estimated_cost_usd += float(_moa_ref_cost)
except (TypeError, ValueError): # pragma: no cover - defensive
pass
agent.session_cost_status = cost_result.status
agent.session_cost_status = sticky_cost_status(
agent.session_cost_status, cost_result.status
)
agent.session_cost_source = cost_result.source

# Persist token counts to session DB for /insights.
Expand Down
3 changes: 2 additions & 1 deletion agent/insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
estimate_usage_cost,
format_duration_compact,
has_known_pricing,
sticky_cost_status,
)


Expand Down Expand Up @@ -580,7 +581,7 @@ def _accumulate(model, provider, base_url, session_id, inp, out,
status = cost_status or "unknown"
d["cost"] += estimate
d["actual_cost"] += float(actual_cost or 0.0)
d["cost_status"] = status
d["cost_status"] = sticky_cost_status(d.get("cost_status"), status)
if has_known_pricing(model, provider or None, base_url):
d["has_pricing"] = True
else:
Expand Down
26 changes: 26 additions & 0 deletions agent/usage_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@
"none",
]

# Priority order for sticky cost_status: once a session reaches a higher
# accuracy tier, it should not silently downgrade on a single provider
# hiccup. "actual" > "included" > "estimated" > "unknown".
_COST_STATUS_PRIORITY: dict[str, int] = {
"actual": 4,
"included": 3,
"estimated": 2,
"unknown": 1,
}


def sticky_cost_status(current: Optional[str], incoming: Optional[str]) -> Optional[str]:
"""Return the higher-priority cost status between *current* and *incoming*.

If *incoming* is ``None`` the current value is kept. If *current* is
``None`` the incoming value is used. Otherwise the one with the higher
priority wins. Equal priority keeps the incoming value (latest wins).
"""
if incoming is None:
return current
if current is None:
return incoming
cur_prio = _COST_STATUS_PRIORITY.get(current, 0)
inc_prio = _COST_STATUS_PRIORITY.get(incoming, 0)
return incoming if inc_prio >= cur_prio else current


@dataclass(frozen=True)
class CanonicalUsage:
Expand Down