Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion .specs/coding-plans.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S

1.6. The catalog **MUST** contain BytePlus 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.

1.7. When no assignable Managed Plan Credential exists for an offering, customer-facing catalog responses **MUST** identify the offering as sold out without exposing credential counts or credential metadata.
1.7. The catalog **MUST** contain BytePlus Coding Plan Pro with Plan ID `byteplus-coding-plan-team-pro`, provider ID `byteplus-coding`, a price of $100 in Kilo Credits, and a 30-day billing period. Its displayed limits **MUST** be described as approximately 9,500 requests every 5 hours, 60,000 requests per week, and 120,000 requests per subscription period. Pro **MUST** support exactly the same model IDs as Lite and **MUST NOT** expose Upstream Auto, Kimi-K2-Thinking, or GLM-4.7.

1.8. When no assignable Managed Plan Credential exists for an offering, customer-facing catalog responses **MUST** identify the offering as sold out without exposing credential counts or credential metadata.

## 2. Subscription and billing

Expand Down
10 changes: 8 additions & 2 deletions .specs/subscription-center.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Updated 2026-06-05 -- KiloClaw final Commit term continuation behavior.
Updated 2026-06-19 -- Current Coding Plan quota presentation and routing independence.
Updated 2026-07-01 -- Coding Plans installed-key deletion blocked in BYOK.
Updated 2026-06-26 -- MiniMax token plan tiers and provider-level Coding Plan exclusivity.
Updated 2026-08-05 -- BytePlus Coding Plan Pro catalog requirements.

## Conventions

Expand Down Expand Up @@ -332,8 +333,13 @@ historical Commit names, prices, invoices, and credit deductions.
offering with provider name, plan name, recurring USD price, billing
period, payment source, and catalog feature copy. MiniMax token offerings
MUST include Token Plan Plus, Token Plan Max, and Token Plan Ultra.
BytePlus offerings MUST include Coding Plan Lite at $20 per 30 days. An
offering with assignable credential capacity MUST show a subscribe action.
BytePlus offerings MUST include Coding Plan Lite at $20 per 30 days and
Coding Plan Pro at $100 per 30 days. Lite MUST display approximately 1,900
requests every 5 hours, 12,000 per week, and 24,000 per subscription
period. Pro MUST display approximately 9,500 requests every 5 hours,
60,000 per week, and 120,000 per subscription period. Both offerings MUST
use the model IDs and exclusions defined in `.specs/coding-plans.md`.
An offering with assignable credential capacity MUST show a subscribe action.
Purchase messaging MUST explain automatic BYOK setup for the selected
provider, and purchase MUST be blocked when any personal BYOK key for that
provider exists, including a disabled key. In that state, the system MUST direct
Expand Down
36 changes: 35 additions & 1 deletion apps/web/src/lib/coding-plans/billing-lifecycle-cron.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ jest.mock('@/lib/autoTopUp', () => ({
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';
const COST_MICRODOLLARS = 20_000_000;
const MAX_COST_MICRODOLLARS = 50_000_000;
const BYTEPLUS_PRO_COST_MICRODOLLARS = 100_000_000;
const dueAt = new Date(Date.now() - 60_000).toISOString();

async function createSubscription(
Expand All @@ -37,7 +39,7 @@ async function createSubscription(
auto_top_up_enabled: autoTopUpEnabled,
});
await uploadKeysToInventory(
planId === BYTEPLUS_PLAN_ID ? 'byteplus-coding' : 'minimax',
planId === BYTEPLUS_PLAN_ID || planId === BYTEPLUS_PRO_PLAN_ID ? 'byteplus-coding' : 'minimax',
planId,
[`cron-key-${crypto.randomUUID()}::minimax-plan-${crypto.randomUUID()}`],
{
Expand Down Expand Up @@ -162,6 +164,38 @@ describe('Coding Plan billing lifecycle cron', () => {
expect(renewal).toEqual([{ description: 'Coding plan renewal: BytePlus Coding Plan Lite' }]);
});

it('renews BytePlus Pro for 100 credits while retaining its assigned credential', async () => {
const { subscriptionId } = await createSubscription(
BYTEPLUS_PRO_COST_MICRODOLLARS * 2,
false,
BYTEPLUS_PRO_PLAN_ID
);

const summary = await runCodingPlanBillingLifecycleCron(db);
const [subscription] = await db
.select()
.from(coding_plan_subscriptions)
.where(eq(coding_plan_subscriptions.id, subscriptionId));
const [credential] = await db
.select()
.from(coding_plan_key_inventory)
.where(eq(coding_plan_key_inventory.id, subscription.key_inventory_id!));
const renewal = await db
.select({ description: credit_transactions.description })
.from(credit_transactions)
.where(eq(credit_transactions.description, 'Coding plan renewal: BytePlus Coding Plan Pro'));

expect(summary.renewals).toBe(1);
expect(subscription).toMatchObject({
plan_id: BYTEPLUS_PRO_PLAN_ID,
provider_id: 'byteplus-coding',
cost_microdollars: BYTEPLUS_PRO_COST_MICRODOLLARS,
billing_period_days: 30,
});
expect(credential.status).toBe('assigned');
expect(renewal).toEqual([{ description: 'Coding plan renewal: BytePlus Coding Plan Pro' }]);
});

it('renews after the subscriber deletes the installed MiniMax BYOK key', async () => {
const { subscriptionId } = await createSubscription(COST_MICRODOLLARS * 2);
const [before] = await db
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/lib/coding-plans/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ const MAX_PLAN_ID = 'minimax-token-plan-max';
const ULTRA_PLAN_ID = 'minimax-token-plan-ultra';
const PROVIDER_ID = 'minimax';
const BYTEPLUS_PLAN_ID = 'byteplus-coding-plan-team-lite';
const BYTEPLUS_PRO_PLAN_ID = 'byteplus-coding-plan-team-pro';
const BYTEPLUS_PROVIDER_ID = 'byteplus-coding';
const COST_MICRODOLLARS = 20_000_000;
const MAX_COST_MICRODOLLARS = 50_000_000;
const ULTRA_COST_MICRODOLLARS = 120_000_000;
const BYTEPLUS_PRO_COST_MICRODOLLARS = 100_000_000;

const validatedInventoryUpload = { validateCredential: async () => true };

Expand Down Expand Up @@ -120,6 +122,19 @@ describe('coding plans', () => {
'Kilo automatically configures BytePlus in your BYOK settings.',
]),
});
expect(CODING_PLAN_CATALOG[BYTEPLUS_PRO_PLAN_ID]).toEqual({
planId: BYTEPLUS_PRO_PLAN_ID,
providerName: 'BytePlus',
name: 'Coding Plan Pro',
providerId: BYTEPLUS_PROVIDER_ID,
coveredModelIds: BYTEPLUS_CODING_MODEL_IDS,
costMicrodollars: BYTEPLUS_PRO_COST_MICRODOLLARS,
billingPeriodDays: 30,
features: expect.arrayContaining([
'For complex, high-intensity development use.',
'Approximately 9,500 requests every 5 hours, 60,000 requests per week, and 120,000 requests per 30-day subscription period.',
]),
});
});

it('activates BytePlus alongside MiniMax using its managed BYOK provider slot', async () => {
Expand Down
48 changes: 41 additions & 7 deletions apps/web/src/lib/coding-plans/inventory-slack-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,17 @@ describe('buildCodingPlanInventorySlackNotification', () => {
revoked: 19,
});
expect(result.notification.text).toBe(
'Coding Plans inventory: 83 available, 156 assigned, 263 loaded. 5 pending revocation. Token Plan Plus: 79 available. Token Plan Max: 3 available. Token Plan Ultra: 1 available.'
'Coding Plans inventory: `83` available, `156` assigned, `263` loaded. `5` pending revocation. MiniMax Token Plan Plus: `79` available, `148` assigned, `251` loaded. MiniMax Token Plan Max: `3` available, `7` assigned, `10` loaded. MiniMax Token Plan Ultra: `1` available, `1` assigned, `2` loaded.'
);

const rendered = blockText(result);
expect(rendered).toContain('MiniMax · Current snapshot');
expect(rendered).toContain('Token Plan Plus');
expect(rendered).toContain('79 available · 148 assigned · 251 loaded');
expect(rendered).toContain('5 pending revocation · 19 revoked');
expect(rendered).toContain('*Total* · Available `83` · Assigned `156` · Loaded `263`');
expect(rendered).toContain('*MiniMax*');
expect(rendered).toContain('Token Plan Plus · Available `79` · Assigned `148` · Loaded `251`');
expect(rendered).not.toContain('*Provider* | *Plan*');
expect(rendered).toContain('`5` credentials are pending revocation');
expect(rendered.match(/pending revocation/g)).toHaveLength(1);
expect(rendered).toContain('Snapshot: 2026-07-30 12:00 UTC');
expect(rendered).toContain('/admin/coding-plans|Open Coding Plans');
expect(rendered).not.toContain('open_coding_plans_inventory');
Expand All @@ -71,11 +74,42 @@ describe('buildCodingPlanInventorySlackNotification', () => {
expect(result.totals.revocationFailed).toBe(2);
expect(result.totals.loaded).toBe(268);
const rendered = blockText(result);
expect(rendered).toContain('2 failed revocation');
expect(rendered).toContain('3 quarantined');
expect(rendered).toContain('`2` credentials failed revocation');
expect(rendered).toContain('`3` quarantined');
expect(rendered).toContain('Action required');
});

it('groups plans into sections by provider', () => {
const result = buildCodingPlanInventorySlackNotification([
{ providerId: 'minimax', planId: 'minimax-token-plan-plus', status: 'available', count: 2 },
{
providerId: 'byteplus-coding',
planId: 'byteplus-coding-plan-team-lite',
status: 'assigned',
count: 4,
},
{
providerId: 'byteplus-coding',
planId: 'byteplus-coding-plan-team-lite',
status: 'available',
count: 1,
},
{
providerId: 'byteplus-coding',
planId: 'byteplus-coding-plan-team-pro',
status: 'available',
count: 3,
},
]);

const rendered = blockText(result);
expect(rendered).toContain('*MiniMax*');
expect(rendered).toContain('Token Plan Plus · Available `2` · Assigned `0` · Loaded `2`');
expect(rendered).toContain('*BytePlus*');
expect(rendered).toContain('Coding Plan Lite · Available `1` · Assigned `4` · Loaded `5`');
expect(rendered).toContain('Coding Plan Pro · Available `3` · Assigned `0` · Loaded `3`');
});

it('renders an explicit empty-inventory state', () => {
const result = buildCodingPlanInventorySlackNotification([]);

Expand All @@ -99,7 +133,7 @@ describe('sendCodingPlanInventorySlackSummary', () => {

expect(getCounts).toHaveBeenCalledWith();
expect(sendNotification).toHaveBeenCalledWith(
expect.objectContaining({ text: expect.stringContaining('83 available') })
expect.objectContaining({ text: expect.stringContaining('`83` available') })
);
});
});
142 changes: 86 additions & 56 deletions apps/web/src/lib/coding-plans/inventory-slack-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ function escapeSlackText(value: string): string {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}

function escapeSlackLabel(value: string): string {
return escapeSlackText(value).replaceAll('`', "'").replaceAll('\n', ' ');
}

function formatCount(count: number): string {
return `\`${count}\``;
}

function humanizeStatus(status: string): string {
return status.replaceAll('_', ' ');
}
Expand Down Expand Up @@ -111,42 +119,58 @@ function inventoryTotals(summaries: InventoryPlanSummary[]): CodingPlanInventory
);
}

function formatPlanSummary(summary: InventoryPlanSummary): string {
const available = statusCount(summary, 'available');
const assigned = statusCount(summary, 'assigned');
const pending = statusCount(summary, 'revocation_pending');
const failed = statusCount(summary, 'revocation_failed');
const revoked = statusCount(summary, 'revoked');
const isCatalogPlan = getCodingPlanCatalog().some(plan => plan.planId === summary.planId);
const displayName = isCatalogPlan
? escapeSlackText(summary.displayName)
: `\`${escapeSlackText(summary.displayName.replaceAll('`', "'"))}\``;
const lines = [
`*${displayName}*`,
`${available} available · ${assigned} assigned · ${summary.loaded} loaded`,
];
function formatInventoryTotals(totals: CodingPlanInventoryTotals): string {
return `*Total* · Available ${formatCount(totals.available)} · Assigned ${formatCount(totals.assigned)} · Loaded ${formatCount(totals.loaded)}`;
}

const lifecycleParts = [
failed > 0 ? `${failed} failed revocation` : null,
pending > 0 ? `${pending} pending revocation` : null,
revoked > 0 ? `${revoked} revoked` : null,
].filter((value): value is string => value !== null);
if (lifecycleParts.length > 0) {
lines.push(`${failed > 0 || pending > 0 ? ':warning: ' : ''}${lifecycleParts.join(' · ')}`);
}
function groupSummariesByProvider(
summaries: InventoryPlanSummary[]
): Map<string, InventoryPlanSummary[]> {
const providerGroups = new Map<string, InventoryPlanSummary[]>();

const unknownStatuses = Object.entries(summary.statusCounts)
.filter(([status]) => !KNOWN_STATUSES.has(status))
.sort(([left], [right]) => left.localeCompare(right));
if (unknownStatuses.length > 0) {
lines.push(
unknownStatuses
.map(([status, count]) => `${count} ${escapeSlackText(humanizeStatus(status))}`)
.join(' · ')
);
for (const summary of summaries) {
const providerSummaries = providerGroups.get(summary.providerName) ?? [];
providerSummaries.push(summary);
providerGroups.set(summary.providerName, providerSummaries);
}

return lines.join('\n');
return providerGroups;
}

function formatProviderSummary(providerName: string, summaries: InventoryPlanSummary[]): string {
const planLines = summaries.map(
summary =>
`${escapeSlackLabel(summary.displayName)} · Available ${formatCount(statusCount(summary, 'available'))} · Assigned ${formatCount(statusCount(summary, 'assigned'))} · Loaded ${formatCount(summary.loaded)}`
);

return [`*${escapeSlackLabel(providerName)}*`, ...planLines].join('\n');
}

function formatUnknownStatuses(summaries: InventoryPlanSummary[]): string | null {
const unknownStatuses = summaries.flatMap(summary =>
Object.entries(summary.statusCounts)
.filter(([status]) => !KNOWN_STATUSES.has(status))
.sort(([left], [right]) => left.localeCompare(right))
.map(([status, count]) => {
const plan = `${summary.providerName} ${summary.displayName}`;
return `${escapeSlackLabel(plan)}: ${formatCount(count)} ${escapeSlackText(humanizeStatus(status))}`;
})
);

return unknownStatuses.length > 0 ? `*Other statuses:* ${unknownStatuses.join(' · ')}` : null;
}

function formatRevocationCallout(totals: CodingPlanInventoryTotals): string {
return [
totals.revocationFailed > 0
? `:rotating_light: *Action required:* ${formatCount(totals.revocationFailed)} credential${totals.revocationFailed === 1 ? '' : 's'} failed revocation.`
: null,
totals.revocationPending > 0
? `:warning: *${formatCount(totals.revocationPending)} credential${totals.revocationPending === 1 ? ' is' : 's are'} pending revocation.*`
: null,
]
.filter((value): value is string => value !== null)
.join('\n');
}

function formatSnapshotTime(timestamp: Date): string {
Expand All @@ -166,16 +190,23 @@ export function buildCodingPlanInventorySlackNotification(
const providerNames = Array.from(new Set(summaries.map(summary => summary.providerName)));
const providerLabel = providerNames.length > 0 ? providerNames.join(', ') : 'All providers';
const planAvailability = summaries
.map(summary => `${summary.displayName}: ${statusCount(summary, 'available')} available`)
.map(
summary =>
`${summary.providerName} ${summary.displayName}: ${formatCount(statusCount(summary, 'available'))} available, ${formatCount(statusCount(summary, 'assigned'))} assigned, ${formatCount(summary.loaded)} loaded`
)
.join('. ');
const attentionFallback = [
totals.revocationFailed > 0 ? `${totals.revocationFailed} failed revocation` : null,
totals.revocationPending > 0 ? `${totals.revocationPending} pending revocation` : null,
totals.revocationFailed > 0
? `${formatCount(totals.revocationFailed)} failed revocation`
: null,
totals.revocationPending > 0
? `${formatCount(totals.revocationPending)} pending revocation`
: null,
]
.filter((value): value is string => value !== null)
.join(', ');
const text = [
`Coding Plans inventory: ${totals.available} available, ${totals.assigned} assigned, ${totals.loaded} loaded`,
`Coding Plans inventory: ${formatCount(totals.available)} available, ${formatCount(totals.assigned)} assigned, ${formatCount(totals.loaded)} loaded`,
attentionFallback || null,
planAvailability || 'No inventory recorded',
]
Expand All @@ -192,15 +223,6 @@ export function buildCodingPlanInventorySlackNotification(
type: 'context',
elements: [{ type: 'mrkdwn', text: `${escapeSlackText(providerLabel)} · Current snapshot` }],
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*${totals.available}*\nAvailable` },
{ type: 'mrkdwn', text: `*${totals.assigned}*\nAssigned` },
{ type: 'mrkdwn', text: `*${totals.loaded}*\nLoaded` },
{ type: 'mrkdwn', text: `*${needsAttention}*\nNeeds attention` },
],
},
{ type: 'divider' },
];

Expand All @@ -210,24 +232,32 @@ export function buildCodingPlanInventorySlackNotification(
text: { type: 'mrkdwn', text: 'No Coding Plans inventory is currently recorded.' },
});
} else {
for (const summary of summaries) {
blocks.push({
type: 'section',
text: { type: 'mrkdwn', text: formatInventoryTotals(totals) },
});

for (const [providerName, providerSummaries] of groupSummariesByProvider(summaries)) {
blocks.push({
type: 'section',
text: { type: 'mrkdwn', text: formatPlanSummary(summary) },
text: {
type: 'mrkdwn',
text: formatProviderSummary(providerName, providerSummaries),
},
});
}

const unknownStatuses = formatUnknownStatuses(summaries);
if (unknownStatuses) {
blocks.push({ type: 'section', text: { type: 'mrkdwn', text: unknownStatuses } });
}
}

if (needsAttention > 0) {
const attentionLines = [
totals.revocationFailed > 0
? `:rotating_light: *Action required:* ${totals.revocationFailed} credential${totals.revocationFailed === 1 ? '' : 's'} failed revocation.`
: null,
totals.revocationPending > 0
? `:warning: *${totals.revocationPending} credential${totals.revocationPending === 1 ? ' is' : 's are'} pending revocation.*`
: null,
].filter((value): value is string => value !== null);
blocks.push({ type: 'section', text: { type: 'mrkdwn', text: attentionLines.join('\n') } });
blocks.push({
type: 'section',
text: { type: 'mrkdwn', text: formatRevocationCallout(totals) },
});
}

blocks.push({
Expand Down
Loading
Loading