@hiper2d/ai-agents 0.2.1 → 0.3.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 CHANGED
@@ -74,6 +74,28 @@ Transcribe (`VOICE_MODEL_CONSTANTS`, prices in `VOICE_MODEL_PRICING`). The `voic
74
74
  direction works for both providers: OpenAI takes it as instructions, Gemini gets it folded
75
75
  into the prompt ("Say gravely: …").
76
76
 
77
+ ### Budget control
78
+
79
+ Per-subject spend caps (a user, a tenant, a job) over UTC day and month windows. Pure and
80
+ storage-agnostic: period keys, an O(1) rolling ledger that is overwritten when the period
81
+ rolls, a verdict function, and `BudgetExceededError` carrying the verdict (`resetsAt`,
82
+ `remainingUSD`) so the host can render "come back at …" instead of a provider failure.
83
+
84
+ ```ts
85
+ import { BudgetController, InMemorySpendStore, BudgetExceededError } from '@hiper2d/ai-agents';
86
+
87
+ const budget = new BudgetController(new InMemorySpendStore(), {
88
+ limits: [{ window: 'day', limitUSD: 5 }, { window: 'month', limitUSD: 20 }],
89
+ });
90
+ await budget.assertWithinBudget(userId); // cheap pre-call guard, throws BudgetExceededError
91
+ const { costUSD } = await agent.ask(...);
92
+ await budget.record(userId, costUSD); // re-checks inside the store's atomic update
93
+ ```
94
+
95
+ Hosts that bill inside their own database transaction skip the controller and call the
96
+ pure pieces there: `ledgerSpend` → `evaluateBudget` → `applySpend`. Implement `SpendStore`
97
+ to back the controller with Redis, Postgres, Firestore, etc.
98
+
77
99
  ### Logging
78
100
 
79
101
  The library logs through an injectable sink — `setLlmLogger(fn)` — so a host app can route
package/dist/index.d.mts CHANGED
@@ -1019,6 +1019,146 @@ declare const GEMINI_AUDIO_TOKENS_PER_SECOND = 25;
1019
1019
  */
1020
1020
  declare function transcribeWithGemini(audioBuffer: ArrayBuffer, apiKey: string, options?: GoogleSttOptions): Promise<GoogleSttResult>;
1021
1021
 
1022
+ /**
1023
+ * Budget control for agent spend — pure and storage-agnostic.
1024
+ *
1025
+ * The problem this solves: an app that runs LLM/voice/image calls on its own keys needs
1026
+ * to bound what one subject (a user, a tenant, a job) can spend per UTC day or month, and
1027
+ * needs the check to be cheap enough to run before EVERY call. The pieces:
1028
+ *
1029
+ * - `periodKey` / `periodEnd`: UTC window keys (`YYYY-MM-DD`, `YYYY-MM`) and reset times.
1030
+ * - `SpendLedger` + `applySpend`: an O(1) rolling ledger for one window. It is overwritten,
1031
+ * not appended, when the period changes, so the persisted record never grows; history
1032
+ * belongs in your per-request stats, not here.
1033
+ * - `evaluateBudget`: the verdict. Pure, no I/O, no throwing.
1034
+ * - `BudgetExceededError`: what to throw when a verdict refuses, carrying the verdict so
1035
+ * the caller can render "come back at <resetsAt>" instead of treating it as a provider
1036
+ * failure.
1037
+ * - `BudgetController` over a `SpendStore`: batteries-included wrapper for projects that
1038
+ * do not need to fold the ledger into their own database transaction. Apps that do
1039
+ * (the werewolf app charges the user, updates the game and writes a stats row in one
1040
+ * Firestore transaction) use the pure functions directly inside that transaction.
1041
+ *
1042
+ * Amounts are USD, rounded to 6 decimals like the rest of the cost accounting.
1043
+ */
1044
+ type SpendWindow = 'day' | 'month';
1045
+ /** Where the money went; free-form beyond the common kinds so apps can add their own. */
1046
+ type SpendKind = 'llm' | 'image' | 'tts' | 'stt' | (string & {});
1047
+ /** UTC key for the window containing `timestamp`: `2026-09-11` for a day, `2026-09` for a month. */
1048
+ declare function periodKey(timestamp: number, window: SpendWindow): string;
1049
+ /** First millisecond of the window after the one containing `timestamp` — i.e. when the budget resets. */
1050
+ declare function periodEnd(timestamp: number, window: SpendWindow): number;
1051
+ /**
1052
+ * Spend recorded for ONE window. `buckets` split the total by whatever the app cares
1053
+ * about (tier, model family, feature); the total is authoritative, buckets are a breakdown.
1054
+ */
1055
+ interface SpendLedger {
1056
+ period: string;
1057
+ totalUSD: number;
1058
+ buckets: Record<string, number>;
1059
+ }
1060
+ interface SpendInput {
1061
+ window: SpendWindow;
1062
+ amountUSD: number;
1063
+ /** Bucket to add the amount to, on top of the total. Omit to update the total only. */
1064
+ bucket?: string;
1065
+ timestamp?: number;
1066
+ }
1067
+ /**
1068
+ * Pure reducer: add `amountUSD` to the ledger for the window containing `timestamp`.
1069
+ * A ledger from an earlier period is discarded and a fresh one started (overwrite
1070
+ * semantics), so the persisted record stays O(1). Non-positive amounts return the
1071
+ * ledger normalized to the current period without recording anything.
1072
+ */
1073
+ declare function applySpend(ledger: Partial<SpendLedger> | null | undefined, input: SpendInput): SpendLedger;
1074
+ /**
1075
+ * Spend already recorded in the window containing `timestamp` — the total, or one bucket.
1076
+ * A missing ledger or one from another period counts as 0.
1077
+ */
1078
+ declare function ledgerSpend(ledger: Partial<SpendLedger> | null | undefined, window: SpendWindow, timestamp?: number, bucket?: string): number;
1079
+ interface BudgetLimit {
1080
+ window: SpendWindow;
1081
+ /** USD ceiling for the window. `Infinity` (or any non-finite) means unlimited. */
1082
+ limitUSD: number;
1083
+ /** Which bucket the limit applies to; informational for the verdict, the caller passes the matching spend. */
1084
+ bucket?: string;
1085
+ }
1086
+ interface BudgetVerdict {
1087
+ allowed: boolean;
1088
+ window: SpendWindow;
1089
+ bucket?: string;
1090
+ limitUSD: number;
1091
+ spentUSD: number;
1092
+ remainingUSD: number;
1093
+ /** Epoch ms when the window rolls over and the spend counts from zero again. */
1094
+ resetsAt: number;
1095
+ }
1096
+ /**
1097
+ * The verdict for one limit given the spend already recorded in its window. Refuses when
1098
+ * spent >= limit (a subject exactly at the cap gets no more calls). Pure: no I/O, no throw.
1099
+ */
1100
+ declare function evaluateBudget(spentUSD: number, limit: BudgetLimit, timestamp?: number): BudgetVerdict;
1101
+ /** The first refusing verdict, or undefined when every window still has room. */
1102
+ declare function firstRefusal(verdicts: BudgetVerdict[]): BudgetVerdict | undefined;
1103
+ /**
1104
+ * Thrown when a budget refuses a call. Not a `ModelError`: nothing was sent to a provider
1105
+ * and nothing is retryable until `verdict.resetsAt`. Apps typically subclass it to attach
1106
+ * user-facing copy, or map it to their own error at the boundary.
1107
+ */
1108
+ declare class BudgetExceededError extends Error {
1109
+ readonly verdict: BudgetVerdict;
1110
+ readonly subject?: string;
1111
+ constructor(verdict: BudgetVerdict, subject?: string, message?: string);
1112
+ static describe(v: BudgetVerdict): string;
1113
+ }
1114
+ declare function isBudgetExceededError(err: unknown): err is BudgetExceededError;
1115
+ type SpendLedgers = Partial<Record<SpendWindow, SpendLedger>>;
1116
+ /**
1117
+ * Persistence for per-subject ledgers. `update` must apply `fn` to the current ledgers
1118
+ * and persist the result atomically for that subject (a transaction, a lock, a
1119
+ * single-threaded map — whatever the backend offers). If `fn` throws, nothing is written.
1120
+ */
1121
+ interface SpendStore {
1122
+ read(subject: string): Promise<SpendLedgers>;
1123
+ update(subject: string, fn: (current: SpendLedgers) => SpendLedgers): Promise<SpendLedgers>;
1124
+ }
1125
+ /** Process-local store: fine for tests, CLIs and single-instance jobs. */
1126
+ declare class InMemorySpendStore implements SpendStore {
1127
+ private readonly ledgers;
1128
+ read(subject: string): Promise<SpendLedgers>;
1129
+ update(subject: string, fn: (current: SpendLedgers) => SpendLedgers): Promise<SpendLedgers>;
1130
+ }
1131
+ interface BudgetControllerOptions {
1132
+ limits: BudgetLimit[];
1133
+ /** Bucket every recorded spend is also added to (e.g. the subject's tier). */
1134
+ bucket?: string;
1135
+ clock?: () => number;
1136
+ }
1137
+ /**
1138
+ * Check-before, record-after budget control for one set of limits.
1139
+ *
1140
+ * `assertWithinBudget` is the cheap pre-call guard; `record` re-evaluates inside the
1141
+ * store's atomic update so concurrent callers cannot all slip past a nearly-exhausted
1142
+ * budget. The overrun that remains is bounded by the calls already in flight when the
1143
+ * cap is crossed — state that bound, do not claim the cap is exact.
1144
+ */
1145
+ declare class BudgetController {
1146
+ private readonly store;
1147
+ private readonly limits;
1148
+ private readonly bucket?;
1149
+ private readonly clock;
1150
+ constructor(store: SpendStore, options: BudgetControllerOptions);
1151
+ private verdicts;
1152
+ check(subject: string): Promise<BudgetVerdict[]>;
1153
+ assertWithinBudget(subject: string): Promise<void>;
1154
+ /**
1155
+ * Record `amountUSD` against every limited window. Re-checks the limits on the
1156
+ * ledgers as they are at write time and throws `BudgetExceededError` (writing
1157
+ * nothing) if any window is already exhausted.
1158
+ */
1159
+ record(subject: string, amountUSD: number): Promise<SpendLedgers>;
1160
+ }
1161
+
1022
1162
  declare abstract class AbstractAgent {
1023
1163
  name: string;
1024
1164
  gameId?: string;
@@ -1433,4 +1573,4 @@ declare class MiniMaxAgent extends AbstractAgent {
1433
1573
  doAskText(messages: AIMessage[]): Promise<[string, string, TokenUsage$1?, string?]>;
1434
1574
  }
1435
1575
 
1436
- export { type AIMessage, ANTHROPIC_REASONING_EFFORTS, API_KEY_CONSTANTS, AbstractAgent, type AgentActivityData, AgentFactory, type AgentLoggingConfig, type AnthropicReasoningEffort, type AnthropicTokenUsage, type ApiKeyMap, BotResponseError, CACHE_TIER_MARKER, ClaudeAgent, type CostCalculationOptions, DEEPSEEK_PEAK_SCHEDULE, DEEPSEEK_REASONING_EFFORTS, DEFAULT_LOGGING_CONFIG, DEFAULT_MAX_OUTPUT_TOKENS, type DeepSeekReasoningEffort, type DeepSeekTokenUsage, DeepSeekV2Agent, FUGU_REASONING_EFFORTS, FuguAgent, type FuguReasoningEffort, GEMINI_AUDIO_TOKENS_PER_SECOND, GEMINI_REASONING_EFFORTS, GLM_REASONING_EFFORTS, type GeminiReasoningEffort, GlmAgent, type GlmReasoningEffort, GoogleAgent, type GoogleSttOptions, type GoogleSttResult, type GoogleTokenUsage, type GoogleTtsAudioOptions, type GoogleTtsResult, GoogleVoiceAgent, Gpt5Agent, GrokAgent, type GrokTokenUsage, type JsonSchemaOptions, KimiAgent, type KimiTokenUsage, type LLMModel, LLM_CONSTANTS, type LlmLogger, type LoggingConfig, MESSAGE_ROLE, MODEL_PRICING, MiniMaxAgent, MistralAgent, type MistralTokenUsage, type Modality, ModelAuthenticationError, type ModelConfig, ModelError, ModelInvalidResponseError, ModelOverloadError, type ModelPricing, ModelQuotaExceededError, ModelRateLimitError, ModelRefusalError, type ModelTag, ModelUnavailableError, OPENAI_REASONING_EFFORTS, type OpenAIReasoningEffort, type OpenAITokenUsage, type OpenAiSttOptions, type OpenAiSttResult, type OpenAiTtsAudioOptions, type OpenAiTtsVoice, OpenAiVoiceAgent, type PeakPricing, type PricingUnit, type ProviderSchema, type TokenUsage as ProviderTokenUsage, type ProviderType, QwenAgent, REASONING_EFFORT_SCALE, type ReasoningEffort, SUPPORTED_VOICE_PROVIDERS, type SpeechRequest, type SpeechResult, type SpeechUsage, SupportedAiKeyNames, SupportedAiModels, type TokenPairUsage, type TokenUsage$1 as TokenUsage, type TranscriptionRequest, type TranscriptionResult, type TranscriptionUsage, VOICE_MODEL_CONSTANTS, VOICE_MODEL_PRICING, VOICE_PROVIDER_API_KEY, type VoiceAgent, VoiceAgentFactory, type VoiceModelPricing, type VoiceProvider, ZodSchemaConverter, buildGoogleTtsPrompt, calculateAnthropicCost, calculateCost, calculateDeepSeekCost, calculateGeminiSttCost, calculateGeminiTtsCost, calculateGoogleCost, calculateGrokCost, calculateKimiCost, calculateMistralCost, calculateModelCost, calculateOpenAICost, calculateOpenAiSttCost, calculateOpenAiTtsCost, clampReasoningEffort, cleanResponse, createCatalog, createVoiceAgent, extractAnthropicTokenUsage, extractTokenUsageFromResponse$2 as extractAnthropicTokenUsageFromResponse, extractDeepSeekTokenUsage, extractTokenUsageFromResponse$5 as extractDeepSeekTokenUsageFromResponse, extractFirstJsonObject, extractGoogleTokenUsage, extractTokenUsageFromResponse$1 as extractGoogleTokenUsageFromResponse, extractGrokTokenUsage, extractTokenUsageFromResponse$3 as extractGrokTokenUsageFromResponse, extractKimiTokenUsage, extractTokenUsageFromResponse$4 as extractKimiTokenUsageFromResponse, extractMistralTokenUsage, extractTokenUsageFromResponse as extractMistralTokenUsageFromResponse, extractOpenAITokenUsage, extractTokenUsageFromResponse$6 as extractOpenAITokenUsageFromResponse, extractTokenUsage, extractUsageAndCalculateCost, generateGoogleTtsAudio, generateOpenAiTtsAudio, generateSchemaInstructions, getModelConfigByApiName, getModelDisplayName, getModelProviderName, getModelTags, getProviderSignatureFields, isHybridThinkingModel, isInPeakWindow, isPeakBilling, isWeekendAt, logger, mergeThinking, modelHasTag, modelIsFast, needsPromptBasedSchema, parseAndValidateLlmJson, pcmToWav, safeValidateResponse, setLlmLogger, stableHashHex, stripInlineThinking, supportsNativeJsonSchema, toAnthropicEffort, toDeepSeekEffort, toFuguEffort, toGeminiEffort, toGlmEffort, toOpenAIEffort, transcribeWithGemini, transcribeWithOpenAi, validateResponse };
1576
+ export { type AIMessage, ANTHROPIC_REASONING_EFFORTS, API_KEY_CONSTANTS, AbstractAgent, type AgentActivityData, AgentFactory, type AgentLoggingConfig, type AnthropicReasoningEffort, type AnthropicTokenUsage, type ApiKeyMap, BotResponseError, BudgetController, type BudgetControllerOptions, BudgetExceededError, type BudgetLimit, type BudgetVerdict, CACHE_TIER_MARKER, ClaudeAgent, type CostCalculationOptions, DEEPSEEK_PEAK_SCHEDULE, DEEPSEEK_REASONING_EFFORTS, DEFAULT_LOGGING_CONFIG, DEFAULT_MAX_OUTPUT_TOKENS, type DeepSeekReasoningEffort, type DeepSeekTokenUsage, DeepSeekV2Agent, FUGU_REASONING_EFFORTS, FuguAgent, type FuguReasoningEffort, GEMINI_AUDIO_TOKENS_PER_SECOND, GEMINI_REASONING_EFFORTS, GLM_REASONING_EFFORTS, type GeminiReasoningEffort, GlmAgent, type GlmReasoningEffort, GoogleAgent, type GoogleSttOptions, type GoogleSttResult, type GoogleTokenUsage, type GoogleTtsAudioOptions, type GoogleTtsResult, GoogleVoiceAgent, Gpt5Agent, GrokAgent, type GrokTokenUsage, InMemorySpendStore, type JsonSchemaOptions, KimiAgent, type KimiTokenUsage, type LLMModel, LLM_CONSTANTS, type LlmLogger, type LoggingConfig, MESSAGE_ROLE, MODEL_PRICING, MiniMaxAgent, MistralAgent, type MistralTokenUsage, type Modality, ModelAuthenticationError, type ModelConfig, ModelError, ModelInvalidResponseError, ModelOverloadError, type ModelPricing, ModelQuotaExceededError, ModelRateLimitError, ModelRefusalError, type ModelTag, ModelUnavailableError, OPENAI_REASONING_EFFORTS, type OpenAIReasoningEffort, type OpenAITokenUsage, type OpenAiSttOptions, type OpenAiSttResult, type OpenAiTtsAudioOptions, type OpenAiTtsVoice, OpenAiVoiceAgent, type PeakPricing, type PricingUnit, type ProviderSchema, type TokenUsage as ProviderTokenUsage, type ProviderType, QwenAgent, REASONING_EFFORT_SCALE, type ReasoningEffort, SUPPORTED_VOICE_PROVIDERS, type SpeechRequest, type SpeechResult, type SpeechUsage, type SpendInput, type SpendKind, type SpendLedger, type SpendLedgers, type SpendStore, type SpendWindow, SupportedAiKeyNames, SupportedAiModels, type TokenPairUsage, type TokenUsage$1 as TokenUsage, type TranscriptionRequest, type TranscriptionResult, type TranscriptionUsage, VOICE_MODEL_CONSTANTS, VOICE_MODEL_PRICING, VOICE_PROVIDER_API_KEY, type VoiceAgent, VoiceAgentFactory, type VoiceModelPricing, type VoiceProvider, ZodSchemaConverter, applySpend, buildGoogleTtsPrompt, calculateAnthropicCost, calculateCost, calculateDeepSeekCost, calculateGeminiSttCost, calculateGeminiTtsCost, calculateGoogleCost, calculateGrokCost, calculateKimiCost, calculateMistralCost, calculateModelCost, calculateOpenAICost, calculateOpenAiSttCost, calculateOpenAiTtsCost, clampReasoningEffort, cleanResponse, createCatalog, createVoiceAgent, evaluateBudget, extractAnthropicTokenUsage, extractTokenUsageFromResponse$2 as extractAnthropicTokenUsageFromResponse, extractDeepSeekTokenUsage, extractTokenUsageFromResponse$5 as extractDeepSeekTokenUsageFromResponse, extractFirstJsonObject, extractGoogleTokenUsage, extractTokenUsageFromResponse$1 as extractGoogleTokenUsageFromResponse, extractGrokTokenUsage, extractTokenUsageFromResponse$3 as extractGrokTokenUsageFromResponse, extractKimiTokenUsage, extractTokenUsageFromResponse$4 as extractKimiTokenUsageFromResponse, extractMistralTokenUsage, extractTokenUsageFromResponse as extractMistralTokenUsageFromResponse, extractOpenAITokenUsage, extractTokenUsageFromResponse$6 as extractOpenAITokenUsageFromResponse, extractTokenUsage, extractUsageAndCalculateCost, firstRefusal, generateGoogleTtsAudio, generateOpenAiTtsAudio, generateSchemaInstructions, getModelConfigByApiName, getModelDisplayName, getModelProviderName, getModelTags, getProviderSignatureFields, isBudgetExceededError, isHybridThinkingModel, isInPeakWindow, isPeakBilling, isWeekendAt, ledgerSpend, logger, mergeThinking, modelHasTag, modelIsFast, needsPromptBasedSchema, parseAndValidateLlmJson, pcmToWav, periodEnd, periodKey, safeValidateResponse, setLlmLogger, stableHashHex, stripInlineThinking, supportsNativeJsonSchema, toAnthropicEffort, toDeepSeekEffort, toFuguEffort, toGeminiEffort, toGlmEffort, toOpenAIEffort, transcribeWithGemini, transcribeWithOpenAi, validateResponse };
package/dist/index.d.ts CHANGED
@@ -1019,6 +1019,146 @@ declare const GEMINI_AUDIO_TOKENS_PER_SECOND = 25;
1019
1019
  */
1020
1020
  declare function transcribeWithGemini(audioBuffer: ArrayBuffer, apiKey: string, options?: GoogleSttOptions): Promise<GoogleSttResult>;
1021
1021
 
1022
+ /**
1023
+ * Budget control for agent spend — pure and storage-agnostic.
1024
+ *
1025
+ * The problem this solves: an app that runs LLM/voice/image calls on its own keys needs
1026
+ * to bound what one subject (a user, a tenant, a job) can spend per UTC day or month, and
1027
+ * needs the check to be cheap enough to run before EVERY call. The pieces:
1028
+ *
1029
+ * - `periodKey` / `periodEnd`: UTC window keys (`YYYY-MM-DD`, `YYYY-MM`) and reset times.
1030
+ * - `SpendLedger` + `applySpend`: an O(1) rolling ledger for one window. It is overwritten,
1031
+ * not appended, when the period changes, so the persisted record never grows; history
1032
+ * belongs in your per-request stats, not here.
1033
+ * - `evaluateBudget`: the verdict. Pure, no I/O, no throwing.
1034
+ * - `BudgetExceededError`: what to throw when a verdict refuses, carrying the verdict so
1035
+ * the caller can render "come back at <resetsAt>" instead of treating it as a provider
1036
+ * failure.
1037
+ * - `BudgetController` over a `SpendStore`: batteries-included wrapper for projects that
1038
+ * do not need to fold the ledger into their own database transaction. Apps that do
1039
+ * (the werewolf app charges the user, updates the game and writes a stats row in one
1040
+ * Firestore transaction) use the pure functions directly inside that transaction.
1041
+ *
1042
+ * Amounts are USD, rounded to 6 decimals like the rest of the cost accounting.
1043
+ */
1044
+ type SpendWindow = 'day' | 'month';
1045
+ /** Where the money went; free-form beyond the common kinds so apps can add their own. */
1046
+ type SpendKind = 'llm' | 'image' | 'tts' | 'stt' | (string & {});
1047
+ /** UTC key for the window containing `timestamp`: `2026-09-11` for a day, `2026-09` for a month. */
1048
+ declare function periodKey(timestamp: number, window: SpendWindow): string;
1049
+ /** First millisecond of the window after the one containing `timestamp` — i.e. when the budget resets. */
1050
+ declare function periodEnd(timestamp: number, window: SpendWindow): number;
1051
+ /**
1052
+ * Spend recorded for ONE window. `buckets` split the total by whatever the app cares
1053
+ * about (tier, model family, feature); the total is authoritative, buckets are a breakdown.
1054
+ */
1055
+ interface SpendLedger {
1056
+ period: string;
1057
+ totalUSD: number;
1058
+ buckets: Record<string, number>;
1059
+ }
1060
+ interface SpendInput {
1061
+ window: SpendWindow;
1062
+ amountUSD: number;
1063
+ /** Bucket to add the amount to, on top of the total. Omit to update the total only. */
1064
+ bucket?: string;
1065
+ timestamp?: number;
1066
+ }
1067
+ /**
1068
+ * Pure reducer: add `amountUSD` to the ledger for the window containing `timestamp`.
1069
+ * A ledger from an earlier period is discarded and a fresh one started (overwrite
1070
+ * semantics), so the persisted record stays O(1). Non-positive amounts return the
1071
+ * ledger normalized to the current period without recording anything.
1072
+ */
1073
+ declare function applySpend(ledger: Partial<SpendLedger> | null | undefined, input: SpendInput): SpendLedger;
1074
+ /**
1075
+ * Spend already recorded in the window containing `timestamp` — the total, or one bucket.
1076
+ * A missing ledger or one from another period counts as 0.
1077
+ */
1078
+ declare function ledgerSpend(ledger: Partial<SpendLedger> | null | undefined, window: SpendWindow, timestamp?: number, bucket?: string): number;
1079
+ interface BudgetLimit {
1080
+ window: SpendWindow;
1081
+ /** USD ceiling for the window. `Infinity` (or any non-finite) means unlimited. */
1082
+ limitUSD: number;
1083
+ /** Which bucket the limit applies to; informational for the verdict, the caller passes the matching spend. */
1084
+ bucket?: string;
1085
+ }
1086
+ interface BudgetVerdict {
1087
+ allowed: boolean;
1088
+ window: SpendWindow;
1089
+ bucket?: string;
1090
+ limitUSD: number;
1091
+ spentUSD: number;
1092
+ remainingUSD: number;
1093
+ /** Epoch ms when the window rolls over and the spend counts from zero again. */
1094
+ resetsAt: number;
1095
+ }
1096
+ /**
1097
+ * The verdict for one limit given the spend already recorded in its window. Refuses when
1098
+ * spent >= limit (a subject exactly at the cap gets no more calls). Pure: no I/O, no throw.
1099
+ */
1100
+ declare function evaluateBudget(spentUSD: number, limit: BudgetLimit, timestamp?: number): BudgetVerdict;
1101
+ /** The first refusing verdict, or undefined when every window still has room. */
1102
+ declare function firstRefusal(verdicts: BudgetVerdict[]): BudgetVerdict | undefined;
1103
+ /**
1104
+ * Thrown when a budget refuses a call. Not a `ModelError`: nothing was sent to a provider
1105
+ * and nothing is retryable until `verdict.resetsAt`. Apps typically subclass it to attach
1106
+ * user-facing copy, or map it to their own error at the boundary.
1107
+ */
1108
+ declare class BudgetExceededError extends Error {
1109
+ readonly verdict: BudgetVerdict;
1110
+ readonly subject?: string;
1111
+ constructor(verdict: BudgetVerdict, subject?: string, message?: string);
1112
+ static describe(v: BudgetVerdict): string;
1113
+ }
1114
+ declare function isBudgetExceededError(err: unknown): err is BudgetExceededError;
1115
+ type SpendLedgers = Partial<Record<SpendWindow, SpendLedger>>;
1116
+ /**
1117
+ * Persistence for per-subject ledgers. `update` must apply `fn` to the current ledgers
1118
+ * and persist the result atomically for that subject (a transaction, a lock, a
1119
+ * single-threaded map — whatever the backend offers). If `fn` throws, nothing is written.
1120
+ */
1121
+ interface SpendStore {
1122
+ read(subject: string): Promise<SpendLedgers>;
1123
+ update(subject: string, fn: (current: SpendLedgers) => SpendLedgers): Promise<SpendLedgers>;
1124
+ }
1125
+ /** Process-local store: fine for tests, CLIs and single-instance jobs. */
1126
+ declare class InMemorySpendStore implements SpendStore {
1127
+ private readonly ledgers;
1128
+ read(subject: string): Promise<SpendLedgers>;
1129
+ update(subject: string, fn: (current: SpendLedgers) => SpendLedgers): Promise<SpendLedgers>;
1130
+ }
1131
+ interface BudgetControllerOptions {
1132
+ limits: BudgetLimit[];
1133
+ /** Bucket every recorded spend is also added to (e.g. the subject's tier). */
1134
+ bucket?: string;
1135
+ clock?: () => number;
1136
+ }
1137
+ /**
1138
+ * Check-before, record-after budget control for one set of limits.
1139
+ *
1140
+ * `assertWithinBudget` is the cheap pre-call guard; `record` re-evaluates inside the
1141
+ * store's atomic update so concurrent callers cannot all slip past a nearly-exhausted
1142
+ * budget. The overrun that remains is bounded by the calls already in flight when the
1143
+ * cap is crossed — state that bound, do not claim the cap is exact.
1144
+ */
1145
+ declare class BudgetController {
1146
+ private readonly store;
1147
+ private readonly limits;
1148
+ private readonly bucket?;
1149
+ private readonly clock;
1150
+ constructor(store: SpendStore, options: BudgetControllerOptions);
1151
+ private verdicts;
1152
+ check(subject: string): Promise<BudgetVerdict[]>;
1153
+ assertWithinBudget(subject: string): Promise<void>;
1154
+ /**
1155
+ * Record `amountUSD` against every limited window. Re-checks the limits on the
1156
+ * ledgers as they are at write time and throws `BudgetExceededError` (writing
1157
+ * nothing) if any window is already exhausted.
1158
+ */
1159
+ record(subject: string, amountUSD: number): Promise<SpendLedgers>;
1160
+ }
1161
+
1022
1162
  declare abstract class AbstractAgent {
1023
1163
  name: string;
1024
1164
  gameId?: string;
@@ -1433,4 +1573,4 @@ declare class MiniMaxAgent extends AbstractAgent {
1433
1573
  doAskText(messages: AIMessage[]): Promise<[string, string, TokenUsage$1?, string?]>;
1434
1574
  }
1435
1575
 
1436
- export { type AIMessage, ANTHROPIC_REASONING_EFFORTS, API_KEY_CONSTANTS, AbstractAgent, type AgentActivityData, AgentFactory, type AgentLoggingConfig, type AnthropicReasoningEffort, type AnthropicTokenUsage, type ApiKeyMap, BotResponseError, CACHE_TIER_MARKER, ClaudeAgent, type CostCalculationOptions, DEEPSEEK_PEAK_SCHEDULE, DEEPSEEK_REASONING_EFFORTS, DEFAULT_LOGGING_CONFIG, DEFAULT_MAX_OUTPUT_TOKENS, type DeepSeekReasoningEffort, type DeepSeekTokenUsage, DeepSeekV2Agent, FUGU_REASONING_EFFORTS, FuguAgent, type FuguReasoningEffort, GEMINI_AUDIO_TOKENS_PER_SECOND, GEMINI_REASONING_EFFORTS, GLM_REASONING_EFFORTS, type GeminiReasoningEffort, GlmAgent, type GlmReasoningEffort, GoogleAgent, type GoogleSttOptions, type GoogleSttResult, type GoogleTokenUsage, type GoogleTtsAudioOptions, type GoogleTtsResult, GoogleVoiceAgent, Gpt5Agent, GrokAgent, type GrokTokenUsage, type JsonSchemaOptions, KimiAgent, type KimiTokenUsage, type LLMModel, LLM_CONSTANTS, type LlmLogger, type LoggingConfig, MESSAGE_ROLE, MODEL_PRICING, MiniMaxAgent, MistralAgent, type MistralTokenUsage, type Modality, ModelAuthenticationError, type ModelConfig, ModelError, ModelInvalidResponseError, ModelOverloadError, type ModelPricing, ModelQuotaExceededError, ModelRateLimitError, ModelRefusalError, type ModelTag, ModelUnavailableError, OPENAI_REASONING_EFFORTS, type OpenAIReasoningEffort, type OpenAITokenUsage, type OpenAiSttOptions, type OpenAiSttResult, type OpenAiTtsAudioOptions, type OpenAiTtsVoice, OpenAiVoiceAgent, type PeakPricing, type PricingUnit, type ProviderSchema, type TokenUsage as ProviderTokenUsage, type ProviderType, QwenAgent, REASONING_EFFORT_SCALE, type ReasoningEffort, SUPPORTED_VOICE_PROVIDERS, type SpeechRequest, type SpeechResult, type SpeechUsage, SupportedAiKeyNames, SupportedAiModels, type TokenPairUsage, type TokenUsage$1 as TokenUsage, type TranscriptionRequest, type TranscriptionResult, type TranscriptionUsage, VOICE_MODEL_CONSTANTS, VOICE_MODEL_PRICING, VOICE_PROVIDER_API_KEY, type VoiceAgent, VoiceAgentFactory, type VoiceModelPricing, type VoiceProvider, ZodSchemaConverter, buildGoogleTtsPrompt, calculateAnthropicCost, calculateCost, calculateDeepSeekCost, calculateGeminiSttCost, calculateGeminiTtsCost, calculateGoogleCost, calculateGrokCost, calculateKimiCost, calculateMistralCost, calculateModelCost, calculateOpenAICost, calculateOpenAiSttCost, calculateOpenAiTtsCost, clampReasoningEffort, cleanResponse, createCatalog, createVoiceAgent, extractAnthropicTokenUsage, extractTokenUsageFromResponse$2 as extractAnthropicTokenUsageFromResponse, extractDeepSeekTokenUsage, extractTokenUsageFromResponse$5 as extractDeepSeekTokenUsageFromResponse, extractFirstJsonObject, extractGoogleTokenUsage, extractTokenUsageFromResponse$1 as extractGoogleTokenUsageFromResponse, extractGrokTokenUsage, extractTokenUsageFromResponse$3 as extractGrokTokenUsageFromResponse, extractKimiTokenUsage, extractTokenUsageFromResponse$4 as extractKimiTokenUsageFromResponse, extractMistralTokenUsage, extractTokenUsageFromResponse as extractMistralTokenUsageFromResponse, extractOpenAITokenUsage, extractTokenUsageFromResponse$6 as extractOpenAITokenUsageFromResponse, extractTokenUsage, extractUsageAndCalculateCost, generateGoogleTtsAudio, generateOpenAiTtsAudio, generateSchemaInstructions, getModelConfigByApiName, getModelDisplayName, getModelProviderName, getModelTags, getProviderSignatureFields, isHybridThinkingModel, isInPeakWindow, isPeakBilling, isWeekendAt, logger, mergeThinking, modelHasTag, modelIsFast, needsPromptBasedSchema, parseAndValidateLlmJson, pcmToWav, safeValidateResponse, setLlmLogger, stableHashHex, stripInlineThinking, supportsNativeJsonSchema, toAnthropicEffort, toDeepSeekEffort, toFuguEffort, toGeminiEffort, toGlmEffort, toOpenAIEffort, transcribeWithGemini, transcribeWithOpenAi, validateResponse };
1576
+ export { type AIMessage, ANTHROPIC_REASONING_EFFORTS, API_KEY_CONSTANTS, AbstractAgent, type AgentActivityData, AgentFactory, type AgentLoggingConfig, type AnthropicReasoningEffort, type AnthropicTokenUsage, type ApiKeyMap, BotResponseError, BudgetController, type BudgetControllerOptions, BudgetExceededError, type BudgetLimit, type BudgetVerdict, CACHE_TIER_MARKER, ClaudeAgent, type CostCalculationOptions, DEEPSEEK_PEAK_SCHEDULE, DEEPSEEK_REASONING_EFFORTS, DEFAULT_LOGGING_CONFIG, DEFAULT_MAX_OUTPUT_TOKENS, type DeepSeekReasoningEffort, type DeepSeekTokenUsage, DeepSeekV2Agent, FUGU_REASONING_EFFORTS, FuguAgent, type FuguReasoningEffort, GEMINI_AUDIO_TOKENS_PER_SECOND, GEMINI_REASONING_EFFORTS, GLM_REASONING_EFFORTS, type GeminiReasoningEffort, GlmAgent, type GlmReasoningEffort, GoogleAgent, type GoogleSttOptions, type GoogleSttResult, type GoogleTokenUsage, type GoogleTtsAudioOptions, type GoogleTtsResult, GoogleVoiceAgent, Gpt5Agent, GrokAgent, type GrokTokenUsage, InMemorySpendStore, type JsonSchemaOptions, KimiAgent, type KimiTokenUsage, type LLMModel, LLM_CONSTANTS, type LlmLogger, type LoggingConfig, MESSAGE_ROLE, MODEL_PRICING, MiniMaxAgent, MistralAgent, type MistralTokenUsage, type Modality, ModelAuthenticationError, type ModelConfig, ModelError, ModelInvalidResponseError, ModelOverloadError, type ModelPricing, ModelQuotaExceededError, ModelRateLimitError, ModelRefusalError, type ModelTag, ModelUnavailableError, OPENAI_REASONING_EFFORTS, type OpenAIReasoningEffort, type OpenAITokenUsage, type OpenAiSttOptions, type OpenAiSttResult, type OpenAiTtsAudioOptions, type OpenAiTtsVoice, OpenAiVoiceAgent, type PeakPricing, type PricingUnit, type ProviderSchema, type TokenUsage as ProviderTokenUsage, type ProviderType, QwenAgent, REASONING_EFFORT_SCALE, type ReasoningEffort, SUPPORTED_VOICE_PROVIDERS, type SpeechRequest, type SpeechResult, type SpeechUsage, type SpendInput, type SpendKind, type SpendLedger, type SpendLedgers, type SpendStore, type SpendWindow, SupportedAiKeyNames, SupportedAiModels, type TokenPairUsage, type TokenUsage$1 as TokenUsage, type TranscriptionRequest, type TranscriptionResult, type TranscriptionUsage, VOICE_MODEL_CONSTANTS, VOICE_MODEL_PRICING, VOICE_PROVIDER_API_KEY, type VoiceAgent, VoiceAgentFactory, type VoiceModelPricing, type VoiceProvider, ZodSchemaConverter, applySpend, buildGoogleTtsPrompt, calculateAnthropicCost, calculateCost, calculateDeepSeekCost, calculateGeminiSttCost, calculateGeminiTtsCost, calculateGoogleCost, calculateGrokCost, calculateKimiCost, calculateMistralCost, calculateModelCost, calculateOpenAICost, calculateOpenAiSttCost, calculateOpenAiTtsCost, clampReasoningEffort, cleanResponse, createCatalog, createVoiceAgent, evaluateBudget, extractAnthropicTokenUsage, extractTokenUsageFromResponse$2 as extractAnthropicTokenUsageFromResponse, extractDeepSeekTokenUsage, extractTokenUsageFromResponse$5 as extractDeepSeekTokenUsageFromResponse, extractFirstJsonObject, extractGoogleTokenUsage, extractTokenUsageFromResponse$1 as extractGoogleTokenUsageFromResponse, extractGrokTokenUsage, extractTokenUsageFromResponse$3 as extractGrokTokenUsageFromResponse, extractKimiTokenUsage, extractTokenUsageFromResponse$4 as extractKimiTokenUsageFromResponse, extractMistralTokenUsage, extractTokenUsageFromResponse as extractMistralTokenUsageFromResponse, extractOpenAITokenUsage, extractTokenUsageFromResponse$6 as extractOpenAITokenUsageFromResponse, extractTokenUsage, extractUsageAndCalculateCost, firstRefusal, generateGoogleTtsAudio, generateOpenAiTtsAudio, generateSchemaInstructions, getModelConfigByApiName, getModelDisplayName, getModelProviderName, getModelTags, getProviderSignatureFields, isBudgetExceededError, isHybridThinkingModel, isInPeakWindow, isPeakBilling, isWeekendAt, ledgerSpend, logger, mergeThinking, modelHasTag, modelIsFast, needsPromptBasedSchema, parseAndValidateLlmJson, pcmToWav, periodEnd, periodKey, safeValidateResponse, setLlmLogger, stableHashHex, stripInlineThinking, supportsNativeJsonSchema, toAnthropicEffort, toDeepSeekEffort, toFuguEffort, toGeminiEffort, toGlmEffort, toOpenAIEffort, transcribeWithGemini, transcribeWithOpenAi, validateResponse };
package/dist/index.js CHANGED
@@ -35,6 +35,8 @@ __export(index_exports, {
35
35
  AbstractAgent: () => AbstractAgent,
36
36
  AgentFactory: () => AgentFactory,
37
37
  BotResponseError: () => BotResponseError,
38
+ BudgetController: () => BudgetController,
39
+ BudgetExceededError: () => BudgetExceededError,
38
40
  CACHE_TIER_MARKER: () => CACHE_TIER_MARKER,
39
41
  ClaudeAgent: () => ClaudeAgent,
40
42
  DEEPSEEK_PEAK_SCHEDULE: () => DEEPSEEK_PEAK_SCHEDULE,
@@ -52,6 +54,7 @@ __export(index_exports, {
52
54
  GoogleVoiceAgent: () => GoogleVoiceAgent,
53
55
  Gpt5Agent: () => Gpt5Agent,
54
56
  GrokAgent: () => GrokAgent,
57
+ InMemorySpendStore: () => InMemorySpendStore,
55
58
  KimiAgent: () => KimiAgent,
56
59
  LLM_CONSTANTS: () => LLM_CONSTANTS,
57
60
  MESSAGE_ROLE: () => MESSAGE_ROLE,
@@ -78,6 +81,7 @@ __export(index_exports, {
78
81
  VOICE_PROVIDER_API_KEY: () => VOICE_PROVIDER_API_KEY,
79
82
  VoiceAgentFactory: () => VoiceAgentFactory,
80
83
  ZodSchemaConverter: () => ZodSchemaConverter,
84
+ applySpend: () => applySpend,
81
85
  buildGoogleTtsPrompt: () => buildGoogleTtsPrompt,
82
86
  calculateAnthropicCost: () => calculateAnthropicCost,
83
87
  calculateCost: () => calculateCost,
@@ -96,6 +100,7 @@ __export(index_exports, {
96
100
  cleanResponse: () => cleanResponse,
97
101
  createCatalog: () => createCatalog,
98
102
  createVoiceAgent: () => createVoiceAgent,
103
+ evaluateBudget: () => evaluateBudget,
99
104
  extractAnthropicTokenUsage: () => extractAnthropicTokenUsage,
100
105
  extractAnthropicTokenUsageFromResponse: () => extractTokenUsageFromResponse5,
101
106
  extractDeepSeekTokenUsage: () => extractDeepSeekTokenUsage,
@@ -113,6 +118,7 @@ __export(index_exports, {
113
118
  extractOpenAITokenUsageFromResponse: () => extractTokenUsageFromResponse,
114
119
  extractTokenUsage: () => extractTokenUsage,
115
120
  extractUsageAndCalculateCost: () => extractUsageAndCalculateCost,
121
+ firstRefusal: () => firstRefusal,
116
122
  generateGoogleTtsAudio: () => generateGoogleTtsAudio,
117
123
  generateOpenAiTtsAudio: () => generateOpenAiTtsAudio,
118
124
  generateSchemaInstructions: () => generateSchemaInstructions,
@@ -121,10 +127,12 @@ __export(index_exports, {
121
127
  getModelProviderName: () => getModelProviderName,
122
128
  getModelTags: () => getModelTags,
123
129
  getProviderSignatureFields: () => getProviderSignatureFields,
130
+ isBudgetExceededError: () => isBudgetExceededError,
124
131
  isHybridThinkingModel: () => isHybridThinkingModel,
125
132
  isInPeakWindow: () => isInPeakWindow,
126
133
  isPeakBilling: () => isPeakBilling,
127
134
  isWeekendAt: () => isWeekendAt,
135
+ ledgerSpend: () => ledgerSpend,
128
136
  logger: () => logger,
129
137
  mergeThinking: () => mergeThinking,
130
138
  modelHasTag: () => modelHasTag,
@@ -132,6 +140,8 @@ __export(index_exports, {
132
140
  needsPromptBasedSchema: () => needsPromptBasedSchema,
133
141
  parseAndValidateLlmJson: () => parseAndValidateLlmJson,
134
142
  pcmToWav: () => pcmToWav,
143
+ periodEnd: () => periodEnd,
144
+ periodKey: () => periodKey,
135
145
  safeValidateResponse: () => safeValidateResponse,
136
146
  setLlmLogger: () => setLlmLogger,
137
147
  stableHashHex: () => stableHashHex,
@@ -2026,6 +2036,152 @@ function createVoiceAgent(provider, apiKey) {
2026
2036
  return VoiceAgentFactory.createAgent(provider, apiKey);
2027
2037
  }
2028
2038
 
2039
+ // src/budget/index.ts
2040
+ function round6(n) {
2041
+ return parseFloat((Number(n) || 0).toFixed(6));
2042
+ }
2043
+ function periodKey(timestamp, window) {
2044
+ const d = new Date(timestamp);
2045
+ const y = d.getUTCFullYear();
2046
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
2047
+ if (window === "month") {
2048
+ return `${y}-${m}`;
2049
+ }
2050
+ const day = String(d.getUTCDate()).padStart(2, "0");
2051
+ return `${y}-${m}-${day}`;
2052
+ }
2053
+ function periodEnd(timestamp, window) {
2054
+ const d = new Date(timestamp);
2055
+ if (window === "month") {
2056
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
2057
+ }
2058
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1);
2059
+ }
2060
+ function normalizeLedger(ledger, period) {
2061
+ if (!ledger || ledger.period !== period) {
2062
+ return { period, totalUSD: 0, buckets: {} };
2063
+ }
2064
+ const buckets = {};
2065
+ for (const [k, v] of Object.entries(ledger.buckets ?? {})) {
2066
+ buckets[k] = round6(v);
2067
+ }
2068
+ return { period, totalUSD: round6(ledger.totalUSD ?? 0), buckets };
2069
+ }
2070
+ function applySpend(ledger, input) {
2071
+ const timestamp = input.timestamp ?? Date.now();
2072
+ const period = periodKey(timestamp, input.window);
2073
+ const current2 = normalizeLedger(ledger, period);
2074
+ const amount = round6(input.amountUSD);
2075
+ if (!(amount > 0)) {
2076
+ return current2;
2077
+ }
2078
+ const buckets = { ...current2.buckets };
2079
+ if (input.bucket) {
2080
+ buckets[input.bucket] = round6((buckets[input.bucket] ?? 0) + amount);
2081
+ }
2082
+ return { period, totalUSD: round6(current2.totalUSD + amount), buckets };
2083
+ }
2084
+ function ledgerSpend(ledger, window, timestamp = Date.now(), bucket) {
2085
+ const current2 = normalizeLedger(ledger, periodKey(timestamp, window));
2086
+ return bucket ? current2.buckets[bucket] ?? 0 : current2.totalUSD;
2087
+ }
2088
+ function evaluateBudget(spentUSD, limit, timestamp = Date.now()) {
2089
+ const spent = round6(spentUSD);
2090
+ const unlimited = !Number.isFinite(limit.limitUSD);
2091
+ const remaining = unlimited ? Number.POSITIVE_INFINITY : Math.max(0, round6(limit.limitUSD - spent));
2092
+ return {
2093
+ allowed: unlimited || spent < limit.limitUSD,
2094
+ window: limit.window,
2095
+ ...limit.bucket ? { bucket: limit.bucket } : {},
2096
+ limitUSD: limit.limitUSD,
2097
+ spentUSD: spent,
2098
+ remainingUSD: remaining,
2099
+ resetsAt: periodEnd(timestamp, limit.window)
2100
+ };
2101
+ }
2102
+ function firstRefusal(verdicts) {
2103
+ return verdicts.find((v) => !v.allowed);
2104
+ }
2105
+ var BudgetExceededError = class _BudgetExceededError extends Error {
2106
+ verdict;
2107
+ subject;
2108
+ constructor(verdict, subject, message) {
2109
+ super(message ?? _BudgetExceededError.describe(verdict));
2110
+ this.name = "BudgetExceededError";
2111
+ this.verdict = verdict;
2112
+ this.subject = subject;
2113
+ }
2114
+ static describe(v) {
2115
+ const when = v.window === "day" ? "daily" : "monthly";
2116
+ return `${when} budget of $${v.limitUSD} exhausted ($${v.spentUSD} spent); resets at ${new Date(v.resetsAt).toISOString()}`;
2117
+ }
2118
+ };
2119
+ function isBudgetExceededError(err) {
2120
+ return err instanceof BudgetExceededError || typeof err === "object" && err !== null && err.name === "BudgetExceededError" && !!err.verdict;
2121
+ }
2122
+ var InMemorySpendStore = class {
2123
+ ledgers = /* @__PURE__ */ new Map();
2124
+ async read(subject) {
2125
+ return { ...this.ledgers.get(subject) ?? {} };
2126
+ }
2127
+ async update(subject, fn) {
2128
+ const next = fn({ ...this.ledgers.get(subject) ?? {} });
2129
+ this.ledgers.set(subject, next);
2130
+ return { ...next };
2131
+ }
2132
+ };
2133
+ var BudgetController = class {
2134
+ constructor(store, options) {
2135
+ this.store = store;
2136
+ this.limits = options.limits;
2137
+ this.bucket = options.bucket;
2138
+ this.clock = options.clock ?? (() => Date.now());
2139
+ }
2140
+ store;
2141
+ limits;
2142
+ bucket;
2143
+ clock;
2144
+ verdicts(ledgers, now) {
2145
+ return this.limits.map((limit) => {
2146
+ const spent = ledgerSpend(ledgers[limit.window], limit.window, now, limit.bucket ?? this.bucket);
2147
+ return evaluateBudget(spent, limit, now);
2148
+ });
2149
+ }
2150
+ async check(subject) {
2151
+ return this.verdicts(await this.store.read(subject), this.clock());
2152
+ }
2153
+ async assertWithinBudget(subject) {
2154
+ const refused = firstRefusal(await this.check(subject));
2155
+ if (refused) {
2156
+ throw new BudgetExceededError(refused, subject);
2157
+ }
2158
+ }
2159
+ /**
2160
+ * Record `amountUSD` against every limited window. Re-checks the limits on the
2161
+ * ledgers as they are at write time and throws `BudgetExceededError` (writing
2162
+ * nothing) if any window is already exhausted.
2163
+ */
2164
+ async record(subject, amountUSD) {
2165
+ const now = this.clock();
2166
+ return this.store.update(subject, (current2) => {
2167
+ const refused = firstRefusal(this.verdicts(current2, now));
2168
+ if (refused) {
2169
+ throw new BudgetExceededError(refused, subject);
2170
+ }
2171
+ const next = { ...current2 };
2172
+ for (const limit of this.limits) {
2173
+ next[limit.window] = applySpend(current2[limit.window], {
2174
+ window: limit.window,
2175
+ amountUSD,
2176
+ bucket: limit.bucket ?? this.bucket,
2177
+ timestamp: now
2178
+ });
2179
+ }
2180
+ return next;
2181
+ });
2182
+ }
2183
+ };
2184
+
2029
2185
  // src/agents/abstract-agent.ts
2030
2186
  var AbstractAgent = class {
2031
2187
  name;
@@ -4755,6 +4911,8 @@ var AgentFactory = class {
4755
4911
  AbstractAgent,
4756
4912
  AgentFactory,
4757
4913
  BotResponseError,
4914
+ BudgetController,
4915
+ BudgetExceededError,
4758
4916
  CACHE_TIER_MARKER,
4759
4917
  ClaudeAgent,
4760
4918
  DEEPSEEK_PEAK_SCHEDULE,
@@ -4772,6 +4930,7 @@ var AgentFactory = class {
4772
4930
  GoogleVoiceAgent,
4773
4931
  Gpt5Agent,
4774
4932
  GrokAgent,
4933
+ InMemorySpendStore,
4775
4934
  KimiAgent,
4776
4935
  LLM_CONSTANTS,
4777
4936
  MESSAGE_ROLE,
@@ -4798,6 +4957,7 @@ var AgentFactory = class {
4798
4957
  VOICE_PROVIDER_API_KEY,
4799
4958
  VoiceAgentFactory,
4800
4959
  ZodSchemaConverter,
4960
+ applySpend,
4801
4961
  buildGoogleTtsPrompt,
4802
4962
  calculateAnthropicCost,
4803
4963
  calculateCost,
@@ -4816,6 +4976,7 @@ var AgentFactory = class {
4816
4976
  cleanResponse,
4817
4977
  createCatalog,
4818
4978
  createVoiceAgent,
4979
+ evaluateBudget,
4819
4980
  extractAnthropicTokenUsage,
4820
4981
  extractAnthropicTokenUsageFromResponse,
4821
4982
  extractDeepSeekTokenUsage,
@@ -4833,6 +4994,7 @@ var AgentFactory = class {
4833
4994
  extractOpenAITokenUsageFromResponse,
4834
4995
  extractTokenUsage,
4835
4996
  extractUsageAndCalculateCost,
4997
+ firstRefusal,
4836
4998
  generateGoogleTtsAudio,
4837
4999
  generateOpenAiTtsAudio,
4838
5000
  generateSchemaInstructions,
@@ -4841,10 +5003,12 @@ var AgentFactory = class {
4841
5003
  getModelProviderName,
4842
5004
  getModelTags,
4843
5005
  getProviderSignatureFields,
5006
+ isBudgetExceededError,
4844
5007
  isHybridThinkingModel,
4845
5008
  isInPeakWindow,
4846
5009
  isPeakBilling,
4847
5010
  isWeekendAt,
5011
+ ledgerSpend,
4848
5012
  logger,
4849
5013
  mergeThinking,
4850
5014
  modelHasTag,
@@ -4852,6 +5016,8 @@ var AgentFactory = class {
4852
5016
  needsPromptBasedSchema,
4853
5017
  parseAndValidateLlmJson,
4854
5018
  pcmToWav,
5019
+ periodEnd,
5020
+ periodKey,
4855
5021
  safeValidateResponse,
4856
5022
  setLlmLogger,
4857
5023
  stableHashHex,