Skip to content

Commit 03993e3

Browse files
feat: prevent new signups of Minimax plans (#6734)
* Prevent new signups of Minimax plans * Fix failing test
1 parent cbfce91 commit 03993e3

9 files changed

Lines changed: 515 additions & 199 deletions

File tree

‎.specs/coding-plans.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S
4646

4747
1.4. A Plan ID **MUST NOT** be treated as the upstream provider identity. Multiple future plans from one upstream provider **MAY** coexist without extending or replacing one another unless their product rules explicitly state otherwise.
4848

49-
1.5. The MiniMax token catalog **MUST** contain Token Plan Plus, Token Plan Max, and Token Plan Ultra. All MiniMax token offerings **MUST** share the ordinary MiniMax BYOK provider ID, `minimax`.
49+
1.5. MiniMax Token Plan Plus, Token Plan Max, and Token Plan Ultra are no longer supported and are being removed from the Plan Catalog. The system **MUST NOT** create a new MiniMax subscription or accept a new MiniMax Availability Notification Intent; customer-facing catalog and purchase surfaces **MUST NOT** offer these plans for a new purchase. A live MiniMax subscription created before closure **MAY** continue under its existing terms while removal is in progress. Plan pricing, naming, and provider ID data for these plans **MAY** remain resolvable for existing subscribers and administrative inventory and remediation surfaces until removal is complete. Any MiniMax token offering that remains resolvable **MUST** continue to share the ordinary MiniMax BYOK provider ID, `minimax`.
5050

5151
1.6. The catalog **MUST** contain BytePlus Enterprise Coding Plan Lite with Plan ID `byteplus-coding-plan-team-lite`, provider ID `byteplus-coding`, a price of $20 in Kilo Credits, and a 30-day billing period. Its displayed limits **MUST** be described as approximately 1,900 requests every 5 hours, 12,000 requests per week, and 24,000 requests per subscription period. The plan's supported model IDs are `dola-seed-2.0-pro`, `dola-seed-2.0-lite`, `dola-seed-2.0-code`, `bytedance-seed-code`, `kimi-k2.5`, `glm-5.1`, `glm-5.2`, `deepseek-v4-flash`, `deepseek-v4-pro`, and `gpt-oss-120b`. Upstream Auto, Kimi-K2-Thinking, and GLM-4.7 **MUST NOT** be exposed.
5252

@@ -168,6 +168,12 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S
168168

169169
8.4. Current quota responses and logs **MUST NOT** contain Managed Plan Credentials, Provider Management Credentials, authorization headers, raw provider bodies or messages, inventory metadata, Upstream Plan IDs, Upstream Usage IDs, usernames, fingerprints, ciphertext, or provider-native quota fields. Provider responses **MUST** be bounded, validated, and normalized to an explicit non-secret subscription quota-window contract before leaving the Cloud boundary. The initial quota-window contract **MUST NOT** represent monetary balances or purchased-credit balances as subscription quota.
170170

171+
### 2026-09-25 -- MiniMax Token Plan closed to new signups
172+
173+
- Closed MiniMax Token Plan Plus, Max, and Ultra to new signups and new Availability Notification Intents, superseding 1.5's prior unconditional catalog requirement.
174+
- Confirmed a live MiniMax subscription created before closure continues under its existing terms while Kilo completes removal of MiniMax from the Plan Catalog.
175+
- Administrative inventory, upload, and remediation surfaces retain MiniMax visibility per 4.2 and 8.2 for as long as any MiniMax inventory or subscription remains.
176+
171177
### 2026-08-11 -- BytePlus Enterprise Coding Plan catalog copy
172178

173179
- Renamed BytePlus catalog offerings to Enterprise Coding Plan Lite and Enterprise Coding Plan Pro.

‎apps/mobile/src/components/home/home-screen.mounted.test.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,12 @@ describe('Home live presentation', () => {
665665
// indicator while the live content is pending, so the platform pull
666666
// control must not also hold the scroll inset open for a second one.
667667
expect(refresh().props.refreshing).toBe(false);
668-
expect(nodes('Skeleton')[0]?.props.className).toContain('min-h-[72px]');
668+
expect(
669+
nodes('View').some(view => {
670+
const className = String(view.props.className ?? '');
671+
return className.includes('min-h-[72px]') && className.includes('rounded-2xl');
672+
})
673+
).toBe(true);
669674
await act(async () => {
670675
pending.resolve(true);
671676
await pending.promise;

‎apps/web/src/components/subscriptions/coding-plans/CodingPlansGroup.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function CodingPlansGroup({
5757
} | null>(null);
5858

5959
const subscriptions = subscriptionQuery.data ?? [];
60-
const catalog = catalogQuery.data ?? [];
60+
const catalog = (catalogQuery.data ?? []).filter(plan => plan.purchasable);
6161
const byokKeys = byokQuery.data ?? [];
6262
const selectedPlan = catalog.find(plan => plan.planId === subscriptionRequest?.planId) ?? null;
6363
const nonTerminalSubscriptions = subscriptions.filter(
@@ -322,6 +322,7 @@ type CodingPlanOffer = {
322322
features: readonly string[];
323323
availabilityStatus: 'available' | 'sold_out';
324324
notificationRequested: boolean;
325+
purchasable: boolean;
325326
};
326327

327328
function CodingPlanAccessNotice({

‎apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts‎

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable drizzle/enforce-delete-with-where */
22
import { eq } from 'drizzle-orm';
33

4+
import { encryptApiKey } from '@/lib/ai-gateway/byok/encryption';
5+
import { BYOK_ENCRYPTION_KEY } from '@/lib/config.server';
46
import {
57
processCodingPlanCancellationAtPeriodEnd,
68
processCodingPlanRenewal,
@@ -23,7 +25,6 @@ import {
2325
jest.mock('@/lib/autoTopUp', () => ({
2426
maybePerformAutoTopUp: jest.fn(async () => undefined),
2527
}));
26-
const PLAN_ID = 'minimax-token-plan-plus';
2728
const MAX_PLAN_ID = 'minimax-token-plan-max';
2829
const BYTEPLUS_PLAN_ID = 'byteplus-coding-plan-team-lite';
2930
const BYTEPLUS_PRO_PLAN_ID = 'byteplus-coding-plan-team-pro';
@@ -35,21 +36,22 @@ const dueAt = new Date(Date.now() - 60_000).toISOString();
3536
async function createSubscription(
3637
balance = COST_MICRODOLLARS,
3738
autoTopUpEnabled = false,
38-
planId: CodingPlanId = PLAN_ID
39+
planId: CodingPlanId = BYTEPLUS_PLAN_ID
3940
) {
4041
const user = await insertTestUser({
4142
total_microdollars_acquired: balance,
4243
microdollars_used: 0,
4344
auto_top_up_enabled: autoTopUpEnabled,
4445
});
46+
const upstreamPlanId = `upstream-plan-${crypto.randomUUID()}`;
4547
await uploadKeysToInventory(
4648
planId === BYTEPLUS_PLAN_ID || planId === BYTEPLUS_PRO_PLAN_ID ? 'byteplus-coding' : 'minimax',
4749
planId,
48-
[`cron-key-${crypto.randomUUID()}::minimax-plan-${crypto.randomUUID()}`],
50+
[`cron-key-${crypto.randomUUID()}::${upstreamPlanId}`],
4951
{
50-
validateCredential: async ({ providerId, planId }) =>
52+
validateCredential: async ({ providerId }) =>
5153
providerId === 'byteplus-coding'
52-
? { valid: true, upstreamUsageId: `seat-${planId}` }
54+
? { valid: true, upstreamUsageId: `seat-${upstreamPlanId}` }
5355
: { valid: true },
5456
}
5557
);
@@ -61,6 +63,57 @@ async function createSubscription(
6163
return { user, subscriptionId: created.subscriptionId };
6264
}
6365

66+
// MiniMax Token Plans are closed to new signups (see pricing.ts). This seeds a
67+
// pre-existing MiniMax subscription directly, bypassing subscribeToCodingPlan,
68+
// to confirm the billing lifecycle cron still renews a grandfathered MiniMax
69+
// subscriber correctly.
70+
async function createGrandfatheredMiniMaxSubscription(planId: CodingPlanId, cost: number) {
71+
const user = await insertTestUser({
72+
total_microdollars_acquired: cost * 2,
73+
microdollars_used: 0,
74+
});
75+
const [inventory] = await db
76+
.insert(coding_plan_key_inventory)
77+
.values({
78+
plan_id: planId,
79+
provider_id: 'minimax',
80+
upstream_plan_id: `minimax-grandfathered-${crypto.randomUUID()}`,
81+
encrypted_api_key: encryptApiKey(`grandfathered-${crypto.randomUUID()}`, BYOK_ENCRYPTION_KEY),
82+
credential_fingerprint: crypto.randomUUID(),
83+
status: 'assigned',
84+
assigned_to_user_id: user.id,
85+
assigned_at: new Date().toISOString(),
86+
})
87+
.returning();
88+
const [installedByok] = await db
89+
.insert(byok_api_keys)
90+
.values({
91+
kilo_user_id: user.id,
92+
provider_id: 'minimax',
93+
encrypted_api_key: inventory.encrypted_api_key!,
94+
management_source: 'coding_plan',
95+
created_by: user.id,
96+
})
97+
.returning();
98+
const [subscription] = await db
99+
.insert(coding_plan_subscriptions)
100+
.values({
101+
user_id: user.id,
102+
plan_id: planId,
103+
provider_id: 'minimax',
104+
key_inventory_id: inventory.id,
105+
installed_byok_key_id: installedByok.id,
106+
status: 'active',
107+
cost_microdollars: cost,
108+
billing_period_days: 30,
109+
current_period_start: new Date().toISOString(),
110+
current_period_end: dueAt,
111+
credit_renewal_at: dueAt,
112+
})
113+
.returning();
114+
return { user, subscriptionId: subscription.id };
115+
}
116+
64117
afterEach(async () => {
65118
jest.mocked(maybePerformAutoTopUp).mockClear();
66119
await db.delete(coding_plan_terms);
@@ -94,25 +147,29 @@ describe('Coding Plan billing lifecycle cron', () => {
94147
createdAt: credit_transactions.created_at,
95148
})
96149
.from(credit_transactions)
97-
.where(eq(credit_transactions.description, 'Coding plan renewal: MiniMax Token Plan Plus'));
150+
.where(
151+
eq(
152+
credit_transactions.description,
153+
'Coding plan renewal: BytePlus Enterprise Coding Plan Lite'
154+
)
155+
);
98156

99157
expect(summary.renewals).toBe(1);
100158
expect(subscription.status).toBe('active');
101159
expect(terms.map(term => term.kind)).toEqual(['activation', 'renewal']);
102160
expect(renewalTransaction).toEqual([
103161
{
104-
description: 'Coding plan renewal: MiniMax Token Plan Plus',
162+
description: 'Coding plan renewal: BytePlus Enterprise Coding Plan Lite',
105163
createdAt: expect.any(String),
106164
},
107165
]);
108166
expect(credential.status).toBe('assigned');
109167
});
110168

111-
it('uses the subscribed MiniMax token plan name and snapshotted cost during renewal', async () => {
112-
const { subscriptionId } = await createSubscription(
113-
MAX_COST_MICRODOLLARS * 2,
114-
false,
115-
MAX_PLAN_ID
169+
it('uses the subscribed MiniMax token plan name and snapshotted cost during renewal for a grandfathered subscriber', async () => {
170+
const { subscriptionId } = await createGrandfatheredMiniMaxSubscription(
171+
MAX_PLAN_ID,
172+
MAX_COST_MICRODOLLARS
116173
);
117174

118175
const summary = await runCodingPlanBillingLifecycleCron(db);
@@ -217,7 +274,7 @@ describe('Coding Plan billing lifecycle cron', () => {
217274
]);
218275
});
219276

220-
it('renews after the subscriber deletes the installed MiniMax BYOK key', async () => {
277+
it('renews after the subscriber deletes the installed provider BYOK key', async () => {
221278
const { subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2);
222279
const [before] = await db
223280
.select()
@@ -396,7 +453,7 @@ describe('Coding Plan billing lifecycle cron', () => {
396453
expect(credential.encrypted_api_key).toBeNull();
397454
});
398455

399-
it('preserves a replacement MiniMax key when scheduled cancellation is processed', async () => {
456+
it('preserves a replacement provider key when scheduled cancellation is processed', async () => {
400457
const { subscriptionId, user } = await createSubscription(COST_MICRODOLLARS);
401458
const [subscription] = await db
402459
.select()

0 commit comments

Comments
 (0)