Skip to content

Track multiple provider plans#300

Open
ozymandiashh wants to merge 1 commit into
getagentseal:mainfrom
ozymandiashh:feat/multi-provider-plans
Open

Track multiple provider plans#300
ozymandiashh wants to merge 1 commit into
getagentseal:mainfrom
ozymandiashh:feat/multi-provider-plans

Conversation

@ozymandiashh
Copy link
Copy Markdown
Contributor

@ozymandiashh ozymandiashh commented May 11, 2026

Summary

This PR adds first-class support for tracking multiple subscription plans at the same time, scoped by provider. It fixes the current limitation where setting a Codex/ChatGPT Pro-style plan overwrites an existing Claude plan, forcing users to manually swap plans just to compare overage.

With this change, users can keep separate Claude, Codex, and Cursor plan budgets configured together, while the dashboard and JSON outputs report one overage summary per active provider plan.

Closes #299.

Why

Today CodeBurn stores one plan in ~/.config/codeburn/config.json:

{
  "plan": {
    "id": "claude-max",
    "monthlyUsd": 200,
    "provider": "claude",
    "resetDay": 1,
    "setAt": "2026-04-01T00:00:00.000Z"
  }
}

That works for a single subscription, but breaks down for users who subscribe to more than one AI coding product. A common setup is:

  • Claude Max for Claude Code usage
  • ChatGPT Pro / Codex usage tracked under the codex provider
  • optionally Cursor Pro tracked under cursor

Before this PR, running a second codeburn plan set ... command replaced the first plan entirely. The dashboard could only show one subscription overage line, even though usage data already supports multiple providers.

What Changed

Provider-keyed plan config

CodeBurn now supports a provider-keyed plans map:

{
  "plans": {
    "claude": {
      "id": "claude-max",
      "monthlyUsd": 200,
      "resetDay": 1,
      "setAt": "2026-04-01T00:00:00.000Z"
    },
    "codex": {
      "id": "custom",
      "monthlyUsd": 200,
      "resetDay": 1,
      "setAt": "2026-04-01T00:00:00.000Z"
    }
  }
}

Existing single-plan configs remain supported. If a user still has the legacy plan key, CodeBurn reads it as a single provider-scoped plan. The next plan write migrates the config forward to the new plans map.

CLI behavior

Setting a provider-specific plan now updates only that provider:

codeburn plan set claude-max
codeburn plan set custom --monthly-usd 200 --provider codex
codeburn plan set cursor-pro

Showing plans lists all configured provider plans:

codeburn plan

Example output shape:

Plans: 2
  claude: Claude Max 20x (claude-max)
    Budget: $200/month
    Reset day: 1
  codex: Custom Plan (codex) (custom)
    Budget: $200/month
    Reset day: 1

Provider-specific reset is now supported:

codeburn plan reset --provider codex

That removes only the Codex plan and leaves the Claude plan intact. A plain reset still clears all plan config:

codeburn plan reset

Preset plans now reject mismatched provider scopes instead of silently ignoring them. For example, this is rejected because claude-max is naturally a Claude plan:

codeburn plan set claude-max --provider codex

Custom plans still support explicit scopes, including all:

codeburn plan set custom --monthly-usd 150 --provider all

Dashboard overage rows

The dashboard overview can now render multiple plan rows, one per active provider plan. For example, a user with Claude Max and a Codex custom plan will see separate overage summaries instead of one overwriting the other:

Claude Max 20x: $406.20 API-equivalent vs $200.00 plan  203.1%
Custom (codex): $118.40 API-equivalent vs $200.00 plan   59.2%

Each row keeps its own:

  • monthly budget
  • current API-equivalent spend
  • percentage used
  • under / near / over status
  • projected month-end spend
  • reset countdown

JSON compatibility

JSON report/status payloads preserve the existing top-level plan field for backwards compatibility, while adding a provider-keyed plans map.

Example shape:

{
  "plan": {
    "id": "claude-max",
    "provider": "claude",
    "budget": 200,
    "spent": 406.2,
    "percentUsed": 203.1,
    "status": "over"
  },
  "plans": {
    "claude": {
      "id": "claude-max",
      "provider": "claude",
      "budget": 200,
      "spent": 406.2,
      "percentUsed": 203.1,
      "status": "over"
    },
    "codex": {
      "id": "custom",
      "provider": "codex",
      "budget": 200,
      "spent": 118.4,
      "percentUsed": 59.2,
      "status": "under"
    }
  }
}

The top-level plan remains the first active plan in provider display order, so existing consumers that only know about a single plan continue to work. New consumers can use the additive plans map.

Avoiding double counting

Aggregate all plans are kept mutually exclusive with provider-specific plans. This avoids showing both:

  • one aggregate row that includes all providers
  • separate provider rows for Claude/Codex/Cursor

which would double-count the same spend in the overage view.

The invariant is enforced in two places:

  • writes: saving an all plan replaces provider-specific plans, and saving a provider-specific plan removes any all plan
  • reads: hand-edited configs that contain both plans.all and provider-specific plans drop the aggregate all entry defensively

Usage calculation

For multiple active plans, CodeBurn parses sessions once across all providers and then filters the in-memory usage by provider and reset period for each plan. This avoids re-reading all sessions once per configured plan.

For a single active provider plan, CodeBurn keeps the existing parser-level provider filter, so single-plan users do not pay the cost of scanning unrelated providers.

Projection math now keys daily spend from API call timestamps when available, which matches the provider filtering logic and handles turns whose user message and assistant call land on different days.

Tests Added / Updated

This PR adds coverage for:

  • provider-keyed plan persistence
  • legacy plan fallback into the new plan map
  • migration on write
  • provider-specific reset behavior
  • reset --provider all behavior
  • defensive normalization of hand-edited configs containing both all and provider-specific plans
  • multi-plan JSON output
  • provider-filtered plan --format json
  • preset/provider mismatch rejection
  • provider display order
  • multi-plan usage calculation with a single parseAllSessions(..., 'all') call
  • single-plan usage preserving parser-level provider filtering
  • projected month-end spend using API call timestamps

Validation

  • npm run build
  • npx vitest run - 47 files / 673 tests passing
  • git diff --check
  • Claude Opus 4.7, effort max: PASS
  • Gemini 3.1 Pro Preview: PASS

Note

npx tsc --noEmit still fails on an existing unrelated error in src/models-report.ts(151,13):

Type 'string' is not assignable to type 'TaskCategory'.

That file is outside this PR's diff.

@ozymandiashh ozymandiashh marked this pull request as ready for review May 11, 2026 13:35
@AgentSeal AgentSeal added needs-testing needs-validation PR requires validation against real-world usage before review and removed needs-testing labels May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-validation PR requires validation against real-world usage before review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Track multiple subscription plans simultaneously (Claude + Codex)

2 participants