@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 +22 -0
- package/dist/index.d.mts +141 -1
- package/dist/index.d.ts +141 -1
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +156 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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,
|