Skip to content

Commit 284a799

Browse files
committed
Remove legacy Tier mutation routes
1 parent d95e07c commit 284a799

16 files changed

Lines changed: 146 additions & 1107 deletions

docs/admin-ui/tiers.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ Creation behavior follows the effective runtime mode:
111111

112112
Use a custom tier or clone an existing tier when one customer needs different model access, model limits, pricing, or capacity. Do not recreate that policy with organization fields. While an active tier is authoritative in `enforce`, the API rejects new organization-level per-model limit maps and organization Asset Access writes; organization-wide RPM/TPM/RPH/RPD/TPD hard caps remain editable.
113113

114-
If an organization already had legacy per-model RPM/TPM maps before its tier was assigned, its Service Policy card shows a warning because those safety caps still apply alongside the tier. First reproduce any required limits on the tier, publish and preview them, then use **Clear legacy model caps**. The confirmation warns that clearing them before the tier is ready can increase allowed traffic.
114+
If an organization already had legacy per-model RPM/TPM maps before its tier was assigned, its Service Policy card shows a warning because those safety caps still apply alongside the tier. First reproduce any required limits on the tier, preview and activate them, then use **Clear legacy model caps**. The confirmation warns that clearing them before the tier is ready can increase allowed traffic.
115115

116-
`shadow` evaluates the staged tier but does not enforce it. For a newly created tier-first organization, DeltaLLM atomically snapshots the selected active version's allowed callable targets into legacy Asset Access so requests continue to work. That legacy mirror remains editable and authoritative during rollout; it intentionally does not follow later tier publications, allowing the preview and mismatch telemetry to expose policy changes before enforcement. Creating a new legacy organization through the API in this mode requires `"legacy_policy_exception": true`, matching the explicit migration checkbox in the drawer. `disabled` also keeps legacy Asset Access authoritative even if an assignment has already been staged. Once mode is `enforce`, the tier becomes authoritative and the organization Asset Access editor is hidden.
116+
`shadow` evaluates the staged tier but does not enforce it. For a newly created tier-first organization, DeltaLLM atomically snapshots the selected active version's allowed callable targets into legacy Asset Access so requests continue to work. That legacy mirror remains editable and authoritative during rollout; it intentionally does not follow later tier activations, allowing the preview and mismatch telemetry to expose policy changes before enforcement. Creating a new legacy organization through the API in this mode requires `"legacy_policy_exception": true`, matching the explicit migration checkbox in the drawer. `disabled` also keeps legacy Asset Access authoritative even if an assignment has already been staged. Once mode is `enforce`, the tier becomes authoritative and the organization Asset Access editor is hidden.
117117

118118
The optional organization RPM, TPM, RPH, RPD, and TPD fields are global hard caps. They apply across all models, teams, and keys in addition to the tier's per-model controls. Leave them blank when no extra organization-wide ceiling is needed. Budgets, budget resets, and audit-content storage also remain organization settings.
119119

@@ -232,9 +232,9 @@ Platform admins can inspect current pool utilization and temporarily boost one o
232232
| `POST` | `/ui/api/tier-capacity/boosts` | Apply a `1`-to-`100` weight multiplier with a Redis TTL of at most seven days |
233233
| `DELETE` | `/ui/api/tier-capacity/boosts` | Remove a temporary boost |
234234

235-
Boost creation and deletion are written to the audit log and attributed to the affected organization. A boost is runtime state: it expires automatically and does not modify the published tier version. See the [Organization Tiers Rollout Runbook](../deployment/organization-tiers-rollout.md) for API examples, metrics, and troubleshooting.
235+
Boost creation and deletion are written to the audit log and attributed to the affected organization. A boost is runtime state: it expires automatically and does not modify the active tier version. See the [Organization Tiers Rollout Runbook](../deployment/organization-tiers-rollout.md) for API examples, metrics, and troubleshooting.
236236

237-
The dashboard reports `live_data.status` as `healthy`, `partial`, or `unavailable`. When Redis cannot supply a live section, its numeric values are `null` and the UI shows an em dash; they are never presented as zero. The published pool configuration remains available from the tier snapshot.
237+
The dashboard reports `live_data.status` as `healthy`, `partial`, or `unavailable`. When Redis cannot supply a live section, its numeric values are `null` and the UI shows an em dash; they are never presented as zero. The active pool configuration remains available from the tier snapshot.
238238

239239
## Recommended User Journey
240240

@@ -328,7 +328,7 @@ You want to sell a Growth plan:
328328
- `gpt-4o` uses `growth-premium-pool`
329329
- customer price is lower than pay-as-you-go
330330

331-
Create one `Growth` tier, publish it, and assign it to every Growth customer.
331+
Create one `Growth` tier, activate it, and assign it to every Growth customer.
332332

333333
When a customer upgrades to Enterprise, assign the Enterprise tier instead of manually changing model access, prices, and limits on that organization.
334334

docs/api/admin.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,17 @@ Tier administration requires platform-admin permission.
130130
|--------|----------|---------|
131131
| `GET`, `POST` | `/ui/api/tiers` | List or create tiers |
132132
| `GET`, `PATCH`, `DELETE` | `/ui/api/tiers/{tier_id}` | Read, update, or delete a tier |
133-
| `POST` | `/ui/api/tiers/{tier_id}/versions` | Create a draft version |
133+
| `POST` | `/ui/api/tiers/bootstrap` | Atomically create a tier and its first draft |
134+
| `GET`, `POST` | `/ui/api/tiers/{tier_id}/versions` | List versions or create a draft version |
134135
| `GET` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}` | Read a version and its policies |
135136
| `POST` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/clone` | Clone a version into a draft |
136-
| `PUT` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/model-policies` | Replace the draft's model policies |
137-
| `PUT` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/capacity-pools` | Replace the draft's capacity pools |
138-
| `POST` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/publish` | Publish a version |
137+
| `GET`, `POST` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/model-policies` | List or create model policies |
138+
| `PATCH`, `DELETE` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/model-policies/{policy_id}` | Update or delete a model policy |
139+
| `POST` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/model-policies/bulk-limits` | Update request limits in bulk |
140+
| `GET`, `POST` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/capacity-pools` | List or create capacity pools |
141+
| `PATCH`, `DELETE` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/capacity-pools/{pool_id}` | Update or delete a capacity pool |
142+
| `GET` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/activation-preview` | Preview activation impact and concurrency guards |
143+
| `POST` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/activate` | Activate a draft using revision and active-version guards |
139144
| `POST` | `/ui/api/tiers/{tier_id}/versions/{tier_version_id}/archive` | Archive a version |
140145
| `GET`, `POST` | `/ui/api/organizations/{organization_id}/tier-assignments` | List or create assignments |
141146
| `PATCH`, `DELETE` | `/ui/api/organizations/{organization_id}/tier-assignments/{assignment_id}` | Update or remove an assignment |

plans/tiers-ux-redesign-remediation-plan.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Tiers UX redesign: code-review remediation plan v2
22

33
> Status: core implementation complete in the isolated `feature/tiers-ux-redesign` worktree; release-targeted verification is in progress. This revision incorporates both code-review rounds, resolves the five follow-up findings, and supersedes conflicting decisions in `tiers-ux-redesign-mapping.md` and earlier revisions of this document.
4+
>
5+
> Removal decision (2026-08-16): after the redesigned client and browser smoke test proved the guarded row-level workflow, immediate breaking removal was authorized. Any compatibility-stage instruction below is retained as historical rollout rationale but is superseded: collection replacement PUTs and bodyless `/publish` were deleted rather than gated.
46
57
## Implementation checkpoint
68

@@ -15,7 +17,7 @@ Completed in the current worktree:
1517
- bounded compatible-capacity-pool lookup for the model-policy editor;
1618
- backend repository/API coverage, UI helper coverage, and real PostgreSQL migration/catalog-query validation.
1719

18-
The legacy replacement and bodyless publish routes remain compatibility-only during the additive rollout stage. Their GA `428` enforcement gate is intentionally not enabled until the redesigned client is deployed and staging telemetry proves there are no remaining legacy callers.
20+
The legacy replacement and bodyless publish routes, their request schemas, service/repository implementations, client methods, audit actions, and compatibility tests have been removed. Guarded row-level mutations and `/activate` are the only supported configuration and activation paths.
1921

2022
## 0. Follow-up review closure
2123

@@ -120,7 +122,7 @@ Semantics:
120122
- A newly created blank version starts at revision `0`.
121123
- A cloned/restored version also starts at revision `0`; its copied contents are its initial configuration.
122124
- Each successful policy/pool create, update, delete, or bulk mutation increments revision exactly once.
123-
- Lifecycle-only changes such as publish/archive update `updated_at` but do not increment configuration revision.
125+
- Lifecycle-only changes such as activation/archive update `updated_at` but do not increment configuration revision.
124126
- `created_by_account_id` is populated for platform-session admins.
125127
- Master-key creation records `created_by_kind='master_key'` with no account ID.
126128
- Existing rows are backfilled as `created_by_kind='unknown'`; do not infer a creator from the publisher.
@@ -634,7 +636,7 @@ On `tier_configuration_stale`:
634636
| Two admins save revision 4 | First succeeds with revision 5; second gets `409`, no row is overwritten |
635637
| Policy/pool ID belongs to another Tier or version | Scoped `404`; neither child row nor either version revision changes |
636638
| Policy is saved while its pool is concurrently removed | Version lock serializes operations; the later operation revalidates against current state |
637-
| Draft is published while an editor is open | Next edit gets non-draft conflict; UI refetches and becomes read-only |
639+
| Draft is activated while an editor is open | Next edit gets non-draft conflict; UI refetches and becomes read-only |
638640
| Bootstrap response is lost | Same idempotency key returns the original tier and Draft v1 |
639641
| Two requests simultaneously use the same principal/key | Second waits on the transaction advisory lock, then replays the first committed Tier/v1 |
640642
| Bootstrap key is reused for different form data | `409` with idempotency-conflict code; no mutation |

src/api/admin/endpoints/tier_schemas.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ class TierModelPolicyRequest(_StrictRequest):
4848
metadata: dict[str, Any] | None = None
4949

5050

51-
class TierModelPolicyReplaceRequest(_StrictRequest):
52-
policies: list[TierModelPolicyRequest]
53-
54-
5551
class TierModelPolicyCreateRequest(TierModelPolicyRequest):
5652
expected_revision: int = Field(ge=0)
5753

@@ -112,10 +108,6 @@ class TierCapacityPoolRequest(_StrictRequest):
112108
metadata: dict[str, Any] | None = None
113109

114110

115-
class TierCapacityPoolReplaceRequest(_StrictRequest):
116-
pools: list[TierCapacityPoolRequest]
117-
118-
119111
class TierCapacityPoolCreateRequest(TierCapacityPoolRequest):
120112
expected_revision: int = Field(ge=0)
121113

src/api/admin/endpoints/tiers.py

Lines changed: 0 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@
1010
TierActivationRequest,
1111
TierCapacityPoolCreateRequest,
1212
TierCapacityPoolPatchRequest,
13-
TierCapacityPoolReplaceRequest,
1413
TierConfigurationMutationRequest,
1514
TierCreateRequest,
1615
TierModelPolicyCreateRequest,
1716
TierModelPolicyBulkLimitsRequest,
1817
TierModelPolicyPatchRequest,
19-
TierModelPolicyReplaceRequest,
2018
TierPatchRequest,
2119
TierVersionCreateRequest,
2220
)
@@ -589,54 +587,6 @@ async def delete_tier_model_policy(
589587
return response
590588

591589

592-
@router.put(
593-
"/ui/api/tiers/{tier_id}/versions/{tier_version_id}/model-policies",
594-
dependencies=_PLATFORM_ADMIN_DEPENDENCY,
595-
)
596-
async def replace_tier_model_policies(
597-
request: Request,
598-
tier_id: str,
599-
tier_version_id: str,
600-
payload: TierModelPolicyReplaceRequest,
601-
) -> dict[str, Any]:
602-
request_start = perf_counter()
603-
service = _tier_service(request)
604-
request_payload = _payload(payload)
605-
policies_payload = request_payload["policies"]
606-
before_detail: dict[str, Any] | None = None
607-
try:
608-
before_detail = await service.get_tier_version_detail(tier_id, tier_version_id)
609-
records = await service.replace_model_policies(
610-
tier_id,
611-
tier_version_id,
612-
policies_payload,
613-
)
614-
except TierAdminError as exc:
615-
raise _http_error(exc) from exc
616-
617-
response = {"data": [serialize_model_policy(record) for record in records]}
618-
tier_policy_invalidation = await _reload_tier_policy_for_audit(request)
619-
await emit_admin_mutation_audit(
620-
request=request,
621-
request_start=request_start,
622-
action=AuditAction.ADMIN_TIER_MODEL_POLICIES_REPLACE,
623-
resource_type="tier_model_policy_set",
624-
resource_id=tier_version_id,
625-
request_payload={
626-
"tier_id": tier_id,
627-
"policy_count": len(policies_payload),
628-
},
629-
response_payload={
630-
"policy_count": len(response["data"]),
631-
"tier_policy_invalidation": tier_policy_invalidation,
632-
},
633-
before={"policy_count": len(before_detail["model_policies"]) if before_detail else 0},
634-
after={"policy_count": len(response["data"])},
635-
metadata={"tier_policy_invalidation": tier_policy_invalidation},
636-
)
637-
return response
638-
639-
640590
@router.get(
641591
"/ui/api/tiers/{tier_id}/versions/{tier_version_id}/capacity-pools",
642592
dependencies=_PLATFORM_ADMIN_DEPENDENCY,
@@ -813,51 +763,6 @@ async def delete_tier_capacity_pool(
813763
return response
814764

815765

816-
@router.put(
817-
"/ui/api/tiers/{tier_id}/versions/{tier_version_id}/capacity-pools",
818-
dependencies=_PLATFORM_ADMIN_DEPENDENCY,
819-
)
820-
async def replace_tier_capacity_pools(
821-
request: Request,
822-
tier_id: str,
823-
tier_version_id: str,
824-
payload: TierCapacityPoolReplaceRequest,
825-
) -> dict[str, Any]:
826-
request_start = perf_counter()
827-
service = _tier_service(request)
828-
request_payload = _payload(payload)
829-
pools_payload = request_payload["pools"]
830-
before_detail: dict[str, Any] | None = None
831-
try:
832-
before_detail = await service.get_tier_version_detail(tier_id, tier_version_id)
833-
records = await service.replace_capacity_pools(
834-
tier_id,
835-
tier_version_id,
836-
pools_payload,
837-
)
838-
except TierAdminError as exc:
839-
raise _http_error(exc) from exc
840-
841-
response = {"data": [serialize_capacity_pool(record) for record in records]}
842-
tier_policy_invalidation = await _reload_tier_policy_for_audit(request)
843-
await emit_admin_mutation_audit(
844-
request=request,
845-
request_start=request_start,
846-
action=AuditAction.ADMIN_TIER_CAPACITY_POOLS_REPLACE,
847-
resource_type="tier_capacity_pool_set",
848-
resource_id=tier_version_id,
849-
request_payload={"tier_id": tier_id, "pool_count": len(pools_payload)},
850-
response_payload={
851-
"pool_count": len(response["data"]),
852-
"tier_policy_invalidation": tier_policy_invalidation,
853-
},
854-
before={"pool_count": len(before_detail["capacity_pools"]) if before_detail else 0},
855-
after={"pool_count": len(response["data"])},
856-
metadata={"tier_policy_invalidation": tier_policy_invalidation},
857-
)
858-
return response
859-
860-
861766
@router.get(
862767
"/ui/api/tiers/{tier_id}/versions/{tier_version_id}/activation-preview",
863768
dependencies=_PLATFORM_ADMIN_DEPENDENCY,
@@ -920,46 +825,6 @@ async def activate_tier_version(
920825
return response
921826

922827

923-
@router.post(
924-
"/ui/api/tiers/{tier_id}/versions/{tier_version_id}/publish",
925-
dependencies=_PLATFORM_ADMIN_DEPENDENCY,
926-
)
927-
async def publish_tier_version(
928-
request: Request,
929-
tier_id: str,
930-
tier_version_id: str,
931-
) -> dict[str, Any]:
932-
request_start = perf_counter()
933-
service = _tier_service(request)
934-
try:
935-
before = serialize_tier_version(
936-
await service.require_version_for_tier(tier_id, tier_version_id)
937-
)
938-
published = await service.publish_tier_version(
939-
tier_id,
940-
tier_version_id,
941-
published_by_account_id=_actor_account_id(request),
942-
)
943-
except TierAdminError as exc:
944-
raise _http_error(exc) from exc
945-
946-
response = serialize_tier_version(published)
947-
tier_policy_invalidation = await _reload_tier_policy_for_audit(request)
948-
await emit_admin_mutation_audit(
949-
request=request,
950-
request_start=request_start,
951-
action=AuditAction.ADMIN_TIER_VERSION_PUBLISH,
952-
resource_type="tier_version",
953-
resource_id=tier_version_id,
954-
request_payload={"tier_id": tier_id},
955-
response_payload={**response, "tier_policy_invalidation": tier_policy_invalidation},
956-
before=before,
957-
after=response,
958-
metadata={"tier_policy_invalidation": tier_policy_invalidation},
959-
)
960-
return response
961-
962-
963828
@router.post(
964829
"/ui/api/tiers/{tier_id}/versions/{tier_version_id}/archive",
965830
dependencies=_PLATFORM_ADMIN_DEPENDENCY,

src/audit/actions.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,13 @@ class AuditAction(str, Enum):
9191
ADMIN_TEAM_MEMBER_ADD = "ADMIN_TEAM_MEMBER_ADD"
9292
ADMIN_TEAM_MEMBER_REMOVE = "ADMIN_TEAM_MEMBER_REMOVE"
9393
ADMIN_TEAM_UPDATE = "ADMIN_TEAM_UPDATE"
94-
ADMIN_TIER_CAPACITY_POOLS_REPLACE = "ADMIN_TIER_CAPACITY_POOLS_REPLACE"
9594
ADMIN_TIER_CAPACITY_POOL_CREATE = "ADMIN_TIER_CAPACITY_POOL_CREATE"
9695
ADMIN_TIER_CAPACITY_POOL_DELETE = "ADMIN_TIER_CAPACITY_POOL_DELETE"
9796
ADMIN_TIER_CAPACITY_POOL_UPDATE = "ADMIN_TIER_CAPACITY_POOL_UPDATE"
9897
ADMIN_TIER_CAPACITY_BOOST_DELETE = "ADMIN_TIER_CAPACITY_BOOST_DELETE"
9998
ADMIN_TIER_CAPACITY_BOOST_UPSERT = "ADMIN_TIER_CAPACITY_BOOST_UPSERT"
10099
ADMIN_TIER_CREATE = "ADMIN_TIER_CREATE"
101100
ADMIN_TIER_DELETE = "ADMIN_TIER_DELETE"
102-
ADMIN_TIER_MODEL_POLICIES_REPLACE = "ADMIN_TIER_MODEL_POLICIES_REPLACE"
103101
ADMIN_TIER_MODEL_POLICY_CREATE = "ADMIN_TIER_MODEL_POLICY_CREATE"
104102
ADMIN_TIER_MODEL_POLICY_BULK_LIMITS = "ADMIN_TIER_MODEL_POLICY_BULK_LIMITS"
105103
ADMIN_TIER_MODEL_POLICY_DELETE = "ADMIN_TIER_MODEL_POLICY_DELETE"
@@ -109,7 +107,6 @@ class AuditAction(str, Enum):
109107
ADMIN_TIER_VERSION_ACTIVATE = "ADMIN_TIER_VERSION_ACTIVATE"
110108
ADMIN_TIER_VERSION_CLONE = "ADMIN_TIER_VERSION_CLONE"
111109
ADMIN_TIER_VERSION_CREATE = "ADMIN_TIER_VERSION_CREATE"
112-
ADMIN_TIER_VERSION_PUBLISH = "ADMIN_TIER_VERSION_PUBLISH"
113110
ADMIN_USER_BLOCK = "ADMIN_USER_BLOCK"
114111
ADMIN_USER_CREATE = "ADMIN_USER_CREATE"
115112
ADMIN_USER_DELETE = "ADMIN_USER_DELETE"

0 commit comments

Comments
 (0)