feat(canvas): LLM Billing section in Config tab (internal#691) #1928
Reference in New Issue
Block a user
Delete Branch "feat/canvas-llm-billing-mode-section"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Canvas Config-tab UI for the per-workspace
llm_billing_modeoverride (internal#691). Adds a newLLMBillingSectionmounted aboveSecretsSectioninConfigTab.tsxthat:"byokk"in the column)Deploy order — this PR ships THIRD:
Until the workspace-server PR lands, the section renders the error banner with the network error (the per-tenant route doesn't exist yet). Cosmetic-only behavior — no broken workflows.
Why the canvas calls the per-tenant route directly (not the CP proxy)
The canvas is hosted on the same origin as the per-tenant workspace-server (canvas is served by the platform container's reverse proxy). Calling the per-tenant route is one HTTP hop; calling CP would be canvas → CP → tenant, two hops. The CP proxy exists for ops + CTO scripts that hold CP bearer auth and don't want to juggle per-tenant
admin_tokenvalues.Test plan
npx vitest run src/components/tabs/config/__tests__/llm-billing-section.test.tsx— 5 tests, all pass{mode: "byok"}and reflects new resolution in UI{mode: null}when user picks Inheritnpx vitest run(full canvas suite) — 220 files, 3369 tests pass, 1 skipped (pre-existing)npx tsc --noEmit— clean on new files (pre-existing main-branch TS errors in Attachment*/KeyValueField tests are not introduced here)npx eslint src/components/tabs/config/llm-billing-section.tsx ...test.tsx— cleanFive-Axis self-review
Correctness: Dropdown current-choice is derived from
workspace_override(raw value), NOTresolved_mode— otherwise "explicitplatform_managedoverride" and "inherit while org happens to beplatform_managed" would conflate, and clicking Save on the second case would write an explicit override when the user expected no change.Safety/Security: Same-origin admin route + existing
X-Molecule-Org-Slug+ Bearer pair fromplatformAuthHeaders(). No new auth surface. No secrets in the response body — the per-tenant route returns enum strings only.Test coverage: 5 tests covering happy GET, BYOK PUT, Inherit-clears PUT, garbled-override banner, error banner. Not exhaustive on every UI state (loading / saving disabled state) but the load-saving spinner is covered by the disabled-during-save assertion in the BYOK test path.
Observability: The UI surfaces
sourcedirectly to the user ("inherited from org default" vs "explicit override on this workspace") so a confused operator can answer "why is this workspace being stripped" without DB or log archeology — the RFC Observability hot-spot is partially answered here for the human-facing channel, and fully answered byMOLECULE_LLM_BILLING_MODE_RESOLVED+ the strip-gate log line on the workspace-server side.Backward-compat: Net-new component, no existing component touched beyond a one-line import + one-line mount in ConfigTab. Until workspace-server lands the section just shows the error banner — graceful degradation. The TS interface is a literal struct (no Go-type bridge needed; canvas has no build dependency on the core repo's Go modules).
CTO attention items
If you decide to flip the architectural fork to Option B (override on CP), the dropdown URL needs to switch from
/admin/workspaces/...to/cp/admin/workspaces/...?org_slug=...— a 5-line diff. Not gating this PR; flagging in case the workspace-server PR's review changes the storage location.🤖 Generated with Claude Code
SOP-Checklist
llm-billing-section.test.tsx(renders resolved-mode + source on inherit, PUTs{mode: "byok"}and reflects the post-write resolution, PUTs{mode: null}on Inherit, yellow warning banner on garbled override, error banner on GET failure). Full canvas suite re-run: 220 files, 3369 tests pass, 1 skipped (pre-existing). The save-disabled spinner state is asserted inline in the BYOK PUT test path.platformAuthHeaders().dev-sop.md. The component shows the error banner on staging until #1927 lands (the per-tenant route 404s); cosmetic-only behaviour, documented in PR body.ConfigTab.tsx. No feature flag, no parallel path, no compat shim. Until #1927 deploys, the section renders the error banner — graceful degradation, not a shim.feedback_principles,feedback_no_silent_checklist_trim(filing every axis),feedback_check_for_parallel_work_before_fix_pr(verified no parallel #691 canvas PR open),reference_workspace_placement_rfc(canvas talks same-origin per-tenant, not via CP — confirms the "Why direct, not proxy" decision in PR body).Adds a new section to the workspace Config tab that surfaces the RESOLVED per-workspace billing mode (from internal#691 resolver), the org-level default, and a dropdown to set / clear the per-workspace override. Surface: - src/components/tabs/config/llm-billing-section.tsx — the Section - src/components/tabs/config/index.ts — re-export - src/components/tabs/ConfigTab.tsx — mounts the section above SecretsSection Talks to the per-tenant workspace-server admin route GET/PUT /admin/workspaces/:id/llm-billing-mode (shipping in the workspace-server PR — feat/per-workspace-llm-billing-mode). The CP proxy at /cp/admin/workspaces/:id/llm-billing-mode exists for ops use; the canvas uses the per-tenant path directly to avoid a CP roundtrip. Dropdown semantics: - "Inherit org default" → PUT {mode: null} (clears override) - "Platform-managed" → PUT {mode: "platform_managed"} - "BYOK" → PUT {mode: "byok"} - "Disabled" → PUT {mode: "disabled"} Garbled persisted overrides surface a yellow warning banner with the raw bad value so the user can clear it explicitly. Stage A: npx vitest run src/components/tabs/config/__tests__/llm-billing-section.test.tsx → 5 tests, 5 pass Full canvas suite: 220 files, 3369 tests pass, 1 skipped (pre-existing). TS / lint clean on the new files (pre-existing main-branch TS errors in unrelated Attachment*/KeyValueField test files are not introduced here). Deploy order: this PR ships THIRD, after CP routes and the workspace-server PR — the routes the dropdown hits won't exist until workspace-server lands. Until then the section renders the error banner with the network error, which is acceptable cosmetic-only behavior. Refs internal#691. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Phase 3 — Five-Axis self-review (canvas)
CR1 self-pass per dev-sop. CR2 from a peer reviewer still needed before merge.
Correctness
workspace_override(raw), NOTresolved_mode. Otherwise "explicitplatform_managedoverride" and "inherit while org happens to beplatform_managed" would conflate, and clicking Save without changing the value would write an explicit override when the user expected no change. Locked in byTestPutWorkspaceLLMBillingMode_ExplicitNullClearsOverrideon the workspace-server side + the inherit-test case here."inherit"→{mode: null}, otherwise{mode: choice}. The per-tenant route's strict-mode parsing (omitted vs null vs string) means we can't fall back to "omit the field" as a shortcut."byokk"from some external write) surface a yellow warning banner with the raw value so the user can clear it explicitly. The dropdown defaults to "inherit" in this case so a click-Save without choosing produces a clean PUT-null clear.Safety / Security
platformAuthHeaders()helper attaches the existing Bearer +X-Molecule-Org-Slugpair — no new auth surface.Test coverage
5 cases via vitest + jsdom:
{mode: "byok"}and reflects the new resolution in UI{mode: null}when user picks InheritNot tested: explicit loading-spinner-while-saving state (low ROI; the
disabledattribute on the<select>during save is asserted indirectly in the BYOK test path).Observability
sourcedirectly to the user: "Resolved mode: byok (explicit override on this workspace)". A confused user can answer "why is this workspace stripped" without an operator round-trip — the RFC Observability hot-spot is partially answered here for the human-facing channel.role="alert"witharia-live="assertive"so screen readers announce them. The "Updated" success banner usesaria-live="polite"so non-critical state changes don't interrupt.Backward-compat
importand one-line mount inConfigTab.tsx.BillingModeResolution— flagged inline in a doc comment.Items flagged for CR2 reviewer
/admin/workspaces/...to/cp/admin/workspaces/...?org_slug=...— a 5-line diff. Not gating.🤖 Generated with Claude Code
Five-Axis review — APPROVED
Fresh-eyes pass on the canvas Config-tab
LLMBillingSectionfor internal#691. Approving — the dropdown wires to the right route, the inherit-vs-explicit distinction is preserved on writes, and the garbled-override fallback is surfaced to the user.Correctness
GET / PUT /admin/workspaces/:id/llm-billing-modeviaapi.get/api.put— same-origin to the per-tenant workspace-server, matching the route #1927 registers underwsAdmin. ✅currentChoice(llm-billing-section.tsx:323): the dropdown's selected option is derived fromworkspace_override(raw), NOTresolved_mode. This is the right call — without it, an inherited workspace whose org happens to beplatform_managedwould be visually indistinguishable from a workspace with an explicitplatform_managedoverride, and Save would write the wrong shape. Comment in code explains the rationale. ✅{mode: null}on the PUT body, not omitted. Matches the workspace-server'sjson.RawMessageparsing in #1927 (explicit-null = clear, missing key = 400). ✅workspace_overrideis non-null but not in the enum set) falls through tocurrentChoice = "inherit"so the user can pick a clean value; the warning banner (line 420) tells them to do exactly that. ✅setResolution(updated)), no follow-up GET roundtrip — matches the workspace-server handler's behaviour of returning the post-write resolution. ✅Safety / Security
api.get/api.putautomatically attachplatformAuthHeaders()(BearerNEXT_PUBLIC_ADMIN_TOKEN+X-Molecule-Org-Slug), which the workspace-server'swsAdmin(=middleware.AdminAuth(db.DB)) gate accepts. Same auth surface as every other canvas admin call. No new surface introduced. ✅resolved_mode,source,org_default,workspace_override). No secret material in the JSON. Cross-workspace leak vector is not present — the UI only ever shows the workspace's own resolution + that workspace's org's default. The org default is by definition org-scoped (the same value everyone in the org sees), not another workspace's value. ✅savingprevents double-submits. Component clears the success banner after 2s — no stale-state confusion. ✅Test coverage
__tests__/llm-billing-section.test.tsx:{mode: "byok"}when user picks BYOK; UI reflects post-write resolution from PUT response.{mode: null}when user picks Inherit."byokk"typo case).Sectionmock with passthrough is the right move — otherwisedefaultOpen={false}hides the dropdown in the test DOM and would force every test through an open-section interaction step. ✅Observability
sourcedirectly to the user ("inherited from org default" / "explicit override on this workspace" / fallback label), which is the RFC observability hot-spot for the human-facing channel — a confused operator can answer "why is this workspace being stripped?" without DB or log archeology. ✅constant_fallback) has a verbose label explaining what happened — "workspace + org defaults missing or unrecognized — defaulted to platform_managed." That's the right diagnostic message. ✅<code>{workspace_override}</code>) so the user can report it precisely. ✅Backward compat
BillingModeResolutionis a literal TS interface (PR body acknowledges this). ✅Minor observations (non-blocking)
setTimeoutis a small leak risk on unmount; React Strict Mode would warn. Not a blocker — typical for transient UX toasts and the component is unlikely to unmount mid-save in practice.Approving as
agent-reviewersincehongmingis the author.core-security review — APPROVED
Security axis check:
cpadmin route already auth-gated by AdminGate.{mode: null}body for inherit-org-default is correctly parsed without writing NULL straight-through; null clears the workspace-level row.Sentinel: APPROVED.
core-qa review — APPROVED
Test coverage check:
npx vitest run— 220 files / 3369 tests pass. TS + lint clean on new files.workspace_override(raw), notresolved_mode— preserves inherit-vs-explicit on writes. Verified.Sentinel: APPROVED.