Repository navigation
feat(providers): add Claude Platform on AWS spend - #5061
Willian-Zhang wants to merge 5 commits into
Conversation
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
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review blockedAutomated 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. |
…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.
|
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| if (now - monthStart < 3600000) { | ||
| return { |
There was a problem hiding this comment.
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 👍 / 👎.
| // 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 } |
There was a problem hiding this comment.
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 👍 / 👎.
| case .claudeplatformaws: | ||
| snapshot.flatMap { ClaudePlatformAWSCostUsageMapping.tokenSnapshot(from: $0) } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 👍 / 👎.
| /// Keyed by the group's position among the variant groups. | ||
| @State private var localSelections: [Int: [String]] = [:] |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 👍 / 👎.
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.
The screenshot predates
76bc665a6, which shortened three strings that were cut off on the card: the local-mode history label is nowLast 30 days(wasLast 30 days (local time)), the history note isAnthropic 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:
GET /v1/organizations/analytics/costand/usage, on the regionalaws-external-anthropic.<region>.api.awsendpoints. It is Claude-only by construction, so there is no Marketplace selector to get wrong.AWS_*variable; the provider uses its ownANTHROPIC_AWS_*keys so existing AWS credentials are never picked up implicitly.What it shows
costUsage: daily cost, requests, and input/output tokens per model, with today's figures filled in for the native spend card.ListWorkspacesis allowed, IDs otherwise) over 30d, MTD, 7d, or Today. The choice is remembered.local(default) orutc, deciding where days and months begin. The API buckets days in UTC, soutcreads daily buckets plus today, whilelocalreads 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:
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
ScriptFetchStrategyso 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.ctx.env.supportsDetailVariants. Documented indocs/plugins.mdandcodexbar-plugin.d.ts.PluginProviderSpec.tokenCostHintLines, so an API-backed history does not fall back to "Estimated from local logs".Provider integration:
UsageStore+TokenCost.swift: projectsnapshot.costUsagefor 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.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 SigV4Authorizationvalue computed independently with Node'scryptofor fixed fixture credentials and clock.ProviderPluginSnapshotContractTests,ProviderDetailVariantGroupTests,ProviderArchitectureGatekeeperTests,ProviderSettingsDescriptorTests,PluginProviderSpecTests,SpendDashboardModelTests, the localization suites, and the menu-card suites.Not verified / known gaps (why this is a draft)
next_pagein the response,pageas the parameter) is tested only against a stub.SpendDashboardModel; only the mapping that feeds it is tested.aws-external-anthropic:ListAccountCost/ListAccountUsageare 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.docs/claudeplatformaws.md.