@hiper2d/ai-agents 0.2.0 → 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;
@@ -1092,8 +1232,20 @@ declare class AgentFactory {
1092
1232
  private static validateLlmTypeAndGet;
1093
1233
  }
1094
1234
 
1235
+ type CacheTtl = '5m' | '1h';
1095
1236
  declare class ClaudeAgent extends AbstractAgent {
1096
1237
  private readonly client;
1238
+ /**
1239
+ * TTL for every breakpoint this agent places (system tiers and the message anchor).
1240
+ * Anthropic bills a 5m write at 1.25x input, a 1h write at 2x, reads at 0.1x, and a read
1241
+ * refreshes the timer on either TTL. Default '1h' because the main consumer runs at human
1242
+ * pace: consecutive calls for one agent measured 12-78 minutes apart, so 5m entries
1243
+ * expired before they were ever read (0-16% hit rate over 30 days, hits only on gaps
1244
+ * under five minutes). Set '5m' for continuous traffic where every call lands inside the
1245
+ * window; there the cheaper write wins. One knob for all breakpoints on purpose: Anthropic
1246
+ * requires 1h entries to precede 5m ones, and a single TTL keeps that trivially true.
1247
+ */
1248
+ cacheTtl: CacheTtl;
1097
1249
  private get defaultParams();
1098
1250
  private readonly logTemplates;
1099
1251
  private readonly errorMessages;
@@ -1150,6 +1302,7 @@ declare class ClaudeAgent extends AbstractAgent {
1150
1302
 
1151
1303
  declare class Gpt5Agent extends AbstractAgent {
1152
1304
  private readonly client;
1305
+ private readonly promptCacheKey;
1153
1306
  private readonly logTemplates;
1154
1307
  private readonly errorMessages;
1155
1308
  constructor(name: string, instruction: string, model: string, apiKey: string, temperature: number, enableThinking?: boolean, agentLoggingConfig?: AgentLoggingConfig);
@@ -1420,4 +1573,4 @@ declare class MiniMaxAgent extends AbstractAgent {
1420
1573
  doAskText(messages: AIMessage[]): Promise<[string, string, TokenUsage$1?, string?]>;
1421
1574
  }
1422
1575
 
1423
- 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;
@@ -1092,8 +1232,20 @@ declare class AgentFactory {
1092
1232
  private static validateLlmTypeAndGet;
1093
1233
  }
1094
1234
 
1235
+ type CacheTtl = '5m' | '1h';
1095
1236
  declare class ClaudeAgent extends AbstractAgent {
1096
1237
  private readonly client;
1238
+ /**
1239
+ * TTL for every breakpoint this agent places (system tiers and the message anchor).
1240
+ * Anthropic bills a 5m write at 1.25x input, a 1h write at 2x, reads at 0.1x, and a read
1241
+ * refreshes the timer on either TTL. Default '1h' because the main consumer runs at human
1242
+ * pace: consecutive calls for one agent measured 12-78 minutes apart, so 5m entries
1243
+ * expired before they were ever read (0-16% hit rate over 30 days, hits only on gaps
1244
+ * under five minutes). Set '5m' for continuous traffic where every call lands inside the
1245
+ * window; there the cheaper write wins. One knob for all breakpoints on purpose: Anthropic
1246
+ * requires 1h entries to precede 5m ones, and a single TTL keeps that trivially true.
1247
+ */
1248
+ cacheTtl: CacheTtl;
1097
1249
  private get defaultParams();
1098
1250
  private readonly logTemplates;
1099
1251
  private readonly errorMessages;
@@ -1150,6 +1302,7 @@ declare class ClaudeAgent extends AbstractAgent {
1150
1302
 
1151
1303
  declare class Gpt5Agent extends AbstractAgent {
1152
1304
  private readonly client;
1305
+ private readonly promptCacheKey;
1153
1306
  private readonly logTemplates;
1154
1307
  private readonly errorMessages;
1155
1308
  constructor(name: string, instruction: string, model: string, apiKey: string, temperature: number, enableThinking?: boolean, agentLoggingConfig?: AgentLoggingConfig);
@@ -1420,4 +1573,4 @@ declare class MiniMaxAgent extends AbstractAgent {
1420
1573
  doAskText(messages: AIMessage[]): Promise<[string, string, TokenUsage$1?, string?]>;
1421
1574
  }
1422
1575
 
1423
- 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 };