@rudderjs/ai 1.5.0 → 1.6.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 +399 -0
- package/boost/guidelines.md +60 -0
- package/dist/agent.d.ts +35 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +118 -16
- package/dist/agent.js.map +1 -1
- package/dist/budget/pricing.d.ts +124 -0
- package/dist/budget/pricing.d.ts.map +1 -0
- package/dist/budget/pricing.js +175 -0
- package/dist/budget/pricing.js.map +1 -0
- package/dist/budget/storage.d.ts +104 -0
- package/dist/budget/storage.d.ts.map +1 -0
- package/dist/budget/storage.js +0 -0
- package/dist/budget/storage.js.map +1 -0
- package/dist/budget/with-budget.d.ts +119 -0
- package/dist/budget/with-budget.d.ts.map +1 -0
- package/dist/budget/with-budget.js +175 -0
- package/dist/budget/with-budget.js.map +1 -0
- package/dist/budget-orm/index.d.ts +96 -0
- package/dist/budget-orm/index.d.ts.map +1 -0
- package/dist/budget-orm/index.js +177 -0
- package/dist/budget-orm/index.js.map +1 -0
- package/dist/commands/ai-eval.d.ts +93 -0
- package/dist/commands/ai-eval.d.ts.map +1 -0
- package/dist/commands/ai-eval.js +378 -0
- package/dist/commands/ai-eval.js.map +1 -0
- package/dist/computer-use/actions.d.ts +214 -0
- package/dist/computer-use/actions.d.ts.map +1 -0
- package/dist/computer-use/actions.js +48 -0
- package/dist/computer-use/actions.js.map +1 -0
- package/dist/computer-use/errors.d.ts +57 -0
- package/dist/computer-use/errors.d.ts.map +1 -0
- package/dist/computer-use/errors.js +76 -0
- package/dist/computer-use/errors.js.map +1 -0
- package/dist/computer-use/index.d.ts +53 -0
- package/dist/computer-use/index.d.ts.map +1 -0
- package/dist/computer-use/index.js +51 -0
- package/dist/computer-use/index.js.map +1 -0
- package/dist/computer-use/playwright.d.ts +76 -0
- package/dist/computer-use/playwright.d.ts.map +1 -0
- package/dist/computer-use/playwright.js +270 -0
- package/dist/computer-use/playwright.js.map +1 -0
- package/dist/computer-use/tool.d.ts +154 -0
- package/dist/computer-use/tool.d.ts.map +1 -0
- package/dist/computer-use/tool.js +210 -0
- package/dist/computer-use/tool.js.map +1 -0
- package/dist/eval/fixtures.d.ts +65 -0
- package/dist/eval/fixtures.d.ts.map +1 -0
- package/dist/eval/fixtures.js +110 -0
- package/dist/eval/fixtures.js.map +1 -0
- package/dist/eval/html-reporter.d.ts +25 -0
- package/dist/eval/html-reporter.d.ts.map +1 -0
- package/dist/eval/html-reporter.js +209 -0
- package/dist/eval/html-reporter.js.map +1 -0
- package/dist/eval/index.d.ts +271 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +510 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/json-reporter.d.ts +43 -0
- package/dist/eval/json-reporter.d.ts.map +1 -0
- package/dist/eval/json-reporter.js +40 -0
- package/dist/eval/json-reporter.js.map +1 -0
- package/dist/fake.d.ts +36 -1
- package/dist/fake.d.ts.map +1 -1
- package/dist/fake.js +49 -2
- package/dist/fake.js.map +1 -1
- package/dist/file-search.d.ts +168 -0
- package/dist/file-search.d.ts.map +1 -0
- package/dist/file-search.js +158 -0
- package/dist/file-search.js.map +1 -0
- package/dist/index.d.ts +22 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/client-tools.d.ts +39 -0
- package/dist/mcp/client-tools.d.ts.map +1 -0
- package/dist/mcp/client-tools.js +147 -0
- package/dist/mcp/client-tools.js.map +1 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server-from-agent.d.ts +24 -0
- package/dist/mcp/server-from-agent.d.ts.map +1 -0
- package/dist/mcp/server-from-agent.js +113 -0
- package/dist/mcp/server-from-agent.js.map +1 -0
- package/dist/mcp/types.d.ts +64 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +6 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory-embedding/index.d.ts +121 -0
- package/dist/memory-embedding/index.d.ts.map +1 -0
- package/dist/memory-embedding/index.js +229 -0
- package/dist/memory-embedding/index.js.map +1 -0
- package/dist/memory-extract.d.ts +60 -0
- package/dist/memory-extract.d.ts.map +1 -0
- package/dist/memory-extract.js +163 -0
- package/dist/memory-extract.js.map +1 -0
- package/dist/memory-inject.d.ts +39 -0
- package/dist/memory-inject.d.ts.map +1 -0
- package/dist/memory-inject.js +135 -0
- package/dist/memory-inject.js.map +1 -0
- package/dist/memory-orm/index.d.ts +118 -0
- package/dist/memory-orm/index.d.ts.map +1 -0
- package/dist/memory-orm/index.js +187 -0
- package/dist/memory-orm/index.js.map +1 -0
- package/dist/memory.d.ts +55 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +132 -0
- package/dist/memory.js.map +1 -0
- package/dist/observers.d.ts +22 -0
- package/dist/observers.d.ts.map +1 -1
- package/dist/observers.js.map +1 -1
- package/dist/provider-tools.d.ts +15 -1
- package/dist/provider-tools.d.ts.map +1 -1
- package/dist/provider-tools.js +21 -1
- package/dist/provider-tools.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +61 -6
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/elevenlabs.d.ts +98 -0
- package/dist/providers/elevenlabs.d.ts.map +1 -0
- package/dist/providers/elevenlabs.js +229 -0
- package/dist/providers/elevenlabs.js.map +1 -0
- package/dist/providers/google.d.ts +83 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +491 -8
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/openai.d.ts +3 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +209 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/voyage.d.ts +91 -0
- package/dist/providers/voyage.d.ts.map +1 -0
- package/dist/providers/voyage.js +166 -0
- package/dist/providers/voyage.js.map +1 -0
- package/dist/queue-job.d.ts +69 -4
- package/dist/queue-job.d.ts.map +1 -1
- package/dist/queue-job.js +114 -11
- package/dist/queue-job.js.map +1 -1
- package/dist/registry.d.ts +3 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +10 -0
- package/dist/registry.js.map +1 -1
- package/dist/server/provider.d.ts.map +1 -1
- package/dist/server/provider.js +23 -1
- package/dist/server/provider.js.map +1 -1
- package/dist/similarity-search.d.ts +163 -0
- package/dist/similarity-search.d.ts.map +1 -0
- package/dist/similarity-search.js +147 -0
- package/dist/similarity-search.js.map +1 -0
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +13 -4
- package/dist/tool.js.map +1 -1
- package/dist/types.d.ts +246 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-stores/index.d.ts +96 -0
- package/dist/vector-stores/index.d.ts.map +1 -0
- package/dist/vector-stores/index.js +153 -0
- package/dist/vector-stores/index.js.map +1 -0
- package/package.json +41 -3
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { BudgetExceededError, ModelPricing, assertKnownModelPricing, } from './pricing.js';
|
|
2
|
+
const BudgetState = Symbol.for('rudderjs.ai.budget.state');
|
|
3
|
+
/**
|
|
4
|
+
* Per-user spend cap middleware (#A6 Phase 3).
|
|
5
|
+
*
|
|
6
|
+
* Pre-debits an input-cost estimate before each provider call (refusing
|
|
7
|
+
* with {@link BudgetExceededError} if the user would exceed any
|
|
8
|
+
* configured cap), then debits the actual cost difference once the
|
|
9
|
+
* `usage` chunk arrives.
|
|
10
|
+
*
|
|
11
|
+
* The pre-debit reserves budget so two concurrent requests can't both
|
|
12
|
+
* pass the check before either is billed — the `BudgetStorage` contract
|
|
13
|
+
* (#A6 Phase 2) requires `checkAndDebit` to be atomic.
|
|
14
|
+
*
|
|
15
|
+
* # Example
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { withBudget, memoryBudgetStorage, ModelPricing } from '@rudderjs/ai'
|
|
19
|
+
*
|
|
20
|
+
* const budgeted = withBudget({
|
|
21
|
+
* user: (ctx) => ctx.context as string, // your app's user-id source
|
|
22
|
+
* budget: () => ({ daily: 0.50, monthly: 10 }), // USD
|
|
23
|
+
* storage: memoryBudgetStorage(),
|
|
24
|
+
* pricing: ModelPricing,
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* class MyAgent extends Agent {
|
|
28
|
+
* middleware() { return [budgeted] }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* # Caveats
|
|
33
|
+
*
|
|
34
|
+
* - **Refunds on errors are not issued.** If the provider call fails
|
|
35
|
+
* after the pre-debit, the estimate stays debited. This avoids the
|
|
36
|
+
* complexity of distinguishing partial-credit cases (the model may
|
|
37
|
+
* have produced output before erroring). Apps that need refund-on-error
|
|
38
|
+
* should subscribe via `onError` and call `storage` directly.
|
|
39
|
+
* - **Cache token deltas not counted.** `TokenUsage` does not yet expose
|
|
40
|
+
* `cacheReadInputTokens` / `cacheWriteInputTokens`; cached requests are
|
|
41
|
+
* billed at the full `inputPer1k` rate today. Refining this is a phase
|
|
42
|
+
* 3.x follow-up that needs a `TokenUsage` widening.
|
|
43
|
+
* - **Tokenizer differences.** The default token estimator is
|
|
44
|
+
* `text.length / 4`. Provider-reported `usage.promptTokens` may differ
|
|
45
|
+
* by a few percent. Pass `estimateTokens: …` for a tiktoken-accurate
|
|
46
|
+
* pre-debit if your caps are tight.
|
|
47
|
+
*/
|
|
48
|
+
export function withBudget(opts) {
|
|
49
|
+
const pricing = opts.pricing ?? ModelPricing;
|
|
50
|
+
const tz = opts.timezone;
|
|
51
|
+
const estimateTokens = opts.estimateTokens ?? defaultEstimateTokens;
|
|
52
|
+
const onExceeded = opts.onExceeded ?? defaultOnExceeded;
|
|
53
|
+
return {
|
|
54
|
+
name: 'budget',
|
|
55
|
+
async onIteration(ctx) {
|
|
56
|
+
// Pre-debit fires before each model call (every step), including
|
|
57
|
+
// step 1. `onIteration` runs after `onStart` but before
|
|
58
|
+
// `prepareStep`/`onConfig('beforeModel')` — so transforms applied by
|
|
59
|
+
// those later hooks (e.g. a `prepareStep` model swap) aren't
|
|
60
|
+
// reflected in this estimate. For v1 that's acceptable; tighten
|
|
61
|
+
// later if it bites.
|
|
62
|
+
const userId = await opts.user(ctx);
|
|
63
|
+
if (userId == null)
|
|
64
|
+
return; // bypass — unauthenticated path or admin
|
|
65
|
+
const modelKey = ctx.model;
|
|
66
|
+
// Fail loud if the model isn't priced — silently zero-costing through
|
|
67
|
+
// a typo'd model is the worst-of-both for budget enforcement.
|
|
68
|
+
const rate = assertKnownModelPricing(modelKey, pricing);
|
|
69
|
+
const caps = await opts.budget({ userId, ctx });
|
|
70
|
+
const definedCaps = [];
|
|
71
|
+
if (caps.daily != null)
|
|
72
|
+
definedCaps.push({ period: 'daily', cap: caps.daily });
|
|
73
|
+
if (caps.monthly != null)
|
|
74
|
+
definedCaps.push({ period: 'monthly', cap: caps.monthly });
|
|
75
|
+
if (definedCaps.length === 0)
|
|
76
|
+
return; // nothing to enforce
|
|
77
|
+
// Estimate input cost for THIS step from the live messages array.
|
|
78
|
+
const estimate = estimateInputCostUsd(ctx.messages, [], rate, estimateTokens);
|
|
79
|
+
// Pre-debit each defined period; throw on first denial.
|
|
80
|
+
for (const { period, cap } of definedCaps) {
|
|
81
|
+
const r = await opts.storage.checkAndDebit({
|
|
82
|
+
userId,
|
|
83
|
+
period,
|
|
84
|
+
cap,
|
|
85
|
+
costUsd: estimate,
|
|
86
|
+
...(tz != null ? { timezone: tz } : {}),
|
|
87
|
+
});
|
|
88
|
+
if (!r.allowed) {
|
|
89
|
+
await onExceeded({ userId, period, spent: r.spent, cap: r.cap, ctx });
|
|
90
|
+
// If onExceeded didn't throw, fall back to the default error so
|
|
91
|
+
// the run is always aborted on a denied debit.
|
|
92
|
+
throw new BudgetExceededError({ userId, period, spent: r.spent, cap: r.cap });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Stash for onUsage true-up. If estimate was high vs actual we just
|
|
96
|
+
// accept the small over-charge; if low, onUsage debits the delta.
|
|
97
|
+
;
|
|
98
|
+
ctx[BudgetState] = {
|
|
99
|
+
userId,
|
|
100
|
+
caps: definedCaps,
|
|
101
|
+
pendingEstimate: estimate,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
async onUsage(ctx, usage) {
|
|
105
|
+
const state = ctx[BudgetState];
|
|
106
|
+
if (!state)
|
|
107
|
+
return; // no user, was bypassed
|
|
108
|
+
const modelKey = ctx.model;
|
|
109
|
+
const rate = pricing[modelKey];
|
|
110
|
+
// If pricing was found at onConfig time it's still found here; this is
|
|
111
|
+
// belt-and-suspenders for the rare case where the agent loop swapped
|
|
112
|
+
// models mid-run (failover). Skip silently — the pre-debit covered
|
|
113
|
+
// estimate, so we under-charge actual on a missing-rate model.
|
|
114
|
+
if (!rate) {
|
|
115
|
+
state.pendingEstimate = 0;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const actualCost = (usage.promptTokens * rate.inputPer1k +
|
|
119
|
+
usage.completionTokens * rate.outputPer1k) / 1000;
|
|
120
|
+
const delta = actualCost - state.pendingEstimate;
|
|
121
|
+
state.pendingEstimate = 0; // clear so a tool round-trip's next onConfig starts fresh
|
|
122
|
+
if (delta <= 0)
|
|
123
|
+
return; // overestimated; accept the small over-charge
|
|
124
|
+
// Always-apply true-up — the response already streamed, we can't
|
|
125
|
+
// unspend. Pass MAX_SAFE_INTEGER as cap so the storage just records
|
|
126
|
+
// the delta. Pre-debit enforcement already happened at onConfig.
|
|
127
|
+
for (const { period } of state.caps) {
|
|
128
|
+
await opts.storage.checkAndDebit({
|
|
129
|
+
userId: state.userId,
|
|
130
|
+
period,
|
|
131
|
+
cap: Number.MAX_SAFE_INTEGER,
|
|
132
|
+
costUsd: delta,
|
|
133
|
+
...(tz != null ? { timezone: tz } : {}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ─── Helpers ──────────────────────────────────────────────
|
|
140
|
+
function defaultEstimateTokens(text) {
|
|
141
|
+
return Math.ceil(text.length / 4);
|
|
142
|
+
}
|
|
143
|
+
function defaultOnExceeded(args) {
|
|
144
|
+
throw new BudgetExceededError({
|
|
145
|
+
userId: args.userId,
|
|
146
|
+
period: args.period,
|
|
147
|
+
spent: args.spent,
|
|
148
|
+
cap: args.cap,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function estimateInputCostUsd(messages, systemPrompts, rate, estimateTokens) {
|
|
152
|
+
// Concatenating all input into one string for the estimator means a
|
|
153
|
+
// single tokenizer pass for tiktoken-backed estimators (cheaper than
|
|
154
|
+
// tokenizing each message separately and summing).
|
|
155
|
+
const parts = [...systemPrompts];
|
|
156
|
+
for (const m of messages)
|
|
157
|
+
parts.push(messageText(m));
|
|
158
|
+
const tokens = estimateTokens(parts.join('\n'));
|
|
159
|
+
return (tokens * rate.inputPer1k) / 1000;
|
|
160
|
+
}
|
|
161
|
+
function messageText(m) {
|
|
162
|
+
if (typeof m.content === 'string')
|
|
163
|
+
return m.content;
|
|
164
|
+
if (Array.isArray(m.content)) {
|
|
165
|
+
let s = '';
|
|
166
|
+
for (const part of m.content)
|
|
167
|
+
s += contentPartText(part);
|
|
168
|
+
return s;
|
|
169
|
+
}
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
function contentPartText(p) {
|
|
173
|
+
return p.type === 'text' ? p.text : '';
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=with-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-budget.js","sourceRoot":"","sources":["../../src/budget/with-budget.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/budget-orm` — ORM-backed {@link BudgetStorage} for #A6 Phase 4.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `memoryBudgetStorage()` (which is
|
|
5
|
+
* single-process only). Persists per-user spend counters in a
|
|
6
|
+
* `BudgetUsage` table via the registered `@rudderjs/orm` adapter — works
|
|
7
|
+
* across queue workers, web processes, and horizontally-scaled deployments.
|
|
8
|
+
*
|
|
9
|
+
* Wire it into your AI middleware:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { withBudget } from '@rudderjs/ai'
|
|
13
|
+
* import { ormBudgetStorage } from '@rudderjs/ai/budget-orm'
|
|
14
|
+
*
|
|
15
|
+
* const budgeted = withBudget({
|
|
16
|
+
* user: (ctx) => ctx.context as string,
|
|
17
|
+
* budget: () => ({ daily: 0.50, monthly: 10 }),
|
|
18
|
+
* storage: ormBudgetStorage(),
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The schema lives at {@link budgetUsagePrismaSchema} — copy it into your
|
|
23
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
24
|
+
* multi-file setup). The `@@unique([userId, period, periodKey])`
|
|
25
|
+
* constraint is the one load-bearing index — without it, the
|
|
26
|
+
* find-or-create path can race and produce duplicate rows.
|
|
27
|
+
*
|
|
28
|
+
* # Atomicity caveat
|
|
29
|
+
*
|
|
30
|
+
* `checkAndDebit` does a read-then-conditional-increment. The increment
|
|
31
|
+
* itself is atomic (`UPDATE col = col + n`), but the cap check sits
|
|
32
|
+
* between the read and the write. Under high concurrency for a single
|
|
33
|
+
* user (more than ~1 in-flight budgeted request at a time), total spend
|
|
34
|
+
* can briefly exceed `cap` by up to `costUsd × concurrency`. For typical
|
|
35
|
+
* apps this is a non-issue.
|
|
36
|
+
*
|
|
37
|
+
* Strict guarantees require a database transaction with serializable
|
|
38
|
+
* isolation or a Redis-backed counter — both planned as follow-ups. File
|
|
39
|
+
* an issue if you hit this in production.
|
|
40
|
+
*/
|
|
41
|
+
import { Model } from '@rudderjs/orm';
|
|
42
|
+
import { type BudgetCheckOptions, type BudgetCheckResult, type BudgetPeriod, type BudgetStorage } from '../budget/storage.js';
|
|
43
|
+
/**
|
|
44
|
+
* Model row backing {@link OrmBudgetStorage}. Exposed so apps that
|
|
45
|
+
* want admin views (e.g. "show me top spenders this month") can use
|
|
46
|
+
* `BudgetUsageRecord.where(...).get()` instead of routing every read
|
|
47
|
+
* through the {@link BudgetStorage} interface.
|
|
48
|
+
*
|
|
49
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
50
|
+
* without it, two concurrent first-writes for the same user/period
|
|
51
|
+
* create duplicate rows and the cap accounting silently drifts.
|
|
52
|
+
*/
|
|
53
|
+
export declare class BudgetUsageRecord extends Model {
|
|
54
|
+
static table: string;
|
|
55
|
+
static fillable: string[];
|
|
56
|
+
id: string;
|
|
57
|
+
userId: string;
|
|
58
|
+
/** `'daily'` or `'monthly'`. */
|
|
59
|
+
period: string;
|
|
60
|
+
/** TZ-aware bucket key — `YYYY-MM-DD` (daily) or `YYYY-MM` (monthly). */
|
|
61
|
+
periodKey: string;
|
|
62
|
+
/** Cumulative USD spend in this period. */
|
|
63
|
+
spent: number;
|
|
64
|
+
createdAt: Date;
|
|
65
|
+
updatedAt: Date | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Production `BudgetStorage` backed by the registered `@rudderjs/orm`
|
|
69
|
+
* adapter. See the module JSDoc for setup + the atomicity caveat.
|
|
70
|
+
*/
|
|
71
|
+
export declare class OrmBudgetStorage implements BudgetStorage {
|
|
72
|
+
checkAndDebit(opts: BudgetCheckOptions): Promise<BudgetCheckResult>;
|
|
73
|
+
/** Apply the read-then-conditional-increment path on an existing row. */
|
|
74
|
+
private _applyIncrementPath;
|
|
75
|
+
reset(userId: string, period: BudgetPeriod, now?: Date, timezone?: string): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Convenience factory — returns a fresh {@link OrmBudgetStorage}
|
|
79
|
+
* instance. Prefer this over `new OrmBudgetStorage()` for symmetry with
|
|
80
|
+
* `memoryBudgetStorage()`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function ormBudgetStorage(): BudgetStorage;
|
|
83
|
+
/**
|
|
84
|
+
* Reference Prisma schema for `OrmBudgetStorage`. Copy into your
|
|
85
|
+
* `prisma/schema/<file>.prisma` (or paste alongside an existing model).
|
|
86
|
+
*
|
|
87
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
88
|
+
* without it the find-or-create path can race and produce duplicate
|
|
89
|
+
* rows, breaking cap accounting.
|
|
90
|
+
*
|
|
91
|
+
* SQLite stores `Float` as `REAL`; Postgres / MySQL as `DOUBLE
|
|
92
|
+
* PRECISION` / `DOUBLE`. All three give 15+ significant digits — more
|
|
93
|
+
* than enough for sub-cent budget tracking.
|
|
94
|
+
*/
|
|
95
|
+
export declare const budgetUsagePrismaSchema = "model BudgetUsage {\n id String @id @default(cuid())\n userId String\n /// 'daily' | 'monthly'\n period String\n /// YYYY-MM-DD (daily) or YYYY-MM (monthly), in the configured timezone\n periodKey String\n /// Cumulative USD spend in this period\n spent Float @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([userId, period, periodKey])\n @@index([userId])\n}\n";
|
|
96
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/budget-orm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AACrC,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,sBAAsB,CAAA;AAI7B;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,OAAgB,KAAK,SAAmB;IACxC,OAAgB,QAAQ,WAA6C;IAE7D,EAAE,EAAS,MAAM,CAAA;IACjB,MAAM,EAAK,MAAM,CAAA;IACzB,gCAAgC;IACxB,MAAM,EAAK,MAAM,CAAA;IACzB,yEAAyE;IACjE,SAAS,EAAE,MAAM,CAAA;IACzB,2CAA2C;IACnC,KAAK,EAAM,MAAM,CAAA;IACjB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAA;CAC/B;AAID;;;GAGG;AACH,qBAAa,gBAAiB,YAAW,aAAa;IAC9C,aAAa,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuDzE,yEAAyE;YAC3D,mBAAmB;IAsB3B,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAQhG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,8bAenC,CAAA"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/budget-orm` — ORM-backed {@link BudgetStorage} for #A6 Phase 4.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `memoryBudgetStorage()` (which is
|
|
5
|
+
* single-process only). Persists per-user spend counters in a
|
|
6
|
+
* `BudgetUsage` table via the registered `@rudderjs/orm` adapter — works
|
|
7
|
+
* across queue workers, web processes, and horizontally-scaled deployments.
|
|
8
|
+
*
|
|
9
|
+
* Wire it into your AI middleware:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { withBudget } from '@rudderjs/ai'
|
|
13
|
+
* import { ormBudgetStorage } from '@rudderjs/ai/budget-orm'
|
|
14
|
+
*
|
|
15
|
+
* const budgeted = withBudget({
|
|
16
|
+
* user: (ctx) => ctx.context as string,
|
|
17
|
+
* budget: () => ({ daily: 0.50, monthly: 10 }),
|
|
18
|
+
* storage: ormBudgetStorage(),
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The schema lives at {@link budgetUsagePrismaSchema} — copy it into your
|
|
23
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
24
|
+
* multi-file setup). The `@@unique([userId, period, periodKey])`
|
|
25
|
+
* constraint is the one load-bearing index — without it, the
|
|
26
|
+
* find-or-create path can race and produce duplicate rows.
|
|
27
|
+
*
|
|
28
|
+
* # Atomicity caveat
|
|
29
|
+
*
|
|
30
|
+
* `checkAndDebit` does a read-then-conditional-increment. The increment
|
|
31
|
+
* itself is atomic (`UPDATE col = col + n`), but the cap check sits
|
|
32
|
+
* between the read and the write. Under high concurrency for a single
|
|
33
|
+
* user (more than ~1 in-flight budgeted request at a time), total spend
|
|
34
|
+
* can briefly exceed `cap` by up to `costUsd × concurrency`. For typical
|
|
35
|
+
* apps this is a non-issue.
|
|
36
|
+
*
|
|
37
|
+
* Strict guarantees require a database transaction with serializable
|
|
38
|
+
* isolation or a Redis-backed counter — both planned as follow-ups. File
|
|
39
|
+
* an issue if you hit this in production.
|
|
40
|
+
*/
|
|
41
|
+
import { Model } from '@rudderjs/orm';
|
|
42
|
+
import { periodKey as buildPeriodKey, } from '../budget/storage.js';
|
|
43
|
+
// ─── ORM Model ────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Model row backing {@link OrmBudgetStorage}. Exposed so apps that
|
|
46
|
+
* want admin views (e.g. "show me top spenders this month") can use
|
|
47
|
+
* `BudgetUsageRecord.where(...).get()` instead of routing every read
|
|
48
|
+
* through the {@link BudgetStorage} interface.
|
|
49
|
+
*
|
|
50
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
51
|
+
* without it, two concurrent first-writes for the same user/period
|
|
52
|
+
* create duplicate rows and the cap accounting silently drifts.
|
|
53
|
+
*/
|
|
54
|
+
export class BudgetUsageRecord extends Model {
|
|
55
|
+
static table = 'budgetUsage';
|
|
56
|
+
static fillable = ['userId', 'period', 'periodKey', 'spent'];
|
|
57
|
+
}
|
|
58
|
+
// ─── BudgetStorage adapter ────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Production `BudgetStorage` backed by the registered `@rudderjs/orm`
|
|
61
|
+
* adapter. See the module JSDoc for setup + the atomicity caveat.
|
|
62
|
+
*/
|
|
63
|
+
export class OrmBudgetStorage {
|
|
64
|
+
async checkAndDebit(opts) {
|
|
65
|
+
if (!Number.isFinite(opts.cap) || opts.cap < 0) {
|
|
66
|
+
throw new Error(`[RudderJS AI] BudgetStorage: cap must be a non-negative finite number, got ${opts.cap}`);
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isFinite(opts.costUsd) || opts.costUsd < 0) {
|
|
69
|
+
throw new Error(`[RudderJS AI] BudgetStorage: costUsd must be a non-negative finite number, got ${opts.costUsd}`);
|
|
70
|
+
}
|
|
71
|
+
const now = opts.now ?? new Date();
|
|
72
|
+
const key = buildPeriodKey(opts.period, now, opts.timezone);
|
|
73
|
+
const existing = await BudgetUsageRecord
|
|
74
|
+
.where('userId', opts.userId)
|
|
75
|
+
.where('period', opts.period)
|
|
76
|
+
.where('periodKey', key)
|
|
77
|
+
.first();
|
|
78
|
+
// ─── No row yet — first write for this period ─────────
|
|
79
|
+
if (!existing) {
|
|
80
|
+
// Pure-read on an empty bucket — still empty after.
|
|
81
|
+
if (opts.costUsd === 0) {
|
|
82
|
+
return { allowed: true, spent: 0, cap: opts.cap };
|
|
83
|
+
}
|
|
84
|
+
// Single debit larger than cap — refuse before creating the row,
|
|
85
|
+
// so we don't pollute storage with denied requests.
|
|
86
|
+
if (opts.costUsd > opts.cap) {
|
|
87
|
+
return { allowed: false, spent: 0, cap: opts.cap };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await BudgetUsageRecord.create({
|
|
91
|
+
userId: opts.userId,
|
|
92
|
+
period: opts.period,
|
|
93
|
+
periodKey: key,
|
|
94
|
+
spent: opts.costUsd,
|
|
95
|
+
});
|
|
96
|
+
return { allowed: true, spent: opts.costUsd, cap: opts.cap };
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
// Race: another caller created the row between our `first()` and
|
|
100
|
+
// `create()`. Re-read and fall through to the increment path.
|
|
101
|
+
// We deliberately don't sniff the error type — any create failure
|
|
102
|
+
// means the row may now exist; let the re-read decide.
|
|
103
|
+
const refetched = await BudgetUsageRecord
|
|
104
|
+
.where('userId', opts.userId)
|
|
105
|
+
.where('period', opts.period)
|
|
106
|
+
.where('periodKey', key)
|
|
107
|
+
.first();
|
|
108
|
+
if (!refetched)
|
|
109
|
+
throw e; // not a unique-constraint race; surface the original error
|
|
110
|
+
return this._applyIncrementPath(refetched, opts);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return this._applyIncrementPath(existing, opts);
|
|
114
|
+
}
|
|
115
|
+
/** Apply the read-then-conditional-increment path on an existing row. */
|
|
116
|
+
async _applyIncrementPath(row, opts) {
|
|
117
|
+
const current = Number(row.spent ?? 0);
|
|
118
|
+
// Pure read.
|
|
119
|
+
if (opts.costUsd === 0) {
|
|
120
|
+
return { allowed: true, spent: current, cap: opts.cap };
|
|
121
|
+
}
|
|
122
|
+
// Cap check — read-then-decide. Atomic under single-writer; under
|
|
123
|
+
// concurrent writers, see the module-level atomicity caveat.
|
|
124
|
+
if (current + opts.costUsd > opts.cap) {
|
|
125
|
+
return { allowed: false, spent: current, cap: opts.cap };
|
|
126
|
+
}
|
|
127
|
+
const updated = await BudgetUsageRecord.increment(row.id, 'spent', opts.costUsd);
|
|
128
|
+
const newSpent = Number(updated?.spent ?? current + opts.costUsd);
|
|
129
|
+
return { allowed: true, spent: newSpent, cap: opts.cap };
|
|
130
|
+
}
|
|
131
|
+
async reset(userId, period, now, timezone) {
|
|
132
|
+
const key = buildPeriodKey(period, now ?? new Date(), timezone);
|
|
133
|
+
await BudgetUsageRecord
|
|
134
|
+
.where('userId', userId)
|
|
135
|
+
.where('period', period)
|
|
136
|
+
.where('periodKey', key)
|
|
137
|
+
.deleteAll();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Convenience factory — returns a fresh {@link OrmBudgetStorage}
|
|
142
|
+
* instance. Prefer this over `new OrmBudgetStorage()` for symmetry with
|
|
143
|
+
* `memoryBudgetStorage()`.
|
|
144
|
+
*/
|
|
145
|
+
export function ormBudgetStorage() {
|
|
146
|
+
return new OrmBudgetStorage();
|
|
147
|
+
}
|
|
148
|
+
// ─── Schema reference ─────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Reference Prisma schema for `OrmBudgetStorage`. Copy into your
|
|
151
|
+
* `prisma/schema/<file>.prisma` (or paste alongside an existing model).
|
|
152
|
+
*
|
|
153
|
+
* The `@@unique([userId, period, periodKey])` constraint is required —
|
|
154
|
+
* without it the find-or-create path can race and produce duplicate
|
|
155
|
+
* rows, breaking cap accounting.
|
|
156
|
+
*
|
|
157
|
+
* SQLite stores `Float` as `REAL`; Postgres / MySQL as `DOUBLE
|
|
158
|
+
* PRECISION` / `DOUBLE`. All three give 15+ significant digits — more
|
|
159
|
+
* than enough for sub-cent budget tracking.
|
|
160
|
+
*/
|
|
161
|
+
export const budgetUsagePrismaSchema = `model BudgetUsage {
|
|
162
|
+
id String @id @default(cuid())
|
|
163
|
+
userId String
|
|
164
|
+
/// 'daily' | 'monthly'
|
|
165
|
+
period String
|
|
166
|
+
/// YYYY-MM-DD (daily) or YYYY-MM (monthly), in the configured timezone
|
|
167
|
+
periodKey String
|
|
168
|
+
/// Cumulative USD spend in this period
|
|
169
|
+
spent Float @default(0)
|
|
170
|
+
createdAt DateTime @default(now())
|
|
171
|
+
updatedAt DateTime @updatedAt
|
|
172
|
+
|
|
173
|
+
@@unique([userId, period, periodKey])
|
|
174
|
+
@@index([userId])
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/budget-orm/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pnpm rudder ai:eval` — discover `evals/**\/*.eval.ts` suites,
|
|
3
|
+
* run each, and report. Console reporter by default; `--json` emits
|
|
4
|
+
* a machine-readable envelope to stdout for CI.
|
|
5
|
+
*
|
|
6
|
+
* Registered from the CLI loader (`packages/cli/src/index.ts`)
|
|
7
|
+
* — the AiProvider doesn't own this so it surfaces even when the
|
|
8
|
+
* user app fails to boot, matching the `command:list --json`
|
|
9
|
+
* graceful-degradation pattern from #349.
|
|
10
|
+
*/
|
|
11
|
+
import type { EvalSuite } from '../eval/index.js';
|
|
12
|
+
type Rudder = {
|
|
13
|
+
command(name: string, handler: (args: string[]) => void | Promise<void>): {
|
|
14
|
+
description(text: string): unknown;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** CLI flags + positional name filter. */
|
|
18
|
+
export interface AiEvalOptions {
|
|
19
|
+
/** Substring filter (case-insensitive) applied to suite names. */
|
|
20
|
+
filter?: string;
|
|
21
|
+
/** Stop on the first failing suite. */
|
|
22
|
+
bail: boolean;
|
|
23
|
+
/** Emit `{ suites: [...] }` JSON to stdout. */
|
|
24
|
+
json: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Run against the real provider, capture each case's assistant
|
|
27
|
+
* turns to `evals/__fixtures__/<suite>/<case>.json`. Existing
|
|
28
|
+
* fixtures are overwritten — diff in your VCS to see what changed.
|
|
29
|
+
* Default `false`.
|
|
30
|
+
*/
|
|
31
|
+
record?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Swap the runtime with `AiFake.fake()` and feed each case its
|
|
34
|
+
* recorded fixture via `respondWithSequence`. Zero API calls,
|
|
35
|
+
* deterministic regression tests. Cases without a fixture fall
|
|
36
|
+
* through to a normal run with a stderr warning. Default `false`.
|
|
37
|
+
*/
|
|
38
|
+
replay?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Path for a self-contained HTML report (#A5 Phase 5). Pasteable
|
|
41
|
+
* into PR comments / Slack threads. Coexists with `--json` (JSON
|
|
42
|
+
* still goes to stdout, HTML goes to disk).
|
|
43
|
+
*/
|
|
44
|
+
html?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Test seam — every external dependency gets an injectable
|
|
48
|
+
* override. The CLI handler defaults each to its real impl.
|
|
49
|
+
*/
|
|
50
|
+
export interface AiEvalDeps {
|
|
51
|
+
cwd?: string;
|
|
52
|
+
stdout?: {
|
|
53
|
+
write(s: string): boolean | void;
|
|
54
|
+
};
|
|
55
|
+
stderr?: {
|
|
56
|
+
write(s: string): boolean | void;
|
|
57
|
+
};
|
|
58
|
+
/** Override the file walk (test harness returns a virtual list). */
|
|
59
|
+
discover?: (cwd: string, pattern: string) => Promise<string[]>;
|
|
60
|
+
/** Override file → suite loader (test harness uses an in-memory map). */
|
|
61
|
+
loadSuite?: (absPath: string) => Promise<EvalSuite | null>;
|
|
62
|
+
/** Override config lookup (test harness skips `@rudderjs/core`). */
|
|
63
|
+
configPattern?: () => string | null | Promise<string | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Override fixtures directory (defaults to `<cwd>/evals/__fixtures__`).
|
|
66
|
+
* Tests point to a tmpdir to keep round-trips off the source tree.
|
|
67
|
+
*/
|
|
68
|
+
fixturesDir?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Register the `ai:eval` command on the rudder runner. */
|
|
71
|
+
export declare function registerAiEvalCommand(rudder: Rudder): void;
|
|
72
|
+
/**
|
|
73
|
+
* Parse the rest-of-line. Recognizes:
|
|
74
|
+
* - boolean flags: `--bail`, `--json`, `--record`, `--replay`
|
|
75
|
+
* - value flags : `--html <path>` or `--html=<path>`
|
|
76
|
+
* - one positional name filter (anything not consumed above)
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseArgs(args: string[]): AiEvalOptions;
|
|
79
|
+
/**
|
|
80
|
+
* Execute the CLI flow. Returns the process exit code (0 = all pass,
|
|
81
|
+
* 1 = at least one suite had a failure or no suites discovered).
|
|
82
|
+
*
|
|
83
|
+
* The handler is `process.exit`-free so tests can drive it directly.
|
|
84
|
+
*/
|
|
85
|
+
export declare function runEvalCli(opts: AiEvalOptions, deps?: AiEvalDeps): Promise<number>;
|
|
86
|
+
/**
|
|
87
|
+
* Recursive walk constrained to a `<dir>/**\/*<suffix>` shape.
|
|
88
|
+
* Returns absolute paths sorted lexicographically for stable test
|
|
89
|
+
* output and predictable `--bail` ordering.
|
|
90
|
+
*/
|
|
91
|
+
export declare function discoverSuiteFiles(cwd: string, pattern: string): Promise<string[]>;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=ai-eval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-eval.d.ts","sourceRoot":"","sources":["../../src/commands/ai-eval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAiC,MAAM,kBAAkB,CAAA;AAUhF,KAAK,MAAM,GAAG;IACZ,OAAO,CACL,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAChD;QAAE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAC1C,CAAA;AAED,0CAA0C;AAC1C,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uCAAuC;IACvC,IAAI,EAAK,OAAO,CAAA;IAChB,+CAA+C;IAC/C,IAAI,EAAK,OAAO,CAAA;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAS,MAAM,CAAA;IACnB,MAAM,CAAC,EAAM;QAAE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;IACjD,MAAM,CAAC,EAAM;QAAE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;IACjD,oEAAoE;IACpE,QAAQ,CAAC,EAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAChE,yEAAyE;IACzE,SAAS,CAAC,EAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IAC3D,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC5D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,2DAA2D;AAC3D,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAO1D;AAMD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CA8BvD;AAID;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAsE5F;AA4JD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAMxF"}
|