@dudousxd/nestjs-agent 1.25.2 → 1.27.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 CHANGED
@@ -418,6 +418,86 @@ 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`: common Anthropic models, from the AWS Price List
465
+ for us-gov-west-1). Any other model in those partitions is left unpriced, and the boot warning names
466
+ the partition and the fix.
467
+
468
+ `priceCatalog.prices` is your own rate card. Each entry seeds a row for its model, and for any
469
+ configured model it matches by normalized id, when that model has **no** row yet (an existing row is
470
+ never overwritten). Entries are checked before the built-in table and models.dev, so they win over
471
+ both. An entry looks like `{ model, input, output, cacheRead?, cacheWrite?, currency?: 'USD', unit?:
472
+ '1M tokens' | '1K tokens' }`. Only USD is accepted, because the ledger records `cost_usd`.
473
+
474
+ A GovCloud app on Claude Sonnet 4.5 (us-gov-west-1 list price, USD per 1M tokens):
475
+
476
+ ```ts
477
+ AgentModule.forRoot({
478
+ // …
479
+ priceCatalog: {
480
+ region: 'us-gov-west-1',
481
+ prices: [
482
+ {
483
+ // Also prices the ARN a turn reports:
484
+ // arn:aws-us-gov:bedrock:us-gov-west-1:<account>:inference-profile/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0
485
+ model: 'us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0',
486
+ input: 3.6,
487
+ output: 18,
488
+ cacheRead: 0.36,
489
+ cacheWrite: 4.5,
490
+ },
491
+ ],
492
+ },
493
+ });
494
+ ```
495
+
496
+ Set `modelsDev: false` to keep the app prices and never fetch models.dev.
497
+
498
+ A row seeded before this existed (an earlier release priced a bare base id at the commercial price)
499
+ is **not** replaced. Correct it with `upsertModelPrice` if you run in GovCloud.
500
+
421
501
  ### Who approves an action, and for how long
422
502
 
423
503
  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, 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
 
@@ -320,6 +329,34 @@ interface AgentModuleOptions {
320
329
  * turns instead.
321
330
  */
322
331
  model?: ModelProvider;
332
+ /**
333
+ * Where a configured model with no price row gets one, at boot. On application bootstrap every
334
+ * model the `model` provider describes (`aiSdkModel` / `aiSdkModels`) that the bound
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
+ * ```
354
+ *
355
+ * Omit → models.dev, except under `NODE_ENV=test` (no network from a test suite unless asked).
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).
358
+ */
359
+ priceCatalog?: PriceCatalogOptions | false;
323
360
  /**
324
361
  * Run turns on something other than this library's loop — e.g. `openCode({ host })`. The engine
325
362
  * binds `AGENT_RUNNER`; the routes, store, sink, approvals and queue stay the library's. STATIC
@@ -349,10 +386,16 @@ interface AgentModuleOptions {
349
386
  * `429` before the turn starts. Omit → reported (usage off the ledger), never enforced — for a
350
387
  * public (anonymous) deployment that means unbounded model spend, so set limits there; they apply
351
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.
352
394
  */
353
395
  quota?: {
354
396
  limits: QuotaLimits;
355
397
  warnAt?: number;
398
+ countEstimatedCost?: boolean;
356
399
  } | QuotaProvider;
357
400
  /**
358
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, 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
 
@@ -320,6 +329,34 @@ interface AgentModuleOptions {
320
329
  * turns instead.
321
330
  */
322
331
  model?: ModelProvider;
332
+ /**
333
+ * Where a configured model with no price row gets one, at boot. On application bootstrap every
334
+ * model the `model` provider describes (`aiSdkModel` / `aiSdkModels`) that the bound
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
+ * ```
354
+ *
355
+ * Omit → models.dev, except under `NODE_ENV=test` (no network from a test suite unless asked).
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).
358
+ */
359
+ priceCatalog?: PriceCatalogOptions | false;
323
360
  /**
324
361
  * Run turns on something other than this library's loop — e.g. `openCode({ host })`. The engine
325
362
  * binds `AGENT_RUNNER`; the routes, store, sink, approvals and queue stay the library's. STATIC
@@ -349,10 +386,16 @@ interface AgentModuleOptions {
349
386
  * `429` before the turn starts. Omit → reported (usage off the ledger), never enforced — for a
350
387
  * public (anonymous) deployment that means unbounded model spend, so set limits there; they apply
351
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.
352
394
  */
353
395
  quota?: {
354
396
  limits: QuotaLimits;
355
397
  warnAt?: number;
398
+ countEstimatedCost?: boolean;
356
399
  } | QuotaProvider;
357
400
  /**
358
401
  * Which models a caller may pick — `GET <base>/models`, a send's `model`, a thread's pinned model