Skip to content

feat(proxy): attribute usage to the CLI that spent it - #1458

Merged
murdore merged 1 commit into
releasefrom
feat/per-cli-usage-attribution
Aug 22, 2026
Merged

murdore merged 1 commit into
releasefrom
feat/per-cli-usage-attribution

Conversation

@murdore

@murdore murdore commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

GET /accounts could say an account burned $40 today. It could not say whether that was Claude Code, OpenCode, or a runaway script — every figure was per-account only, and one pooled account is routinely shared by several CLIs.

The signal already existed and was discarded. detectClientApp() derived a label from User-Agent, wrote it into an OTel span attribute, and stopped. RequestLogEntry never carried it, so the ledger had nothing to group by.

What lands

Requests record the calling client and the raw User-Agent. The ledger splits each account's totals into byClient:

"usage": {
  "requests": 412, "costUsd": 18.44,
  "byClient": {
    "claude-code": { "requests": 380, "costUsd": 17.90 },
    "opencode":    { "requests": 32,  "costUsd": 0.54 }
  }
}

The split reconciles with the total — summing byClient requests and cost returns requests and costUsd, asserted in a test — so a dashboard showing both side by side cannot contradict itself.

Two decisions worth challenging

The raw header is stored, not just the derived name. The classifier knows only prefixes observed in real traffic: claude-cli/ and ai/, both verified against this machine's proxy logs. I deliberately did not guess prefixes for OpenCode, Qwen or Copilot — a guessed mapping is indistinguishable from a working one until it collides and files a client under the wrong name. Keeping the raw header means an unclassified client stays attributable by its own User-Agent instead of collapsing into one bucket, and naming it later is one line in clientAttribution.ts once the header has actually been seen.

unknown and unattributed are separate buckets. The first is a client we saw and could not name; the second is a row logged before attribution existed, carrying no header at all. Folding them together would make months of history look like an unidentified tool.

Verification

Driven through the surface a CLI uses — real POSTs to a spawned proxy carrying real User-Agent headers, with attribution read back out of the request log the proxy itself wrote:

attributed 2 distinct clients from live traffic

That case includes a caller the classifier cannot name, asserting its raw header survives verbatim — the scenario where this feature is most needed and easiest to get wrong.

Check Result
tsc --noEmit --strict clean
eslint src test 0 errors (54 pre-existing warnings)
pnpm run build clean
continuous-test-suite-proxy 66 passed
continuous-test-suite-codex 31 passed

Independent of #1453, #1454, #1455 — no overlapping files with the first two; touches accountLedger.ts which #1455 does not.

Copilot AI lite review requested due to automatic review settings August 22, 2026 10:29
@coderabbitai

coderabbitai Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@murdore, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d83754c6-27be-4447-a01b-b227894a5355

📥 Commits

Reviewing files that changed from the base of the PR and between b123505 and cc15be2.

📒 Files selected for processing (9)
  • docs/features/proxy-accounts-endpoint.md
  • src/lib/proxy/accountLedger.ts
  • src/lib/proxy/clientAttribution.ts
  • src/lib/server/routes/claudeProxyRoutes.ts
  • src/lib/server/routes/codexProxyRoutes.ts
  • src/lib/server/routes/openaiProxyRoutes.ts
  • src/lib/types/proxy.ts
  • src/lib/types/proxyClient.ts
  • test/continuous-test-suite-proxy.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: cc15be2dc4023abe7859c99dbc0d089ee13ec5c3
  • Message: feat(proxy): attribute usage to the CLI that spent it
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@github-actions

github-actions Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Documentation Validation Results

🚀 Documentation validation passed!

Check Status Result
Frontmatter Validation ✅ Passed
TypeScript Check ✅ Passed
Build ✅ Passed
Link Validation ✅ Passed

📦 Build artifact uploaded successfully. Ready for deployment preview.

Commit: 0ba640f3f7a01390e3de0cedda4ad240b2a76d93 | Workflow: View logs

@Tara-ag Tara-ag 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.

Review Summary

This PR adds per-CLI attribution for proxy usage tracking, enabling operators to see which CLI tools are consuming quota from pooled Claude accounts.

Changes Made

  1. New module: src/lib/proxy/clientAttribution.ts - Extracts client names from User-Agent headers
  2. Type updates: Added clientApp and userAgent fields to RequestLogEntry, added CliClientUsageTotals type
  3. Ledger logic: Updated accountLedger.ts to aggregate usage by client while maintaining account-level totals
  4. Route integration: Three proxy routes (Claude, Codex, OpenAI bridge) now capture client attribution from headers
  5. Tests: Added comprehensive tests for attribution detection and ledger splitting

Impact Analysis

  • Blast radius: ~9,300 nodes affected (primarily proxy accounting system)
  • Affected flows: Request logging, usage aggregation, account quota tracking
  • Backward compatible: Pre-attribution traffic falls into "unattributed" bucket without breaking existing dashboards
  • Risk level: Low - additive feature with isolated scope to proxy accounting

Review Scope

Focused on:

  • ✅ Security (no secrets or auth issues introduced)
  • ✅ Architecture (follows existing factory/registry patterns)
  • ✅ Type safety (proper typing throughout)
  • ✅ Correctness (logic matches design intent)
  • ✅ Testing (E2E tests verify live behavior)
  • ✅ Backward compatibility (existing account totals preserved)

Verdict: APPROVED

No CRITICAL or MAJOR issues found. The implementation is clean, well-tested, and follows established patterns in the codebase.

GET /accounts could say an account burned $40 today. It could not say whether
that was Claude Code, OpenCode, or a runaway script — every figure was
per-account only, and one pooled account is routinely shared by several CLIs.

The signal already existed and was thrown away. detectClientApp() derived a
label from User-Agent, wrote it into an OTel span attribute, and stopped there;
RequestLogEntry never carried it, so the ledger had nothing to group by.

Requests now record the calling client and the raw User-Agent, and the ledger
splits each account's totals into byClient alongside them. The split reconciles
with the account total — summing byClient requests and cost gives back requests
and costUsd, asserted in a test — so a dashboard showing both cannot contradict
itself.

Two decisions worth stating.

The raw header is stored, not just the derived name. The classifier only knows
prefixes seen in real traffic: claude-cli/ and ai/, both verified against this
machine's proxy logs. Guessing prefixes for the other CLIs would be
indistinguishable from working until one collided and filed a client under the
wrong name. Keeping the raw header means an unclassified client is still
attributable by its own User-Agent instead of collapsing into one bucket with
every other unknown, and naming it later is a one-line addition.

"unknown" and "unattributed" are separate buckets. The first is a client we saw
and could not name; the second is a row logged before attribution existed and
carries no header at all. Folding them together would make months of history
look like an unidentified tool.

Verified through the surface a CLI uses: real POSTs to a spawned proxy carrying
real User-Agent headers, with the attribution read back out of the request log
the proxy itself wrote — including the case where the classifier cannot name
the caller and must preserve its header verbatim.

Rebased onto current release, which had added the Codex configurator
round-trip test and the Codex model-discovery route in the same files. Both
conflicts were unions — every test must survive — so the suite now registers
the Codex round-trip alongside both attribution cases, each with its own
category.

Verified after the rebase: typecheck 4831 files 0 errors, eslint 0 errors,
prettier clean repo-wide, proxy suite 68 passed / 0 failed / 6 skipped, with
both attribution cases and the Codex round-trip that came in from release
passing together.

The Yama check on the previous push failed for infrastructure reasons, not a
finding: its LiteLLM upstream answered 502 on all three retries, so no review
was generated.
@murdore
murdore force-pushed the feat/per-cli-usage-attribution branch from 71558a2 to cc15be2 Compare August 22, 2026 15:58
@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@Tara-ag

Tara-ag commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

🔍 Review Summary

This PR adds client attribution tracking for the Claude proxy subscription system. It enables operators to see which CLI (Claude Code, OpenCode, etc.) is consuming tokens from a shared subscription account.

Changes Made

  • New module: src/lib/proxy/clientAttribution.ts - extracts client name from User-Agent headers
  • Updated types: Added clientApp and userAgent fields to RequestLogEntry
  • Updated ledger: accountLedger.ts now aggregates usage by client
  • Updated routes: All three proxy routes (Claude, Codex, OpenAI bridge) now capture client attribution
  • Added tests: Two E2E tests verifying attribution works correctly

Review Scope

Reviewed all 9 changed files systematically:

  1. ✅ docs/features/proxy-accounts-endpoint.md - Documentation updated with examples
  2. ✅ src/lib/proxy/accountLedger.ts - Ledger aggregation logic verified
  3. ✅ src/lib/proxy/clientAttribution.ts - New module, no issues found
  4. ✅ src/lib/server/routes/claudeProxyRoutes.ts - Integration correct
  5. ✅ src/lib/server/routes/codexProxyRoutes.ts - Integration correct
  6. ✅ src/lib/server/routes/openaiProxyRoutes.ts - Integration correct
  7. ✅ src/lib/types/proxy.ts - Type definitions correct
  8. ✅ src/lib/types/proxyClient.ts - Type definitions correct
  9. ✅ test/continuous-test-suite-proxy.ts - E2E tests added and verified

Impact Analysis

  • Blast radius: Changes touch 84 execution flows (primarily proxy accounting)
  • Breaking changes: None - all additions are backward-compatible
  • Public API changes: Internal proxy logging types only, no SDK changes
  • Test coverage: Comprehensive E2E tests for both live attribution and ledger aggregation

Key Design Decisions Verified

  • ✅ Only prefixes observed in real traffic (claude-cli/, ai/)
  • ✅ Longest prefix wins (prevents shadowing)
  • ✅ Raw User-Agent preserved for unknown clients (don't lose attribution info)
  • ✅ Never throws - attribution failure doesn't break requests
  • ✅ Capped User-Agent storage at 200 chars (prevent unbounded attacker input)
  • ✅ Legacy data handled gracefully (returns "unattributed")

Final Decision

APPROVED - No blocking issues found. The implementation is clean, well-tested, and follows all project standards.

@murdore
murdore merged commit d13d210 into release Aug 22, 2026
22 checks passed
@murdore
murdore deleted the feat/per-cli-usage-attribution branch August 22, 2026 16:10
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 11.18.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants