From 951475fb627315c372a32825deff1f298ef27838 Mon Sep 17 00:00:00 2001 From: DavidKoleczek <45405824+DavidKoleczek@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:19:34 -0400 Subject: [PATCH] fix(http): emit copilot_usage.total_nano_aiu for exact opencode cost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opencode re-derives session cost from token counts times a static per-model rate table and ignores the exact usage.cost_usd already sent on the wire. With Anthropic prompt caching this overstates cost: cache-read tokens are folded into input at the full input rate and cache-creation tokens are dropped (recorded as 0). Emit a top-level copilot_usage.total_nano_aiu = round(cost_usd * 1e11) as a sibling of usage on the terminal stop / tool_calls chunk. opencode reads this off the raw SSE chunk and overrides its token-times-rate estimate with total_nano_aiu / 1e11, so the displayed cost equals cost_usd exactly. The field is additive and ignored by clients that do not read it. This is only consumed when opencode enables raw-chunk passthrough, which requires the provider id to contain "github-copilot" -- the companion change in amplifier-app-opencode. Both are required together. Refs: microsoft-amplifier/amplifier-support#352 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --- src/amplifier_agent_http/_wire.py | 6 ++++++ src/amplifier_agent_http/routes/chat_completions.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/amplifier_agent_http/_wire.py b/src/amplifier_agent_http/_wire.py index b801e90c..4e032c96 100644 --- a/src/amplifier_agent_http/_wire.py +++ b/src/amplifier_agent_http/_wire.py @@ -180,6 +180,7 @@ def stop_chunk( completion_tokens: int = 0, cached_tokens: int = 0, cost_usd: str | None = None, + total_nano_aiu: int | None = None, include_usage: bool = True, ) -> dict[str, Any]: """Final chunk -- empty delta, finish_reason: stop, optional usage block. @@ -206,6 +207,8 @@ def stop_chunk( cached_tokens=cached_tokens, cost_usd=cost_usd, ) + if total_nano_aiu is not None: + chunk["copilot_usage"] = {"total_nano_aiu": total_nano_aiu} return chunk @@ -262,6 +265,7 @@ def tool_calls_stop_chunk( completion_tokens: int = 0, cached_tokens: int = 0, cost_usd: str | None = None, + total_nano_aiu: int | None = None, include_usage: bool = True, ) -> dict[str, Any]: """Terminal chunk for a turn that ends with host-delegated tool calls. @@ -287,6 +291,8 @@ def tool_calls_stop_chunk( cached_tokens=cached_tokens, cost_usd=cost_usd, ) + if total_nano_aiu is not None: + chunk["copilot_usage"] = {"total_nano_aiu": total_nano_aiu} return chunk diff --git a/src/amplifier_agent_http/routes/chat_completions.py b/src/amplifier_agent_http/routes/chat_completions.py index 8ef1d2c3..a5c18b0e 100644 --- a/src/amplifier_agent_http/routes/chat_completions.py +++ b/src/amplifier_agent_http/routes/chat_completions.py @@ -26,7 +26,7 @@ import logging import time from collections.abc import AsyncGenerator -from decimal import Decimal, InvalidOperation +from decimal import ROUND_HALF_UP, Decimal, InvalidOperation from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request, status @@ -427,6 +427,14 @@ async def _stream_chat_completion( # runs the tool host-side, then re-POSTs. # - "stop" for the normal end-of-turn path (with or without text). cost_str_final: str | None = str(usage_cost) if usage_cost is not None else None + # opencode reads a top-level copilot_usage.total_nano_aiu and overrides its + # own token x rate cost estimate with total_nano_aiu / 1e11, so emitting + # round(cost_usd * 1e11) makes its displayed cost exactly cost_usd. + total_nano_aiu: int | None = ( + int((usage_cost * Decimal(10**11)).to_integral_value(rounding=ROUND_HALF_UP)) + if usage_cost is not None + else None + ) if finish_reason_tool_calls: yield sse_data( tool_calls_stop_chunk( @@ -436,6 +444,7 @@ async def _stream_chat_completion( completion_tokens=usage_completion, cached_tokens=usage_cached, cost_usd=cost_str_final, + total_nano_aiu=total_nano_aiu, include_usage=True, ) ) @@ -448,6 +457,7 @@ async def _stream_chat_completion( completion_tokens=usage_completion, cached_tokens=usage_cached, cost_usd=cost_str_final, + total_nano_aiu=total_nano_aiu, include_usage=True, ) )