@theokit/sdk-budget 0.1.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.
@@ -0,0 +1,191 @@
1
+ import { BudgetTracker } from '@theokit/sdk';
2
+
3
+ /**
4
+ * UTC-aligned calendar window helpers (ADR D382).
5
+ *
6
+ * - `1h` — relative (now - 1 hour).
7
+ * - `1d` — UTC midnight (current UTC day).
8
+ * - `1w` — UTC monday 00:00:00 (current UTC week, Monday is week start).
9
+ * - `30d` — relative 30 days.
10
+ * - `365d` — relative 365 days.
11
+ *
12
+ * `1d` and `1w` are calendar-aligned because users expect "1 USD per day"
13
+ * = "since midnight UTC", not a rolling 24h.
14
+ * `30d`/`365d` are relative because nobody expects "since the 1st".
15
+ *
16
+ * @internal
17
+ */
18
+ declare function startOfDayUtc(now?: Date): Date;
19
+ declare function startOfWeekUtc(now?: Date): Date;
20
+ /** Returns the inclusive start timestamp (ms) for the given window relative to `now`. */
21
+ declare function windowStartMs(window: BudgetWindow, now?: Date): number;
22
+
23
+ /**
24
+ * Budget enforcement (ADRs D383, D386, EC-7/8/9).
25
+ *
26
+ * - `preflightCheck(name, estimatedUsd)` — em `block` mode, throw
27
+ * `BudgetExceededError` antes da LLM call se qualquer limit seria
28
+ * excedido (EC-9 — caller chama dentro do mutex section).
29
+ * - `chargeAndCheckThresholds(name, actualUsd)` — apply charge ao
30
+ * ledger + invoca onThreshold/onExceed callbacks isolated em
31
+ * try/catch (EC-8).
32
+ *
33
+ * @internal
34
+ */
35
+ /**
36
+ * Throws BudgetExceededError if `mode === "block"` and any limit
37
+ * would be exceeded. No-op for audit/warn modes (post-charge checks
38
+ * handle those).
39
+ *
40
+ * Caller invokes this BEFORE the LLM call.
41
+ */
42
+ declare function preflightCheck(name: string, estimatedUsd: number): void;
43
+ /**
44
+ * Charge the budget + dispatch threshold/exceed callbacks (EC-8 isolated).
45
+ *
46
+ * - In `audit` mode: charge only, no callbacks.
47
+ * - In `warn` mode: charge + onThreshold (80/95) + onExceed (100). No throw.
48
+ * - In `block` mode: charge + onThreshold + onExceed. No throw post-call
49
+ * (preflightCheck already prevented exceed for the upcoming call;
50
+ * this protects against simultaneous-call races where multiple sends
51
+ * each pass preflight independently — last one to charge may still
52
+ * tip a limit. We document the case rather than retroactively throw).
53
+ */
54
+ declare function chargeAndCheckThresholds(name: string, actualUsd: number): Promise<void>;
55
+
56
+ /**
57
+ * In-process Budget ledger (ADR D385).
58
+ *
59
+ * Singleton mutex-protected. Stores per-budget ChargeLog[] arrays;
60
+ * `spentIn(window)` filters by timestamp.
61
+ *
62
+ * EC-6: GC eviction runs DENTRO do mesmo mutex que charge — sem race.
63
+ * EC-9: charge() é called inside same critical section as preflight
64
+ * check by Budget enforcement.
65
+ *
66
+ * Persistence cross-restart: deferred to v0.2 (JsonFile pattern).
67
+ *
68
+ * @internal
69
+ */
70
+ /** Charge a budget. Idempotent across concurrent calls via withCwdMutex. */
71
+ declare function charge(name: string, amountUsd: number): Promise<void>;
72
+ /** Return total spend in the given window for `name`. Snapshot read (no mutex needed). */
73
+ declare function spentIn(name: string, window: BudgetWindow, now?: Date): number;
74
+
75
+ /**
76
+ * normalizeUsage — convert provider-shaped raw `usage` object to
77
+ * canonical `TokenUsage`. Ports Hermes Agent's `normalize_usage`
78
+ * (referencia/hermes-agent/agent/usage_pricing.py:672-742).
79
+ *
80
+ * Handles 3 API shapes:
81
+ * - Anthropic Messages: 4 explicit buckets (input/output/cache_read/cache_creation).
82
+ * - OpenAI Chat Completions: prompt_tokens INCLUDES cache; subtract cached_tokens.
83
+ * - OpenAI Responses (Codex): input_tokens INCLUDES cache; same subtraction.
84
+ *
85
+ * Edge cases:
86
+ * - cline#10266 — OpenAI-compat proxies (OpenRouter, Vercel AI Gateway,
87
+ * Cline) routing Claude expose Anthropic-style top-level fields
88
+ * (cache_read_input_tokens / cache_creation_input_tokens). Both
89
+ * locations are checked with top-level fallback.
90
+ * - Null/undefined fields → 0 via `int()` coerce.
91
+ * - String token counts → parsed via int.
92
+ * - Negative values → clamped to 0 (defensive against proxy bugs).
93
+ *
94
+ * @internal
95
+ */
96
+ type ApiMode = "anthropic_messages" | "openai_chat_completions" | "openai_responses";
97
+ declare function inferApiMode(provider: string): ApiMode;
98
+ declare function normalizeUsage(rawUsage: unknown, opts: {
99
+ provider: string;
100
+ apiMode?: ApiMode;
101
+ }): TokenUsage;
102
+
103
+ /**
104
+ * Internal Budget registry — keeps live `BudgetOptions` per name.
105
+ * Singleton; no persistence (D385).
106
+ *
107
+ * @internal
108
+ */
109
+ declare function createBudget(opts: BudgetOptions): BudgetHandle;
110
+ declare function getBudget(name: string): BudgetHandle | undefined;
111
+ declare function listBudgets(): readonly BudgetHandle[];
112
+ declare function deleteBudget(name: string): boolean;
113
+ declare function snapshotAll(): readonly BudgetSnapshot[];
114
+ declare function getBudgetOptionsRaw(name: string): BudgetOptions | undefined;
115
+ declare function defaultMode(opts: BudgetOptions): BudgetMode;
116
+
117
+ /**
118
+ * Built-in per-model USD pricing table (SDK 2.0 Phase 2 / sdk-budget T2.X).
119
+ *
120
+ * Prices are in USD per 1M tokens (input / output). Sources: each
121
+ * provider's public pricing page; values verified 2026-06 against
122
+ * the same tier used in sdk-core's `internal/budget/pricing-data.json`.
123
+ *
124
+ * The table is intentionally CONSERVATIVE — only the most-used models
125
+ * land here. Consumers needing exhaustive coverage can supply an
126
+ * override via `createUsdBudgetTracker({ pricing })`.
127
+ *
128
+ * @public
129
+ */
130
+ interface ModelPricing {
131
+ /** USD per 1,000,000 input tokens. */
132
+ readonly inputPerMillionUsd: number;
133
+ /** USD per 1,000,000 output tokens. */
134
+ readonly outputPerMillionUsd: number;
135
+ }
136
+ /** Built-in pricing table — exhaustive enough for v0.1; users supply overrides for niche models. */
137
+ declare const BUILTIN_PRICING: Readonly<Record<string, ModelPricing>>;
138
+ /**
139
+ * Compute USD cost for a single token event. Returns 0 (NOT throws)
140
+ * when the model is unknown — consumers should override the pricing
141
+ * table to add their model.
142
+ */
143
+ declare function computeUsdCost(pricing: Readonly<Record<string, ModelPricing>>, model: string, type: "input" | "output", tokens: number): number;
144
+
145
+ /**
146
+ * `createUsdBudgetTracker` — USD-cost-aware `BudgetTracker` impl shipped
147
+ * in `@theokit/sdk-budget` (SDK 2.0 Phase 2 / T2.X).
148
+ *
149
+ * Extends the counter pattern from `createCounterBudgetTracker`
150
+ * (sdk-core reference impl) with per-model USD cost computation via
151
+ * the `BUILTIN_PRICING` table.
152
+ *
153
+ * Use cases:
154
+ * - Cap total spend per agent run (`maxUsd`).
155
+ * - Cap total tokens (`maxTokens`) AND USD ceiling simultaneously.
156
+ * - Observe cumulative USD from outside (`getTotalUsd()`).
157
+ *
158
+ * Tracker.check() returns `allowed: false` when ANY of the configured
159
+ * caps is exceeded. `reason` is one of:
160
+ * - `"token_limit"` — `maxTokens` reached
161
+ * - `"cost_limit"` — `maxUsd` reached
162
+ *
163
+ * Layered design (mirrors counter impl):
164
+ * - `track()` is sync + non-throwing (clamps invalid values).
165
+ * - `check()` is sync (allowed: true unless a cap is exceeded).
166
+ * - `getTotal()` returns the SDK-required token + iteration totals
167
+ * ONLY (USD is exposed via the bonus `getTotalUsd()` method).
168
+ *
169
+ * @public
170
+ */
171
+
172
+ /** Options for `createUsdBudgetTracker`. */
173
+ interface UsdBudgetTrackerOptions {
174
+ /** Hard ceiling on total tokens (input + output combined). */
175
+ readonly maxTokens?: number;
176
+ /** Hard ceiling on cumulative USD spend. */
177
+ readonly maxUsd?: number;
178
+ /** Optional override map — model id → pricing entry. Merged onto BUILTIN_PRICING. */
179
+ readonly pricing?: Readonly<Record<string, ModelPricing>>;
180
+ }
181
+ /**
182
+ * Build a fresh USD-aware tracker. The returned object exposes the
183
+ * `BudgetTracker` contract PLUS `getTotalUsd()` + `nextIteration()`
184
+ * helpers for explicit iteration counting.
185
+ */
186
+ declare function createUsdBudgetTracker(options?: UsdBudgetTrackerOptions): BudgetTracker & {
187
+ nextIteration(): void;
188
+ getTotalUsd(): number;
189
+ };
190
+
191
+ export { BUILTIN_PRICING, type ModelPricing, type UsdBudgetTrackerOptions, charge, chargeAndCheckThresholds, computeUsdCost, createBudget, createUsdBudgetTracker, defaultMode, deleteBudget, getBudget, getBudgetOptionsRaw, inferApiMode, listBudgets, normalizeUsage, preflightCheck, snapshotAll, spentIn, startOfDayUtc, startOfWeekUtc, windowStartMs };
@@ -0,0 +1,191 @@
1
+ import { BudgetTracker } from '@theokit/sdk';
2
+
3
+ /**
4
+ * UTC-aligned calendar window helpers (ADR D382).
5
+ *
6
+ * - `1h` — relative (now - 1 hour).
7
+ * - `1d` — UTC midnight (current UTC day).
8
+ * - `1w` — UTC monday 00:00:00 (current UTC week, Monday is week start).
9
+ * - `30d` — relative 30 days.
10
+ * - `365d` — relative 365 days.
11
+ *
12
+ * `1d` and `1w` are calendar-aligned because users expect "1 USD per day"
13
+ * = "since midnight UTC", not a rolling 24h.
14
+ * `30d`/`365d` are relative because nobody expects "since the 1st".
15
+ *
16
+ * @internal
17
+ */
18
+ declare function startOfDayUtc(now?: Date): Date;
19
+ declare function startOfWeekUtc(now?: Date): Date;
20
+ /** Returns the inclusive start timestamp (ms) for the given window relative to `now`. */
21
+ declare function windowStartMs(window: BudgetWindow, now?: Date): number;
22
+
23
+ /**
24
+ * Budget enforcement (ADRs D383, D386, EC-7/8/9).
25
+ *
26
+ * - `preflightCheck(name, estimatedUsd)` — em `block` mode, throw
27
+ * `BudgetExceededError` antes da LLM call se qualquer limit seria
28
+ * excedido (EC-9 — caller chama dentro do mutex section).
29
+ * - `chargeAndCheckThresholds(name, actualUsd)` — apply charge ao
30
+ * ledger + invoca onThreshold/onExceed callbacks isolated em
31
+ * try/catch (EC-8).
32
+ *
33
+ * @internal
34
+ */
35
+ /**
36
+ * Throws BudgetExceededError if `mode === "block"` and any limit
37
+ * would be exceeded. No-op for audit/warn modes (post-charge checks
38
+ * handle those).
39
+ *
40
+ * Caller invokes this BEFORE the LLM call.
41
+ */
42
+ declare function preflightCheck(name: string, estimatedUsd: number): void;
43
+ /**
44
+ * Charge the budget + dispatch threshold/exceed callbacks (EC-8 isolated).
45
+ *
46
+ * - In `audit` mode: charge only, no callbacks.
47
+ * - In `warn` mode: charge + onThreshold (80/95) + onExceed (100). No throw.
48
+ * - In `block` mode: charge + onThreshold + onExceed. No throw post-call
49
+ * (preflightCheck already prevented exceed for the upcoming call;
50
+ * this protects against simultaneous-call races where multiple sends
51
+ * each pass preflight independently — last one to charge may still
52
+ * tip a limit. We document the case rather than retroactively throw).
53
+ */
54
+ declare function chargeAndCheckThresholds(name: string, actualUsd: number): Promise<void>;
55
+
56
+ /**
57
+ * In-process Budget ledger (ADR D385).
58
+ *
59
+ * Singleton mutex-protected. Stores per-budget ChargeLog[] arrays;
60
+ * `spentIn(window)` filters by timestamp.
61
+ *
62
+ * EC-6: GC eviction runs DENTRO do mesmo mutex que charge — sem race.
63
+ * EC-9: charge() é called inside same critical section as preflight
64
+ * check by Budget enforcement.
65
+ *
66
+ * Persistence cross-restart: deferred to v0.2 (JsonFile pattern).
67
+ *
68
+ * @internal
69
+ */
70
+ /** Charge a budget. Idempotent across concurrent calls via withCwdMutex. */
71
+ declare function charge(name: string, amountUsd: number): Promise<void>;
72
+ /** Return total spend in the given window for `name`. Snapshot read (no mutex needed). */
73
+ declare function spentIn(name: string, window: BudgetWindow, now?: Date): number;
74
+
75
+ /**
76
+ * normalizeUsage — convert provider-shaped raw `usage` object to
77
+ * canonical `TokenUsage`. Ports Hermes Agent's `normalize_usage`
78
+ * (referencia/hermes-agent/agent/usage_pricing.py:672-742).
79
+ *
80
+ * Handles 3 API shapes:
81
+ * - Anthropic Messages: 4 explicit buckets (input/output/cache_read/cache_creation).
82
+ * - OpenAI Chat Completions: prompt_tokens INCLUDES cache; subtract cached_tokens.
83
+ * - OpenAI Responses (Codex): input_tokens INCLUDES cache; same subtraction.
84
+ *
85
+ * Edge cases:
86
+ * - cline#10266 — OpenAI-compat proxies (OpenRouter, Vercel AI Gateway,
87
+ * Cline) routing Claude expose Anthropic-style top-level fields
88
+ * (cache_read_input_tokens / cache_creation_input_tokens). Both
89
+ * locations are checked with top-level fallback.
90
+ * - Null/undefined fields → 0 via `int()` coerce.
91
+ * - String token counts → parsed via int.
92
+ * - Negative values → clamped to 0 (defensive against proxy bugs).
93
+ *
94
+ * @internal
95
+ */
96
+ type ApiMode = "anthropic_messages" | "openai_chat_completions" | "openai_responses";
97
+ declare function inferApiMode(provider: string): ApiMode;
98
+ declare function normalizeUsage(rawUsage: unknown, opts: {
99
+ provider: string;
100
+ apiMode?: ApiMode;
101
+ }): TokenUsage;
102
+
103
+ /**
104
+ * Internal Budget registry — keeps live `BudgetOptions` per name.
105
+ * Singleton; no persistence (D385).
106
+ *
107
+ * @internal
108
+ */
109
+ declare function createBudget(opts: BudgetOptions): BudgetHandle;
110
+ declare function getBudget(name: string): BudgetHandle | undefined;
111
+ declare function listBudgets(): readonly BudgetHandle[];
112
+ declare function deleteBudget(name: string): boolean;
113
+ declare function snapshotAll(): readonly BudgetSnapshot[];
114
+ declare function getBudgetOptionsRaw(name: string): BudgetOptions | undefined;
115
+ declare function defaultMode(opts: BudgetOptions): BudgetMode;
116
+
117
+ /**
118
+ * Built-in per-model USD pricing table (SDK 2.0 Phase 2 / sdk-budget T2.X).
119
+ *
120
+ * Prices are in USD per 1M tokens (input / output). Sources: each
121
+ * provider's public pricing page; values verified 2026-06 against
122
+ * the same tier used in sdk-core's `internal/budget/pricing-data.json`.
123
+ *
124
+ * The table is intentionally CONSERVATIVE — only the most-used models
125
+ * land here. Consumers needing exhaustive coverage can supply an
126
+ * override via `createUsdBudgetTracker({ pricing })`.
127
+ *
128
+ * @public
129
+ */
130
+ interface ModelPricing {
131
+ /** USD per 1,000,000 input tokens. */
132
+ readonly inputPerMillionUsd: number;
133
+ /** USD per 1,000,000 output tokens. */
134
+ readonly outputPerMillionUsd: number;
135
+ }
136
+ /** Built-in pricing table — exhaustive enough for v0.1; users supply overrides for niche models. */
137
+ declare const BUILTIN_PRICING: Readonly<Record<string, ModelPricing>>;
138
+ /**
139
+ * Compute USD cost for a single token event. Returns 0 (NOT throws)
140
+ * when the model is unknown — consumers should override the pricing
141
+ * table to add their model.
142
+ */
143
+ declare function computeUsdCost(pricing: Readonly<Record<string, ModelPricing>>, model: string, type: "input" | "output", tokens: number): number;
144
+
145
+ /**
146
+ * `createUsdBudgetTracker` — USD-cost-aware `BudgetTracker` impl shipped
147
+ * in `@theokit/sdk-budget` (SDK 2.0 Phase 2 / T2.X).
148
+ *
149
+ * Extends the counter pattern from `createCounterBudgetTracker`
150
+ * (sdk-core reference impl) with per-model USD cost computation via
151
+ * the `BUILTIN_PRICING` table.
152
+ *
153
+ * Use cases:
154
+ * - Cap total spend per agent run (`maxUsd`).
155
+ * - Cap total tokens (`maxTokens`) AND USD ceiling simultaneously.
156
+ * - Observe cumulative USD from outside (`getTotalUsd()`).
157
+ *
158
+ * Tracker.check() returns `allowed: false` when ANY of the configured
159
+ * caps is exceeded. `reason` is one of:
160
+ * - `"token_limit"` — `maxTokens` reached
161
+ * - `"cost_limit"` — `maxUsd` reached
162
+ *
163
+ * Layered design (mirrors counter impl):
164
+ * - `track()` is sync + non-throwing (clamps invalid values).
165
+ * - `check()` is sync (allowed: true unless a cap is exceeded).
166
+ * - `getTotal()` returns the SDK-required token + iteration totals
167
+ * ONLY (USD is exposed via the bonus `getTotalUsd()` method).
168
+ *
169
+ * @public
170
+ */
171
+
172
+ /** Options for `createUsdBudgetTracker`. */
173
+ interface UsdBudgetTrackerOptions {
174
+ /** Hard ceiling on total tokens (input + output combined). */
175
+ readonly maxTokens?: number;
176
+ /** Hard ceiling on cumulative USD spend. */
177
+ readonly maxUsd?: number;
178
+ /** Optional override map — model id → pricing entry. Merged onto BUILTIN_PRICING. */
179
+ readonly pricing?: Readonly<Record<string, ModelPricing>>;
180
+ }
181
+ /**
182
+ * Build a fresh USD-aware tracker. The returned object exposes the
183
+ * `BudgetTracker` contract PLUS `getTotalUsd()` + `nextIteration()`
184
+ * helpers for explicit iteration counting.
185
+ */
186
+ declare function createUsdBudgetTracker(options?: UsdBudgetTrackerOptions): BudgetTracker & {
187
+ nextIteration(): void;
188
+ getTotalUsd(): number;
189
+ };
190
+
191
+ export { BUILTIN_PRICING, type ModelPricing, type UsdBudgetTrackerOptions, charge, chargeAndCheckThresholds, computeUsdCost, createBudget, createUsdBudgetTracker, defaultMode, deleteBudget, getBudget, getBudgetOptionsRaw, inferApiMode, listBudgets, normalizeUsage, preflightCheck, snapshotAll, spentIn, startOfDayUtc, startOfWeekUtc, windowStartMs };