Summary
When an agent delegates work to a worker (a sub-thread), the sub-thread's TokenUsage satellite records model: (unknown) instead of the model that actually ran. Because cost is derived from ModelPricing keyed by model, (unknown) matches no price → the sub-thread's tokens are reported at $0 and are invisible in per-model roll-ups. The parent thread records its model correctly; only delegated work is affected.
Observed (two independent runs, 5 sub-threads — 5/5 affected)
Run 1 — orchestrator on a premium model, two delegations:
| Thread |
_Usage key (model) |
Priced? |
| Parent (orchestrator) |
Provider/…/<real-model-A> |
✅ |
| Worker sub-thread (write) |
(unknown) |
❌ $0 |
| Worker sub-thread (verify) |
(unknown) |
❌ $0 |
Run 2 — orchestrator on a mid-tier model, three delegations:
| Thread |
_Usage key (model) |
Priced? |
| Parent (orchestrator) |
Provider/…/<real-model-B> |
✅ |
| Worker sub-thread (notify) |
(unknown) |
❌ $0 |
| Worker sub-thread (write) |
(unknown) |
❌ $0 |
| Worker sub-thread (correction) |
(unknown) |
❌ $0 |
Every sub-thread carries real, non-zero token counts but model (unknown) — across different orchestrator models and different tasks.
Impact
- Sub-thread token cost is reported as $0 (no
ModelPricing entry matches (unknown)).
- Per-model usage roll-up is broken for all delegated work.
- The headline cost/usage figure undercounts any delegation-heavy task twice over: sub-thread usage lives on a separate
_Usage node and is unpriced. In Run 2 above, roughly 13% of the run's total tokens were priced at $0.
Root cause (hypothesis)
TokenUsageNodeType.RecordUsage(...) is called with actualModel ?? request.ModelName in the terminal round handlers (ThreadExecution.cs, ~L2108 / L2199 / L2305). For a delegation sub-thread round, both are null/empty — the sub-thread submission doesn't carry the resolved model id into its RoundParams.ModelName, and the streaming updates don't populate update.ModelId — so RecordUsage falls to the string.IsNullOrWhiteSpace(modelId) ? "(unknown)" fallback (TokenUsage.cs). The parent round populates the model (from update.ModelId); the sub-thread round does not.
Steps to reproduce
- Configure an agent with delegation (one or more hierarchy worker agents).
- Run a task that delegates at least one sub-task to a worker sub-thread; let it complete.
- Read the usage satellites:
{parentThread}/_Usage/* and {subThread}/_Usage/*.
- Observe: the parent has
_Usage/{realModel} (priced); each sub-thread has _Usage/(unknown) with real token counts.
- The cost derived for the sub-thread rounds is $0 (no
ModelPricing match for (unknown)).
Expected: each sub-thread's _Usage is keyed by the model that actually executed it, so its tokens price correctly and roll up per model.
Suggested direction
Thread the resolved model id into the delegated sub-thread's round — populate RoundParams.ModelName (or the streaming update.ModelId) for sub-thread execution so the existing actualModel ?? request.ModelName fallback resolves to the real model. Alternatively, have a sub-thread inherit the parent/agent's resolved model when its own submission specifies none.
Summary
When an agent delegates work to a worker (a sub-thread), the sub-thread's
TokenUsagesatellite recordsmodel: (unknown)instead of the model that actually ran. Because cost is derived fromModelPricingkeyed by model,(unknown)matches no price → the sub-thread's tokens are reported at $0 and are invisible in per-model roll-ups. The parent thread records its model correctly; only delegated work is affected.Observed (two independent runs, 5 sub-threads — 5/5 affected)
Run 1 — orchestrator on a premium model, two delegations:
_Usagekey (model)Provider/…/<real-model-A>(unknown)(unknown)Run 2 — orchestrator on a mid-tier model, three delegations:
_Usagekey (model)Provider/…/<real-model-B>(unknown)(unknown)(unknown)Every sub-thread carries real, non-zero token counts but model
(unknown)— across different orchestrator models and different tasks.Impact
ModelPricingentry matches(unknown))._Usagenode and is unpriced. In Run 2 above, roughly 13% of the run's total tokens were priced at $0.Root cause (hypothesis)
TokenUsageNodeType.RecordUsage(...)is called withactualModel ?? request.ModelNamein the terminal round handlers (ThreadExecution.cs, ~L2108 / L2199 / L2305). For a delegation sub-thread round, both are null/empty — the sub-thread submission doesn't carry the resolved model id into itsRoundParams.ModelName, and the streaming updates don't populateupdate.ModelId— soRecordUsagefalls to thestring.IsNullOrWhiteSpace(modelId) ? "(unknown)"fallback (TokenUsage.cs). The parent round populates the model (fromupdate.ModelId); the sub-thread round does not.Steps to reproduce
{parentThread}/_Usage/*and{subThread}/_Usage/*._Usage/{realModel}(priced); each sub-thread has_Usage/(unknown)with real token counts.ModelPricingmatch for(unknown)).Expected: each sub-thread's
_Usageis keyed by the model that actually executed it, so its tokens price correctly and roll up per model.Suggested direction
Thread the resolved model id into the delegated sub-thread's round — populate
RoundParams.ModelName(or the streamingupdate.ModelId) for sub-thread execution so the existingactualModel ?? request.ModelNamefallback resolves to the real model. Alternatively, have a sub-thread inherit the parent/agent's resolved model when its own submission specifies none.