aden-ts 0.1.1 → 0.2.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/dist/index.d.mts CHANGED
@@ -140,6 +140,8 @@ interface BudgetRule {
140
140
  alerts: BudgetAlert[];
141
141
  /** Notification settings */
142
142
  notifications: BudgetNotifications;
143
+ /** Legacy context_id for backwards compatibility */
144
+ context_id?: string;
143
145
  }
144
146
  /**
145
147
  * Throttle rule - rate limiting
@@ -285,13 +287,13 @@ interface ControlAgentOptions {
285
287
  /**
286
288
  * Enable hybrid enforcement (local + server-side validation).
287
289
  * When enabled, budgets above the threshold are validated with the server.
288
- * Default: false
290
+ * Default: true
289
291
  */
290
292
  enableHybridEnforcement?: boolean;
291
293
  /**
292
294
  * Budget usage threshold (percentage) at which to start server validation.
293
295
  * Requests below this threshold use local-only enforcement.
294
- * Default: 80
296
+ * Default: 5
295
297
  */
296
298
  serverValidationThreshold?: number;
297
299
  /**
@@ -308,7 +310,7 @@ interface ControlAgentOptions {
308
310
  /**
309
311
  * Minimum remaining budget (USD) that triggers forced server validation.
310
312
  * Only applies when adaptiveThresholdEnabled is true.
311
- * Default: 1.0
313
+ * Default: 5.0
312
314
  */
313
315
  adaptiveMinRemainingUsd?: number;
314
316
  /**
@@ -2354,7 +2356,26 @@ declare class ControlAgent implements IControlAgent {
2354
2356
  */
2355
2357
  private matchesBlockRule;
2356
2358
  /**
2357
- * Find budgets that apply to the given request based on budget type
2359
+ * Get action priority for finding most restrictive decision.
2360
+ * Higher priority = more restrictive.
2361
+ */
2362
+ private getActionPriority;
2363
+ /**
2364
+ * Evaluate a single budget using local-only enforcement.
2365
+ * Returns a decision if the budget triggers an action, null otherwise.
2366
+ */
2367
+ private evaluateBudgetLocally;
2368
+ /**
2369
+ * Find budgets that apply to the given request based on budget type.
2370
+ *
2371
+ * Matching logic by budget type:
2372
+ * - global: Matches ALL requests
2373
+ * - agent: Matches if request.metadata.agent == budget.name or budget.id
2374
+ * - tenant: Matches if request.metadata.tenant_id == budget.name or budget.id
2375
+ * - customer: Matches if request.metadata.customer_id == budget.name or budget.id
2376
+ * - feature: Matches if request.metadata.feature == budget.name or budget.id
2377
+ * - tag: Matches if any request.metadata.tags intersect with budget.tags
2378
+ * - legacy (context_id): Matches if request.context_id == budget.context_id
2358
2379
  */
2359
2380
  private findApplicableBudgets;
2360
2381
  /**
package/dist/index.d.ts CHANGED
@@ -140,6 +140,8 @@ interface BudgetRule {
140
140
  alerts: BudgetAlert[];
141
141
  /** Notification settings */
142
142
  notifications: BudgetNotifications;
143
+ /** Legacy context_id for backwards compatibility */
144
+ context_id?: string;
143
145
  }
144
146
  /**
145
147
  * Throttle rule - rate limiting
@@ -285,13 +287,13 @@ interface ControlAgentOptions {
285
287
  /**
286
288
  * Enable hybrid enforcement (local + server-side validation).
287
289
  * When enabled, budgets above the threshold are validated with the server.
288
- * Default: false
290
+ * Default: true
289
291
  */
290
292
  enableHybridEnforcement?: boolean;
291
293
  /**
292
294
  * Budget usage threshold (percentage) at which to start server validation.
293
295
  * Requests below this threshold use local-only enforcement.
294
- * Default: 80
296
+ * Default: 5
295
297
  */
296
298
  serverValidationThreshold?: number;
297
299
  /**
@@ -308,7 +310,7 @@ interface ControlAgentOptions {
308
310
  /**
309
311
  * Minimum remaining budget (USD) that triggers forced server validation.
310
312
  * Only applies when adaptiveThresholdEnabled is true.
311
- * Default: 1.0
313
+ * Default: 5.0
312
314
  */
313
315
  adaptiveMinRemainingUsd?: number;
314
316
  /**
@@ -2354,7 +2356,26 @@ declare class ControlAgent implements IControlAgent {
2354
2356
  */
2355
2357
  private matchesBlockRule;
2356
2358
  /**
2357
- * Find budgets that apply to the given request based on budget type
2359
+ * Get action priority for finding most restrictive decision.
2360
+ * Higher priority = more restrictive.
2361
+ */
2362
+ private getActionPriority;
2363
+ /**
2364
+ * Evaluate a single budget using local-only enforcement.
2365
+ * Returns a decision if the budget triggers an action, null otherwise.
2366
+ */
2367
+ private evaluateBudgetLocally;
2368
+ /**
2369
+ * Find budgets that apply to the given request based on budget type.
2370
+ *
2371
+ * Matching logic by budget type:
2372
+ * - global: Matches ALL requests
2373
+ * - agent: Matches if request.metadata.agent == budget.name or budget.id
2374
+ * - tenant: Matches if request.metadata.tenant_id == budget.name or budget.id
2375
+ * - customer: Matches if request.metadata.customer_id == budget.name or budget.id
2376
+ * - feature: Matches if request.metadata.feature == budget.name or budget.id
2377
+ * - tag: Matches if any request.metadata.tags intersect with budget.tags
2378
+ * - legacy (context_id): Matches if request.context_id == budget.context_id
2358
2379
  */
2359
2380
  private findApplicableBudgets;
2360
2381
  /**
package/dist/index.js CHANGED
@@ -1886,12 +1886,12 @@ var ControlAgent = class {
1886
1886
  instanceId: options.instanceId ?? (0, import_crypto6.randomUUID)(),
1887
1887
  onAlert: options.onAlert ?? (() => {
1888
1888
  }),
1889
- // Hybrid enforcement options
1890
- enableHybridEnforcement: options.enableHybridEnforcement ?? false,
1891
- serverValidationThreshold: options.serverValidationThreshold ?? 80,
1889
+ // Hybrid enforcement options (defaults match Python SDK)
1890
+ enableHybridEnforcement: options.enableHybridEnforcement ?? true,
1891
+ serverValidationThreshold: options.serverValidationThreshold ?? 5,
1892
1892
  serverValidationTimeoutMs: options.serverValidationTimeoutMs ?? 2e3,
1893
1893
  adaptiveThresholdEnabled: options.adaptiveThresholdEnabled ?? true,
1894
- adaptiveMinRemainingUsd: options.adaptiveMinRemainingUsd ?? 1,
1894
+ adaptiveMinRemainingUsd: options.adaptiveMinRemainingUsd ?? 5,
1895
1895
  samplingEnabled: options.samplingEnabled ?? true,
1896
1896
  samplingBaseRate: options.samplingBaseRate ?? 0.1,
1897
1897
  samplingFullValidationPercent: options.samplingFullValidationPercent ?? 95,
@@ -2139,66 +2139,53 @@ var ControlAgent = class {
2139
2139
  }
2140
2140
  if (policy.budgets) {
2141
2141
  const applicableBudgets = this.findApplicableBudgets(policy.budgets, request);
2142
- if (this.options.enableHybridEnforcement) {
2142
+ if (applicableBudgets.length > 0) {
2143
+ let mostRestrictiveDecision = null;
2144
+ let mostRestrictivePriority = -1;
2143
2145
  for (const budget of applicableBudgets) {
2144
- const decision = await this.evaluateBudgetWithHybridEnforcement(
2145
- request,
2146
- budget,
2147
- throttleInfo
2148
- );
2149
- if (decision) {
2150
- return decision;
2146
+ let decision = null;
2147
+ if (this.options.enableHybridEnforcement) {
2148
+ decision = await this.evaluateBudgetWithHybridEnforcement(
2149
+ request,
2150
+ budget,
2151
+ throttleInfo
2152
+ );
2153
+ } else {
2154
+ decision = this.evaluateBudgetLocally(request, budget, throttleInfo);
2151
2155
  }
2152
- if (policy.degradations) {
2156
+ if (!decision && policy.degradations) {
2153
2157
  for (const degrade of policy.degradations) {
2154
2158
  if (degrade.from_model === request.model && degrade.trigger === "budget_threshold" && degrade.threshold_percent) {
2155
2159
  const usagePercent = budget.spent / budget.limit * 100;
2156
2160
  if (usagePercent >= degrade.threshold_percent) {
2157
- return {
2161
+ decision = {
2158
2162
  action: "degrade",
2159
2163
  reason: `Budget "${budget.name}" at ${usagePercent.toFixed(1)}% (threshold: ${degrade.threshold_percent}%)`,
2160
2164
  degradeToModel: degrade.to_model,
2161
2165
  ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2162
2166
  };
2167
+ break;
2163
2168
  }
2164
2169
  }
2165
2170
  }
2166
2171
  }
2167
- }
2168
- } else {
2169
- for (const budget of applicableBudgets) {
2170
- const projectedSpend = budget.spent + (request.estimated_cost ?? 0);
2171
- if (projectedSpend > budget.limit) {
2172
- if (budget.limitAction === "degrade" && budget.degradeToModel) {
2173
- return {
2174
- action: "degrade",
2175
- reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`,
2176
- degradeToModel: budget.degradeToModel,
2177
- ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2178
- };
2172
+ if (decision) {
2173
+ const priority = this.getActionPriority(decision.action);
2174
+ if (priority > mostRestrictivePriority) {
2175
+ mostRestrictiveDecision = decision;
2176
+ mostRestrictivePriority = priority;
2179
2177
  }
2180
- const action = budget.limitAction === "kill" ? "block" : budget.limitAction;
2181
- return {
2182
- action,
2183
- reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`
2184
- };
2185
- }
2186
- if (policy.degradations) {
2187
- for (const degrade of policy.degradations) {
2188
- if (degrade.from_model === request.model && degrade.trigger === "budget_threshold" && degrade.threshold_percent) {
2189
- const usagePercent = budget.spent / budget.limit * 100;
2190
- if (usagePercent >= degrade.threshold_percent) {
2191
- return {
2192
- action: "degrade",
2193
- reason: `Budget "${budget.name}" at ${usagePercent.toFixed(1)}% (threshold: ${degrade.threshold_percent}%)`,
2194
- degradeToModel: degrade.to_model,
2195
- ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2196
- };
2197
- }
2198
- }
2178
+ if (decision.action === "block") {
2179
+ break;
2199
2180
  }
2200
2181
  }
2201
2182
  }
2183
+ if (mostRestrictiveDecision) {
2184
+ if (throttleInfo && mostRestrictiveDecision.action !== "block" && !mostRestrictiveDecision.throttleDelayMs) {
2185
+ mostRestrictiveDecision.throttleDelayMs = throttleInfo.delayMs;
2186
+ }
2187
+ return mostRestrictiveDecision;
2188
+ }
2202
2189
  }
2203
2190
  }
2204
2191
  if (policy.degradations) {
@@ -2291,36 +2278,96 @@ var ControlAgent = class {
2291
2278
  return true;
2292
2279
  }
2293
2280
  /**
2294
- * Find budgets that apply to the given request based on budget type
2281
+ * Get action priority for finding most restrictive decision.
2282
+ * Higher priority = more restrictive.
2283
+ */
2284
+ getActionPriority(action) {
2285
+ const priority = {
2286
+ allow: 0,
2287
+ alert: 1,
2288
+ throttle: 2,
2289
+ degrade: 3,
2290
+ block: 4
2291
+ };
2292
+ return priority[action] ?? 0;
2293
+ }
2294
+ /**
2295
+ * Evaluate a single budget using local-only enforcement.
2296
+ * Returns a decision if the budget triggers an action, null otherwise.
2297
+ */
2298
+ evaluateBudgetLocally(request, budget, throttleInfo) {
2299
+ const projectedSpend = budget.spent + (request.estimated_cost ?? 0);
2300
+ if (projectedSpend > budget.limit) {
2301
+ if (budget.limitAction === "degrade" && budget.degradeToModel) {
2302
+ return {
2303
+ action: "degrade",
2304
+ reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`,
2305
+ degradeToModel: budget.degradeToModel,
2306
+ ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2307
+ };
2308
+ }
2309
+ const action = budget.limitAction === "kill" ? "block" : budget.limitAction;
2310
+ return {
2311
+ action,
2312
+ reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`
2313
+ };
2314
+ }
2315
+ return null;
2316
+ }
2317
+ /**
2318
+ * Find budgets that apply to the given request based on budget type.
2319
+ *
2320
+ * Matching logic by budget type:
2321
+ * - global: Matches ALL requests
2322
+ * - agent: Matches if request.metadata.agent == budget.name or budget.id
2323
+ * - tenant: Matches if request.metadata.tenant_id == budget.name or budget.id
2324
+ * - customer: Matches if request.metadata.customer_id == budget.name or budget.id
2325
+ * - feature: Matches if request.metadata.feature == budget.name or budget.id
2326
+ * - tag: Matches if any request.metadata.tags intersect with budget.tags
2327
+ * - legacy (context_id): Matches if request.context_id == budget.context_id
2295
2328
  */
2296
2329
  findApplicableBudgets(budgets, request) {
2297
2330
  const result = [];
2298
2331
  const metadata = request.metadata || {};
2299
2332
  for (const budget of budgets) {
2333
+ if (budget.context_id && request.context_id) {
2334
+ if (budget.context_id === request.context_id) {
2335
+ result.push(budget);
2336
+ continue;
2337
+ }
2338
+ }
2300
2339
  switch (budget.type) {
2301
2340
  case "global":
2302
2341
  result.push(budget);
2303
2342
  break;
2304
- case "agent":
2305
- if (metadata.agent_id && budget.id.includes(String(metadata.agent_id))) {
2343
+ case "agent": {
2344
+ const agent = metadata.agent;
2345
+ if (agent && (agent === budget.name || agent === budget.id)) {
2306
2346
  result.push(budget);
2307
2347
  }
2308
2348
  break;
2309
- case "tenant":
2310
- if (metadata.tenant_id && budget.id.includes(String(metadata.tenant_id))) {
2349
+ }
2350
+ case "tenant": {
2351
+ const tenantId = metadata.tenant_id;
2352
+ if (tenantId && (tenantId === budget.name || tenantId === budget.id)) {
2311
2353
  result.push(budget);
2312
2354
  }
2313
2355
  break;
2314
- case "customer":
2315
- if (metadata.customer_id && budget.id.includes(String(metadata.customer_id)) || request.context_id && budget.id.includes(request.context_id)) {
2356
+ }
2357
+ case "customer": {
2358
+ const customerId = metadata.customer_id;
2359
+ if (customerId && (customerId === budget.name || customerId === budget.id) || request.context_id && (request.context_id === budget.name || request.context_id === budget.id)) {
2316
2360
  result.push(budget);
2317
2361
  }
2318
2362
  break;
2319
- case "feature":
2320
- if (metadata.feature && budget.id.includes(String(metadata.feature))) {
2363
+ }
2364
+ case "feature": {
2365
+ const feature = metadata.feature;
2366
+ if (feature && (feature === budget.name || feature === budget.id)) {
2321
2367
  result.push(budget);
2322
2368
  }
2323
2369
  break;
2370
+ }
2324
2371
  case "tag":
2325
2372
  if (budget.tags && Array.isArray(metadata.tags)) {
2326
2373
  const requestTags = metadata.tags;
@@ -2370,13 +2417,48 @@ var ControlAgent = class {
2370
2417
  if (this.cachedPolicy?.budgets && event.total_tokens > 0) {
2371
2418
  const estimatedCost = this.estimateCost(event);
2372
2419
  const contextId2 = this.options.getContextId?.();
2420
+ const metadata = event.metadata || {};
2373
2421
  for (const budget of this.cachedPolicy.budgets) {
2374
- if (budget.type === "global") {
2375
- budget.spent += estimatedCost;
2376
- } else if (budget.type === "customer" && contextId2) {
2377
- if (budget.id === contextId2 || budget.id.includes(contextId2)) {
2378
- budget.spent += estimatedCost;
2422
+ let shouldUpdate = false;
2423
+ switch (budget.type) {
2424
+ case "global":
2425
+ shouldUpdate = true;
2426
+ break;
2427
+ case "agent": {
2428
+ const agent = metadata.agent;
2429
+ shouldUpdate = Boolean(agent && (agent === budget.name || agent === budget.id));
2430
+ break;
2431
+ }
2432
+ case "tenant": {
2433
+ const tenantId = metadata.tenant_id;
2434
+ shouldUpdate = Boolean(tenantId && (tenantId === budget.name || tenantId === budget.id));
2435
+ break;
2379
2436
  }
2437
+ case "customer": {
2438
+ const customerId = metadata.customer_id;
2439
+ shouldUpdate = Boolean(
2440
+ customerId && (customerId === budget.name || customerId === budget.id) || contextId2 && (contextId2 === budget.name || contextId2 === budget.id)
2441
+ );
2442
+ break;
2443
+ }
2444
+ case "feature": {
2445
+ const feature = metadata.feature;
2446
+ shouldUpdate = Boolean(feature && (feature === budget.name || feature === budget.id));
2447
+ break;
2448
+ }
2449
+ case "tag": {
2450
+ if (budget.tags && Array.isArray(metadata.tags)) {
2451
+ const requestTags = metadata.tags;
2452
+ shouldUpdate = budget.tags.some((tag) => requestTags.includes(tag));
2453
+ }
2454
+ break;
2455
+ }
2456
+ }
2457
+ if (!shouldUpdate && budget.context_id && contextId2) {
2458
+ shouldUpdate = budget.context_id === contextId2;
2459
+ }
2460
+ if (shouldUpdate) {
2461
+ budget.spent += estimatedCost;
2380
2462
  }
2381
2463
  }
2382
2464
  }
@@ -2441,7 +2523,7 @@ var ControlAgent = class {
2441
2523
  if (this.options.adaptiveThresholdEnabled) {
2442
2524
  if (remainingBudgetUsd <= this.options.adaptiveMinRemainingUsd) {
2443
2525
  console.debug(
2444
- `[aden] Remaining budget $${remainingBudgetUsd.toFixed(2)} <= $${this.options.adaptiveMinRemainingUsd.toFixed(2)}, forcing validation`
2526
+ `[aden] Remaining budget $${remainingBudgetUsd.toFixed(4)} <= $${this.options.adaptiveMinRemainingUsd.toFixed(2)}, forcing validation`
2445
2527
  );
2446
2528
  return true;
2447
2529
  }
@@ -2578,7 +2660,7 @@ var ControlAgent = class {
2578
2660
  }
2579
2661
  if (this.shouldValidateWithServer(usagePercent, remaining, limit)) {
2580
2662
  console.debug(
2581
- `[aden] Budget '${budget.id}' at ${usagePercent.toFixed(1)}% ($${currentSpend.toFixed(2)}/$${limit.toFixed(2)}), validating with server`
2663
+ `[aden] Budget '${budget.name}' at ${usagePercent.toFixed(1)}% ($${currentSpend.toFixed(6)}/$${limit.toFixed(6)}), validating with server`
2582
2664
  );
2583
2665
  const validation = await this.validateBudgetWithServer(
2584
2666
  budget.id,
package/dist/index.mjs CHANGED
@@ -1769,12 +1769,12 @@ var ControlAgent = class {
1769
1769
  instanceId: options.instanceId ?? randomUUID6(),
1770
1770
  onAlert: options.onAlert ?? (() => {
1771
1771
  }),
1772
- // Hybrid enforcement options
1773
- enableHybridEnforcement: options.enableHybridEnforcement ?? false,
1774
- serverValidationThreshold: options.serverValidationThreshold ?? 80,
1772
+ // Hybrid enforcement options (defaults match Python SDK)
1773
+ enableHybridEnforcement: options.enableHybridEnforcement ?? true,
1774
+ serverValidationThreshold: options.serverValidationThreshold ?? 5,
1775
1775
  serverValidationTimeoutMs: options.serverValidationTimeoutMs ?? 2e3,
1776
1776
  adaptiveThresholdEnabled: options.adaptiveThresholdEnabled ?? true,
1777
- adaptiveMinRemainingUsd: options.adaptiveMinRemainingUsd ?? 1,
1777
+ adaptiveMinRemainingUsd: options.adaptiveMinRemainingUsd ?? 5,
1778
1778
  samplingEnabled: options.samplingEnabled ?? true,
1779
1779
  samplingBaseRate: options.samplingBaseRate ?? 0.1,
1780
1780
  samplingFullValidationPercent: options.samplingFullValidationPercent ?? 95,
@@ -2022,66 +2022,53 @@ var ControlAgent = class {
2022
2022
  }
2023
2023
  if (policy.budgets) {
2024
2024
  const applicableBudgets = this.findApplicableBudgets(policy.budgets, request);
2025
- if (this.options.enableHybridEnforcement) {
2025
+ if (applicableBudgets.length > 0) {
2026
+ let mostRestrictiveDecision = null;
2027
+ let mostRestrictivePriority = -1;
2026
2028
  for (const budget of applicableBudgets) {
2027
- const decision = await this.evaluateBudgetWithHybridEnforcement(
2028
- request,
2029
- budget,
2030
- throttleInfo
2031
- );
2032
- if (decision) {
2033
- return decision;
2029
+ let decision = null;
2030
+ if (this.options.enableHybridEnforcement) {
2031
+ decision = await this.evaluateBudgetWithHybridEnforcement(
2032
+ request,
2033
+ budget,
2034
+ throttleInfo
2035
+ );
2036
+ } else {
2037
+ decision = this.evaluateBudgetLocally(request, budget, throttleInfo);
2034
2038
  }
2035
- if (policy.degradations) {
2039
+ if (!decision && policy.degradations) {
2036
2040
  for (const degrade of policy.degradations) {
2037
2041
  if (degrade.from_model === request.model && degrade.trigger === "budget_threshold" && degrade.threshold_percent) {
2038
2042
  const usagePercent = budget.spent / budget.limit * 100;
2039
2043
  if (usagePercent >= degrade.threshold_percent) {
2040
- return {
2044
+ decision = {
2041
2045
  action: "degrade",
2042
2046
  reason: `Budget "${budget.name}" at ${usagePercent.toFixed(1)}% (threshold: ${degrade.threshold_percent}%)`,
2043
2047
  degradeToModel: degrade.to_model,
2044
2048
  ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2045
2049
  };
2050
+ break;
2046
2051
  }
2047
2052
  }
2048
2053
  }
2049
2054
  }
2050
- }
2051
- } else {
2052
- for (const budget of applicableBudgets) {
2053
- const projectedSpend = budget.spent + (request.estimated_cost ?? 0);
2054
- if (projectedSpend > budget.limit) {
2055
- if (budget.limitAction === "degrade" && budget.degradeToModel) {
2056
- return {
2057
- action: "degrade",
2058
- reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`,
2059
- degradeToModel: budget.degradeToModel,
2060
- ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2061
- };
2055
+ if (decision) {
2056
+ const priority = this.getActionPriority(decision.action);
2057
+ if (priority > mostRestrictivePriority) {
2058
+ mostRestrictiveDecision = decision;
2059
+ mostRestrictivePriority = priority;
2062
2060
  }
2063
- const action = budget.limitAction === "kill" ? "block" : budget.limitAction;
2064
- return {
2065
- action,
2066
- reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`
2067
- };
2068
- }
2069
- if (policy.degradations) {
2070
- for (const degrade of policy.degradations) {
2071
- if (degrade.from_model === request.model && degrade.trigger === "budget_threshold" && degrade.threshold_percent) {
2072
- const usagePercent = budget.spent / budget.limit * 100;
2073
- if (usagePercent >= degrade.threshold_percent) {
2074
- return {
2075
- action: "degrade",
2076
- reason: `Budget "${budget.name}" at ${usagePercent.toFixed(1)}% (threshold: ${degrade.threshold_percent}%)`,
2077
- degradeToModel: degrade.to_model,
2078
- ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2079
- };
2080
- }
2081
- }
2061
+ if (decision.action === "block") {
2062
+ break;
2082
2063
  }
2083
2064
  }
2084
2065
  }
2066
+ if (mostRestrictiveDecision) {
2067
+ if (throttleInfo && mostRestrictiveDecision.action !== "block" && !mostRestrictiveDecision.throttleDelayMs) {
2068
+ mostRestrictiveDecision.throttleDelayMs = throttleInfo.delayMs;
2069
+ }
2070
+ return mostRestrictiveDecision;
2071
+ }
2085
2072
  }
2086
2073
  }
2087
2074
  if (policy.degradations) {
@@ -2174,36 +2161,96 @@ var ControlAgent = class {
2174
2161
  return true;
2175
2162
  }
2176
2163
  /**
2177
- * Find budgets that apply to the given request based on budget type
2164
+ * Get action priority for finding most restrictive decision.
2165
+ * Higher priority = more restrictive.
2166
+ */
2167
+ getActionPriority(action) {
2168
+ const priority = {
2169
+ allow: 0,
2170
+ alert: 1,
2171
+ throttle: 2,
2172
+ degrade: 3,
2173
+ block: 4
2174
+ };
2175
+ return priority[action] ?? 0;
2176
+ }
2177
+ /**
2178
+ * Evaluate a single budget using local-only enforcement.
2179
+ * Returns a decision if the budget triggers an action, null otherwise.
2180
+ */
2181
+ evaluateBudgetLocally(request, budget, throttleInfo) {
2182
+ const projectedSpend = budget.spent + (request.estimated_cost ?? 0);
2183
+ if (projectedSpend > budget.limit) {
2184
+ if (budget.limitAction === "degrade" && budget.degradeToModel) {
2185
+ return {
2186
+ action: "degrade",
2187
+ reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`,
2188
+ degradeToModel: budget.degradeToModel,
2189
+ ...throttleInfo && { throttleDelayMs: throttleInfo.delayMs }
2190
+ };
2191
+ }
2192
+ const action = budget.limitAction === "kill" ? "block" : budget.limitAction;
2193
+ return {
2194
+ action,
2195
+ reason: `Budget "${budget.name}" exceeded: $${projectedSpend.toFixed(4)} > $${budget.limit}`
2196
+ };
2197
+ }
2198
+ return null;
2199
+ }
2200
+ /**
2201
+ * Find budgets that apply to the given request based on budget type.
2202
+ *
2203
+ * Matching logic by budget type:
2204
+ * - global: Matches ALL requests
2205
+ * - agent: Matches if request.metadata.agent == budget.name or budget.id
2206
+ * - tenant: Matches if request.metadata.tenant_id == budget.name or budget.id
2207
+ * - customer: Matches if request.metadata.customer_id == budget.name or budget.id
2208
+ * - feature: Matches if request.metadata.feature == budget.name or budget.id
2209
+ * - tag: Matches if any request.metadata.tags intersect with budget.tags
2210
+ * - legacy (context_id): Matches if request.context_id == budget.context_id
2178
2211
  */
2179
2212
  findApplicableBudgets(budgets, request) {
2180
2213
  const result = [];
2181
2214
  const metadata = request.metadata || {};
2182
2215
  for (const budget of budgets) {
2216
+ if (budget.context_id && request.context_id) {
2217
+ if (budget.context_id === request.context_id) {
2218
+ result.push(budget);
2219
+ continue;
2220
+ }
2221
+ }
2183
2222
  switch (budget.type) {
2184
2223
  case "global":
2185
2224
  result.push(budget);
2186
2225
  break;
2187
- case "agent":
2188
- if (metadata.agent_id && budget.id.includes(String(metadata.agent_id))) {
2226
+ case "agent": {
2227
+ const agent = metadata.agent;
2228
+ if (agent && (agent === budget.name || agent === budget.id)) {
2189
2229
  result.push(budget);
2190
2230
  }
2191
2231
  break;
2192
- case "tenant":
2193
- if (metadata.tenant_id && budget.id.includes(String(metadata.tenant_id))) {
2232
+ }
2233
+ case "tenant": {
2234
+ const tenantId = metadata.tenant_id;
2235
+ if (tenantId && (tenantId === budget.name || tenantId === budget.id)) {
2194
2236
  result.push(budget);
2195
2237
  }
2196
2238
  break;
2197
- case "customer":
2198
- if (metadata.customer_id && budget.id.includes(String(metadata.customer_id)) || request.context_id && budget.id.includes(request.context_id)) {
2239
+ }
2240
+ case "customer": {
2241
+ const customerId = metadata.customer_id;
2242
+ if (customerId && (customerId === budget.name || customerId === budget.id) || request.context_id && (request.context_id === budget.name || request.context_id === budget.id)) {
2199
2243
  result.push(budget);
2200
2244
  }
2201
2245
  break;
2202
- case "feature":
2203
- if (metadata.feature && budget.id.includes(String(metadata.feature))) {
2246
+ }
2247
+ case "feature": {
2248
+ const feature = metadata.feature;
2249
+ if (feature && (feature === budget.name || feature === budget.id)) {
2204
2250
  result.push(budget);
2205
2251
  }
2206
2252
  break;
2253
+ }
2207
2254
  case "tag":
2208
2255
  if (budget.tags && Array.isArray(metadata.tags)) {
2209
2256
  const requestTags = metadata.tags;
@@ -2253,13 +2300,48 @@ var ControlAgent = class {
2253
2300
  if (this.cachedPolicy?.budgets && event.total_tokens > 0) {
2254
2301
  const estimatedCost = this.estimateCost(event);
2255
2302
  const contextId2 = this.options.getContextId?.();
2303
+ const metadata = event.metadata || {};
2256
2304
  for (const budget of this.cachedPolicy.budgets) {
2257
- if (budget.type === "global") {
2258
- budget.spent += estimatedCost;
2259
- } else if (budget.type === "customer" && contextId2) {
2260
- if (budget.id === contextId2 || budget.id.includes(contextId2)) {
2261
- budget.spent += estimatedCost;
2305
+ let shouldUpdate = false;
2306
+ switch (budget.type) {
2307
+ case "global":
2308
+ shouldUpdate = true;
2309
+ break;
2310
+ case "agent": {
2311
+ const agent = metadata.agent;
2312
+ shouldUpdate = Boolean(agent && (agent === budget.name || agent === budget.id));
2313
+ break;
2314
+ }
2315
+ case "tenant": {
2316
+ const tenantId = metadata.tenant_id;
2317
+ shouldUpdate = Boolean(tenantId && (tenantId === budget.name || tenantId === budget.id));
2318
+ break;
2262
2319
  }
2320
+ case "customer": {
2321
+ const customerId = metadata.customer_id;
2322
+ shouldUpdate = Boolean(
2323
+ customerId && (customerId === budget.name || customerId === budget.id) || contextId2 && (contextId2 === budget.name || contextId2 === budget.id)
2324
+ );
2325
+ break;
2326
+ }
2327
+ case "feature": {
2328
+ const feature = metadata.feature;
2329
+ shouldUpdate = Boolean(feature && (feature === budget.name || feature === budget.id));
2330
+ break;
2331
+ }
2332
+ case "tag": {
2333
+ if (budget.tags && Array.isArray(metadata.tags)) {
2334
+ const requestTags = metadata.tags;
2335
+ shouldUpdate = budget.tags.some((tag) => requestTags.includes(tag));
2336
+ }
2337
+ break;
2338
+ }
2339
+ }
2340
+ if (!shouldUpdate && budget.context_id && contextId2) {
2341
+ shouldUpdate = budget.context_id === contextId2;
2342
+ }
2343
+ if (shouldUpdate) {
2344
+ budget.spent += estimatedCost;
2263
2345
  }
2264
2346
  }
2265
2347
  }
@@ -2324,7 +2406,7 @@ var ControlAgent = class {
2324
2406
  if (this.options.adaptiveThresholdEnabled) {
2325
2407
  if (remainingBudgetUsd <= this.options.adaptiveMinRemainingUsd) {
2326
2408
  console.debug(
2327
- `[aden] Remaining budget $${remainingBudgetUsd.toFixed(2)} <= $${this.options.adaptiveMinRemainingUsd.toFixed(2)}, forcing validation`
2409
+ `[aden] Remaining budget $${remainingBudgetUsd.toFixed(4)} <= $${this.options.adaptiveMinRemainingUsd.toFixed(2)}, forcing validation`
2328
2410
  );
2329
2411
  return true;
2330
2412
  }
@@ -2461,7 +2543,7 @@ var ControlAgent = class {
2461
2543
  }
2462
2544
  if (this.shouldValidateWithServer(usagePercent, remaining, limit)) {
2463
2545
  console.debug(
2464
- `[aden] Budget '${budget.id}' at ${usagePercent.toFixed(1)}% ($${currentSpend.toFixed(2)}/$${limit.toFixed(2)}), validating with server`
2546
+ `[aden] Budget '${budget.name}' at ${usagePercent.toFixed(1)}% ($${currentSpend.toFixed(6)}/$${limit.toFixed(6)}), validating with server`
2465
2547
  );
2466
2548
  const validation = await this.validateBudgetWithServer(
2467
2549
  budget.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aden-ts",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "LLM Observability & Cost Control SDK - Real-time usage tracking, budget enforcement, and cost control for OpenAI, Anthropic, and Gemini",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",