Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .specs/coding-plans.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S

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.

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`.
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`.

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.

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

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.

### 2026-09-25 -- MiniMax Token Plan closed to new signups

- Closed MiniMax Token Plan Plus, Max, and Ultra to new signups and new Availability Notification Intents, superseding 1.5's prior unconditional catalog requirement.
- Confirmed a live MiniMax subscription created before closure continues under its existing terms while Kilo completes removal of MiniMax from the Plan Catalog.
- 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.

### 2026-08-11 -- BytePlus Enterprise Coding Plan catalog copy

- Renamed BytePlus catalog offerings to Enterprise Coding Plan Lite and Enterprise Coding Plan Pro.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function CodingPlansGroup({
} | null>(null);

const subscriptions = subscriptionQuery.data ?? [];
const catalog = catalogQuery.data ?? [];
const catalog = (catalogQuery.data ?? []).filter(plan => plan.purchasable);
const byokKeys = byokQuery.data ?? [];
const selectedPlan = catalog.find(plan => plan.planId === subscriptionRequest?.planId) ?? null;
const nonTerminalSubscriptions = subscriptions.filter(
Expand Down Expand Up @@ -322,6 +322,7 @@ type CodingPlanOffer = {
features: readonly string[];
availabilityStatus: 'available' | 'sold_out';
notificationRequested: boolean;
purchasable: boolean;
};

function CodingPlanAccessNotice({
Expand Down
85 changes: 71 additions & 14 deletions apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable drizzle/enforce-delete-with-where */
import { eq } from 'drizzle-orm';

import { encryptApiKey } from '@/lib/ai-gateway/byok/encryption';
import { BYOK_ENCRYPTION_KEY } from '@/lib/config.server';
import {
processCodingPlanCancellationAtPeriodEnd,
processCodingPlanRenewal,
Expand All @@ -23,7 +25,6 @@ import {
jest.mock('@/lib/autoTopUp', () => ({
maybePerformAutoTopUp: jest.fn(async () => undefined),
}));
const PLAN_ID = 'minimax-token-plan-plus';
const MAX_PLAN_ID = 'minimax-token-plan-max';
const BYTEPLUS_PLAN_ID = 'byteplus-coding-plan-team-lite';
const BYTEPLUS_PRO_PLAN_ID = 'byteplus-coding-plan-team-pro';
Expand All @@ -35,21 +36,22 @@ const dueAt = new Date(Date.now() - 60_000).toISOString();
async function createSubscription(
balance = COST_MICRODOLLARS,
autoTopUpEnabled = false,
planId: CodingPlanId = PLAN_ID
planId: CodingPlanId = BYTEPLUS_PLAN_ID
) {
const user = await insertTestUser({
total_microdollars_acquired: balance,
microdollars_used: 0,
auto_top_up_enabled: autoTopUpEnabled,
});
const upstreamPlanId = `upstream-plan-${crypto.randomUUID()}`;
await uploadKeysToInventory(
planId === BYTEPLUS_PLAN_ID || planId === BYTEPLUS_PRO_PLAN_ID ? 'byteplus-coding' : 'minimax',
planId,
[`cron-key-${crypto.randomUUID()}::minimax-plan-${crypto.randomUUID()}`],
[`cron-key-${crypto.randomUUID()}::${upstreamPlanId}`],
{
validateCredential: async ({ providerId, planId }) =>
validateCredential: async ({ providerId }) =>
providerId === 'byteplus-coding'
? { valid: true, upstreamUsageId: `seat-${planId}` }
? { valid: true, upstreamUsageId: `seat-${upstreamPlanId}` }
: { valid: true },
}
);
Expand All @@ -61,6 +63,57 @@ async function createSubscription(
return { user, subscriptionId: created.subscriptionId };
}

// MiniMax Token Plans are closed to new signups (see pricing.ts). This seeds a
// pre-existing MiniMax subscription directly, bypassing subscribeToCodingPlan,
// to confirm the billing lifecycle cron still renews a grandfathered MiniMax
// subscriber correctly.
async function createGrandfatheredMiniMaxSubscription(planId: CodingPlanId, cost: number) {
const user = await insertTestUser({
total_microdollars_acquired: cost * 2,
microdollars_used: 0,
});
const [inventory] = await db
.insert(coding_plan_key_inventory)
.values({
plan_id: planId,
provider_id: 'minimax',
upstream_plan_id: `minimax-grandfathered-${crypto.randomUUID()}`,
encrypted_api_key: encryptApiKey(`grandfathered-${crypto.randomUUID()}`, BYOK_ENCRYPTION_KEY),
credential_fingerprint: crypto.randomUUID(),
status: 'assigned',
assigned_to_user_id: user.id,
assigned_at: new Date().toISOString(),
})
.returning();
const [installedByok] = await db
.insert(byok_api_keys)
.values({
kilo_user_id: user.id,
provider_id: 'minimax',
encrypted_api_key: inventory.encrypted_api_key!,
management_source: 'coding_plan',
created_by: user.id,
})
.returning();
const [subscription] = await db
.insert(coding_plan_subscriptions)
.values({
user_id: user.id,
plan_id: planId,
provider_id: 'minimax',
key_inventory_id: inventory.id,
installed_byok_key_id: installedByok.id,
status: 'active',
cost_microdollars: cost,
billing_period_days: 30,
current_period_start: new Date().toISOString(),
current_period_end: dueAt,
credit_renewal_at: dueAt,
})
.returning();
return { user, subscriptionId: subscription.id };
}

afterEach(async () => {
jest.mocked(maybePerformAutoTopUp).mockClear();
await db.delete(coding_plan_terms);
Expand Down Expand Up @@ -94,25 +147,29 @@ describe('Coding Plan billing lifecycle cron', () => {
createdAt: credit_transactions.created_at,
})
.from(credit_transactions)
.where(eq(credit_transactions.description, 'Coding plan renewal: MiniMax Token Plan Plus'));
.where(
eq(
credit_transactions.description,
'Coding plan renewal: BytePlus Enterprise Coding Plan Lite'
)
);

expect(summary.renewals).toBe(1);
expect(subscription.status).toBe('active');
expect(terms.map(term => term.kind)).toEqual(['activation', 'renewal']);
expect(renewalTransaction).toEqual([
{
description: 'Coding plan renewal: MiniMax Token Plan Plus',
description: 'Coding plan renewal: BytePlus Enterprise Coding Plan Lite',
createdAt: expect.any(String),
},
]);
expect(credential.status).toBe('assigned');
});

it('uses the subscribed MiniMax token plan name and snapshotted cost during renewal', async () => {
const { subscriptionId } = await createSubscription(
MAX_COST_MICRODOLLARS * 2,
false,
MAX_PLAN_ID
it('uses the subscribed MiniMax token plan name and snapshotted cost during renewal for a grandfathered subscriber', async () => {
const { subscriptionId } = await createGrandfatheredMiniMaxSubscription(
MAX_PLAN_ID,
MAX_COST_MICRODOLLARS
);

const summary = await runCodingPlanBillingLifecycleCron(db);
Expand Down Expand Up @@ -217,7 +274,7 @@ describe('Coding Plan billing lifecycle cron', () => {
]);
});

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

it('preserves a replacement MiniMax key when scheduled cancellation is processed', async () => {
it('preserves a replacement provider key when scheduled cancellation is processed', async () => {
const { subscriptionId, user } = await createSubscription(COST_MICRODOLLARS);
const [subscription] = await db
.select()
Expand Down
Loading
Loading