Skip to content

🧾 fix: Bill Subagent Child-Run Model Usage in Parent Transactions - #13683

Merged
danny-avila merged 3 commits into
devfrom
claude/subagent-usage-billing
Jun 13, 2026
Merged

danny-avila merged 3 commits into
devfrom
claude/subagent-usage-billing

Conversation

@danny-avila

@danny-avila danny-avila commented Jun 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

I fixed a billing leak where model calls made inside subagent child runs were never billed: the child graph executes outside the run's streamEvents loop, so its chat_model_end events never reach ModelEndHandler — child usage never landed in collectedUsage, spendTokens never recorded it, and balance enforcement was blind to it. The fix consumes the new subagentUsageSink from the agents SDK (LibreChat-AI/agents#236) and routes child usage through the existing transaction pipeline.

  • Added createSubagentUsageSink(collectedUsage) to packages/api/src/agents/usage.ts: receives the SDK's per-call SubagentUsageEvents and pushes them onto the same collectedUsage array ModelEndHandler fills, tagged usage_type: 'subagent' with the child's model and provider (the provider drives the existing subset-vs-additive cache pricing semantics in splitUsage).
  • Wired the sink at all three createRun call sites — client.js (chat), openai.js (OpenAI-compat completions), and responses.js (Responses API, both handlers) — so every surface that can spawn subagents bills them; nested subagents report through the same sink.
  • Routed usage_type: 'subagent' entries into their own group in recordCollectedUsage, billed in full (transactions + balance) under transaction context 'subagent' with the child's model. The abort path reuses recordCollectedUsage, so child usage on aborted runs is billed too.
  • Excluded subagent completions from the reported output_tokens total: that result becomes the parent response message's tokenCount (via getStreamUsage), and child output the parent never saw would distort next-turn context accounting; input_tokens likewise stays pinned to the parent's first message usage.
  • Forwarded subagentUsageSink through createRun into Run.create, and widened the UsageMetadata.usage_type union with 'subagent'.
  • Typed the sink structurally (local mirror of the SDK's SubagentUsageEvent, Run.create config built as a variable) so the build and CI pass against the published @librechat/agents@3.2.33. The PR is safe to merge before the SDK release: older SDK versions ignore the extra config field, and the sink activates once the dependency is bumped past 3.2.33 (💸 feat: Report Subagent Child-Run Model Usage via Usage Sink agents#236). The structural types can then be swapped for Pick<RunConfig, 'subagentUsageSink'> and the SDK type import — call sites are commented accordingly.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

  • Added specs in packages/api/src/agents/usage.spec.ts: subagent entries billed under context 'subagent' with the child model while excluded from reported totals; leading subagent entry cannot hijack input_tokens; cache-token entries route through structured spend; createSubagentUsageSink tagging, detail preservation, missing-model/provider omission, no-usage guard; and a sink → collectedUsage → recordCollectedUsage round-trip asserting child transactions alongside the parent's.
  • Updated the full-replacement @librechat/api mocks in openai.spec.js / responses.unit.spec.js for the new export.
  • Verified against the published @librechat/agents@3.2.33: tsc for packages/api clean, ESLint/Prettier/import-sort clean on all changed files, and all touched suites pass (107 packages/api usage/run tests, 284 server/controllers/agents tests, full packages/api unit suite green locally).
  • Live-verified the SDK side end-to-end with real Anthropic and OpenAI credentials: the child's model call arrives through the sink with correct model/provider tagging and real token counts while parent calls stay on the ModelEndHandler path.

Test Configuration:

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

Copilot AI review requested due to automatic review settings June 11, 2026 13:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a billing gap where token usage from subagent child runs was not being recorded/spent, by introducing a subagentUsageSink that funnels child-run usage into the existing collectedUsage → transaction pipeline and accounting for it separately as usage_type: 'subagent'.

Changes:

  • Added createSubagentUsageSink(collectedUsage) and updated recordCollectedUsage to bill subagent usage under a dedicated 'subagent' transaction context while excluding subagent completions from reported output_tokens.
  • Threaded subagentUsageSink through createRun and wired it into the main agent execution surfaces (chat, OpenAI-compatible completions, and Responses API).
  • Added/updated unit tests and mocks to cover the new sink behavior and subagent usage segregation.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/api/src/stream/interfaces/IJobStore.ts Extends UsageMetadata.usage_type union to include 'subagent'.
packages/api/src/agents/usage.ts Adds createSubagentUsageSink and bills usage_type: 'subagent' separately, excluding it from reported output totals.
packages/api/src/agents/usage.spec.ts Adds unit coverage for subagent segregation and sink behavior.
packages/api/src/agents/run.ts Extends createRun signature to accept/pass subagentUsageSink into Run.create.
api/server/controllers/agents/responses.js Passes subagentUsageSink when creating runs for Responses API.
api/server/controllers/agents/openai.js Passes subagentUsageSink when creating runs for OpenAI-compatible chat completions.
api/server/controllers/agents/client.js Passes subagentUsageSink for the main chat agent client runs.
api/server/controllers/agents/tests/responses.unit.spec.js Updates @librechat/api mock to include createSubagentUsageSink.
api/server/controllers/agents/tests/openai.spec.js Updates @librechat/api mock to include createSubagentUsageSink.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 790 to 794
appConfig,
subagentUsageSink,
streaming = true,
streamUsage = true,
}: {
Comment on lines 1 to 4
import { logger } from '@librechat/data-schemas';
import { Providers } from 'librechat-data-provider';
import type { SubagentUsageEvent } from '@librechat/agents';
import type { TCustomConfig, TTransactionsConfig } from 'librechat-data-provider';
@danny-avila

Copy link
Copy Markdown
Collaborator Author

Reviewed against the context usage tracker (#13670) — the sink design and the excludeFromOutputTotal handling both look right, and billing under a dedicated 'subagent' transaction context is exactly the separation the spend records need.

One integration seam once both PRs land: the sink pushes child usage straight into collectedUsage, bypassing ModelEndHandler's SSE emit — so the tracker's session-cost display and the resume backfill (ResumeState.collectedUsage, persisted via trackTokenUsage from streamed on_token_usage events) won't include subagent spend even though transactions now do. Suggestion: where the sink is wired (or composed in initialize.js, where emitEvent(res, streamId, …) is in scope), also emit each event as on_token_usage with usage_type: 'subagent'. The client side is already prepared — tagged usage folds into totals/cost without touching the live context estimate (handled in #13670, commit 7489c2b).

@danny-avila
danny-avila marked this pull request as ready for review June 11, 2026 14:01
@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: ❌ deploy failed

The deploy failed — the previous index (if any) continues to be served.
Deploy run

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0995b6438

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

initialSessions,
calibrationRatio,
indexTokenCountMap,
subagentUsageSink,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require the SDK version that emits subagent usage

This wires subagentUsageSink into Run.create, but the repository is still locked to @librechat/agents 3.2.33 (package-lock.json resolves node_modules/@librechat/agents to 3.2.33), and the new comment here says that versions <= 3.2.33 ignore this field at runtime. In installs that use the committed lockfile, subagent child runs will never call the sink, so their usage still will not be added to collectedUsage or billed; bump the dependency/lockfile to the SDK version that actually supports the option before relying on this path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and intentional — called out in the PR description: the sink is inert on the locked 3.2.33 and activates with the dependency/lockfile bump once LibreChat-AI/agents#236 ships in a release. The structural typing exists precisely so this PR can merge green ahead of that release without a hard ordering constraint; the bump (plus swapping the structural mirrors for the SDK's Pick<RunConfig, 'subagentUsageSink'> / type import, commented at each site) is the follow-up that turns billing on.

@danny-avila
danny-avila changed the base branch from main to dev June 13, 2026 15:27
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0995b6438

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

initialSessions,
calibrationRatio,
indexTokenCountMap,
subagentUsageSink,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bump agents SDK before relying on subagentUsageSink

In lockfile-based installs (Dockerfile and CI run npm ci), package-lock.json still resolves @librechat/agents to 3.2.33, and the new comment directly above this block says that SDK version ignores subagentUsageSink at runtime. In that environment Run.create never calls the sink, so subagent child-run usage still never reaches collectedUsage and remains unbilled; update the dependency/lockfile to a version that implements the callback before wiring billing through it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by b6e85b8 (pushed after this review): both api/package.json and packages/api/package.json now pin ^3.2.35, and package-lock.json resolves @librechat/agents to 3.2.35 — the release that implements subagentUsageSink. So npm ci in Docker/CI installs the version that actually invokes the callback, and subagent child-run usage reaches collectedUsage and gets billed. Good catch flagging the lockfile specifically.

user: { id: userId },
/** Bills subagent child-run model calls (reported outside the
* streamEvents loop) into the same collectedUsage array. */
subagentUsageSink: createSubagentUsageSink(collectedUsage),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report billed subagent tokens in API usage

For Agents API requests that invoke a subagent, this sink feeds only the billing array; the user-visible Chat Completions usage is still built later from tracker/aggregator.usage, which is updated only by the on_chat_model_end stream handler above. That means recordCollectedUsage charges the child-run tokens, but the API response's usage totals omit them, so API clients see lower token counts than the balance actually spent. Please also fold sink events into the API usage tracker or derive the response usage from the billed usage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation, but deferring this one consistent with the OpenAI-compatible surface's move into the @librechat/agents SDK — api/server/controllers/agents/openai.js and responses.js are part of the handler surface being migrated, and handler fixes here were already descoped from #13636 for the same reason. Scope of the discrepancy: billing is correct (recordCollectedUsage charges the child-run tokens from the sink), and the main chat path is unaffected (its response tokenCount derives from recordCollectedUsage's result, which intentionally excludes subagent output from the parent's context count while still billing it). The only gap is the Chat-Completions API usage totals under-reporting vs. balance for API clients that invoke a subagent. Tracking it as a known deferred item to fold into the API usage tracker (or derive response usage from billed usage) as part of the SDK migration, rather than growing the legacy controller here.

@danny-avila
danny-avila force-pushed the claude/subagent-usage-billing branch from e0995b6 to b6e85b8 Compare June 13, 2026 17:50
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6e85b8baf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

user: { id: userId },
/** Bills subagent child-run model calls (reported outside the
* streamEvents loop) into the same collectedUsage array. */
subagentUsageSink: createSubagentUsageSink(collectedUsage),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include subagent tokens in API usage reports

When an OpenAI-compatible request runs an agent that calls a subagent, this sink now appends the child call to collectedUsage so it is billed, but the usage returned to the API client is still built from the streaming tracker/non-streaming aggregator updated only by on_chat_model_end. Those handlers never see child-run events, so clients receive a usage total that omits tokens they were charged for; compose the sink to update the API usage tracker/aggregator (and apply the same pattern to the Responses API wiring) or otherwise expose the billed subagent usage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same item as the earlier thread on this line — re-detected on the lockfile-bump commit (the openai.js code is unchanged). Intentionally deferred: openai.js/responses.js are part of the OpenAI-compatible surface being migrated into the @librechat/agents SDK (handler fixes here were descoped from #13636 for the same reason). Billing is correct via the sink; only the Chat-Completions usage field under-reports for API clients that invoke a subagent, and the main chat path is unaffected. Tracked as a known deferred item to fold into the API usage tracker (or derive response usage from billed usage) during the SDK migration, rather than growing the legacy controller in this PR.

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: ❌ deploy failed

The deploy failed — the previous index (if any) continues to be served.
Deploy run

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: b6e85b8baf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila merged commit 3c3837b into dev Jun 13, 2026
21 checks passed
@danny-avila
danny-avila deleted the claude/subagent-usage-billing branch June 13, 2026 18:55
danny-avila added a commit that referenced this pull request Jun 13, 2026
…nts 3.2.35)

Brings in #13683 (subagent usage billing via subagentUsageSink, 3-way
recordCollectedUsage partition, agents SDK 3.2.35 lockfile). usage.ts and
IJobStore.ts auto-merged cleanly — the context-usage changes (splitUsage,
resolveCompletionTokens, inputTokensIncludesCache, contextUsage/tokenUsage
fields) compose with the subagent segregation. No code conflicts.
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request Jun 18, 2026
…breChat-AI#13683)

* 🧾 fix: Bill Subagent Child-Run Model Usage in Parent Transactions

* 🩹 fix: Type Subagent Usage Sink Structurally Until SDK Release

* 🔧 chore: Update @librechat/agents dependency to version 3.2.35 in package-lock.json and related package.json files
ThomasVuNguyen pushed a commit to ThomasVuNguyen/LibreChat that referenced this pull request Jul 15, 2026
…breChat-AI#13683)

* 🧾 fix: Bill Subagent Child-Run Model Usage in Parent Transactions

* 🩹 fix: Type Subagent Usage Sink Structurally Until SDK Release

* 🔧 chore: Update @librechat/agents dependency to version 3.2.35 in package-lock.json and related package.json files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants