@plumbus/core 0.6.19 → 0.7.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.
Files changed (159) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +5 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts +5 -0
  5. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts.map +1 -0
  6. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js +15 -0
  7. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js.map +1 -0
  8. package/dist/ai/ai/cost-tracker.d.ts +4 -3
  9. package/dist/ai/ai/model-pricing.d.ts +14 -5
  10. package/dist/ai/ai/usage-client.d.ts +2 -0
  11. package/dist/ai/ai/usage-validation.d.ts +3 -0
  12. package/dist/ai/ai-service.d.ts.map +1 -1
  13. package/dist/ai/ai-service.js +101 -33
  14. package/dist/ai/ai-service.js.map +1 -1
  15. package/dist/ai/cost-tracker.d.ts +4 -3
  16. package/dist/ai/cost-tracker.d.ts.map +1 -1
  17. package/dist/ai/cost-tracker.js +33 -14
  18. package/dist/ai/cost-tracker.js.map +1 -1
  19. package/dist/ai/errors/data-errors.d.ts +22 -0
  20. package/dist/ai/errors/index.d.ts +21 -0
  21. package/dist/ai/errors/plumbus-error.d.ts +12 -0
  22. package/dist/ai/index.d.ts +1 -1
  23. package/dist/ai/index.d.ts.map +1 -1
  24. package/dist/ai/index.js +1 -1
  25. package/dist/ai/index.js.map +1 -1
  26. package/dist/ai/model-pricing.d.ts +14 -5
  27. package/dist/ai/model-pricing.d.ts.map +1 -1
  28. package/dist/ai/model-pricing.js +88 -45
  29. package/dist/ai/model-pricing.js.map +1 -1
  30. package/dist/ai/provider.d.ts.map +1 -1
  31. package/dist/ai/provider.js +22 -9
  32. package/dist/ai/provider.js.map +1 -1
  33. package/dist/ai/rag/pipeline.d.ts +2 -0
  34. package/dist/ai/rag/pipeline.d.ts.map +1 -1
  35. package/dist/ai/rag/pipeline.js +6 -1
  36. package/dist/ai/rag/pipeline.js.map +1 -1
  37. package/dist/ai/security.d.ts.map +1 -1
  38. package/dist/ai/security.js +1 -0
  39. package/dist/ai/security.js.map +1 -1
  40. package/dist/ai/tool-loop.d.ts +1 -0
  41. package/dist/ai/tool-loop.d.ts.map +1 -1
  42. package/dist/ai/tool-loop.js +23 -4
  43. package/dist/ai/tool-loop.js.map +1 -1
  44. package/dist/ai/types/errors.d.ts +14 -0
  45. package/dist/ai/usage-client.d.ts +2 -0
  46. package/dist/ai/usage-client.d.ts.map +1 -1
  47. package/dist/ai/usage-client.js +2 -0
  48. package/dist/ai/usage-client.js.map +1 -1
  49. package/dist/ai/usage-validation.d.ts +4 -0
  50. package/dist/ai/usage-validation.d.ts.map +1 -0
  51. package/dist/ai/usage-validation.js +23 -0
  52. package/dist/ai/usage-validation.js.map +1 -0
  53. package/dist/ai/validation.d.ts +1 -5
  54. package/dist/ai/validation.d.ts.map +1 -1
  55. package/dist/api/cookies.d.ts.map +1 -1
  56. package/dist/api/cookies.js +7 -2
  57. package/dist/api/cookies.js.map +1 -1
  58. package/dist/audit/service.d.ts.map +1 -1
  59. package/dist/audit/service.js +21 -3
  60. package/dist/audit/service.js.map +1 -1
  61. package/dist/audit/writer.d.ts.map +1 -1
  62. package/dist/audit/writer.js +7 -2
  63. package/dist/audit/writer.js.map +1 -1
  64. package/dist/auth/adapter.d.ts +5 -1
  65. package/dist/auth/adapter.d.ts.map +1 -1
  66. package/dist/auth/adapter.js +36 -6
  67. package/dist/auth/adapter.js.map +1 -1
  68. package/dist/auth/index.d.ts +1 -0
  69. package/dist/auth/index.d.ts.map +1 -1
  70. package/dist/auth/index.js +1 -0
  71. package/dist/auth/index.js.map +1 -1
  72. package/dist/auth/jwt-policy.d.ts +3 -0
  73. package/dist/auth/jwt-policy.d.ts.map +1 -0
  74. package/dist/auth/jwt-policy.js +11 -0
  75. package/dist/auth/jwt-policy.js.map +1 -0
  76. package/dist/auth/oidc-adapter.d.ts.map +1 -1
  77. package/dist/auth/oidc-adapter.js +30 -13
  78. package/dist/auth/oidc-adapter.js.map +1 -1
  79. package/dist/auth/saml-adapter.d.ts +7 -1
  80. package/dist/auth/saml-adapter.d.ts.map +1 -1
  81. package/dist/auth/saml-adapter.js +42 -5
  82. package/dist/auth/saml-adapter.js.map +1 -1
  83. package/dist/cli/commands/create.d.ts.map +1 -1
  84. package/dist/cli/commands/create.js +21 -0
  85. package/dist/cli/commands/create.js.map +1 -1
  86. package/dist/cli/commands/generate.d.ts.map +1 -1
  87. package/dist/cli/commands/generate.js +4 -3
  88. package/dist/cli/commands/generate.js.map +1 -1
  89. package/dist/cli/commands/init.d.ts +1 -1
  90. package/dist/cli/commands/init.d.ts.map +1 -1
  91. package/dist/cli/commands/init.js +7 -1
  92. package/dist/cli/commands/init.js.map +1 -1
  93. package/dist/cli/commands/mcp.d.ts.map +1 -1
  94. package/dist/cli/commands/mcp.js +4 -3
  95. package/dist/cli/commands/mcp.js.map +1 -1
  96. package/dist/cli/commands/translation.d.ts.map +1 -1
  97. package/dist/cli/commands/translation.js +8 -4
  98. package/dist/cli/commands/translation.js.map +1 -1
  99. package/dist/cli/mcp-serve-context.d.ts.map +1 -1
  100. package/dist/cli/mcp-serve-context.js +25 -6
  101. package/dist/cli/mcp-serve-context.js.map +1 -1
  102. package/dist/cli/scaffold-validation.d.ts +6 -0
  103. package/dist/cli/scaffold-validation.d.ts.map +1 -0
  104. package/dist/cli/scaffold-validation.js +57 -0
  105. package/dist/cli/scaffold-validation.js.map +1 -0
  106. package/dist/cli/scaffold-write.d.ts.map +1 -1
  107. package/dist/cli/scaffold-write.js +13 -3
  108. package/dist/cli/scaffold-write.js.map +1 -1
  109. package/dist/cli/templates/resources.d.ts.map +1 -1
  110. package/dist/cli/templates/resources.js +10 -0
  111. package/dist/cli/templates/resources.js.map +1 -1
  112. package/dist/cli/utils.d.ts +1 -1
  113. package/dist/cli/utils.d.ts.map +1 -1
  114. package/dist/cli/utils.js +13 -2
  115. package/dist/cli/utils.js.map +1 -1
  116. package/dist/config/loader.d.ts.map +1 -1
  117. package/dist/config/loader.js +7 -4
  118. package/dist/config/loader.js.map +1 -1
  119. package/dist/errors/http.d.ts.map +1 -1
  120. package/dist/errors/http.js +8 -3
  121. package/dist/errors/http.js.map +1 -1
  122. package/dist/events/queue.d.ts.map +1 -1
  123. package/dist/events/queue.js +27 -4
  124. package/dist/events/queue.js.map +1 -1
  125. package/dist/execution/capability-executor.js +2 -1
  126. package/dist/execution/capability-executor.js.map +1 -1
  127. package/dist/execution/context-factory.d.ts.map +1 -1
  128. package/dist/execution/context-factory.js +6 -2
  129. package/dist/execution/context-factory.js.map +1 -1
  130. package/dist/flows/engine.d.ts.map +1 -1
  131. package/dist/flows/engine.js +28 -18
  132. package/dist/flows/engine.js.map +1 -1
  133. package/dist/index.d.ts +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +1 -1
  136. package/dist/index.js.map +1 -1
  137. package/dist/runtime/start-worker-pool.d.ts.map +1 -1
  138. package/dist/runtime/start-worker-pool.js +21 -9
  139. package/dist/runtime/start-worker-pool.js.map +1 -1
  140. package/dist/server/bootstrap.d.ts.map +1 -1
  141. package/dist/server/bootstrap.js +13 -9
  142. package/dist/server/bootstrap.js.map +1 -1
  143. package/dist/testing/e2e.js +1 -1
  144. package/dist/testing/e2e.js.map +1 -1
  145. package/dist/types/audit.d.ts +2 -0
  146. package/dist/types/audit.d.ts.map +1 -1
  147. package/dist/types/context.d.ts +11 -1
  148. package/dist/types/context.d.ts.map +1 -1
  149. package/instructions/README.md +1 -0
  150. package/instructions/ai.md +10 -2
  151. package/instructions/deployment.md +5 -2
  152. package/instructions/flows.md +3 -0
  153. package/instructions/framework.md +7 -0
  154. package/instructions/mcp.md +5 -2
  155. package/instructions/peer-dependencies.md +28 -64
  156. package/instructions/security.md +8 -1
  157. package/instructions/upgrading-0.5-capabilities.md +4 -1
  158. package/instructions/upgrading-security-release.md +65 -0
  159. package/package.json +11 -10
@@ -0,0 +1,5 @@
1
+ import { type AIService, type RAGPipelineConfig, type RunToolLoopParams } from '@plumbus/core';
2
+ export declare function legacyConsumer(ai: AIService, params: RunToolLoopParams): Promise<number>;
3
+ export declare const legacyEmbeddingCallback: RAGPipelineConfig['onEmbeddingCost'];
4
+ export declare const uncheckedEstimate: number;
5
+ //# sourceMappingURL=legacy-cost-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-cost-api.d.ts","sourceRoot":"","sources":["../../../src/ai/__typecheck-fixtures__/legacy-cost-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAC;AAEvB,wBAAsB,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9F;AAED,eAAO,MAAM,uBAAuB,EAAE,iBAAiB,CAAC,iBAAiB,CAGxE,CAAC;AAGF,eAAO,MAAM,iBAAiB,EAAE,MAAyC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { calculateModelCost, estimateModelCost, runToolLoop, } from '@plumbus/core';
2
+ export async function legacyConsumer(ai, params) {
3
+ const result = await ai.generateWithUsage({ prompt: 'p', input: {} });
4
+ const cost = result.cost;
5
+ const loop = await runToolLoop(ai, params);
6
+ const total = loop.aggregatedCost;
7
+ return cost + total + calculateModelCost(1, 1, 'local');
8
+ }
9
+ export const legacyEmbeddingCallback = (info) => {
10
+ const numericCost = info.cost;
11
+ void numericCost;
12
+ };
13
+ // @ts-expect-error Unknown-aware estimates cannot be treated as definitely priced.
14
+ export const uncheckedEstimate = estimateModelCost(1, 1, 'local');
15
+ //# sourceMappingURL=legacy-cost-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-cost-api.js","sourceRoot":"","sources":["../../../src/ai/__typecheck-fixtures__/legacy-cost-api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,GAIZ,MAAM,eAAe,CAAC;AAEvB,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,EAAa,EAAE,MAAyB;IAC3E,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,iBAAiB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;IACtE,MAAM,IAAI,GAAW,MAAM,CAAC,IAAI,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAW,IAAI,CAAC,cAAc,CAAC;IAC1C,OAAO,IAAI,GAAG,KAAK,GAAG,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAyC,CAAC,IAAI,EAAE,EAAE;IACpF,MAAM,WAAW,GAAW,IAAI,CAAC,IAAI,CAAC;IACtC,KAAK,WAAW,CAAC;AACnB,CAAC,CAAC;AAEF,mFAAmF;AACnF,MAAM,CAAC,MAAM,iBAAiB,GAAW,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC"}
@@ -27,7 +27,7 @@ export interface AICostRecord {
27
27
  connectionMinutes?: number;
28
28
  participantMinutes?: number;
29
29
  };
30
- /** Actual cost from provider API, or null if API unavailable */
30
+ /** Provider-reported or catalog-estimated USD cost; null when unknown. */
31
31
  cost: number | null;
32
32
  latencyMs: number;
33
33
  tenantId?: string;
@@ -55,8 +55,9 @@ export interface BudgetConfig {
55
55
  * optional so pre-0.3.0 call sites keep compiling; the tracker defaults
56
56
  * `status` to `'success'` internally.
57
57
  */
58
- export type AICostRecordInput = Omit<AICostRecord, 'id' | 'timestamp' | 'status'> & {
58
+ export type AICostRecordInput = Omit<AICostRecord, 'id' | 'timestamp' | 'status' | 'cost'> & {
59
59
  status?: AICostRecord['status'];
60
+ cost?: number | null;
60
61
  };
61
62
  export interface CostTracker {
62
63
  record(entry: AICostRecordInput): void;
@@ -71,7 +72,7 @@ export interface CostTracker {
71
72
  }): BudgetCheckResult;
72
73
  getDailyUsage(tenantId?: string): DailyUsage;
73
74
  getRecords(): AICostRecord[];
74
- /** Fetch actual costs from provider usage APIs and update records */
75
+ /** Fetch provider totals for reconciliation; does not infer per-request prices from aggregates. */
75
76
  syncCosts?(): Promise<UsageSyncResult>;
76
77
  }
77
78
  export interface BudgetCheckResult {
@@ -14,6 +14,10 @@ export interface ModelRate {
14
14
  inputPerMTok: number;
15
15
  /** USD per 1M output tokens (0 for embedding / moderation / free models) */
16
16
  outputPerMTok: number;
17
+ /** Published cache-read USD per MTok; defaults to 0.1× input when omitted. */
18
+ cachedInputPerMTok?: number;
19
+ /** Above this inclusive input-token boundary, charge 2× input/cache and 1.5× output. */
20
+ longContextThreshold?: number;
17
21
  /**
18
22
  * Capability classification. Optional on the type for backward compat —
19
23
  * existing consumers that destructure only the rate fields keep working —
@@ -34,18 +38,23 @@ export declare function findModelRate(model: string): ModelRate | null;
34
38
  * catalog. Order is not guaranteed.
35
39
  */
36
40
  export declare function allKnownModels(): ReadonlyArray<readonly [string, ModelRate]>;
41
+ /** Legacy numeric cost helper. Use estimateModelCost when unknown must differ from free. */
42
+ export declare function calculateModelCost(inputTokens: number, outputTokens: number, model: string, options?: {
43
+ cachedInputTokens?: number;
44
+ cacheWriteTokens?: number;
45
+ }): number;
37
46
  /**
38
47
  * Calculate the USD cost for a single AI request based on published per-token rates.
39
48
  *
40
49
  * Pricing adjustments applied:
41
- * - **Cached input tokens**: charged at 0.1× the base input rate (both providers).
50
+ * - **Cached input tokens**: published per-model rate, defaulting to 0.1× input.
42
51
  * - **Cache write tokens**: charged at 1.25× the base input rate (Anthropic 5-min cache).
43
52
  * - **Long context premium**: for Claude Sonnet 4 / 4.5, when total input exceeds
44
- * 200K tokens the entire request is charged at 2× input / 1.5× output.
53
+ * 200K tokens, or GPT-5.6 Sol over 272K, charge 2× input/cache and 1.5× output.
45
54
  *
46
- * Returns 0 for unknown/unsupported models (Ollama, custom endpoints).
55
+ * Returns undefined for unknown/unsupported models; explicitly free adapters may report zero.
47
56
  */
48
- export declare function calculateModelCost(inputTokens: number, outputTokens: number, model: string, options?: {
57
+ export declare function estimateModelCost(inputTokens: number, outputTokens: number, model: string, options?: {
49
58
  cachedInputTokens?: number;
50
59
  cacheWriteTokens?: number;
51
- }): number;
60
+ }): number | undefined;
@@ -22,6 +22,8 @@ export interface UsageClientConfig {
22
22
  apiKey: string;
23
23
  baseUrl?: string;
24
24
  organizationId?: string;
25
+ /** Billing endpoint timeout in milliseconds (default 10000). */
26
+ timeoutMs?: number;
25
27
  }
26
28
  export declare function createUsageAPIClient(config: UsageClientConfig): UsageAPIClient;
27
29
  export declare class UsageAPIError extends Error {
@@ -0,0 +1,3 @@
1
+ import type { TokenUsage } from './provider.js';
2
+ export declare function validateTokenUsage(usage: TokenUsage): TokenUsage;
3
+ export declare function normalizeCost(cost: unknown): number | null;
@@ -1 +1 @@
1
- {"version":3,"file":"ai-service.d.ts","sourceRoot":"","sources":["../../src/ai/ai-service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,aAAa,EAIb,SAAS,EAIV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,iBAAiB,EAAoB,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,KAAK,EAAE,YAAY,EAAqB,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,KAAK,iBAAiB,EAUvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAIzB,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,YAAY,EACpB,WAAW,CAAC,EAAE,aAAa,KACxB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAG1B,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,cAAc,CAAC,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,0DAA0D,CAAC;IACjF,gFAAgF;IAChF,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QACE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,iBAAiB,CAAC;QAC9B,mCAAmC;QACnC,eAAe,CAAC,EAAE,eAAe,CAAC;KACnC,CACF,CAAC,kCAAkC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,iBAAiB,CAAC,GAC5D,eAAe,CAMjB;AAUD,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CA2uClE"}
1
+ {"version":3,"file":"ai-service.d.ts","sourceRoot":"","sources":["../../src/ai/ai-service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,aAAa,EAIb,SAAS,EAIV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,iBAAiB,EAAoB,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,KAAK,EAAE,YAAY,EAAqB,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAGnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,KAAK,iBAAiB,EAUvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAIzB,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,YAAY,EACpB,WAAW,CAAC,EAAE,aAAa,KACxB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAG1B,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,cAAc,CAAC,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,0DAA0D,CAAC;IACjF,gFAAgF;IAChF,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QACE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,iBAAiB,CAAC;QAC9B,mCAAmC;QACnC,eAAe,CAAC,EAAE,eAAe,CAAC;KACnC,CACF,CAAC,kCAAkC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,iBAAiB,CAAC,GAC5D,eAAe,CAMjB;AAUD,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CA8zClE"}
@@ -3,7 +3,8 @@
3
3
  // Integrates: provider adapter, prompt registry, validation, cost tracking, security, RAG, explainability
4
4
  import { z } from 'zod';
5
5
  import { AIBudgetExceededError, AISecurityBlockedError } from '../errors/data-errors.js';
6
- import { calculateModelCost } from './model-pricing.js';
6
+ import { normalizeCost, validateTokenUsage } from './usage-validation.js';
7
+ import { estimateModelCost as calculateModelCost, findModelRate } from './model-pricing.js';
7
8
  import { normalizeFinishReason, } from './provider.js';
8
9
  import { AIIncompleteOutputError, AIRefusalError } from './refusal.js';
9
10
  import { checkPromptSecurity } from './security.js';
@@ -32,7 +33,36 @@ export function createAIService(config) {
32
33
  const available = Object.keys(providers).join(', ');
33
34
  throw new Error(`AI provider "${name}" not configured. Available: ${available}`);
34
35
  }
35
- return adapter;
36
+ // Every provider attempt, including validation retries, checks the budget.
37
+ const checkRequest = (request) => {
38
+ const input = JSON.stringify({
39
+ system: request.system,
40
+ prompt: request.prompt,
41
+ messages: request.messages,
42
+ tools: request.tools,
43
+ responseSchema: request.responseSchema,
44
+ });
45
+ checkBudget(Math.ceil(Buffer.byteLength(input, 'utf8') / 4) + (request.maxTokens ?? 0));
46
+ };
47
+ return {
48
+ ...adapter,
49
+ name: adapter.name,
50
+ embed: (request) => adapter.embed(request),
51
+ async complete(request) {
52
+ checkRequest(request);
53
+ const response = await adapter.complete(request);
54
+ validateTokenUsage(response.usage);
55
+ return { ...response, cost: normalizeCost(response.cost) ?? undefined };
56
+ },
57
+ async *stream(request) {
58
+ checkRequest(request);
59
+ for await (const event of adapter.stream(request)) {
60
+ if (event.usage)
61
+ validateTokenUsage(event.usage);
62
+ yield { ...event, cost: normalizeCost(event.cost) ?? undefined };
63
+ }
64
+ },
65
+ };
36
66
  }
37
67
  function getSingleTextFieldName(schema) {
38
68
  const shape = schema instanceof z.ZodObject ? schema.shape : undefined;
@@ -94,12 +124,25 @@ export function createAIService(config) {
94
124
  * so the hook gets a fully-formed `AICostRecord`.
95
125
  */
96
126
  async function recordProviderCost(entry, costContext) {
127
+ entry = {
128
+ ...entry,
129
+ tenantId: config.budget ? config.budget.tenantId : entry.tenantId,
130
+ actor: config.budget ? config.budget.actor : entry.actor,
131
+ cost: (entry.cost == null ? undefined : normalizeCost(entry.cost)) ??
132
+ (entry.cost == null && entry.usage.totalTokens > 0 && findModelRate(entry.model)
133
+ ? calculateModelCost(entry.usage.inputTokens, entry.usage.outputTokens, entry.model, {
134
+ cachedInputTokens: entry.usage.cachedInputTokens,
135
+ cacheWriteTokens: entry.usage.cacheWriteTokens,
136
+ })
137
+ : null),
138
+ };
97
139
  if (costTracker) {
98
140
  costTracker.record(entry);
99
141
  }
100
142
  if (onAICostRecorded) {
101
143
  const finalEntry = {
102
144
  ...entry,
145
+ cost: normalizeCost(entry.cost),
103
146
  status: entry.status ?? 'success',
104
147
  id: crypto.randomUUID(),
105
148
  timestamp: new Date(),
@@ -147,15 +190,12 @@ export function createAIService(config) {
147
190
  // Simple template resolution: replace top-level {{key}} placeholders.
148
191
  const substitutedKeys = new Set();
149
192
  const renderTemplate = (template) => {
150
- let text = template;
151
- for (const [key, value] of Object.entries(input)) {
152
- const placeholder = `{{${key}}}`;
153
- if (text.includes(placeholder)) {
154
- substitutedKeys.add(key);
155
- text = text.replaceAll(placeholder, String(value));
156
- }
157
- }
158
- return text;
193
+ return template.replace(/\{\{([^{}]+)\}\}/g, (placeholder, key) => {
194
+ if (!Object.hasOwn(input, key))
195
+ return placeholder;
196
+ substitutedKeys.add(key);
197
+ return String(input[key]);
198
+ });
159
199
  };
160
200
  const system = def.system ? renderTemplate(def.system) : undefined;
161
201
  const text = renderTemplate(def.description ?? promptName);
@@ -356,6 +396,11 @@ export function createAIService(config) {
356
396
  throw err;
357
397
  }
358
398
  const latencyMs = performance.now() - start;
399
+ const cost = providerCost ??
400
+ calculateModelCost(totalUsage.inputTokens, totalUsage.outputTokens, resolvedModel, {
401
+ cachedInputTokens: totalUsage.cachedInputTokens,
402
+ cacheWriteTokens: totalUsage.cacheWriteTokens,
403
+ });
359
404
  // Track cost (success path)
360
405
  await recordProviderCost({
361
406
  model: resolvedModel,
@@ -363,7 +408,7 @@ export function createAIService(config) {
363
408
  promptName: hasPromptDef ? params.prompt : undefined,
364
409
  operation: 'generate',
365
410
  usage: totalUsage,
366
- cost: null,
411
+ cost: cost ?? null,
367
412
  latencyMs,
368
413
  tenantId: config.budget?.tenantId,
369
414
  actor: config.budget?.actor,
@@ -376,7 +421,7 @@ export function createAIService(config) {
376
421
  promptName: hasPromptDef ? params.prompt : undefined,
377
422
  model: promptInfo.model ?? config.defaultModel,
378
423
  provider: activeProvider.name,
379
- input: params.input,
424
+ input: inputForAI,
380
425
  output: result,
381
426
  usage: totalUsage,
382
427
  validation: {
@@ -389,11 +434,6 @@ export function createAIService(config) {
389
434
  latencyMs,
390
435
  });
391
436
  }
392
- const cost = providerCost ??
393
- calculateModelCost(totalUsage.inputTokens, totalUsage.outputTokens, resolvedModel, {
394
- cachedInputTokens: totalUsage.cachedInputTokens,
395
- cacheWriteTokens: totalUsage.cacheWriteTokens,
396
- });
397
437
  if (toolsEnabled &&
398
438
  toolFinishNormalized === 'tool_calls' &&
399
439
  toolCallsResult &&
@@ -405,7 +445,8 @@ export function createAIService(config) {
405
445
  usage: totalUsage,
406
446
  model: resolvedModel,
407
447
  provider: activeProvider.name,
408
- cost,
448
+ cost: cost ?? 0,
449
+ costAvailable: cost != null,
409
450
  };
410
451
  }
411
452
  let resolvedFinishReason = finalFinishReason;
@@ -418,13 +459,17 @@ export function createAIService(config) {
418
459
  usage: totalUsage,
419
460
  model: resolvedModel,
420
461
  provider: activeProvider.name,
421
- cost,
462
+ cost: cost ?? 0,
463
+ costAvailable: cost != null,
422
464
  };
423
465
  }
424
466
  function generateWithUsage(config) {
425
467
  return _generateCore(config);
426
468
  }
427
469
  return {
470
+ withContext(identity) {
471
+ return createAIService({ ...config, budget: { ...identity } });
472
+ },
428
473
  features: { perCallProviderModelReasoning: true },
429
474
  recordProviderCost,
430
475
  checkProviderCostBudget(config = {}) {
@@ -604,17 +649,18 @@ export function createAIService(config) {
604
649
  }
605
650
  // Compute cost and model info for the done event
606
651
  const streamCost = streamProviderCost ??
607
- (streamUsage
652
+ (streamUsage && findModelRate(resolvedModel)
608
653
  ? calculateModelCost(streamUsage.inputTokens, streamUsage.outputTokens, resolvedModel, {
609
654
  cachedInputTokens: streamUsage.cachedInputTokens,
610
655
  cacheWriteTokens: streamUsage.cacheWriteTokens,
611
656
  })
612
- : 0);
657
+ : undefined);
613
658
  const doneBase = {
614
659
  usage: streamUsage,
615
660
  model: resolvedModel,
616
661
  provider: activeProvider.name,
617
662
  cost: streamCost,
663
+ costAvailable: streamCost != null,
618
664
  finishReason: providerFinishReason,
619
665
  };
620
666
  if (request.responseSchema &&
@@ -626,7 +672,7 @@ export function createAIService(config) {
626
672
  promptName: hasPromptDef ? params.prompt : undefined,
627
673
  operation: 'generate',
628
674
  usage: failureUsage,
629
- cost: null,
675
+ cost: streamCost ?? null,
630
676
  latencyMs: 0,
631
677
  tenantId: config.budget?.tenantId,
632
678
  actor: config.budget?.actor,
@@ -650,7 +696,7 @@ export function createAIService(config) {
650
696
  promptName: hasPromptDef ? params.prompt : undefined,
651
697
  operation: 'generate',
652
698
  usage: streamUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
653
- cost: null,
699
+ cost: streamCost ?? null,
654
700
  latencyMs: performance.now() - streamStart,
655
701
  tenantId: config.budget?.tenantId,
656
702
  actor: config.budget?.actor,
@@ -662,7 +708,7 @@ export function createAIService(config) {
662
708
  promptName: hasPromptDef ? params.prompt : undefined,
663
709
  model: resolvedModel,
664
710
  provider: activeProvider.name,
665
- input: params.input,
711
+ input: inputForAI,
666
712
  output: singleTextField
667
713
  ? { [singleTextField]: fullText.trim() }
668
714
  : { text: fullText.trim() },
@@ -686,6 +732,7 @@ export function createAIService(config) {
686
732
  let validatedData;
687
733
  let validationFellBackToNonStreaming = false;
688
734
  let fallbackUsage;
735
+ let fallbackCost;
689
736
  try {
690
737
  const parsed = JSON.parse(fullText);
691
738
  validatedData = promptDef.output.parse(parsed);
@@ -731,6 +778,9 @@ export function createAIService(config) {
731
778
  const validated = await generateWithValidation(activeProvider, { ...request, responseFormat: 'json' }, promptDef.output, { ...config.validation, model: resolvedModel, provider: activeProvider.name });
732
779
  validatedData = validated.data;
733
780
  fallbackUsage = validated.usage;
781
+ fallbackCost =
782
+ validated.cost ??
783
+ calculateModelCost(validated.usage.inputTokens, validated.usage.outputTokens, resolvedModel, validated.usage);
734
784
  }
735
785
  catch (fallbackErr) {
736
786
  // Bill the caller for the combined streamUsage + whatever the
@@ -769,6 +819,11 @@ export function createAIService(config) {
769
819
  const successUsage = validationFellBackToNonStreaming
770
820
  ? sumUsage(streamUsage, fallbackUsage)
771
821
  : (streamUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 });
822
+ const successCost = validationFellBackToNonStreaming
823
+ ? streamCost != null && fallbackCost != null
824
+ ? streamCost + fallbackCost
825
+ : undefined
826
+ : streamCost;
772
827
  await recordProviderCost({
773
828
  model: resolvedModel,
774
829
  provider: activeProvider.name,
@@ -776,7 +831,7 @@ export function createAIService(config) {
776
831
  operation: 'generate',
777
832
  fallbackUsed: validationFellBackToNonStreaming || undefined,
778
833
  usage: successUsage,
779
- cost: null,
834
+ cost: successCost ?? null,
780
835
  latencyMs: 0,
781
836
  tenantId: config.budget?.tenantId,
782
837
  actor: config.budget?.actor,
@@ -786,6 +841,9 @@ export function createAIService(config) {
786
841
  type: 'done',
787
842
  data: validatedData,
788
843
  ...doneBase,
844
+ usage: successUsage,
845
+ cost: successCost,
846
+ costAvailable: successCost != null,
789
847
  validationFallbackFired: validationFellBackToNonStreaming,
790
848
  };
791
849
  }
@@ -796,7 +854,7 @@ export function createAIService(config) {
796
854
  promptName: hasPromptDef ? params.prompt : undefined,
797
855
  operation: 'generate',
798
856
  usage: streamUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
799
- cost: null,
857
+ cost: streamCost ?? null,
800
858
  latencyMs: 0,
801
859
  tenantId: config.budget?.tenantId,
802
860
  actor: config.budget?.actor,
@@ -807,6 +865,8 @@ export function createAIService(config) {
807
865
  },
808
866
  async extract(params) {
809
867
  const start = performance.now();
868
+ const { inputForAI, securityResult } = applyPromptSecurity({ text: params.text });
869
+ const textForAI = String(inputForAI.text);
810
870
  checkBudget();
811
871
  // extract uses default provider (no prompt-level routing)
812
872
  const activeProvider = resolveProvider();
@@ -814,7 +874,7 @@ export function createAIService(config) {
814
874
  const systemPrompt = 'Extract structured data from the following text. Return valid JSON matching the required schema.';
815
875
  const request = {
816
876
  system: systemPrompt,
817
- prompt: params.text,
877
+ prompt: textForAI,
818
878
  model: config.defaultModel,
819
879
  responseFormat: 'json',
820
880
  responseSchema: config.enableStrictStructuredOutputs
@@ -855,7 +915,9 @@ export function createAIService(config) {
855
915
  provider: activeProvider.name,
856
916
  operation: 'extract',
857
917
  usage: validated.usage,
858
- cost: null,
918
+ cost: validated.cost ??
919
+ calculateModelCost(validated.usage.inputTokens, validated.usage.outputTokens, resolvedModel, validated.usage) ??
920
+ null,
859
921
  latencyMs,
860
922
  tenantId: config.budget?.tenantId,
861
923
  actor: config.budget?.actor,
@@ -866,7 +928,8 @@ export function createAIService(config) {
866
928
  operation: 'extract',
867
929
  model: config.defaultModel,
868
930
  provider: activeProvider.name,
869
- input: { text: params.text },
931
+ input: inputForAI,
932
+ securityWarnings: securityResult?.warnings.map((warning) => warning.message),
870
933
  output: validated.data,
871
934
  usage: validated.usage,
872
935
  validation: { passed: validated.attempts === 1, attempts: validated.attempts },
@@ -879,6 +942,8 @@ export function createAIService(config) {
879
942
  },
880
943
  async classify(params) {
881
944
  const start = performance.now();
945
+ const { inputForAI, securityResult } = applyPromptSecurity({ text: params.text });
946
+ const textForAI = String(inputForAI.text);
882
947
  checkBudget();
883
948
  // classify uses default provider (no prompt-level routing)
884
949
  const activeProvider = resolveProvider();
@@ -886,7 +951,7 @@ export function createAIService(config) {
886
951
  const systemPrompt = 'Classify the following text into one or more of the provided labels. Return a JSON array of matching label strings.';
887
952
  const request = {
888
953
  system: systemPrompt,
889
- prompt: `Labels: ${JSON.stringify(params.labels)}\n\nText: ${params.text}`,
954
+ prompt: `Labels: ${JSON.stringify(params.labels)}\n\nText: ${textForAI}`,
890
955
  model: config.defaultModel,
891
956
  responseFormat: 'json',
892
957
  responseSchema: config.enableStrictStructuredOutputs
@@ -930,7 +995,9 @@ export function createAIService(config) {
930
995
  provider: activeProvider.name,
931
996
  operation: 'classify',
932
997
  usage: validated.usage,
933
- cost: null,
998
+ cost: validated.cost ??
999
+ calculateModelCost(validated.usage.inputTokens, validated.usage.outputTokens, resolvedModel, validated.usage) ??
1000
+ null,
934
1001
  latencyMs,
935
1002
  tenantId: config.budget?.tenantId,
936
1003
  actor: config.budget?.actor,
@@ -941,7 +1008,8 @@ export function createAIService(config) {
941
1008
  operation: 'classify',
942
1009
  model: config.defaultModel,
943
1010
  provider: activeProvider.name,
944
- input: { labels: params.labels, text: params.text },
1011
+ input: { labels: params.labels, ...inputForAI },
1012
+ securityWarnings: securityResult?.warnings.map((warning) => warning.message),
945
1013
  output: result,
946
1014
  usage: validated.usage,
947
1015
  validation: { passed: validated.attempts === 1, attempts: validated.attempts },