Files
AIHostingTycoon/packages/game-engine/src/systems/market/apiTierSystem.ts
T
josh 901db02a6b
CI / build-and-push (push) Successful in 28s
Replace decorative overload policy with real serving pipeline and dedicated Serving page
The old overload policy had dead controls (maxQueueDepth, rateLimitPerCustomer never read)
and trivial flat penalties. This replaces it with a full serving pipeline where deployed
models form a fleet, requests route through priority/degradation logic, and policy choices
create meaningful strategic tradeoffs.

New serving pipeline: fleet building from deployed models (size/quant/MoE multipliers),
demand categorization by 5 priority tiers, enterprise capacity reservation, priority-ordered
serving with overflow behaviors (queue/reject/degrade), auto-degradation to faster models
under load, and Batch API to fill idle capacity at discounted rates.

4 new research nodes gate features progressively: Intelligent Request Routing, Priority
Queue System, Request Batching, and Auto-Scaling. New dedicated Serving page with pipeline
metrics, model fleet utilization, and research-gated policy controls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-25 12:42:09 -04:00

118 lines
4.0 KiB
TypeScript

import type { ApiTierState, ApiTierId, DeveloperEcosystem, TierServingMetrics } from '@ai-tycoon/shared';
import {
API_TIER_ORDER,
API_CONVERSION_RATES,
API_TIER_CHURN_RATES,
API_TOKENS_PER_DEVELOPER_PER_TICK,
REJECTION_CHURN_MULTIPLIER,
} from '@ai-tycoon/shared';
export interface ApiTickResult {
apiTiers: ApiTierState;
apiRevenue: number;
totalApiTokenDemand: number;
}
export function processApiTiers(
tiers: ApiTierState,
playerDevCustomers: number,
modelQuality: number,
seasonalApiMultiplier: number,
ecosystem: DeveloperEcosystem,
apiPaidMetrics: TierServingMetrics,
apiFreeMetrics: TierServingMetrics,
): ApiTickResult {
const updated: ApiTierState = {
tiers: { ...tiers.tiers },
totalDevelopers: 0,
totalTokensPerTick: 0,
};
for (const id of API_TIER_ORDER) {
updated.tiers[id] = { ...tiers.tiers[id], config: { ...tiers.tiers[id].config } };
}
if (modelQuality <= 0) {
return { apiTiers: updated, apiRevenue: 0, totalApiTokenDemand: 0 };
}
const targetFreeDevelopers = playerDevCustomers * 0.1 * seasonalApiMultiplier;
const freeGrowth = (targetFreeDevelopers - updated.tiers.free.developerCount) * 0.03;
updated.tiers.free.developerCount = Math.max(0, updated.tiers.free.developerCount + freeGrowth);
const freeChurn = updated.tiers.free.developerCount * API_TIER_CHURN_RATES.free;
updated.tiers.free.developerCount = Math.max(0, updated.tiers.free.developerCount - freeChurn);
updated.tiers.free.churnRate = API_TIER_CHURN_RATES.free;
const prevTierMap: Record<ApiTierId, ApiTierId | null> = {
free: null,
payg: 'free',
scale: 'payg',
'enterprise-api': 'scale',
};
for (const id of API_TIER_ORDER) {
if (id === 'free') continue;
const tier = updated.tiers[id];
if (!tier.config.isActive) continue;
const prevId = prevTierMap[id];
if (!prevId) continue;
const prevTier = updated.tiers[prevId];
const convKey = `${prevId}->${id}`;
const baseRate = API_CONVERSION_RATES[convKey] ?? 0;
const ecosystemBoost = 1 + ecosystem.ecosystemScore / 200;
const convRate = baseRate * Math.max(0.1, modelQuality) * ecosystemBoost * seasonalApiMultiplier;
const converting = prevTier.developerCount * convRate;
prevTier.developerCount = Math.max(0, prevTier.developerCount - converting);
tier.developerCount += converting;
tier.churnRate = API_TIER_CHURN_RATES[id];
const churned = tier.developerCount * tier.churnRate;
tier.developerCount = Math.max(0, tier.developerCount - churned);
}
let totalDevelopers = 0;
let totalTokens = 0;
let apiRevenue = 0;
for (const id of API_TIER_ORDER) {
const tier = updated.tiers[id];
totalDevelopers += tier.developerCount;
const tokensPerDev = API_TOKENS_PER_DEVELOPER_PER_TICK[id];
tier.tokensPerTick = tier.developerCount * tokensPerDev;
totalTokens += tier.tokensPerTick;
apiRevenue += tier.developerCount * (tier.config.monthlyFee / 86400);
apiRevenue += (tier.tokensPerTick / 1_000_000) * tier.config.outputTokenPrice;
}
updated.totalDevelopers = totalDevelopers;
updated.totalTokensPerTick = totalTokens;
const freeRejectRate = apiFreeMetrics.demandTokens > 0
? apiFreeMetrics.rejectedTokens / apiFreeMetrics.demandTokens : 0;
if (freeRejectRate > 0) {
const extraChurn = updated.tiers.free.developerCount * freeRejectRate * 0.01 * REJECTION_CHURN_MULTIPLIER;
updated.tiers.free.developerCount = Math.max(0, updated.tiers.free.developerCount - extraChurn);
}
const paidRejectRate = apiPaidMetrics.demandTokens > 0
? apiPaidMetrics.rejectedTokens / apiPaidMetrics.demandTokens : 0;
if (paidRejectRate > 0) {
for (const id of API_TIER_ORDER) {
if (id === 'free') continue;
const extraChurn = updated.tiers[id].developerCount * paidRejectRate * 0.005 * REJECTION_CHURN_MULTIPLIER;
updated.tiers[id].developerCount = Math.max(0, updated.tiers[id].developerCount - extraChurn);
}
}
return {
apiTiers: updated,
apiRevenue: Math.max(0, apiRevenue),
totalApiTokenDemand: totalTokens,
};
}