You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A web_search tool call issues one auxiliary DeepSeek Messages request per query, and those requests are billed, but the harness records only the request — never the usage. The durable session log therefore cannot account for the tokens those calls cost, and a provider bill cannot be reconciled from the logs.
Where it happens
packages/web/tool-web/src/search.ts (runSearchQueries) calls ctx.web.search({ query, maxResults }) once per entry in the model's queries array, so one model action becomes several provider requests.
packages/web/web-search-deepseek/src/provider.ts appends the log-only web/deepseek-search-llm-request event immediately before dispatch, then parses the response only for web_search_tool_result blocks and cited_text citations.
packages/web/web-search-deepseek/src/types.ts declares the response envelope as { content?: ContentBlock[] }. The usage object that the Anthropic Messages API returns is not part of the type, so it is dropped at parse time and never becomes durable.
Why it matters
The session log is the only local account of model spend. packages/llm/token-meter/src/usage-projection.ts folds assistant/message and assistant/attempt into the tokenUsage projection, which is what the composer's usage pill reads (packages/client/ui-chat/src/client/chat/StatsPills.tsx). Auxiliary calls are outside that fold, so a session that used web search understates what it was billed, and no log record attributes the difference to a model or to a call.
How large it gets
This is not a rounding error. In a single hour of a multi-query research fan-out — a few hundred auxiliary search calls inside one conversation tree — the provider's billed usage for that hour was materially larger than every session total for the same hour combined, and none of the difference could be attributed from the logs. The auxiliary share sits mostly in uncached input with a smaller output component, which is consistent with a server-side search loop ingesting retrieved content into its own context.
Related gap in the same shape
packages/compaction/compaction/src/types.ts does carry usage on compaction/summary, but tokenUsage does not fold it either. Automatic session titles stream through ctx.llm.stream() in packages/session/session-title-llm/src/index.ts and record no usage at all. So the general problem is "auxiliary model calls are billed but live outside session token accounting", not something specific to web search.
Suggested direction
Short term: declare the Messages usage record on the search response type, map it into the harness TokenUsage buckets, and append a log-only session event beside the request record. input_tokens is already disjoint from the cache buckets, so it maps directly; an exact total is derivable only when both cache buckets are reported. The record should be written as soon as the response body parses, because a billed call whose response carries no citeable result still fails the search. Adding an event type is vocabulary growth, so it stays same-version.
Longer term: give auxiliary call usage one provider-neutral home — a single event that any auxiliary caller appends and that tokenUsage folds, with compaction/summary.usage moving onto it — so the usage pill matches what the provider bills.
I have a local implementation of (1): the provider option, the new event, and package tests covering bucket mapping, the billed-but-resultless case, and recorder-throw containment, with the changed source files at full coverage. Happy to share the diff if it would be useful.
I can also post a Chinese translation of this report as a reply if that is easier to read.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
A
web_searchtool call issues one auxiliary DeepSeek Messages request per query, and those requests are billed, but the harness records only the request — never the usage. The durable session log therefore cannot account for the tokens those calls cost, and a provider bill cannot be reconciled from the logs.Where it happens
packages/web/tool-web/src/search.ts(runSearchQueries) callsctx.web.search({ query, maxResults })once per entry in the model'squeriesarray, so one model action becomes several provider requests.packages/web/web-search-deepseek/src/provider.tsappends the log-onlyweb/deepseek-search-llm-requestevent immediately before dispatch, then parses the response only forweb_search_tool_resultblocks andcited_textcitations.packages/web/web-search-deepseek/src/types.tsdeclares the response envelope as{ content?: ContentBlock[] }. Theusageobject that the Anthropic Messages API returns is not part of the type, so it is dropped at parse time and never becomes durable.Why it matters
The session log is the only local account of model spend.
packages/llm/token-meter/src/usage-projection.tsfoldsassistant/messageandassistant/attemptinto thetokenUsageprojection, which is what the composer's usage pill reads (packages/client/ui-chat/src/client/chat/StatsPills.tsx). Auxiliary calls are outside that fold, so a session that used web search understates what it was billed, and no log record attributes the difference to a model or to a call.How large it gets
This is not a rounding error. In a single hour of a multi-query research fan-out — a few hundred auxiliary search calls inside one conversation tree — the provider's billed usage for that hour was materially larger than every session total for the same hour combined, and none of the difference could be attributed from the logs. The auxiliary share sits mostly in uncached input with a smaller output component, which is consistent with a server-side search loop ingesting retrieved content into its own context.
Related gap in the same shape
packages/compaction/compaction/src/types.tsdoes carryusageoncompaction/summary, buttokenUsagedoes not fold it either. Automatic session titles stream throughctx.llm.stream()inpackages/session/session-title-llm/src/index.tsand record no usage at all. So the general problem is "auxiliary model calls are billed but live outside session token accounting", not something specific to web search.Suggested direction
usagerecord on the search response type, map it into the harnessTokenUsagebuckets, and append a log-only session event beside the request record.input_tokensis already disjoint from the cache buckets, so it maps directly; an exact total is derivable only when both cache buckets are reported. The record should be written as soon as the response body parses, because a billed call whose response carries no citeable result still fails the search. Adding an event type is vocabulary growth, so it stays same-version.tokenUsagefolds, withcompaction/summary.usagemoving onto it — so the usage pill matches what the provider bills.I have a local implementation of (1): the provider option, the new event, and package tests covering bucket mapping, the billed-but-resultless case, and recorder-throw containment, with the changed source files at full coverage. Happy to share the diff if it would be useful.
I can also post a Chinese translation of this report as a reply if that is easier to read.
All reactions