Replace decorative overload policy with real serving pipeline and dedicated Serving page
CI / build-and-push (push) Successful in 28s
CI / build-and-push (push) Successful in 28s
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>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { GameState, MarketState, BenchmarkResult, Competitor } from '@ai-tycoon/shared';
|
||||
import { CONSUMER_TOKENS_PER_SUBSCRIBER } from '@ai-tycoon/shared';
|
||||
import type { GameState, MarketState, BenchmarkResult } from '@ai-tycoon/shared';
|
||||
import { CONSUMER_TOKENS_PER_SUBSCRIBER, API_TOKENS_PER_DEVELOPER_PER_TICK, BATCH_API_DEMAND_PER_DEV, makeInitialServingMetrics } from '@ai-tycoon/shared';
|
||||
import type { TrafficPriority, TierServingMetrics } from '@ai-tycoon/shared';
|
||||
import { BENCHMARKS } from '../../data/benchmarks';
|
||||
import { computeSeasonal } from './seasonalSystem';
|
||||
import { updateObsolescence } from './obsolescenceSystem';
|
||||
@@ -9,6 +10,9 @@ import { processApiTiers } from './apiTierSystem';
|
||||
import { processProductLines } from './productLines';
|
||||
import { processDeveloperEcosystem } from './developerEcosystem';
|
||||
import { processEnterprisePipeline } from './enterprisePipeline';
|
||||
import { processServingPipeline } from './servingPipeline';
|
||||
import type { DemandByTier } from './servingPipeline';
|
||||
import type { ResearchBonuses } from '../researchBonuses';
|
||||
|
||||
export interface MarketTickResult {
|
||||
marketState: MarketState;
|
||||
@@ -44,24 +48,26 @@ function getSegmentQuality(
|
||||
return weightedSum / totalWeight;
|
||||
}
|
||||
|
||||
export function processMarketV2(state: GameState, currentTickCapacity: number): MarketTickResult {
|
||||
export function processMarketV2(
|
||||
state: GameState,
|
||||
currentTickCapacity: number,
|
||||
effectiveInferenceFlops?: number,
|
||||
researchBonuses?: ResearchBonuses,
|
||||
): MarketTickResult {
|
||||
const consumerQuality = getSegmentQuality('consumer', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
||||
const enterpriseQuality = getSegmentQuality('enterprise', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
||||
const modelQuality = state.models.benchmarkResults.length > 0
|
||||
? (consumerQuality + enterpriseQuality) / 2
|
||||
: state.models.bestDeployedModelScore / 100;
|
||||
|
||||
// --- Seasonal ---
|
||||
const seasonal = computeSeasonal(state.meta.tickCount);
|
||||
|
||||
// --- Obsolescence ---
|
||||
const obsolescence = updateObsolescence(
|
||||
state.market.obsolescence,
|
||||
state.meta.currentEra,
|
||||
state.meta.tickCount,
|
||||
);
|
||||
|
||||
// --- Developer Ecosystem ---
|
||||
const freeApiDevs = state.market.apiTiers.tiers.free.developerCount;
|
||||
const totalApiDevs = state.market.apiTiers.totalDevelopers;
|
||||
const engineeringCount = state.talent.departments.engineering.headcount;
|
||||
@@ -75,7 +81,6 @@ export function processMarketV2(state: GameState, currentTickCapacity: number):
|
||||
state.meta.currentEra,
|
||||
);
|
||||
|
||||
// --- TAM & Market Shares ---
|
||||
const chatProduct = state.models.productLines.find(p => p.type === 'chat-product');
|
||||
const textApi = state.models.productLines.find(p => p.type === 'text-api');
|
||||
|
||||
@@ -106,32 +111,7 @@ export function processMarketV2(state: GameState, currentTickCapacity: number):
|
||||
const playerDevCustomers = tam.segments.developer.shares.find(s => s.playerId === 'player')?.customers ?? 0;
|
||||
const playerEntCustomers = tam.segments.enterprise.shares.find(s => s.playerId === 'player')?.customers ?? 0;
|
||||
|
||||
// --- Consumer Tiers ---
|
||||
const consumerDemandEstimate = state.market.consumerTiers.totalUsers * CONSUMER_TOKENS_PER_SUBSCRIBER;
|
||||
const demandCapacityRatio = currentTickCapacity > 0
|
||||
? consumerDemandEstimate / currentTickCapacity
|
||||
: consumerDemandEstimate > 0 ? 10 : 0;
|
||||
|
||||
const consumerResult = processConsumerTiers(
|
||||
state.market.consumerTiers,
|
||||
playerConsumerCustomers,
|
||||
modelQuality,
|
||||
seasonal.multipliers.consumer,
|
||||
demandCapacityRatio,
|
||||
state.infrastructure.networkLatencyPenalty,
|
||||
state.market.overloadPolicy,
|
||||
);
|
||||
|
||||
// --- API Tiers ---
|
||||
const apiResult = processApiTiers(
|
||||
state.market.apiTiers,
|
||||
playerDevCustomers,
|
||||
modelQuality,
|
||||
seasonal.multipliers.api,
|
||||
devEcosystem,
|
||||
);
|
||||
|
||||
// --- Product Lines ---
|
||||
// --- Product Lines (compute first to get token demand) ---
|
||||
const productResult = processProductLines(
|
||||
state.market.codeAssistant,
|
||||
state.market.agentsPlatform,
|
||||
@@ -142,22 +122,103 @@ export function processMarketV2(state: GameState, currentTickCapacity: number):
|
||||
seasonal.multipliers.enterprise,
|
||||
);
|
||||
|
||||
// --- Enterprise Pipeline ---
|
||||
// --- Pre-compute demand estimates by tier for serving pipeline ---
|
||||
const consumerTiers = state.market.consumerTiers;
|
||||
const apiTiers = state.market.apiTiers;
|
||||
const enterprise = state.market.enterprise;
|
||||
|
||||
const consumerPaidTokens = (consumerTiers.tiers.plus.userCount + consumerTiers.tiers.pro.userCount + consumerTiers.tiers.team.userCount) * CONSUMER_TOKENS_PER_SUBSCRIBER;
|
||||
const consumerFreeTokens = consumerTiers.tiers.free.userCount * CONSUMER_TOKENS_PER_SUBSCRIBER;
|
||||
|
||||
const apiPaidTokens =
|
||||
apiTiers.tiers.payg.developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK.payg
|
||||
+ apiTiers.tiers.scale.developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK.scale
|
||||
+ apiTiers.tiers['enterprise-api'].developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK['enterprise-api']
|
||||
+ productResult.codeAssistantTokenDemand;
|
||||
const apiFreeTokens = apiTiers.tiers.free.developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK.free;
|
||||
|
||||
let enterpriseTokens = 0;
|
||||
for (const contract of enterprise.activeContracts) {
|
||||
enterpriseTokens += contract.tokensPerTick;
|
||||
}
|
||||
enterpriseTokens += productResult.agentsPlatformTokenDemand;
|
||||
|
||||
const demandByTier: DemandByTier = {
|
||||
'enterprise': enterpriseTokens,
|
||||
'api-paid': apiPaidTokens,
|
||||
'consumer-paid': consumerPaidTokens,
|
||||
'api-free': apiFreeTokens,
|
||||
'consumer-free': consumerFreeTokens,
|
||||
};
|
||||
|
||||
// --- Batch API demand ---
|
||||
let batchDemand = 0;
|
||||
if (state.market.overloadPolicy.batchApiEnabled) {
|
||||
for (const id of ['free', 'payg', 'scale', 'enterprise-api'] as const) {
|
||||
batchDemand += apiTiers.tiers[id].developerCount * (BATCH_API_DEMAND_PER_DEV[id] ?? 0);
|
||||
}
|
||||
batchDemand *= Math.max(0.1, modelQuality);
|
||||
}
|
||||
|
||||
const completedResearch = state.research?.completedResearch ?? [];
|
||||
|
||||
// --- Serving Pipeline ---
|
||||
const servingResult = processServingPipeline({
|
||||
modelsState: state.models,
|
||||
effectiveInferenceFlops: effectiveInferenceFlops ?? currentTickCapacity,
|
||||
overloadPolicy: state.market.overloadPolicy,
|
||||
demandByTier,
|
||||
batchApi: {
|
||||
...state.market.batchApi,
|
||||
totalBatchDemand: batchDemand,
|
||||
},
|
||||
modelQuality,
|
||||
researchUnlocks: {
|
||||
servingRoutingUnlocked: completedResearch.includes('request-routing'),
|
||||
priorityQueuesUnlocked: completedResearch.includes('priority-queues'),
|
||||
batchApiUnlocked: completedResearch.includes('request-batching'),
|
||||
autoScalingBonus: completedResearch.includes('auto-scaling') ? 0.2 : 0,
|
||||
},
|
||||
});
|
||||
|
||||
const sm = servingResult.servingMetrics;
|
||||
|
||||
// --- Consumer Tiers (now with serving metrics) ---
|
||||
const consumerResult = processConsumerTiers(
|
||||
state.market.consumerTiers,
|
||||
playerConsumerCustomers,
|
||||
modelQuality,
|
||||
seasonal.multipliers.consumer,
|
||||
state.infrastructure.networkLatencyPenalty,
|
||||
sm.tierMetrics['consumer-paid'],
|
||||
sm.tierMetrics['consumer-free'],
|
||||
);
|
||||
|
||||
// --- API Tiers (now with serving metrics) ---
|
||||
const apiResult = processApiTiers(
|
||||
state.market.apiTiers,
|
||||
playerDevCustomers,
|
||||
modelQuality,
|
||||
seasonal.multipliers.api,
|
||||
devEcosystem,
|
||||
sm.tierMetrics['api-paid'],
|
||||
sm.tierMetrics['api-free'],
|
||||
);
|
||||
|
||||
// --- Enterprise Pipeline (now with serving metrics) ---
|
||||
const salesDept = state.talent.departments.sales;
|
||||
const salesHeadcount = salesDept.headcount;
|
||||
const salesEffectiveness = salesDept.effectiveness;
|
||||
|
||||
const enterpriseResult = processEnterprisePipeline(
|
||||
state.market.enterprise,
|
||||
state.reputation.score,
|
||||
state.models.bestDeployedModelScore,
|
||||
state.models.bestDeployedSafetyScore,
|
||||
salesHeadcount,
|
||||
salesEffectiveness,
|
||||
salesDept.headcount,
|
||||
salesDept.effectiveness,
|
||||
devEcosystem,
|
||||
seasonal.multipliers.enterprise,
|
||||
state.meta.tickCount,
|
||||
demandCapacityRatio,
|
||||
sm.tierMetrics['enterprise'],
|
||||
);
|
||||
|
||||
// --- Aggregate revenue ---
|
||||
@@ -165,9 +226,10 @@ export function processMarketV2(state: GameState, currentTickCapacity: number):
|
||||
+ productResult.codeAssistantRevenue
|
||||
+ productResult.agentsPlatformRevenue;
|
||||
|
||||
const apiRevenue = apiResult.apiRevenue
|
||||
let apiRevenue = apiResult.apiRevenue
|
||||
+ enterpriseResult.contractRevenue
|
||||
- enterpriseResult.slaPenalties;
|
||||
- enterpriseResult.slaPenalties
|
||||
+ servingResult.batchRevenue;
|
||||
|
||||
const totalTokenDemand = consumerResult.totalConsumerTokenDemand
|
||||
+ apiResult.totalApiTokenDemand
|
||||
@@ -186,26 +248,7 @@ export function processMarketV2(state: GameState, currentTickCapacity: number):
|
||||
const openSourceCount = state.market.openSourcedModels.length;
|
||||
if (openSourceCount > 0) {
|
||||
const revenueReduction = openSourceCount * 0.10 * 0.3;
|
||||
const adjustedApiRevenue = apiRevenue * (1 - revenueReduction);
|
||||
return {
|
||||
marketState: {
|
||||
...state.market,
|
||||
tam,
|
||||
consumerTiers: consumerResult.consumerTiers,
|
||||
apiTiers: apiResult.apiTiers,
|
||||
codeAssistant: productResult.codeAssistant,
|
||||
agentsPlatform: productResult.agentsPlatform,
|
||||
enterprise: enterpriseResult.enterprise,
|
||||
developerEcosystem: devEcosystem,
|
||||
seasonalPhase: seasonal.phase,
|
||||
seasonalMultiplier: seasonal.multipliers.consumer,
|
||||
obsolescence,
|
||||
subscriberHistory,
|
||||
},
|
||||
apiRevenue: Math.max(0, adjustedApiRevenue),
|
||||
subscriptionRevenue,
|
||||
totalTokenDemand,
|
||||
};
|
||||
apiRevenue = apiRevenue * (1 - revenueReduction);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -221,6 +264,8 @@ export function processMarketV2(state: GameState, currentTickCapacity: number):
|
||||
seasonalPhase: seasonal.phase,
|
||||
seasonalMultiplier: seasonal.multipliers.consumer,
|
||||
obsolescence,
|
||||
servingMetrics: sm,
|
||||
batchApi: servingResult.batchApi,
|
||||
subscriberHistory,
|
||||
},
|
||||
apiRevenue: Math.max(0, apiRevenue),
|
||||
|
||||
Reference in New Issue
Block a user