Summary
The ACP server (src/cli/acp.rs) runs jcode as an ACP (Agent Communication Protocol) backend, but the session/prompt response only contains stopReason. Provider token usage that the daemon already emits as ServerEvent::TokenUsage is dropped, so ACP hosts that meter token consumption (cost dashboards, quotas, billing attribution) report 0 tokens for every jcode-driven task.
Context
jcode's daemon protocol emits token accounting per provider request:
ServerEvent::TokenUsage {
input: u64,
output: u64,
cache_read_input: Option<u64>,
cache_creation_input: Option<u64>,
}
The ACP layer (AcpRuntime::run_prompt in src/cli/acp.rs) consumes events via session.read_event() but ignores TokenUsage, and finishes the RPC with:
{ "stopReason": "end_turn" }
ACP hosts in the hermes family (e.g. Multica) read token counts from the session/prompt result — top-level usage with inputTokens/outputTokens/cachedReadTokens/cacheWriteTokens, or _meta.usage — and persist them per task. With jcode those hosts see an empty usage map, so cost dashboards stay at zero.
Reproduction
- Run
jcode acp.
initialize → session/new → session/prompt.
- Inspect the JSON-RPC result: no
usage field regardless of how many tokens the provider streamed.
Proposed change (implemented and verified locally)
Aggregate ServerEvent::TokenUsage over the turn in run_prompt and include it in the result, plus _meta.modelId from the last ServerEvent::ModelChanged for usage attribution:
{
"stopReason": "end_turn",
"usage": {
"inputTokens": 5777,
"outputTokens": 121,
"cachedReadTokens": 3072,
"cacheWriteTokens": 0
},
"_meta": { "modelId": "deepseek-v4-flash" }
}
Key points:
- Omit the
usage block when all counters are zero (keeps the response unchanged for providers without usage).
cachedReadTokens maps from cache_read_input, cacheWriteTokens from cache_creation_input.
- Track the last
ModelChanged during the turn for _meta.modelId; leave it absent when none arrived.
Environment
- jcode v0.68.0, Linux x86_64, DeepSeek provider (OpenAI-compatible chat completions).
- Verified end-to-end: Multica's hermes-family ACP client now records
input_tokens / output_tokens / cache_read_tokens for jcode agents.
Summary
The ACP server (
src/cli/acp.rs) runs jcode as an ACP (Agent Communication Protocol) backend, but thesession/promptresponse only containsstopReason. Provider token usage that the daemon already emits asServerEvent::TokenUsageis dropped, so ACP hosts that meter token consumption (cost dashboards, quotas, billing attribution) report 0 tokens for every jcode-driven task.Context
jcode's daemon protocol emits token accounting per provider request:
The ACP layer (
AcpRuntime::run_promptinsrc/cli/acp.rs) consumes events viasession.read_event()but ignoresTokenUsage, and finishes the RPC with:{ "stopReason": "end_turn" }ACP hosts in the hermes family (e.g. Multica) read token counts from the
session/promptresult — top-levelusagewithinputTokens/outputTokens/cachedReadTokens/cacheWriteTokens, or_meta.usage— and persist them per task. With jcode those hosts see an empty usage map, so cost dashboards stay at zero.Reproduction
jcode acp.initialize→session/new→session/prompt.usagefield regardless of how many tokens the provider streamed.Proposed change (implemented and verified locally)
Aggregate
ServerEvent::TokenUsageover the turn inrun_promptand include it in the result, plus_meta.modelIdfrom the lastServerEvent::ModelChangedfor usage attribution:{ "stopReason": "end_turn", "usage": { "inputTokens": 5777, "outputTokens": 121, "cachedReadTokens": 3072, "cacheWriteTokens": 0 }, "_meta": { "modelId": "deepseek-v4-flash" } }Key points:
usageblock when all counters are zero (keeps the response unchanged for providers without usage).cachedReadTokensmaps fromcache_read_input,cacheWriteTokensfromcache_creation_input.ModelChangedduring the turn for_meta.modelId; leave it absent when none arrived.Environment
input_tokens/output_tokens/cache_read_tokensfor jcode agents.