Skip to content

Delegation sub-thread token usage is recorded under model (unknown) — sub-thread cost is unpriced ($0) #369

Description

@sierragolflima

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

  1. Configure an agent with delegation (one or more hierarchy worker agents).
  2. Run a task that delegates at least one sub-task to a worker sub-thread; let it complete.
  3. Read the usage satellites: {parentThread}/_Usage/* and {subThread}/_Usage/*.
  4. Observe: the parent has _Usage/{realModel} (priced); each sub-thread has _Usage/(unknown) with real token counts.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions