@hiper2d/ai-agents 0.2.1 → 0.3.1

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,37 @@ 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
+
99
+ To guard every LLM call without touching call sites, install a process-wide pre-ask hook
100
+ once at startup. It runs before each `askText` / `askWithZodSchema` with the agent, so a
101
+ throwing hook refuses the call before anything reaches the provider:
102
+
103
+ ```ts
104
+ import { setBeforeAskHook } from '@hiper2d/ai-agents';
105
+ setBeforeAskHook(async agent => { if (agent.userId) await budget.assertWithinBudget(agent.userId); });
106
+ ```
107
+
77
108
  ### Logging
78
109
 
79
110
  The library logs through an injectable sink — `setLlmLogger(fn)` — so a host app can route
package/dist/index.d.mts CHANGED
@@ -1019,6 +1019,155 @@ 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
+
1162
+ /**
1163
+ * Runs before every ask on every LLM agent, with the agent about to call the provider.
1164
+ * Throw to refuse the call (nothing is sent). The intended use is budget control: a host
1165
+ * installs one hook that reads `agent.userId` and asserts the subject's spend budget, so
1166
+ * no call site can forget the check. Async hooks are awaited.
1167
+ */
1168
+ type BeforeAskHook = (agent: AbstractAgent) => void | Promise<void>;
1169
+ /** Install (or clear, with no argument) the process-wide pre-ask hook. Call once at startup. */
1170
+ declare function setBeforeAskHook(hook?: BeforeAskHook): void;
1022
1171
  declare abstract class AbstractAgent {
1023
1172
  name: string;
1024
1173
  gameId?: string;
@@ -1433,4 +1582,4 @@ declare class MiniMaxAgent extends AbstractAgent {
1433
1582
  doAskText(messages: AIMessage[]): Promise<[string, string, TokenUsage$1?, string?]>;
1434
1583
  }
1435
1584
 
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 };
1585
+ export { type AIMessage, ANTHROPIC_REASONING_EFFORTS, API_KEY_CONSTANTS, AbstractAgent, type AgentActivityData, AgentFactory, type AgentLoggingConfig, type AnthropicReasoningEffort, type AnthropicTokenUsage, type ApiKeyMap, type BeforeAskHook, 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, setBeforeAskHook, setLlmLogger, stableHashHex, stripInlineThinking, supportsNativeJsonSchema, toAnthropicEffort, toDeepSeekEffort, toFuguEffort, toGeminiEffort, toGlmEffort, toOpenAIEffort, transcribeWithGemini, transcribeWithOpenAi, validateResponse };
package/dist/index.d.ts CHANGED
@@ -1019,6 +1019,155 @@ 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
+
1162
+ /**
1163
+ * Runs before every ask on every LLM agent, with the agent about to call the provider.
1164
+ * Throw to refuse the call (nothing is sent). The intended use is budget control: a host
1165
+ * installs one hook that reads `agent.userId` and asserts the subject's spend budget, so
1166
+ * no call site can forget the check. Async hooks are awaited.
1167
+ */
1168
+ type BeforeAskHook = (agent: AbstractAgent) => void | Promise<void>;
1169
+ /** Install (or clear, with no argument) the process-wide pre-ask hook. Call once at startup. */
1170
+ declare function setBeforeAskHook(hook?: BeforeAskHook): void;
1022
1171
  declare abstract class AbstractAgent {
1023
1172
  name: string;
1024
1173
  gameId?: string;
@@ -1433,4 +1582,4 @@ declare class MiniMaxAgent extends AbstractAgent {
1433
1582
  doAskText(messages: AIMessage[]): Promise<[string, string, TokenUsage$1?, string?]>;
1434
1583
  }
1435
1584
 
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 };
1585
+ export { type AIMessage, ANTHROPIC_REASONING_EFFORTS, API_KEY_CONSTANTS, AbstractAgent, type AgentActivityData, AgentFactory, type AgentLoggingConfig, type AnthropicReasoningEffort, type AnthropicTokenUsage, type ApiKeyMap, type BeforeAskHook, 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, setBeforeAskHook, setLlmLogger, stableHashHex, stripInlineThinking, supportsNativeJsonSchema, toAnthropicEffort, toDeepSeekEffort, toFuguEffort, toGeminiEffort, toGlmEffort, toOpenAIEffort, transcribeWithGemini, transcribeWithOpenAi, validateResponse };