@dudousxd/nestjs-agent 1.26.0 → 1.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -418,6 +418,88 @@ AgentModule.forRoot({
418
418
 
419
419
  In anonymous mode the limits apply per browser (each is its own actor).
420
420
 
421
+ ### Estimated cost is persisted, and the quota counts it
422
+
423
+ A provider that reports no cost, such as Bedrock or OpenAI and Anthropic called directly, still gets
424
+ a cost for every turn: its tokens times the model's price row. That estimate is written to the usage
425
+ ledger: `agent_token_usage.cost_usd`, with `cost_source = 'estimate'`. Before this, the estimate
426
+ reached only the stream frame and the message, so the ledger held NULL and the quota's USD windows
427
+ read $0. A cost the provider reports (OpenRouter, the Vercel AI Gateway) is written as before, with
428
+ `cost_source = 'provider'`. That reported figure always wins over an estimate. A turn with neither
429
+ keeps `cost_usd` NULL and `cost_source` NULL.
430
+
431
+ - **Schema.** `cost_source` is a nullable column that the boot schema heal adds to an existing
432
+ `agent_token_usage`, with no app migration. Rows written before it existed keep `cost_source`
433
+ NULL. They only ever held a reported cost.
434
+ - **Frozen at write time.** An estimate uses the price row in effect for that turn. Changing a price
435
+ later does not rewrite history. A row with NULL `cost_usd` is still estimated at read time by the
436
+ governance read-model, as before.
437
+ - **Quota.** The USD windows (`limits.day.usd`, `limits.month.usd`) sum every priced row, estimates
438
+ included. That is the default because otherwise a USD ceiling on a provider that reports no cost
439
+ would read $0 and never block: the budget would exist but never enforce. To budget on
440
+ provider-reported (gateway) cost only, set `quota: { limits, countEstimatedCost: false }`.
441
+ `quotaToday` / `usageBetween` return `{ usedTokens, costUsd, estimatedCostUsd }`, where
442
+ `costUsd` includes the estimated share.
443
+ - History-summary usage rows are written before the run reads its price list, so they stay
444
+ unpriced in the ledger. The governance read-model still estimates them.
445
+
446
+ ### Pricing at boot: your own prices, and AWS Bedrock (GovCloud, China)
447
+
448
+ On bootstrap `AgentModule` gives every configured model (`aiSdkModel` / `aiSdkModels` describe
449
+ theirs) that the bound `AGENT_PRICING_STORE` has no row for a price row, so its turns record a
450
+ `cost_usd`. It never overwrites a row that exists. The sources, in order: `priceCatalog.prices`, the
451
+ built-in GovCloud Bedrock table, then the [models.dev](https://models.dev) list price. A model still
452
+ unpriced gets one boot warning naming it, the reason, and the `priceCatalog.prices` entry that fixes it.
453
+
454
+ Bedrock reports no cost, so a Bedrock turn is priced off the row for the exact model id the turn
455
+ reports — often an inference-profile ARN. At boot each Bedrock id is normalized: an ARN
456
+ (`arn:aws[-us-gov|-cn]:bedrock:<region>:<account>:(inference-profile|foundation-model|application-inference-profile)/<id>`)
457
+ or a geo-prefixed profile (`us.`, `eu.`, `apac.`, `us-gov.`, `global.`, …) is looked up as the full
458
+ id, then the profile id, then the base foundation-model id, and the row is written under the id the
459
+ model is configured with (an ARN stays an ARN). An ARN is never looked up as an OpenRouter id.
460
+
461
+ models.dev lists **commercial** Bedrock prices. For a model in **GovCloud** (`aws-us-gov`) or
462
+ **China** (`aws-cn`) they are never seeded. The partition comes from the id (the ARN, or a `us-gov.`
463
+ prefix), else `priceCatalog.region`, else `AWS_REGION` / `AWS_DEFAULT_REGION`. GovCloud models get
464
+ a built-in table instead (`BEDROCK_BUILTIN_PRICES`, from the AWS Price List for us-gov-west-1:
465
+ the Anthropic models, Amazon Nova Micro/Lite/Pro and Titan Text Embeddings V2, Meta Llama 3 8B/70B,
466
+ OpenAI gpt-oss-20b/120b, NVIDIA Nemotron, and xAI Grok 4.6 — with cache-read and cache-write prices
467
+ where AWS publishes them). Any other model in those partitions is left unpriced, and the boot warning names
468
+ the partition and the fix.
469
+
470
+ `priceCatalog.prices` is your own rate card. Each entry seeds a row for its model, and for any
471
+ configured model it matches by normalized id, when that model has **no** row yet (an existing row is
472
+ never overwritten). Entries are checked before the built-in table and models.dev, so they win over
473
+ both. An entry looks like `{ model, input, output, cacheRead?, cacheWrite?, currency?: 'USD', unit?:
474
+ '1M tokens' | '1K tokens' }`. Only USD is accepted, because the ledger records `cost_usd`.
475
+
476
+ A GovCloud app on Claude Sonnet 4.5 (us-gov-west-1 list price, USD per 1M tokens):
477
+
478
+ ```ts
479
+ AgentModule.forRoot({
480
+ // …
481
+ priceCatalog: {
482
+ region: 'us-gov-west-1',
483
+ prices: [
484
+ {
485
+ // Also prices the ARN a turn reports:
486
+ // arn:aws-us-gov:bedrock:us-gov-west-1:<account>:inference-profile/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0
487
+ model: 'us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0',
488
+ input: 3.6,
489
+ output: 18,
490
+ cacheRead: 0.36,
491
+ cacheWrite: 4.5,
492
+ },
493
+ ],
494
+ },
495
+ });
496
+ ```
497
+
498
+ Set `modelsDev: false` to keep the app prices and never fetch models.dev.
499
+
500
+ A row seeded before this existed (an earlier release priced a bare base id at the commercial price)
501
+ is **not** replaced. Correct it with `upsertModelPrice` if you run in GovCloud.
502
+
421
503
  ### Who approves an action, and for how long
422
504
 
423
505
  Action handlers can implement an optional read-only `preflight(input, ctx, { phase })`. The
@@ -1,4 +1,4 @@
1
- import { ResolveToolUiCatalog, ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, createFunctionalTool, QuotaProvider, QuotaQuery, QuotaReport, ActionApprovalMode, BackgroundActorResolver, ActionProposal, TextActionProposalConfig, ModelsDevOptions, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, Actor, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason } from '@dudousxd/nestjs-agent-core';
1
+ import { ResolveToolUiCatalog, ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, createFunctionalTool, QuotaProvider, QuotaQuery, QuotaReport, ActionApprovalMode, BackgroundActorResolver, ActionProposal, TextActionProposalConfig, PriceCatalogOptions, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, Actor, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason } from '@dudousxd/nestjs-agent-core';
2
2
  import { Catalog } from '@dudousxd/nestjs-agent-core/genui';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
4
  import { Provider, InjectionToken, Type, FactoryProvider, CanActivate, ModuleMetadata, OptionalFactoryDependency } from '@nestjs/common';
@@ -146,6 +146,13 @@ interface LedgerQuotaOptions {
146
146
  * Stamped on every window that has a ceiling. Omit → no warnings.
147
147
  */
148
148
  warnAt?: number;
149
+ /**
150
+ * Whether the USD windows count ESTIMATED cost (tokens × the price row, for a provider that reports
151
+ * none, e.g. Bedrock, OpenAI or Anthropic direct) as well as provider-reported cost. Default `true`:
152
+ * a USD ceiling that ignored estimates would read $0 and never block for every such provider, which
153
+ * is a budget that does not enforce. Set `false` to budget on provider-reported (gateway) cost only.
154
+ */
155
+ countEstimatedCost?: boolean;
149
156
  }
150
157
  /**
151
158
  * The default {@link QuotaProvider}: windows read off the usage ledger the loop already writes.
@@ -163,6 +170,8 @@ declare class LedgerQuotaProvider implements QuotaProvider {
163
170
  constructor(store: AgentStore, limits?: QuotaLimits, options?: LedgerQuotaOptions);
164
171
  report(query: QuotaQuery): Promise<QuotaReport>;
165
172
  private dayWindow;
173
+ /** The window's USD: every priced row, or (with `countEstimatedCost: false`) reported cost only. */
174
+ private usd;
166
175
  private window;
167
176
  }
168
177
 
@@ -323,15 +332,31 @@ interface AgentModuleOptions {
323
332
  /**
324
333
  * Where a configured model with no price row gets one, at boot. On application bootstrap every
325
334
  * model the `model` provider describes (`aiSdkModel` / `aiSdkModels`) that the bound
326
- * `AGENT_PRICING_STORE` does not price gets the current [models.dev](https://models.dev) list price
327
- * written as its row — only when it has none, so a price you set is never overwritten. A model that
328
- * still has no price, and whose provider reports no cost (anything but OpenRouter / the Vercel AI
329
- * Gateway), is named in one boot warning: it would record a `null` cost on every turn.
335
+ * `AGENT_PRICING_STORE` does not price gets a row — only when it has none, so a price you set is
336
+ * never overwritten — from, in order: `prices` (your own rate card), the built-in AWS GovCloud
337
+ * Bedrock table (for a GovCloud model), then the current [models.dev](https://models.dev) list price
338
+ * (commercial; never used for a GovCloud / China Bedrock model). A model that still has no price, and
339
+ * whose provider reports no cost (anything but OpenRouter / the Vercel AI Gateway), is named in one
340
+ * boot warning with the reason and the `prices` entry that fixes it.
341
+ *
342
+ * Bedrock ids are normalized: an entry for the base id or the `us-gov.` profile id also prices the
343
+ * inference-profile ARN a turn reports. The AWS partition comes from the id (ARN, `us-gov.` prefix),
344
+ * else `region`, else `AWS_REGION` / `AWS_DEFAULT_REGION`.
345
+ *
346
+ * ```ts
347
+ * priceCatalog: {
348
+ * region: 'us-gov-west-1',
349
+ * prices: [
350
+ * { model: 'us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0', input: 3.6, output: 18, cacheRead: 0.36, cacheWrite: 4.5 },
351
+ * ],
352
+ * }
353
+ * ```
330
354
  *
331
355
  * Omit → models.dev, except under `NODE_ENV=test` (no network from a test suite unless asked).
332
- * `{ url, fetch }` points at a mirror; `false` never fetches (the warning still runs).
356
+ * `{ url, fetch }` points at a mirror; `modelsDev: false` never fetches; `false` turns off every
357
+ * source but the built-in table (the warning still runs).
333
358
  */
334
- priceCatalog?: ModelsDevOptions | false;
359
+ priceCatalog?: PriceCatalogOptions | false;
335
360
  /**
336
361
  * Run turns on something other than this library's loop — e.g. `openCode({ host })`. The engine
337
362
  * binds `AGENT_RUNNER`; the routes, store, sink, approvals and queue stay the library's. STATIC
@@ -361,10 +386,16 @@ interface AgentModuleOptions {
361
386
  * `429` before the turn starts. Omit → reported (usage off the ledger), never enforced — for a
362
387
  * public (anonymous) deployment that means unbounded model spend, so set limits there; they apply
363
388
  * per actor, which is per browser in anonymous mode.
389
+ *
390
+ * The USD windows sum the ledger's `cost_usd`: provider-reported cost AND the loop's estimate for a
391
+ * provider that reports none (marked `cost_source = 'estimate'`). `countEstimatedCost: false` counts
392
+ * reported cost only. The default (`true`) is there because otherwise a USD ceiling on Bedrock /
393
+ * OpenAI / Anthropic direct would read $0 and never block.
364
394
  */
365
395
  quota?: {
366
396
  limits: QuotaLimits;
367
397
  warnAt?: number;
398
+ countEstimatedCost?: boolean;
368
399
  } | QuotaProvider;
369
400
  /**
370
401
  * Which models a caller may pick — `GET <base>/models`, a send's `model`, a thread's pinned model
@@ -1,4 +1,4 @@
1
- import { ResolveToolUiCatalog, ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, createFunctionalTool, QuotaProvider, QuotaQuery, QuotaReport, ActionApprovalMode, BackgroundActorResolver, ActionProposal, TextActionProposalConfig, ModelsDevOptions, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, Actor, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason } from '@dudousxd/nestjs-agent-core';
1
+ import { ResolveToolUiCatalog, ModelProvider, AgentStore, ToolRegistry, RolesPolicy, ApprovalPolicy, QuotaStore, TokenStreamSink, PromptBuilder, PromptContributor, Persona, ToolTransientRetrySetting, Retriever, AgentPricingStore, HistoryPolicy, InputProcessor, OutputProcessor, AgentIntake, SkillsConfig, MemoryConfig, ToolSpec, ToolHandler, createFunctionalTool, QuotaProvider, QuotaQuery, QuotaReport, ActionApprovalMode, BackgroundActorResolver, ActionProposal, TextActionProposalConfig, PriceCatalogOptions, ModelCatalog, EmptyRoles, ActorResolver, AgentHistoryWindow, SkillProvider, ScopeResolver, MemoryProvider, AgentRegistry, PersonaCatalogEntry, Actor, AttachmentStagingStore, ChatQueueStore, ChatQueueState, AgentRunner, AgentRunInput, AgentStreamEvent, QueuePauseReason } from '@dudousxd/nestjs-agent-core';
2
2
  import { Catalog } from '@dudousxd/nestjs-agent-core/genui';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
4
  import { Provider, InjectionToken, Type, FactoryProvider, CanActivate, ModuleMetadata, OptionalFactoryDependency } from '@nestjs/common';
@@ -146,6 +146,13 @@ interface LedgerQuotaOptions {
146
146
  * Stamped on every window that has a ceiling. Omit → no warnings.
147
147
  */
148
148
  warnAt?: number;
149
+ /**
150
+ * Whether the USD windows count ESTIMATED cost (tokens × the price row, for a provider that reports
151
+ * none, e.g. Bedrock, OpenAI or Anthropic direct) as well as provider-reported cost. Default `true`:
152
+ * a USD ceiling that ignored estimates would read $0 and never block for every such provider, which
153
+ * is a budget that does not enforce. Set `false` to budget on provider-reported (gateway) cost only.
154
+ */
155
+ countEstimatedCost?: boolean;
149
156
  }
150
157
  /**
151
158
  * The default {@link QuotaProvider}: windows read off the usage ledger the loop already writes.
@@ -163,6 +170,8 @@ declare class LedgerQuotaProvider implements QuotaProvider {
163
170
  constructor(store: AgentStore, limits?: QuotaLimits, options?: LedgerQuotaOptions);
164
171
  report(query: QuotaQuery): Promise<QuotaReport>;
165
172
  private dayWindow;
173
+ /** The window's USD: every priced row, or (with `countEstimatedCost: false`) reported cost only. */
174
+ private usd;
166
175
  private window;
167
176
  }
168
177
 
@@ -323,15 +332,31 @@ interface AgentModuleOptions {
323
332
  /**
324
333
  * Where a configured model with no price row gets one, at boot. On application bootstrap every
325
334
  * model the `model` provider describes (`aiSdkModel` / `aiSdkModels`) that the bound
326
- * `AGENT_PRICING_STORE` does not price gets the current [models.dev](https://models.dev) list price
327
- * written as its row — only when it has none, so a price you set is never overwritten. A model that
328
- * still has no price, and whose provider reports no cost (anything but OpenRouter / the Vercel AI
329
- * Gateway), is named in one boot warning: it would record a `null` cost on every turn.
335
+ * `AGENT_PRICING_STORE` does not price gets a row — only when it has none, so a price you set is
336
+ * never overwritten — from, in order: `prices` (your own rate card), the built-in AWS GovCloud
337
+ * Bedrock table (for a GovCloud model), then the current [models.dev](https://models.dev) list price
338
+ * (commercial; never used for a GovCloud / China Bedrock model). A model that still has no price, and
339
+ * whose provider reports no cost (anything but OpenRouter / the Vercel AI Gateway), is named in one
340
+ * boot warning with the reason and the `prices` entry that fixes it.
341
+ *
342
+ * Bedrock ids are normalized: an entry for the base id or the `us-gov.` profile id also prices the
343
+ * inference-profile ARN a turn reports. The AWS partition comes from the id (ARN, `us-gov.` prefix),
344
+ * else `region`, else `AWS_REGION` / `AWS_DEFAULT_REGION`.
345
+ *
346
+ * ```ts
347
+ * priceCatalog: {
348
+ * region: 'us-gov-west-1',
349
+ * prices: [
350
+ * { model: 'us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0', input: 3.6, output: 18, cacheRead: 0.36, cacheWrite: 4.5 },
351
+ * ],
352
+ * }
353
+ * ```
330
354
  *
331
355
  * Omit → models.dev, except under `NODE_ENV=test` (no network from a test suite unless asked).
332
- * `{ url, fetch }` points at a mirror; `false` never fetches (the warning still runs).
356
+ * `{ url, fetch }` points at a mirror; `modelsDev: false` never fetches; `false` turns off every
357
+ * source but the built-in table (the warning still runs).
333
358
  */
334
- priceCatalog?: ModelsDevOptions | false;
359
+ priceCatalog?: PriceCatalogOptions | false;
335
360
  /**
336
361
  * Run turns on something other than this library's loop — e.g. `openCode({ host })`. The engine
337
362
  * binds `AGENT_RUNNER`; the routes, store, sink, approvals and queue stay the library's. STATIC
@@ -361,10 +386,16 @@ interface AgentModuleOptions {
361
386
  * `429` before the turn starts. Omit → reported (usage off the ledger), never enforced — for a
362
387
  * public (anonymous) deployment that means unbounded model spend, so set limits there; they apply
363
388
  * per actor, which is per browser in anonymous mode.
389
+ *
390
+ * The USD windows sum the ledger's `cost_usd`: provider-reported cost AND the loop's estimate for a
391
+ * provider that reports none (marked `cost_source = 'estimate'`). `countEstimatedCost: false` counts
392
+ * reported cost only. The default (`true`) is there because otherwise a USD ceiling on Bedrock /
393
+ * OpenAI / Anthropic direct would read $0 and never block.
364
394
  */
365
395
  quota?: {
366
396
  limits: QuotaLimits;
367
397
  warnAt?: number;
398
+ countEstimatedCost?: boolean;
368
399
  } | QuotaProvider;
369
400
  /**
370
401
  * Which models a caller may pick — `GET <base>/models`, a send's `model`, a thread's pinned model
@@ -6170,7 +6170,7 @@ var LedgerQuotaProvider = class {
6170
6170
  if (this.store.usageBetween !== void 0) {
6171
6171
  const range = (0, import_nestjs_agent_core34.quotaPeriodRange)("month", now);
6172
6172
  const used = await this.store.usageBetween(actorRef, range.fromDay, range.toDay);
6173
- windows.push(this.window("month", used.usedTokens, used.costUsd, range.resetsAt));
6173
+ windows.push(this.window("month", used.usedTokens, this.usd(used), range.resetsAt));
6174
6174
  }
6175
6175
  const blocked = (0, import_nestjs_agent_core34.exhaustedWindow)(windows);
6176
6176
  const warning = (0, import_nestjs_agent_core34.quotaWarning)(windows);
@@ -6186,8 +6186,15 @@ var LedgerQuotaProvider = class {
6186
6186
  }
6187
6187
  async dayWindow(actorRef, now) {
6188
6188
  const range = (0, import_nestjs_agent_core34.quotaPeriodRange)("day", now);
6189
- const { usedTokens, costUsd } = await this.store.quotaToday(actorRef, range.fromDay);
6190
- return this.window("day", usedTokens, costUsd, range.resetsAt);
6189
+ const used = await this.store.quotaToday(actorRef, range.fromDay);
6190
+ return this.window("day", used.usedTokens, this.usd(used), range.resetsAt);
6191
+ }
6192
+ /** The window's USD: every priced row, or (with `countEstimatedCost: false`) reported cost only. */
6193
+ usd(used) {
6194
+ if (this.options.countEstimatedCost === false) {
6195
+ return Math.max(0, used.costUsd - (used.estimatedCostUsd ?? 0));
6196
+ }
6197
+ return used.costUsd;
6191
6198
  }
6192
6199
  window(period, usedTokens, usedUsd, resetsAt) {
6193
6200
  const limits = this.limits[period] ?? {};
@@ -7289,9 +7296,14 @@ function sharedProviders(durable, engine) {
7289
7296
  // `quota` is either a provider of the host's own or ceilings for the built-in ledger one;
7290
7297
  // omitted, the ledger provider still reports usage, with no ceiling to block on.
7291
7298
  provide: import_nestjs_agent_core40.AGENT_QUOTA_PROVIDER,
7292
- useFactory: /* @__PURE__ */ __name((o, store) => isQuotaProvider(o.quota) ? o.quota : new LedgerQuotaProvider(store, o.quota?.limits, o.quota?.warnAt !== void 0 ? {
7293
- warnAt: o.quota.warnAt
7294
- } : {}), "useFactory"),
7299
+ useFactory: /* @__PURE__ */ __name((o, store) => isQuotaProvider(o.quota) ? o.quota : new LedgerQuotaProvider(store, o.quota?.limits, {
7300
+ ...o.quota?.warnAt !== void 0 ? {
7301
+ warnAt: o.quota.warnAt
7302
+ } : {},
7303
+ ...o.quota?.countEstimatedCost !== void 0 ? {
7304
+ countEstimatedCost: o.quota.countEstimatedCost
7305
+ } : {}
7306
+ }), "useFactory"),
7295
7307
  inject: [
7296
7308
  import_nestjs_agent_core40.AGENT_OPTIONS,
7297
7309
  import_nestjs_agent_core40.AGENT_STORE