octaflow 0.14.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,466 @@
1
+ import { S as ok, b as noopLogger, f as defineStep, l as buildWorkflow, x as err } from "../core-ERYGlYsS.js";
2
+ import { wrapEmbeddingModel, wrapLanguageModel } from "ai";
3
+ //#region src/ai/instrumented-model.ts
4
+ function createUsageAccumulator() {
5
+ const total = {
6
+ inputTokens: 0,
7
+ outputTokens: 0,
8
+ cacheReadTokens: 0,
9
+ cacheWriteTokens: 0,
10
+ modelId: ""
11
+ };
12
+ let hasAny = false;
13
+ return {
14
+ record(usage) {
15
+ total.inputTokens += usage.inputTokens;
16
+ total.outputTokens += usage.outputTokens;
17
+ total.cacheReadTokens += usage.cacheReadTokens;
18
+ total.cacheWriteTokens += usage.cacheWriteTokens;
19
+ if (usage.modelId) total.modelId = usage.modelId;
20
+ hasAny = true;
21
+ },
22
+ get() {
23
+ return { ...total };
24
+ },
25
+ hasUsage() {
26
+ return hasAny;
27
+ }
28
+ };
29
+ }
30
+ /**
31
+ * Wrap a language model with usage-tracking middleware. The accumulator collects
32
+ * token usage from every doGenerate/doStream call routed through the wrapped model.
33
+ */
34
+ function createInstrumentedModel(model, accumulator) {
35
+ return wrapLanguageModel({
36
+ model,
37
+ middleware: {
38
+ specificationVersion: "v4",
39
+ wrapGenerate: async ({ doGenerate }) => {
40
+ const result = await doGenerate();
41
+ const usage = result.usage;
42
+ if (usage) accumulator.record({
43
+ inputTokens: usage.inputTokens?.total ?? 0,
44
+ outputTokens: usage.outputTokens?.total ?? 0,
45
+ cacheReadTokens: usage.inputTokens?.cacheRead ?? 0,
46
+ cacheWriteTokens: usage.inputTokens?.cacheWrite ?? 0,
47
+ modelId: model.modelId
48
+ });
49
+ return result;
50
+ },
51
+ wrapStream: async ({ doStream }) => {
52
+ const result = await doStream();
53
+ const wrappedStream = result.stream.pipeThrough(new TransformStream({ transform(chunk, controller) {
54
+ controller.enqueue(chunk);
55
+ if (chunk.type === "finish" && chunk.usage) {
56
+ const u = chunk.usage;
57
+ accumulator.record({
58
+ inputTokens: u.inputTokens?.total ?? 0,
59
+ outputTokens: u.outputTokens?.total ?? 0,
60
+ cacheReadTokens: u.inputTokens?.cacheRead ?? 0,
61
+ cacheWriteTokens: u.inputTokens?.cacheWrite ?? 0,
62
+ modelId: model.modelId
63
+ });
64
+ }
65
+ } }));
66
+ return {
67
+ ...result,
68
+ stream: wrappedStream
69
+ };
70
+ }
71
+ }
72
+ });
73
+ }
74
+ //#endregion
75
+ //#region src/ai/instrumented-embedding-model.ts
76
+ function createEmbeddingUsageAccumulator() {
77
+ let inputTokens = 0;
78
+ let callCount = 0;
79
+ let modelId = "";
80
+ return {
81
+ record(tokens, recordedModelId) {
82
+ inputTokens += tokens;
83
+ callCount += 1;
84
+ if (recordedModelId) modelId = recordedModelId;
85
+ },
86
+ get() {
87
+ return {
88
+ inputTokens,
89
+ callCount,
90
+ modelId
91
+ };
92
+ },
93
+ hasUsage() {
94
+ return callCount > 0;
95
+ },
96
+ reset() {
97
+ inputTokens = 0;
98
+ callCount = 0;
99
+ modelId = "";
100
+ }
101
+ };
102
+ }
103
+ /**
104
+ * Wrap an embedding model with usage-tracking middleware. The accumulator collects
105
+ * token usage from every doEmbed call routed through the wrapped model — both
106
+ * `embed()` (single value) and `embedMany()` (batched values) invoke doEmbed one or
107
+ * more times, and each invocation is recorded.
108
+ */
109
+ function createInstrumentedEmbeddingModel(model, accumulator) {
110
+ return wrapEmbeddingModel({
111
+ model,
112
+ middleware: {
113
+ specificationVersion: "v4",
114
+ wrapEmbed: async ({ doEmbed }) => {
115
+ const result = await doEmbed();
116
+ accumulator.record(result.usage?.tokens ?? 0, model.modelId);
117
+ return result;
118
+ }
119
+ }
120
+ });
121
+ }
122
+ //#endregion
123
+ //#region src/ai/cost.ts
124
+ /** Default pricing (USD per million tokens), as of 2026-04. */
125
+ const DEFAULT_MODEL_PRICING = {
126
+ "text-embedding-3-small": {
127
+ inputPerMillion: .02,
128
+ outputPerMillion: 0,
129
+ cacheReadPerMillion: 0,
130
+ cacheWritePerMillion: 0
131
+ },
132
+ "text-embedding-3-large": {
133
+ inputPerMillion: .13,
134
+ outputPerMillion: 0,
135
+ cacheReadPerMillion: 0,
136
+ cacheWritePerMillion: 0
137
+ },
138
+ "claude-opus-4-7": {
139
+ inputPerMillion: 5,
140
+ outputPerMillion: 25,
141
+ cacheReadPerMillion: .5,
142
+ cacheWritePerMillion: 6.25
143
+ },
144
+ "claude-opus-4-6": {
145
+ inputPerMillion: 15,
146
+ outputPerMillion: 75,
147
+ cacheReadPerMillion: 1.5,
148
+ cacheWritePerMillion: 18.75
149
+ },
150
+ "claude-sonnet-4-6": {
151
+ inputPerMillion: 3,
152
+ outputPerMillion: 15,
153
+ cacheReadPerMillion: .3,
154
+ cacheWritePerMillion: 3.75
155
+ },
156
+ "claude-sonnet-4-5-20250929": {
157
+ inputPerMillion: 3,
158
+ outputPerMillion: 15,
159
+ cacheReadPerMillion: .3,
160
+ cacheWritePerMillion: 3.75
161
+ },
162
+ "claude-haiku-4-5-20251001": {
163
+ inputPerMillion: .8,
164
+ outputPerMillion: 4,
165
+ cacheReadPerMillion: .08,
166
+ cacheWritePerMillion: 1
167
+ }
168
+ };
169
+ function priciest(pricing) {
170
+ let best;
171
+ for (const p of Object.values(pricing)) if (!best || p.outputPerMillion > best.outputPerMillion) best = p;
172
+ return best ?? {
173
+ inputPerMillion: 0,
174
+ outputPerMillion: 0,
175
+ cacheReadPerMillion: 0,
176
+ cacheWritePerMillion: 0
177
+ };
178
+ }
179
+ /** Build a cost estimator returning microdollars. */
180
+ function createCostEstimator(options = {}) {
181
+ const pricing = options.pricing ?? DEFAULT_MODEL_PRICING;
182
+ const fallback = options.fallback ?? priciest(pricing);
183
+ return (usage, modelId) => {
184
+ const p = pricing[modelId] ?? fallback;
185
+ const input = usage.inputTokens / 1e6 * p.inputPerMillion;
186
+ const output = usage.outputTokens / 1e6 * p.outputPerMillion;
187
+ const cacheRead = usage.cacheReadTokens / 1e6 * p.cacheReadPerMillion;
188
+ const cacheWrite = usage.cacheWriteTokens / 1e6 * p.cacheWritePerMillion;
189
+ return Math.round((input + output + cacheRead + cacheWrite) * 1e6);
190
+ };
191
+ }
192
+ /** Convenience estimator using the default pricing table. */
193
+ const estimateCostMicros = createCostEstimator();
194
+ //#endregion
195
+ //#region src/ai/define-ai-step.ts
196
+ /**
197
+ * Define a type-safe AI workflow step. Identical to flow-core's `defineStep`
198
+ * except the handler context is fixed to `AiContext<THost>`, so handlers get a
199
+ * ready-to-use `ctx.context.model` (instrumented) and `ctx.context.host`.
200
+ */
201
+ function defineAiStep(config) {
202
+ return defineStep(config);
203
+ }
204
+ /** Build a typed AI workflow from `defineAiStep` definitions. */
205
+ function buildAiWorkflow(config) {
206
+ return buildWorkflow(config);
207
+ }
208
+ //#endregion
209
+ //#region src/ai/hooks.ts
210
+ /**
211
+ * Wire the AI behaviors (model instrumentation, token/cost capture, quota,
212
+ * usage rollups) into a set of flow-core lifecycle hooks. Pass the result as the
213
+ * engine's `hooks`. flow-core stays AI-free; everything AI lives here.
214
+ */
215
+ function createAiWorkflowHooks(deps) {
216
+ const estimate = deps.costEstimator ?? createCostEstimator();
217
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
218
+ return {
219
+ async onBeforeStart() {
220
+ if (deps.quotaPolicy) {
221
+ const q = await deps.quotaPolicy.checkQuota();
222
+ if (!q.ok) return q;
223
+ }
224
+ let metadata;
225
+ if (deps.modelResolver.resolveKeySource) metadata = { keySource: await deps.modelResolver.resolveKeySource() };
226
+ return {
227
+ ok: true,
228
+ value: { metadata }
229
+ };
230
+ },
231
+ async buildStepContext(args) {
232
+ const base = await deps.modelResolver.resolveModel(args);
233
+ const usage = createUsageAccumulator();
234
+ return {
235
+ model: createInstrumentedModel(base, usage),
236
+ host: deps.modelResolver.resolveHost ? await deps.modelResolver.resolveHost(args) : void 0,
237
+ usage
238
+ };
239
+ },
240
+ async onAfterStep({ workflowId, stepId, context }) {
241
+ const acc = context.usage;
242
+ if (!acc.hasUsage()) return;
243
+ const usage = acc.get();
244
+ const costMicros = estimate(usage, usage.modelId);
245
+ await deps.usageRecorder.recordStepUsage({
246
+ workflowId,
247
+ stepId,
248
+ usage,
249
+ costMicros
250
+ });
251
+ await deps.usageRecorder.incrementWorkflowUsage({
252
+ workflowId,
253
+ usage,
254
+ costMicros
255
+ });
256
+ },
257
+ async onWorkflowCompleted({ workflowId, workflow }) {
258
+ if (!deps.usageRecorder.recordWorkflowDaily) return;
259
+ const keySource = workflow.metadata?.keySource ?? "platform";
260
+ const date = now().toISOString().split("T")[0];
261
+ await deps.usageRecorder.recordWorkflowDaily({
262
+ workflowId,
263
+ workflowType: workflow.type,
264
+ keySource,
265
+ date
266
+ });
267
+ }
268
+ };
269
+ }
270
+ //#endregion
271
+ //#region src/ai/usage-window.ts
272
+ /**
273
+ * Calendar-window helpers shared by the quota and usage-aggregation engines.
274
+ * All windows are computed in **UTC**, matching flow's existing rollup date
275
+ * derivation (`now().toISOString().split('T')[0]`).
276
+ */
277
+ /** Format a `Date` as a `YYYY-MM-DD` calendar date in UTC. */
278
+ function toIsoDate(date) {
279
+ return date.toISOString().split("T")[0];
280
+ }
281
+ /** First day (`YYYY-MM-01`) of the calendar month containing the given `YYYY-MM-DD` date. */
282
+ function monthStartOf(isoDate) {
283
+ return isoDate.slice(0, 7) + "-01";
284
+ }
285
+ //#endregion
286
+ //#region src/ai/quota.ts
287
+ /**
288
+ * Convenience defaults a consumer can spread over partial per-scope limits.
289
+ * flow does **not** apply these automatically — resolution policy lives in the
290
+ * injected {@link AiQuotaConfigResolver}.
291
+ */
292
+ const DEFAULT_AI_QUOTA = {
293
+ maxConcurrentWorkflows: 3,
294
+ maxWorkflowsPerDay: 50,
295
+ maxWorkflowsPerMonth: 500
296
+ };
297
+ /**
298
+ * Build a store-agnostic AI quota service. Enforcement order is concurrent →
299
+ * day → month; the first tripped window short-circuits with its error.
300
+ */
301
+ function createAiQuotaService(deps) {
302
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
303
+ async function checkQuota(partitionKey) {
304
+ const config = await deps.getQuota(partitionKey);
305
+ if (!config) return ok(void 0);
306
+ const running = await deps.store.countRunningWorkflows(partitionKey);
307
+ if (config.maxConcurrentWorkflows !== null && running >= config.maxConcurrentWorkflows) return err({
308
+ key: "ai_quota_exceeded",
309
+ reason: "concurrent_limit",
310
+ message: `Maximum concurrent AI workflows reached (${config.maxConcurrentWorkflows}). Please wait for running workflows to complete.`,
311
+ limit: config.maxConcurrentWorkflows,
312
+ current: running
313
+ });
314
+ const today = toIsoDate(now());
315
+ if (config.maxWorkflowsPerDay !== null) {
316
+ const todayCount = await deps.store.sumWorkflowCount({
317
+ partitionKey,
318
+ startDate: today,
319
+ endDate: today
320
+ }) + running;
321
+ if (todayCount >= config.maxWorkflowsPerDay) return err({
322
+ key: "ai_quota_exceeded",
323
+ reason: "daily_limit",
324
+ message: `Daily AI workflow limit reached (${config.maxWorkflowsPerDay}). Try again tomorrow.`,
325
+ limit: config.maxWorkflowsPerDay,
326
+ current: todayCount
327
+ });
328
+ }
329
+ if (config.maxWorkflowsPerMonth !== null) {
330
+ const monthStart = monthStartOf(today);
331
+ const monthCount = await deps.store.sumWorkflowCount({
332
+ partitionKey,
333
+ startDate: monthStart,
334
+ endDate: today
335
+ }) + running;
336
+ if (monthCount >= config.maxWorkflowsPerMonth) return err({
337
+ key: "ai_quota_exceeded",
338
+ reason: "monthly_limit",
339
+ message: `Monthly AI workflow limit reached (${config.maxWorkflowsPerMonth}). Contact support to increase your limit.`,
340
+ limit: config.maxWorkflowsPerMonth,
341
+ current: monthCount
342
+ });
343
+ }
344
+ return ok(void 0);
345
+ }
346
+ return { checkQuota };
347
+ }
348
+ //#endregion
349
+ //#region src/ai/usage-aggregation.ts
350
+ function messageOf(error, fallback) {
351
+ return error instanceof Error ? error.message : fallback;
352
+ }
353
+ /** Build a store-agnostic AI usage-aggregation service. */
354
+ function createAiUsageAggregationService(deps) {
355
+ const { store } = deps;
356
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
357
+ const logger = deps.logger ?? noopLogger;
358
+ async function recordWorkflowCompletion(data) {
359
+ try {
360
+ await store.addDailyUsage({
361
+ partitionKey: data.partitionKey,
362
+ date: data.date,
363
+ workflowType: data.workflowType,
364
+ keySource: data.keySource,
365
+ workflowCount: 1,
366
+ usage: data.usage,
367
+ estimatedCostMicros: data.estimatedCostMicros
368
+ });
369
+ return ok(void 0);
370
+ } catch (error) {
371
+ const message = messageOf(error, "Unknown aggregation error");
372
+ logger.error("Failed to record workflow completion", error instanceof Error ? error : new Error(message));
373
+ return err({
374
+ key: "ai_usage_error",
375
+ message
376
+ });
377
+ }
378
+ }
379
+ async function recordEmbeddingUsage(data) {
380
+ try {
381
+ await store.addDailyUsage({
382
+ partitionKey: data.partitionKey,
383
+ date: data.date,
384
+ workflowType: data.workflowType,
385
+ keySource: data.keySource,
386
+ workflowCount: 1,
387
+ usage: {
388
+ inputTokens: data.inputTokens,
389
+ outputTokens: 0,
390
+ cacheReadTokens: 0,
391
+ cacheWriteTokens: 0
392
+ },
393
+ estimatedCostMicros: data.estimatedCostMicros
394
+ });
395
+ return ok(void 0);
396
+ } catch (error) {
397
+ const message = messageOf(error, "Unknown aggregation error");
398
+ logger.error("Failed to record embedding usage", error instanceof Error ? error : new Error(message));
399
+ return err({
400
+ key: "ai_usage_error",
401
+ message
402
+ });
403
+ }
404
+ }
405
+ async function getUsageSummary(params) {
406
+ try {
407
+ return ok(await store.aggregateByDate(params));
408
+ } catch (error) {
409
+ return err({
410
+ key: "ai_usage_error",
411
+ message: messageOf(error, "Unknown query error")
412
+ });
413
+ }
414
+ }
415
+ async function getUsageByWorkflowType(params) {
416
+ try {
417
+ return ok(await store.aggregateByWorkflowType(params));
418
+ } catch (error) {
419
+ return err({
420
+ key: "ai_usage_error",
421
+ message: messageOf(error, "Unknown query error")
422
+ });
423
+ }
424
+ }
425
+ async function getCurrentQuotaUsage(params) {
426
+ try {
427
+ const today = toIsoDate(now());
428
+ const monthStart = monthStartOf(today);
429
+ const { partitionKey } = params;
430
+ const dayQuery = {
431
+ partitionKey,
432
+ startDate: today,
433
+ endDate: today
434
+ };
435
+ const monthQuery = {
436
+ partitionKey,
437
+ startDate: monthStart,
438
+ endDate: today
439
+ };
440
+ const [todayCount, monthCount, runningCount] = await Promise.all([
441
+ store.sumWorkflowCount(dayQuery),
442
+ store.sumWorkflowCount(monthQuery),
443
+ store.countRunningWorkflows(partitionKey)
444
+ ]);
445
+ return ok({
446
+ today: { workflowCount: todayCount },
447
+ thisMonth: { workflowCount: monthCount },
448
+ running: { count: runningCount }
449
+ });
450
+ } catch (error) {
451
+ return err({
452
+ key: "ai_usage_error",
453
+ message: messageOf(error, "Unknown query error")
454
+ });
455
+ }
456
+ }
457
+ return {
458
+ recordWorkflowCompletion,
459
+ recordEmbeddingUsage,
460
+ getUsageSummary,
461
+ getUsageByWorkflowType,
462
+ getCurrentQuotaUsage
463
+ };
464
+ }
465
+ //#endregion
466
+ export { DEFAULT_AI_QUOTA, DEFAULT_MODEL_PRICING, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createUsageAccumulator, defineAiStep, estimateCostMicros };