Skip to content

fix(rag): correctly attribute and track query embedding usage for active sessions#3603

Open
Piyush0049 wants to merge 5 commits into
docker:mainfrom
Piyush0049:fix/rag-usage-tracking
Open

fix(rag): correctly attribute and track query embedding usage for active sessions#3603
Piyush0049 wants to merge 5 commits into
docker:mainfrom
Piyush0049:fix/rag-usage-tracking

Conversation

@Piyush0049

Copy link
Copy Markdown
Contributor

Description

This PR resolves a long-standing TODO in the RAG Manager to properly track, aggregate, and emit embedding token usage for agent search queries, ensuring they are accurately billed and attributed to the active session telemetry.

Previously, VectorStore relied on a hidden global usageHandler callback attached to the Embedder. This blindly fired during any embedding generation and logged the tokens as Indexing usage. As a result, tokens consumed by active RAG queries during a chat session were miscategorized as background file-indexing operations and completely missed by the session token tracker.

Changes Made

  • Refactored Embedder for Purity: Removed the usageHandler callback side-effect. Embed() and EmbedBatch() now explicitly return (tokens, cost) alongside the vectors.
  • Separated Query vs Indexing Attribution: VectorStore now manually triggers recordUsage() only during indexFile(). Query tokens are cleanly returned up the stack.
  • Updated Strategy Interfaces: Strategy.Query() was updated to return a new types.Usage struct, allowing Manager.Query() to cleanly aggregate usage across parallel search strategies (e.g., in a hybrid RAG configuration).
  • Wired Telemetry: Hooked the RAG tool handler (handleQueryRAG) to capture the aggregated usage and emit it via telemetry.RecordTokenUsage(ctx, ...). Because the tool execution context inherently carries the telemetry.Client, this seamlessly solves the missing session attribution.

Testing

  • Verified go build ./... passes without errors.
  • Executed task test ./pkg/rag/... to ensure all strategy mocks, implementations, and fallback batches aggregate token counts safely without regressions.
  • Confirmed high-concurrency batching loops are correctly protected by mutex locks for token accumulation.

@Piyush0049
Piyush0049 requested a review from a team as a code owner July 11, 2026 14:20
@aheritier aheritier added area/rag For work/issues that have to do with the RAG features area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Jul 11, 2026

@Sayt-0 Sayt-0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for tackling this long-standing TODO. The direction is right: removing the hidden usageHandler side effect in favor of explicit return values fixes a real accounting bug (query embedding tokens were credited to indexing and never reached session telemetry). Build and tests pass locally, and the concurrent accumulation in embedBatchOptimized is correctly done under the existing mutex.

A few points need to be addressed before merge:

# Finding Severity Location
1 The TODO comment this PR resolves is still present blocking pkg/rag/manager.go:424-426
2 Tool name passed as the model parameter of RecordTokenUsage, polluting the model dimension in telemetry blocking pkg/tools/builtin/rag/rag.go:182
3 The cost returned by Embed/EmbedBatch is ignored by every caller, which recomputes it via calculateCost suggestion pkg/rag/embed/embed.go, pkg/rag/strategy/vector_store.go
4 No test covers the new behavior (usage aggregation, telemetry emission) strongly recommended pkg/rag/
5 Partial usage is discarded on error paths, a small accounting regression vs the callback approach note pkg/rag/strategy/vector_store.go, pkg/rag/embed/embed.go

Details in the inline comments. None of this questions the overall approach; once 1 and 2 are addressed and some test coverage is added, this is good to go.

Comment thread pkg/rag/manager.go Outdated
Comment on lines 425 to 426
// For queries during agent execution, usage should be added to agent's session
// This requires passing session context through the RAG tool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This TODO is resolved by this PR but the comment is left in place (the TODO starts one line above, at line 424). It should be removed, or rewritten to describe the new behavior: usage is returned to the caller and emitted by the RAG tool handler. Keeping it as-is will mislead the next reader into thinking query usage is still untracked.

Comment thread pkg/tools/builtin/rag/rag.go Outdated
}

if usage.TotalTokens > 0 || usage.Cost > 0 {
telemetry.RecordTokenUsage(ctx, t.toolName, usage.TotalTokens, 0, usage.Cost)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

RecordTokenUsage(ctx, model string, ...) expects a model name as its first argument and feeds TokenEvent.ModelName. Passing t.toolName here means the tool name (rag, or any custom toolset name) shows up as a model in analytics, which breaks per-model token aggregation.

The embedding model is known to VectorStore (s.modelID). One option: add a Model string field to types.Usage, set it in VectorStore.Query, and use it here (falling back to t.toolName when empty, e.g. for BM25-only configs which report no usage anyway). If mixing embedding models per query is a concern with hybrid configs, emitting one event per strategy usage would also work.

Comment thread pkg/rag/embed/embed.go Outdated
// Embed generates an embedding for a single text
// Emits usage event immediately via handler if set
func (e *Embedder) Embed(ctx context.Context, text string) ([]float64, error) {
func (e *Embedder) Embed(ctx context.Context, text string) ([]float64, int64, float64, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

([]float64, int64, float64, error) carries two anonymous numeric returns, which is easy to misread or swap at call sites. Since this PR introduces types.Usage, returning ([]float64, types.Usage, error) (and the batch equivalent) would be clearer. pkg/rag/types has no dependency back on this package, so no import cycle.

Related: the returned cost is currently dead weight. Both call sites (VectorStore.Query, indexFile) discard it and recompute cost via calculateCost (models.dev pricing). Either the provider-reported cost should be trusted and used, or it should be dropped from the return values to keep a single source of truth. The pre-PR comment on the removed handler ("matches how chat completions calculate cost in runtime.go") suggests calculateCost is the intended source, in which case dropping the cost return is the consistent choice.

Comment thread pkg/rag/manager.go
return nil, types.Usage{}, fmt.Errorf("strategy %s failed: %w", result.name, result.err)
}

usage.Add(result.usage)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The core claim of this PR (usage is aggregated across strategies and surfaced to the caller) has no test. Suggested minimal coverage:

  • a multi-strategy Manager.Query test asserting usage equals the sum of per-strategy usages (a staticStrategy returning non-zero types.Usage is enough),
  • a handleQueryRAG test asserting RecordTokenUsage fires with the aggregated values (and does not fire when usage is zero),
  • optionally a Usage.Add unit test.

Comment thread pkg/rag/strategy/vector_store.go Outdated
func (s *VectorStore) Query(ctx context.Context, query string, numResults int, threshold float64) ([]database.SearchResult, error) {
queryEmbedding, err := s.embedder.Embed(ctx, query)
func (s *VectorStore) Query(ctx context.Context, query string, numResults int, threshold float64) ([]database.SearchResult, types.Usage, error) {
queryEmbedding, tokens, _, err := s.embedder.Embed(ctx, query)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two small accounting regressions vs the callback approach, acceptable if intentional but worth stating:

  • In Query, if SearchSimilarVectors fails after a successful Embed, the tokens already consumed are returned as types.Usage{} and never recorded anywhere.
  • In indexFile, recordUsage now fires once per file after the whole EmbedBatch completes. On mid-batch failure, tokens consumed by successful batches are not recorded (previously the per-batch handler recorded them incrementally). This also coarsens TUI usage-event granularity from per-batch to per-file on large files.

Returning the partial usage alongside the error, or documenting that failed operations are not accounted, would settle this.

…usage in RAG

* Add ModelID to types.Usage and implement aggregation in Usage.Add()
* Refactor EmbedBatch to accumulate partial tokens on mid-batch error
* Update VectorStore to always record partial usage and set correct ModelID
* Update handleQueryRAG to use usage.ModelID for RecordTokenUsage telemetry
* Remove unused cost return values from Embedder interfaces
* Add unit tests for Usage.Add, Manager.Query usage aggregation, and EmbedBatch partial usage
@Piyush0049

Copy link
Copy Markdown
Contributor Author

@Sayt-0 Thank you for the review, I have pushed an update addressing all your feedback.

  • Removed the obsolete TODO in the manager
  • Rerouted token usage into telemetry.RecordTokenUsage using the accurate ModelID
  • Added ModelID to types.Usage with proper string concatenation
  • Updated EmbedBatch to accumulate and return partial token usages on errors
  • Added unit tests for Usage.Add, Manager.Query aggregation, and EmbedBatch partial usage

Please do let me know if any other changes are required.

Updates the usage tracking implementation to supply the newly required context cancellation callback to strategy.Query added in upstream main, resolving the CI signature mismatch errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/rag For work/issues that have to do with the RAG features area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants