Skip to content

feat(providers): add Claude Platform on AWS spend - #5061

Open
Willian-Zhang wants to merge 5 commits into
steipete:mainfrom
Willian-Zhang:feat/claude-platform-aws
Open

Willian-Zhang wants to merge 5 commits into
steipete:mainfrom
Willian-Zhang:feat/claude-platform-aws

Conversation

@Willian-Zhang

@Willian-Zhang Willian-Zhang commented Nov 11, 2026 •

Copy link
Copy Markdown

Summary

Adds Claude Platform on AWS as a bundled plugin provider: month-to-date spend, the 30-day cost/token/request history (native inline spend dashboard and Usage & Spend), optional budgets, and spend by region, model, and workspace over four periods.

plugin

The screenshot predates 76bc665a6, which shortened three strings that were cut off on the card: the local-mode history label is now Last 30 days (was Last 30 days (local time)), the history note is Anthropic analytics · may differ from AWS invoice, and the projection caption reads $52.86/day (7-day avg).

Refs #4794. That issue was closed for lack of a precise source, verified sample data, and a bounded authentication design. This PR proposes a different source than the Cost Explorer selector discussed there, and addresses those points:

  • Source: Anthropic's Usage and Cost Analytics API (beta), GET /v1/organizations/analytics/cost and /usage, on the regional aws-external-anthropic.<region>.api.aws endpoints. It is Claude-only by construction, so there is no Marketplace selector to get wrong.
  • Authentication boundary: the script signs each fixed-origin GET with SigV4 itself (pure-JS SHA-256/HMAC) from a long-lived access key held in two secure settings. There is no AWS profile, credential-provider chain, session token, or ambient AWS_* variable; the provider uses its own ANTHROPIC_AWS_* keys so existing AWS credentials are never picked up implicitly.
  • Bounded origins: the manifest declares 16 fixed regional origins (the manifest limit). The user lists the regions that hold workspaces; each endpoint only reports its own region, so they are read and summed. A region whose month-total request fails fails the refresh rather than understating spend.

What it shows

  • This month (cost meter), Today, At list prices (when different), request count, Data as of (oldest watermark across regions), and Next data expected (four hours after it).
  • Projected this month: spend so far plus the seven-day daily average for the time left after the watermark. Shown only when at least three of the last seven completed days had spend; it never alters the reported spend.
  • Budgets (optional monthly and daily, USD): add the Today and This month usage bars and the date a monthly budget would run out. They never cap AWS billing.
  • 30-day history through costUsage: daily cost, requests, and input/output tokens per model, with today's figures filled in for the native spend card.
  • By region (when more than one).
  • By model / By workspace, one at a time behind two pickers on the card: Model (top eight) or Workspace (top ten; names when ListWorkspaces is allowed, IDs otherwise) over 30d, MTD, 7d, or Today. The choice is remembered.
  • Time zone setting, local (default) or utc, deciding where days and months begin. The API buckets days in UTC, so utc reads daily buckets plus today, while local reads hourly buckets and cuts days at local midnight.

Missing daily or token series keep the month total and omit the history and the non-MTD periods instead of inventing zeros. Series longer than one page are read by following the cursor (up to 20 pages).

Refresh behavior

The analytics data only changes when its watermark advances (typically every four hours), so each region's last complete result is kept in the plugin runtime's in-memory cache:

  • Until four hours after the watermark, refreshes reuse it and re-check at most hourly.
  • After that, one month-total request (at most every ten minutes) looks for a newer watermark.
  • Only a newer watermark triggers the cost and token series requests.

The cached result is dropped at midnight in the configured time zone, is keyed by a hash of the access key ID and the time zone, is never persisted, and is not kept when a series was unavailable. The descriptor shares one ScriptFetchStrategy so the runtime (and its cache) survives between refreshes. A manual refresh follows the same rules; the script cannot tell a manual refresh from a timed one.

Changes outside the provider's own files

Host feature, in its own commit (feat(plugins): let detail sections be switchable variants), which I am happy to split into a separate PR:

  • ProviderDetailSection.variant (1–3 labels). Consecutive variant sections form a group; the menu card shows one at a time behind capsule pickers, stores the choice per provider and group, and re-measures the menu row on a switch. Text surfaces list every section.
  • Limits become 8 ordinary plus 16 variant sections per snapshot. Hosts advertise ctx.env.supportsDetailVariants. Documented in docs/plugins.md and codexbar-plugin.d.ts.
  • PluginProviderSpec.tokenCostHintLines, so an API-backed history does not fall back to "Estimated from local logs".

Provider integration:

  • UsageStore+TokenCost.swift: project snapshot.costUsage for this provider through a small mapping that fills today's figures.
  • SpendDashboardModel.swift: its daily labels are UTC days (alongside Mistral, OpenRouter, and xAI) unless the snapshot declares local days; the day helper now takes the provider input to read that.
  • Gatekeeper allowlist entries and palette fingerprints for those constructs and the new color.
  • Generated manifests, provider docs, site locales, llms.txt, and the social card (114 providers).

Validation

  • make check: passes.
  • ClaudePlatformAWSPluginTests: 14 tests (13 on both engines), covering the cache states, the projection, budgets, the period breakdowns, cursor paging, the local time zone, and an exact SigV4 Authorization value computed independently with Node's crypto for fixed fixture credentials and clock.
  • Also passing on the latest commit: ProviderPluginSnapshotContractTests, ProviderDetailVariantGroupTests, ProviderArchitectureGatekeeperTests, ProviderSettingsDescriptorTests, PluginProviderSpecTests, SpendDashboardModelTests, the localization suites, and the menu-card suites.
  • A broad filtered run (5,932 tests) passed twice on the first commit only. The full suite was not run locally; leaving it to CI.

Not verified / known gaps (why this is a draft)

  • The bundled build runs against a real Claude Platform on AWS organization on my machine, but nothing from that run is recorded here.
  • The cursor contract (next_page in the response, page as the parameter) is tested only against a stub.
  • No dashboard test covers the local-day branch in SpendDashboardModel; only the mapping that feeds it is tested.
  • The analytics actions aws-external-anthropic:ListAccountCost / ListAccountUsage are documented by Anthropic, and a policy naming them worked in my account, but they are missing from AWS's published IAM action list, so the IAM policy editor warns about them.
  • Amounts are Anthropic's analytics figures (after discounts, before credits) and can differ from the AWS Marketplace invoice; data lags about 4 hours. Both are documented in docs/claudeplatformaws.md.
  • Claude Platform on AWS API keys (Bearer) are not supported; only access keys were tested.
  • Localized strings for the 22 non-English catalogs were machine-translated.
  • Test fixtures follow the documented response shape with synthetic values.

Read month-to-date spend, 30-day cost and token history, and per-workspace and per-region spend from the Usage and Cost Analytics API on the regional aws-external-anthropic endpoints.

The bundled plugin signs each fixed-origin GET with SigV4 inside the script from a long-lived access key, so the plugin host gains no AWS credential-provider or SigV4 support. It reads no AWS profile, session token, or ambient AWS_* variable.

Refs steipete#4794
@clawsweeper

clawsweeper Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review blocked

Automated review did not run, so no review verdict was produced.

Reason: The input-safety check rejected material in this revision. No detected value, path, or scanner output is reproduced here.

Automatic review is on hold, including after source changes. Request a fresh re-review after resolving the failure; maintainers can also release the hold through an intentional scanner-policy update.

Next step: If this is a genuine credential, remove and rotate it. If it is an intentional test fixture, a maintainer must review and qualify it.

View the workflow run.

@clawsweeper clawsweeper Bot added the clawsweeper:intake:feature Preliminary submission classification; full source review pending. label Nov 11, 2026
…k is due

Keep each region's last complete result in the plugin runtime cache and read only the month total, at a bounded cadence, to look for a newer data watermark; the daily series are re-read only when it advances. Share one fetch strategy so the runtime and its cache survive between refreshes.

Add a month projection from the seven-day average and show when the next data is expected.
A section may carry a variant (1-3 labels). Consecutive variant sections form a group that the menu card shows one at a time behind capsule pickers, remembering the choice per provider and re-measuring the menu row on a switch. Hosts advertise ctx.env.supportsDetailVariants; snapshots may carry 8 ordinary plus 16 variant sections. PluginProviderSpec gains tokenCostHintLines so API-backed histories need not show the local-log estimate note.
…rkspace, and period

Adds the per-model breakdown, monthly and daily budgets, card pickers for Model/Workspace over 30d/MTD/7d/Today, a local or UTC time zone setting, cursor paging for series, and the native Today figure. Drops the duplicate budget bar and the local-log estimate note.
@Willian-Zhang
Willian-Zhang marked this pull request as ready for review November 11, 2026 20:46
@Willian-Zhang

Copy link
Copy Markdown
Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T21:36:53.967527Z 76bc665 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 90b13cce38

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

meteredCostUSD: history.meteredCostUSD,
costProvenance: history.costProvenance,
daily: history.daily,
updatedAt: history.updatedAt)

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 Preserve the real refresh date for local-day history

When the default local mode is used in UTC+12 to UTC+14, history.updatedAt is the plugin mapper's synthetic noon-UTC timestamp for windowEnd, which falls on the following local calendar day. SpendDashboardModel.sourceCoverageInterval now anchors this provider's local-day coverage to that timestamp, so it can exclude the oldest actual bucket, treat tomorrow as covered, and make the 30-day aggregate inconsistent; carry snapshot.updatedAt into this projected snapshot instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 76bc665. The projected snapshot now carries noon on the history's last date in the local zone rather than the mapper's noon-UTC stamp, so it stays on the right local day east of UTC+12. I rebuilt the date instead of using snapshot.updatedAt so a history reused across midnight keeps its own last day. Covered by local-day history keeps its last day east of UTC+12.

.padding(2)
.background(Capsule().fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(0.12)))
.fixedSize()
.menuCardInteractiveControl()

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 Mark variant pickers as interactive in overview rows

When Merged Overview uses the Compact layout and a built-in provider has no rate-window metrics (the default here unless budgets are configured), the overview renders the full card but passes model.containsInteractiveControls to the outer menu row. That predicate does not inspect variant details, so the row ignores this registered interactive region and handles a picker click as provider-row selection instead of forwarding it to the button; include variant presence in that predicate.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 76bc665. containsInteractiveControls now also counts detail sections that carry a variant, so the overview row forwards picker presses. Covered by rows with a variant picker forward its presses.

}

private func group(_ group: ProviderDetailVariantGroup) -> some View {
let stored = self.localSelection ?? self.provider.flatMap { self.settings?.detailVariantSelection(for: $0) }

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 Store a separate selection for each variant group

When a snapshot contains more than one variant run, which the new grouping API explicitly permits, every group reads and writes the same localSelection and one persisted array. Changing the second group therefore makes the first group fall back to its initial section (or switch too when labels overlap), and only the last compatible choice survives reopening; key selection state by group identity/index as well as provider.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 76bc665. View state and the stored choice are now keyed by provider and the group's position among the variant groups. Covered by each variant group of a provider remembers its own selection.

// Sums spend per label and lists the largest, folding the remainder into one row.
const breakdown = (period, field, limit) => {
const totals = {};
for (const row of period.rows) totals[row[field]] = (totals[row[field]] || 0) + row.amount;

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 Aggregate workspace labels without prototype-key collisions

If a customer names a workspace __proto__, constructor, or another inherited object key, the name passes ctx.isDetailLabel but this accumulation reads the inherited property instead of zero. The key is then omitted or gets a nonnumeric value, so the By workspace row and its percentage silently underreport spend even though the overall total is correct; use a Map or a null-prototype object for these externally supplied labels.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 76bc665. The breakdown totals are summed in a null-prototype object. Covered by labels matching inherited object keys keep their spend, which uses constructor and __proto__ as labels on both engines.

- Anchor local-day history to its last local date, so coverage is right east of UTC+12.
- Forward variant picker presses in overview rows.
- Remember a selection per variant group, not one per provider.
- Sum breakdown labels in a null-prototype object so inherited keys keep their spend.
- Shorten the history label, the history note, and the projection caption that were cut off on the card.
@Willian-Zhang

Copy link
Copy Markdown
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: 76bc665a63

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +252 to +253
if (now - monthStart < 3600000) {
return {

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 Retain spend history during the first month hour

During the first hour after the configured month boundary, this return skips the still-queryable 30-day cost and usage series along with the unqueryable month-total range. UsageStore.publishProviderDerivedTokenSnapshot treats a missing costUsage for this provider as confirmed empty, so a refresh in that window clears the previous history and leaves the spend dashboard empty even though the prior 29 days are available; keep the zero MTD result without omitting the history.

Useful? React with 👍 / 👎.

Comment on lines +1432 to +1435
// A snapshot whose history label declares local days (Claude Platform on AWS with its default time zone
// setting) is already cut on the dashboard's own day boundaries.
guard !ClaudePlatformAWSCostUsageMapping.usesLocalDays(provider, historyLabel: input.snapshot.historyLabel)
else { return displayCalendar }

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 Preserve the source zone for local-day history

When this provider uses its default local mode but Usage & Spend is pinned to a different zone, this branch treats Mac-local day keys as display-calendar days. Fresh evidence beyond the fixed noon-UTC case is the supported timezone mismatch: in Auckland with a UTC dashboard, local noon for 2026-11-11 is still 2026-11-10Z, so coverage ends on the 10th while the 2026-11-11 entry is parsed as the 11th and is excluded or invalidates the aggregate; carry the source zone with the snapshot (or use UTC buckets) before converting to the display calendar.

Useful? React with 👍 / 👎.

Comment on lines +751 to +752
case .claudeplatformaws:
snapshot.flatMap { ClaudePlatformAWSCostUsageMapping.tokenSnapshot(from: $0) }

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 Pass UTC bucket semantics to the history chart

When ANTHROPIC_AWS_TIME_ZONE=utc on a Mac west of UTC, this projected snapshot reaches CostHistoryChartMenuView, whose bucketCalendar special-cases only Mistral and therefore interprets a key such as 2026-08-14 as local August 14 instead of mapping midnight UTC to local August 13. The new UTC mode consequently shifts every history-chart day relative to the spend dashboard; pass the snapshot's day convention into the chart or conditionally apply UTC mapping for this provider.

Useful? React with 👍 / 👎.

rows: rows,
chart: chart)
chart: chart,
variant: section.variant)

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 Redact variant labels in private mode

When Hide Personal Info is enabled and a plugin uses an email address as a variant choice, which the generic string contract permits, this branch redacts every section/title/row/chart string but copies section.variant untouched, and ProviderDetailVariantPicker renders that string directly. This leaks the email through the picker; keep the canonical variant for selection if necessary, but redact its displayed label.

Useful? React with 👍 / 👎.

Comment on lines +114 to +115
/// Keyed by the group's position among the variant groups.
@State private var localSelections: [Int: [String]] = [:]

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 Scope in-memory picker state by provider

When an erased menu-card host is recycled from provider A to provider B and both expose compatible variant values, SwiftUI retains this @State at the same structural position, so B reads A's localSelections before consulting B's persisted setting. Fresh evidence beyond the prior per-group fix is that takeRecyclableMenuCardView explicitly falls back to any compatible row for Overview-to-provider transitions; key the in-memory state by provider too or reset it when provider changes.

Useful? React with 👍 / 👎.

rows: rows,
chart: section.chart)
chart: section.chart,
variant: section.variant)

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 Localize displayed variant labels

When the app uses a non-English locale, both localization paths copy variant values verbatim, so the new provider's picker remains English even though Model, Workspace, Today, 30d, and 7d already have translations in all 23 locale catalogs. Keep the canonical strings for selection and persistence, but apply L to the labels rendered by the picker.

Useful? React with 👍 / 👎.

This branch has not been deployed

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

Labels

clawsweeper:intake:feature Preliminary submission classification; full source review pending.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant