@littlebearapps/create-platform 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +36 -6
- package/dist/prompts.d.ts +14 -2
- package/dist/prompts.js +29 -7
- package/dist/templates.js +78 -0
- package/package.json +3 -2
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PID Controller for Intelligent Degradation
|
|
5
|
+
*
|
|
6
|
+
* Provides smooth throttle rate calculation (0.0-1.0) instead of binary ON/OFF.
|
|
7
|
+
* State is stored in KV, making the controller stateless per invocation.
|
|
8
|
+
*
|
|
9
|
+
* Key principle: PID provides smooth degradation, circuit breakers remain the emergency stop.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// TYPES
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* PID controller gains and configuration.
|
|
18
|
+
* Tuned for budget-based throttling where:
|
|
19
|
+
* - setpoint (0.70) = target 70% budget utilisation
|
|
20
|
+
* - Output range [0, 1] = throttle rate (0=no throttle, 1=full throttle)
|
|
21
|
+
*/
|
|
22
|
+
export interface PIDConfig {
|
|
23
|
+
/** Proportional gain - responds to current error */
|
|
24
|
+
kp: number;
|
|
25
|
+
/** Integral gain - responds to accumulated error */
|
|
26
|
+
ki: number;
|
|
27
|
+
/** Derivative gain - responds to rate of change */
|
|
28
|
+
kd: number;
|
|
29
|
+
/** Target budget utilisation (0.0-1.0) */
|
|
30
|
+
setpoint: number;
|
|
31
|
+
/** Minimum output value (default: 0) */
|
|
32
|
+
outputMin: number;
|
|
33
|
+
/** Maximum output value (default: 1) */
|
|
34
|
+
outputMax: number;
|
|
35
|
+
/** Maximum integral accumulation to prevent windup */
|
|
36
|
+
integralMax: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Persisted PID state stored in KV.
|
|
41
|
+
* Key: STATE:PID:{feature_id}
|
|
42
|
+
*/
|
|
43
|
+
export interface PIDState {
|
|
44
|
+
/** Accumulated error (integral term) */
|
|
45
|
+
integral: number;
|
|
46
|
+
/** Previous error for derivative calculation */
|
|
47
|
+
prevError: number;
|
|
48
|
+
/** Last update timestamp (ms) */
|
|
49
|
+
lastUpdate: number;
|
|
50
|
+
/** Current throttle rate output */
|
|
51
|
+
throttleRate: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Input for PID computation.
|
|
56
|
+
*/
|
|
57
|
+
export interface PIDInput {
|
|
58
|
+
/** Current budget utilisation (0.0-1.0+, can exceed 1.0 if over budget) */
|
|
59
|
+
currentUsage: number;
|
|
60
|
+
/** Time since last update in milliseconds */
|
|
61
|
+
deltaTimeMs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Output from PID computation.
|
|
66
|
+
*/
|
|
67
|
+
export interface PIDOutput {
|
|
68
|
+
/** Computed throttle rate (0.0-1.0) */
|
|
69
|
+
throttleRate: number;
|
|
70
|
+
/** Updated state to persist */
|
|
71
|
+
newState: PIDState;
|
|
72
|
+
/** Debug info for monitoring */
|
|
73
|
+
debug: {
|
|
74
|
+
error: number;
|
|
75
|
+
pTerm: number;
|
|
76
|
+
iTerm: number;
|
|
77
|
+
dTerm: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// DEFAULT CONFIGURATION
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Default PID configuration tuned for budget-based throttling.
|
|
87
|
+
*
|
|
88
|
+
* Rationale:
|
|
89
|
+
* - kp=0.5: Moderate response to current error (50% of error -> throttle change)
|
|
90
|
+
* - ki=0.1: Slow integral to avoid oscillation, correct steady-state error
|
|
91
|
+
* - kd=0.05: Light derivative to dampen sudden changes
|
|
92
|
+
* - setpoint=0.70: Target 70% budget utilisation, leaving 30% headroom
|
|
93
|
+
* - integralMax=2.0: Prevent integral windup during sustained over-budget
|
|
94
|
+
*/
|
|
95
|
+
export const DEFAULT_PID_CONFIG: PIDConfig = {
|
|
96
|
+
kp: 0.5,
|
|
97
|
+
ki: 0.1,
|
|
98
|
+
kd: 0.05,
|
|
99
|
+
setpoint: 0.7,
|
|
100
|
+
outputMin: 0,
|
|
101
|
+
outputMax: 1,
|
|
102
|
+
integralMax: 2.0,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a fresh PID state (for new features or reset).
|
|
107
|
+
*/
|
|
108
|
+
export function createPIDState(): PIDState {
|
|
109
|
+
return {
|
|
110
|
+
integral: 0,
|
|
111
|
+
prevError: 0,
|
|
112
|
+
lastUpdate: Date.now(),
|
|
113
|
+
throttleRate: 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// PID COMPUTATION
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compute PID output for throttle rate.
|
|
123
|
+
*
|
|
124
|
+
* This is a stateless function - pass in current state, get new state back.
|
|
125
|
+
* The caller is responsible for persisting state to KV.
|
|
126
|
+
*
|
|
127
|
+
* @param state - Current PID state from KV (or fresh state for new features)
|
|
128
|
+
* @param input - Current usage and timing information
|
|
129
|
+
* @param config - PID configuration (defaults provided)
|
|
130
|
+
* @returns New throttle rate and updated state
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const state = await getPIDState(featureId, env);
|
|
135
|
+
* const input = { currentUsage: 0.85, deltaTimeMs: 60000 };
|
|
136
|
+
* const output = computePID(state, input);
|
|
137
|
+
* await savePIDState(featureId, output.newState, env);
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function computePID(
|
|
141
|
+
state: PIDState,
|
|
142
|
+
input: PIDInput,
|
|
143
|
+
config: PIDConfig = DEFAULT_PID_CONFIG
|
|
144
|
+
): PIDOutput {
|
|
145
|
+
// Calculate error: positive = over budget, negative = under budget
|
|
146
|
+
// error > 0 means we need MORE throttling
|
|
147
|
+
const error = input.currentUsage - config.setpoint;
|
|
148
|
+
|
|
149
|
+
// Convert deltaTime to seconds for consistent gains regardless of update frequency
|
|
150
|
+
const dt = Math.max(input.deltaTimeMs / 1000, 0.001); // Minimum 1ms to avoid division issues
|
|
151
|
+
|
|
152
|
+
// Proportional term: immediate response to current error
|
|
153
|
+
const pTerm = config.kp * error;
|
|
154
|
+
|
|
155
|
+
// Integral term: accumulated error over time (with anti-windup)
|
|
156
|
+
let newIntegral = state.integral + error * dt;
|
|
157
|
+
// Clamp integral to prevent windup
|
|
158
|
+
newIntegral = Math.max(-config.integralMax, Math.min(config.integralMax, newIntegral));
|
|
159
|
+
const iTerm = config.ki * newIntegral;
|
|
160
|
+
|
|
161
|
+
// Derivative term: rate of change of error (damping)
|
|
162
|
+
const derivative = (error - state.prevError) / dt;
|
|
163
|
+
const dTerm = config.kd * derivative;
|
|
164
|
+
|
|
165
|
+
// Sum all terms
|
|
166
|
+
let output = pTerm + iTerm + dTerm;
|
|
167
|
+
|
|
168
|
+
// Clamp output to valid range
|
|
169
|
+
output = Math.max(config.outputMin, Math.min(config.outputMax, output));
|
|
170
|
+
|
|
171
|
+
// Create new state
|
|
172
|
+
const newState: PIDState = {
|
|
173
|
+
integral: newIntegral,
|
|
174
|
+
prevError: error,
|
|
175
|
+
lastUpdate: Date.now(),
|
|
176
|
+
throttleRate: output,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
throttleRate: output,
|
|
181
|
+
newState,
|
|
182
|
+
debug: {
|
|
183
|
+
error,
|
|
184
|
+
pTerm,
|
|
185
|
+
iTerm,
|
|
186
|
+
dTerm,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// =============================================================================
|
|
192
|
+
// KV PERSISTENCE HELPERS
|
|
193
|
+
// =============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* KV key for PID state.
|
|
197
|
+
*/
|
|
198
|
+
export function pidStateKey(featureId: string): string {
|
|
199
|
+
return `STATE:PID:${featureId}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* KV key for throttle rate (read by SDK).
|
|
204
|
+
*/
|
|
205
|
+
export function throttleRateKey(featureId: string): string {
|
|
206
|
+
return `CONFIG:FEATURE:${featureId}:THROTTLE_RATE`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get PID state from KV, returning fresh state if not found.
|
|
211
|
+
*/
|
|
212
|
+
export async function getPIDState(featureId: string, kv: KVNamespace): Promise<PIDState> {
|
|
213
|
+
const key = pidStateKey(featureId);
|
|
214
|
+
const data = await kv.get(key, 'json');
|
|
215
|
+
if (data && typeof data === 'object') {
|
|
216
|
+
return data as PIDState;
|
|
217
|
+
}
|
|
218
|
+
return createPIDState();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Save PID state to KV with 24h TTL.
|
|
223
|
+
* Also writes throttle rate to separate key with 5min TTL for SDK consumption.
|
|
224
|
+
*/
|
|
225
|
+
export async function savePIDState(
|
|
226
|
+
featureId: string,
|
|
227
|
+
state: PIDState,
|
|
228
|
+
kv: KVNamespace
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const stateKey = pidStateKey(featureId);
|
|
231
|
+
const rateKey = throttleRateKey(featureId);
|
|
232
|
+
|
|
233
|
+
// Save state with 24h TTL (for persistence across updates)
|
|
234
|
+
await kv.put(stateKey, JSON.stringify(state), { expirationTtl: 86400 });
|
|
235
|
+
|
|
236
|
+
// Save throttle rate separately with 5min TTL (for SDK quick access)
|
|
237
|
+
// Only write if throttle rate > 0 to avoid unnecessary KV writes
|
|
238
|
+
if (state.throttleRate > 0.001) {
|
|
239
|
+
await kv.put(rateKey, state.throttleRate.toString(), { expirationTtl: 300 });
|
|
240
|
+
} else {
|
|
241
|
+
// Delete the key if throttle rate is essentially 0
|
|
242
|
+
await kv.delete(rateKey);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get current throttle rate for SDK consumption.
|
|
248
|
+
* Returns 0 if no throttle rate is set.
|
|
249
|
+
*/
|
|
250
|
+
export async function getThrottleRate(featureId: string, kv: KVNamespace): Promise<number> {
|
|
251
|
+
const key = throttleRateKey(featureId);
|
|
252
|
+
const value = await kv.get(key);
|
|
253
|
+
if (value) {
|
|
254
|
+
const rate = parseFloat(value);
|
|
255
|
+
return isNaN(rate) ? 0 : Math.max(0, Math.min(1, rate));
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// =============================================================================
|
|
261
|
+
// UTILITY FUNCTIONS
|
|
262
|
+
// =============================================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Calculate budget utilisation from current usage and limit.
|
|
266
|
+
*
|
|
267
|
+
* @param currentUsage - Current period usage value
|
|
268
|
+
* @param budgetLimit - Budget limit for the period
|
|
269
|
+
* @returns Utilisation ratio (0.0-1.0+, can exceed 1.0 if over budget)
|
|
270
|
+
*/
|
|
271
|
+
export function calculateUtilisation(currentUsage: number, budgetLimit: number): number {
|
|
272
|
+
if (budgetLimit <= 0) return 0;
|
|
273
|
+
return currentUsage / budgetLimit;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Determine if PID update is needed based on time since last update.
|
|
278
|
+
*
|
|
279
|
+
* @param lastUpdate - Timestamp of last PID update (ms)
|
|
280
|
+
* @param minIntervalMs - Minimum interval between updates (default: 60s)
|
|
281
|
+
* @returns True if update is due
|
|
282
|
+
*/
|
|
283
|
+
export function shouldUpdatePID(lastUpdate: number, minIntervalMs: number = 60_000): boolean {
|
|
284
|
+
return Date.now() - lastUpdate >= minIntervalMs;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Format throttle rate for logging (percentage with 1 decimal).
|
|
289
|
+
*/
|
|
290
|
+
export function formatThrottleRate(rate: number): string {
|
|
291
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
292
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BCU (Budget Consumption Unit) Cost Allocator
|
|
3
|
+
*
|
|
4
|
+
* Provides scarcity-weighted quota enforcement for intelligent degradation.
|
|
5
|
+
* BCU normalises different resource types into a single unit for budget comparison.
|
|
6
|
+
*
|
|
7
|
+
* Key difference from costs.ts:
|
|
8
|
+
* - costs.ts: Financial reporting (actual USD costs)
|
|
9
|
+
* - economics.ts: Scarcity-weighted quota enforcement (relative resource pressure)
|
|
10
|
+
*
|
|
11
|
+
* BCU weights reflect scarcity and impact, not just cost:
|
|
12
|
+
* - AI neurons are expensive AND scarce (weight: 100)
|
|
13
|
+
* - D1 writes have durability implications (weight: 10)
|
|
14
|
+
* - KV writes are moderately constrained (weight: 1)
|
|
15
|
+
* - Requests are abundant but need tracking (weight: 0.001)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { FeatureMetrics } from '@littlebearapps/platform-sdk';
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// TYPES
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resource type for BCU calculation.
|
|
26
|
+
*/
|
|
27
|
+
export type ResourceType =
|
|
28
|
+
| 'aiNeurons'
|
|
29
|
+
| 'aiRequests'
|
|
30
|
+
| 'd1Writes'
|
|
31
|
+
| 'd1Reads'
|
|
32
|
+
| 'd1RowsWritten'
|
|
33
|
+
| 'd1RowsRead'
|
|
34
|
+
| 'kvWrites'
|
|
35
|
+
| 'kvReads'
|
|
36
|
+
| 'kvDeletes'
|
|
37
|
+
| 'kvLists'
|
|
38
|
+
| 'r2ClassA'
|
|
39
|
+
| 'r2ClassB'
|
|
40
|
+
| 'doRequests'
|
|
41
|
+
| 'doGbSeconds'
|
|
42
|
+
| 'queueMessages'
|
|
43
|
+
| 'vectorizeQueries'
|
|
44
|
+
| 'vectorizeInserts'
|
|
45
|
+
| 'workflowInvocations'
|
|
46
|
+
| 'requests'
|
|
47
|
+
| 'cpuMs';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* BCU weights per resource type.
|
|
51
|
+
*/
|
|
52
|
+
export type BCUWeights = Record<ResourceType, number>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Result of BCU calculation.
|
|
56
|
+
*/
|
|
57
|
+
export interface BCUResult {
|
|
58
|
+
/** Total BCU value */
|
|
59
|
+
total: number;
|
|
60
|
+
/** Breakdown by resource type */
|
|
61
|
+
breakdown: Partial<Record<ResourceType, number>>;
|
|
62
|
+
/** Dominant resource (highest BCU contribution) */
|
|
63
|
+
dominantResource: ResourceType | null;
|
|
64
|
+
/** Percentage contribution of dominant resource */
|
|
65
|
+
dominantPercentage: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Budget state with BCU tracking.
|
|
70
|
+
*/
|
|
71
|
+
export interface BCUBudgetState {
|
|
72
|
+
/** Current period BCU consumption */
|
|
73
|
+
currentBCU: number;
|
|
74
|
+
/** Budget limit in BCU */
|
|
75
|
+
limitBCU: number;
|
|
76
|
+
/** Utilisation ratio (0.0-1.0+) */
|
|
77
|
+
utilisation: number;
|
|
78
|
+
/** Whether budget is exceeded */
|
|
79
|
+
exceeded: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// SCARCITY WEIGHTS
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default BCU weights reflecting resource scarcity and impact.
|
|
88
|
+
*
|
|
89
|
+
* Philosophy:
|
|
90
|
+
* - Expensive resources that are hard to scale get high weights
|
|
91
|
+
* - Cheap, abundant resources get low weights
|
|
92
|
+
* - Writes are weighted higher than reads (durability implications)
|
|
93
|
+
*
|
|
94
|
+
* TODO: Adjust these weights based on your specific scarcity constraints.
|
|
95
|
+
*/
|
|
96
|
+
export const DEFAULT_BCU_WEIGHTS: BCUWeights = {
|
|
97
|
+
// AI Resources - Most expensive and scarce
|
|
98
|
+
aiNeurons: 100, // $0.011 per 1K neurons, compute-intensive
|
|
99
|
+
aiRequests: 50, // Each AI call is significant
|
|
100
|
+
|
|
101
|
+
// D1 Database - Writes are expensive, reads are cheap
|
|
102
|
+
d1Writes: 10, // Deprecated field (use d1RowsWritten)
|
|
103
|
+
d1Reads: 0.01, // Deprecated field (use d1RowsRead)
|
|
104
|
+
d1RowsWritten: 10, // $1.00 per million, durability critical
|
|
105
|
+
d1RowsRead: 0.01, // $0.001 per billion, cheap
|
|
106
|
+
|
|
107
|
+
// KV - Writes constrained, reads abundant
|
|
108
|
+
kvWrites: 1, // $5.00 per million
|
|
109
|
+
kvReads: 0.1, // $0.50 per million
|
|
110
|
+
kvDeletes: 1, // Same cost as writes
|
|
111
|
+
kvLists: 1, // Same cost as writes
|
|
112
|
+
|
|
113
|
+
// R2 - Operations are relatively cheap
|
|
114
|
+
r2ClassA: 0.5, // $4.50 per million (PUT, POST, LIST)
|
|
115
|
+
r2ClassB: 0.05, // $0.36 per million (GET, HEAD)
|
|
116
|
+
|
|
117
|
+
// Durable Objects - Request-based pricing
|
|
118
|
+
doRequests: 0.5, // $0.15 per million
|
|
119
|
+
doGbSeconds: 5, // $12.50 per million GB-seconds
|
|
120
|
+
|
|
121
|
+
// Queues - Moderate pricing
|
|
122
|
+
queueMessages: 0.5, // $0.40 per million
|
|
123
|
+
|
|
124
|
+
// Vectorize - Query-intensive
|
|
125
|
+
vectorizeQueries: 1, // $0.01 per million dimensions
|
|
126
|
+
vectorizeInserts: 2, // Writes more expensive
|
|
127
|
+
|
|
128
|
+
// Workflows - Still in beta
|
|
129
|
+
workflowInvocations: 1, // Placeholder
|
|
130
|
+
|
|
131
|
+
// General compute
|
|
132
|
+
requests: 0.001, // Very cheap, 10M included
|
|
133
|
+
cpuMs: 0.01, // $0.02 per million ms
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// BCU CALCULATION
|
|
138
|
+
// =============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Calculate BCU (Budget Consumption Units) from metrics.
|
|
142
|
+
*
|
|
143
|
+
* BCU provides a normalised measure of resource consumption that accounts
|
|
144
|
+
* for scarcity, not just cost. This enables fair quota allocation across
|
|
145
|
+
* features with different resource profiles.
|
|
146
|
+
*
|
|
147
|
+
* @param metrics - Feature metrics from telemetry
|
|
148
|
+
* @param weights - BCU weights (defaults provided)
|
|
149
|
+
* @returns BCU result with total, breakdown, and dominant resource
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const metrics = { aiNeurons: 1000, d1RowsWritten: 100, requests: 50 };
|
|
154
|
+
* const result = calculateBCU(metrics);
|
|
155
|
+
* // result.total = 100000 + 1000 + 0.05 = 101000.05
|
|
156
|
+
* // result.dominantResource = 'aiNeurons'
|
|
157
|
+
* // result.dominantPercentage = 99.01
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function calculateBCU(
|
|
161
|
+
metrics: FeatureMetrics,
|
|
162
|
+
weights: BCUWeights = DEFAULT_BCU_WEIGHTS
|
|
163
|
+
): BCUResult {
|
|
164
|
+
const breakdown: Partial<Record<ResourceType, number>> = {};
|
|
165
|
+
let total = 0;
|
|
166
|
+
let maxContribution = 0;
|
|
167
|
+
let dominantResource: ResourceType | null = null;
|
|
168
|
+
|
|
169
|
+
// Calculate BCU for each non-zero metric
|
|
170
|
+
const metricEntries: [ResourceType, number | undefined][] = [
|
|
171
|
+
['aiNeurons', metrics.aiNeurons],
|
|
172
|
+
['aiRequests', metrics.aiRequests],
|
|
173
|
+
['d1Writes', metrics.d1Writes],
|
|
174
|
+
['d1Reads', metrics.d1Reads],
|
|
175
|
+
['d1RowsWritten', metrics.d1RowsWritten],
|
|
176
|
+
['d1RowsRead', metrics.d1RowsRead],
|
|
177
|
+
['kvWrites', metrics.kvWrites],
|
|
178
|
+
['kvReads', metrics.kvReads],
|
|
179
|
+
['kvDeletes', metrics.kvDeletes],
|
|
180
|
+
['kvLists', metrics.kvLists],
|
|
181
|
+
['r2ClassA', metrics.r2ClassA],
|
|
182
|
+
['r2ClassB', metrics.r2ClassB],
|
|
183
|
+
['doRequests', metrics.doRequests],
|
|
184
|
+
// Note: doGbSeconds not in FeatureMetrics - omitted
|
|
185
|
+
['queueMessages', metrics.queueMessages],
|
|
186
|
+
['vectorizeQueries', metrics.vectorizeQueries],
|
|
187
|
+
['vectorizeInserts', metrics.vectorizeInserts],
|
|
188
|
+
['workflowInvocations', metrics.workflowInvocations],
|
|
189
|
+
['requests', metrics.requests],
|
|
190
|
+
['cpuMs', metrics.cpuMs],
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
for (const [resource, value] of metricEntries) {
|
|
194
|
+
if (value && value > 0) {
|
|
195
|
+
const weight = weights[resource];
|
|
196
|
+
const contribution = value * weight;
|
|
197
|
+
breakdown[resource] = contribution;
|
|
198
|
+
total += contribution;
|
|
199
|
+
|
|
200
|
+
if (contribution > maxContribution) {
|
|
201
|
+
maxContribution = contribution;
|
|
202
|
+
dominantResource = resource;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const dominantPercentage = total > 0 ? (maxContribution / total) * 100 : 0;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
total,
|
|
211
|
+
breakdown,
|
|
212
|
+
dominantResource,
|
|
213
|
+
dominantPercentage,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Calculate BCU from raw metric values (not FeatureMetrics object).
|
|
219
|
+
* Useful when processing individual metric updates.
|
|
220
|
+
*/
|
|
221
|
+
export function calculateBCUForResource(
|
|
222
|
+
resource: ResourceType,
|
|
223
|
+
value: number,
|
|
224
|
+
weights: BCUWeights = DEFAULT_BCU_WEIGHTS
|
|
225
|
+
): number {
|
|
226
|
+
return value * weights[resource];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// BUDGET ENFORCEMENT
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check BCU budget state for a feature.
|
|
235
|
+
*
|
|
236
|
+
* @param currentBCU - Current BCU consumption
|
|
237
|
+
* @param limitBCU - Budget limit in BCU
|
|
238
|
+
* @returns Budget state with utilisation info
|
|
239
|
+
*/
|
|
240
|
+
export function checkBCUBudget(currentBCU: number, limitBCU: number): BCUBudgetState {
|
|
241
|
+
const utilisation = limitBCU > 0 ? currentBCU / limitBCU : 0;
|
|
242
|
+
return {
|
|
243
|
+
currentBCU,
|
|
244
|
+
limitBCU,
|
|
245
|
+
utilisation,
|
|
246
|
+
exceeded: currentBCU > limitBCU,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Convert a USD budget to BCU budget.
|
|
252
|
+
* Useful for setting feature budgets based on dollar allocations.
|
|
253
|
+
*
|
|
254
|
+
* This is an approximation based on the most common resource mix.
|
|
255
|
+
* For precise conversion, you'd need the expected resource profile.
|
|
256
|
+
*
|
|
257
|
+
* @param usdBudget - Budget in USD
|
|
258
|
+
* @returns Approximate BCU budget
|
|
259
|
+
*/
|
|
260
|
+
export function usdToBCU(usdBudget: number): number {
|
|
261
|
+
// Approximation: assume average workload is 60% requests, 20% D1, 10% KV, 10% AI
|
|
262
|
+
// Average BCU per dollar ~= $1 buys approximately 10000 BCU in this mix
|
|
263
|
+
return usdBudget * 10000;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Convert BCU to approximate USD.
|
|
268
|
+
* Inverse of usdToBCU for reporting.
|
|
269
|
+
*/
|
|
270
|
+
export function bcuToUSD(bcu: number): number {
|
|
271
|
+
return bcu / 10000;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// =============================================================================
|
|
275
|
+
// UTILITY FUNCTIONS
|
|
276
|
+
// =============================================================================
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get human-readable description of dominant resource.
|
|
280
|
+
*/
|
|
281
|
+
export function describeDominantResource(resource: ResourceType | null): string {
|
|
282
|
+
if (!resource) return 'none';
|
|
283
|
+
|
|
284
|
+
const descriptions: Record<ResourceType, string> = {
|
|
285
|
+
aiNeurons: 'AI compute (neurons)',
|
|
286
|
+
aiRequests: 'AI API calls',
|
|
287
|
+
d1Writes: 'D1 writes (legacy)',
|
|
288
|
+
d1Reads: 'D1 reads (legacy)',
|
|
289
|
+
d1RowsWritten: 'D1 rows written',
|
|
290
|
+
d1RowsRead: 'D1 rows read',
|
|
291
|
+
kvWrites: 'KV writes',
|
|
292
|
+
kvReads: 'KV reads',
|
|
293
|
+
kvDeletes: 'KV deletes',
|
|
294
|
+
kvLists: 'KV list operations',
|
|
295
|
+
r2ClassA: 'R2 Class A ops',
|
|
296
|
+
r2ClassB: 'R2 Class B ops',
|
|
297
|
+
doRequests: 'Durable Object requests',
|
|
298
|
+
doGbSeconds: 'Durable Object compute',
|
|
299
|
+
queueMessages: 'Queue messages',
|
|
300
|
+
vectorizeQueries: 'Vectorize queries',
|
|
301
|
+
vectorizeInserts: 'Vectorize inserts',
|
|
302
|
+
workflowInvocations: 'Workflow invocations',
|
|
303
|
+
requests: 'HTTP requests',
|
|
304
|
+
cpuMs: 'CPU time',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return descriptions[resource] || resource;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Format BCU result for logging.
|
|
312
|
+
*/
|
|
313
|
+
export function formatBCUResult(result: BCUResult): string {
|
|
314
|
+
const dominant = result.dominantResource
|
|
315
|
+
? `${describeDominantResource(result.dominantResource)} (${result.dominantPercentage.toFixed(1)}%)`
|
|
316
|
+
: 'none';
|
|
317
|
+
return `BCU: ${result.total.toFixed(2)}, dominant: ${dominant}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get resource-specific BCU breakdown for detailed reporting.
|
|
322
|
+
*/
|
|
323
|
+
export function getTopContributors(
|
|
324
|
+
result: BCUResult,
|
|
325
|
+
topN: number = 3
|
|
326
|
+
): { resource: ResourceType; bcu: number; percentage: number }[] {
|
|
327
|
+
const entries = Object.entries(result.breakdown) as [ResourceType, number][];
|
|
328
|
+
return entries
|
|
329
|
+
.sort((a, b) => b[1] - a[1])
|
|
330
|
+
.slice(0, topN)
|
|
331
|
+
.map(([resource, bcu]) => ({
|
|
332
|
+
resource,
|
|
333
|
+
bcu,
|
|
334
|
+
percentage: result.total > 0 ? (bcu / result.total) * 100 : 0,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Combine BCU results from multiple metrics.
|
|
340
|
+
*/
|
|
341
|
+
export function combineBCUResults(results: BCUResult[]): BCUResult {
|
|
342
|
+
const combined: Partial<Record<ResourceType, number>> = {};
|
|
343
|
+
let total = 0;
|
|
344
|
+
|
|
345
|
+
for (const result of results) {
|
|
346
|
+
for (const [resource, value] of Object.entries(result.breakdown) as [ResourceType, number][]) {
|
|
347
|
+
combined[resource] = (combined[resource] || 0) + value;
|
|
348
|
+
}
|
|
349
|
+
total += result.total;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Find dominant resource in combined
|
|
353
|
+
let maxContribution = 0;
|
|
354
|
+
let dominantResource: ResourceType | null = null;
|
|
355
|
+
for (const [resource, value] of Object.entries(combined) as [ResourceType, number][]) {
|
|
356
|
+
if (value > maxContribution) {
|
|
357
|
+
maxContribution = value;
|
|
358
|
+
dominantResource = resource;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
total,
|
|
364
|
+
breakdown: combined,
|
|
365
|
+
dominantResource,
|
|
366
|
+
dominantPercentage: total > 0 ? (maxContribution / total) * 100 : 0,
|
|
367
|
+
};
|
|
368
|
+
}
|