@stackfactor/agent-utils 1.0.19 → 1.0.20

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.
@@ -1,6 +1,7 @@
1
1
  declare const _default: {
2
2
  HTTP_CODES: {
3
3
  BAD_REQUEST: number;
4
+ PAYMENT_REQUIRED: number;
4
5
  UNPROCESSABLE_ENTITY: number;
5
6
  INTERNAL_SERVER_ERROR: number;
6
7
  BAD_GATEWAY: number;
@@ -9,6 +10,7 @@ declare const _default: {
9
10
  UNABLE_TO_GENERATE_CONTENT: string;
10
11
  UNEXPECTED_ERROR: string;
11
12
  UNSUPPORTED_MODEL: string;
13
+ QUOTA_EXHAUSTED: string;
12
14
  };
13
15
  };
14
16
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../src/const.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,wBAaE"}
1
+ {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../src/const.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,wBAgBE"}
package/dist/cjs/const.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
4
  HTTP_CODES: {
5
5
  BAD_REQUEST: 400,
6
+ PAYMENT_REQUIRED: 402,
6
7
  UNPROCESSABLE_ENTITY: 422,
7
8
  INTERNAL_SERVER_ERROR: 500,
8
9
  BAD_GATEWAY: 502,
@@ -11,5 +12,6 @@ exports.default = {
11
12
  UNABLE_TO_GENERATE_CONTENT: "Unable to generate content",
12
13
  UNEXPECTED_ERROR: "An unexpected error occured. If the issue persists please contact the StackFactor support team at support@stackfactor.ai",
13
14
  UNSUPPORTED_MODEL: "The specified model is not supported",
15
+ QUOTA_EXHAUSTED: "Agent session quota exhausted: no remaining budget for additional LLM calls.",
14
16
  },
15
17
  };
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";wBAgWQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAmfF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCAxYO,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDA6rBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CA7vB8B,GAAG,KAAG,MAAM;;AAwzBzD,wBAOE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";wBAogBQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAmgBF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCAhZO,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDA4sBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CA5wB8B,GAAG,KAAG,MAAM;;AAu0BzD,wBAOE"}
@@ -16,12 +16,157 @@ const zod_to_json_schema_1 = require("zod-to-json-schema");
16
16
  const JSON_ESCAPE_INSTRUCTION = `
17
17
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
18
18
  - Newlines → \\n
19
- - Tabs → \\t
19
+ - Tabs → \\t
20
20
  - Carriage returns → \\r
21
21
  - Double quotes inside strings → \\"
22
22
  - Backslashes → \\\\
23
23
  Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
24
24
  `.trim();
25
+ /**
26
+ * Reads `global.quota` (set by the StackFactor host before invoking `main()`) and
27
+ * throws a `PAYMENT_REQUIRED` error when the session has no remaining budget. No-op
28
+ * when `global.quota` is absent (e.g. local tests) or `remaining` is not a number,
29
+ * which keeps the library usable outside the StackFactor runtime.
30
+ */
31
+ const assertQuotaAvailable = () => {
32
+ const quota = globalThis.quota;
33
+ if (!quota || typeof quota.remaining !== "number")
34
+ return;
35
+ if (quota.remaining <= 0) {
36
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.PAYMENT_REQUIRED, const_js_1.default.ERROR.QUOTA_EXHAUSTED);
37
+ }
38
+ };
39
+ /**
40
+ * Subtracts the given USD cost from `global.quota.remaining` and adds it to
41
+ * `global.quota.usedThisSession`. Silently no-ops when the quota globals are not
42
+ * present so callers do not need to branch. Negative, zero, or non-finite costs
43
+ * are ignored to keep counters monotonic.
44
+ */
45
+ const recordCost = (cost) => {
46
+ if (!cost || cost <= 0 || !Number.isFinite(cost))
47
+ return;
48
+ const quota = globalThis.quota;
49
+ if (!quota)
50
+ return;
51
+ if (typeof quota.remaining === "number") {
52
+ quota.remaining -= cost;
53
+ }
54
+ if (typeof quota.usedThisSession === "number") {
55
+ quota.usedThisSession += cost;
56
+ }
57
+ else {
58
+ quota.usedThisSession = cost;
59
+ }
60
+ };
61
+ /**
62
+ * Looks up the per-model pricing entry from `config.modelPricing`. Returns `null`
63
+ * when pricing is not configured for the model, in which case cost recording
64
+ * becomes a no-op (the call still succeeds — pricing data is the host's
65
+ * responsibility, not the agent's).
66
+ */
67
+ const getModelPrice = (modelName, config) => {
68
+ const pricing = config?.modelPricing;
69
+ if (!pricing)
70
+ return null;
71
+ return pricing[modelName] || null;
72
+ };
73
+ /**
74
+ * Computes USD cost for a text LLM call. `config.modelPricing[modelName]` is
75
+ * expected to provide `input` and `output` rates in dollars per million tokens.
76
+ */
77
+ const calculateTextCost = (modelName, usage, config) => {
78
+ if (!usage)
79
+ return 0;
80
+ const price = getModelPrice(modelName, config);
81
+ if (!price)
82
+ return 0;
83
+ const inputCost = ((usage.input_tokens || 0) / 1_000_000) * (price.input || 0);
84
+ const outputCost = ((usage.output_tokens || 0) / 1_000_000) * (price.output || 0);
85
+ return inputCost + outputCost;
86
+ };
87
+ /**
88
+ * Computes USD cost for an image generation call. `config.modelPricing[modelName]`
89
+ * is expected to provide a `perImage` rate in dollars.
90
+ */
91
+ const calculateImageCost = (modelName, numImages, config) => {
92
+ const price = getModelPrice(modelName, config);
93
+ if (!price)
94
+ return 0;
95
+ return (numImages || 0) * (price.perImage || 0);
96
+ };
97
+ /**
98
+ * Extracts a normalized token-usage object from a single LangChain `invoke()`
99
+ * response. Reads from `usage_metadata` first (standardized in LangChain v1),
100
+ * then falls back to provider-specific shapes under `response_metadata`.
101
+ */
102
+ const extractUsageFromInvoke = (response) => {
103
+ if (!response)
104
+ return null;
105
+ const um = response.usage_metadata;
106
+ if (um) {
107
+ return {
108
+ input_tokens: um.input_tokens || 0,
109
+ output_tokens: um.output_tokens || 0,
110
+ };
111
+ }
112
+ const rm = response.response_metadata;
113
+ if (rm?.usage) {
114
+ return {
115
+ input_tokens: rm.usage.input_tokens || rm.usage.prompt_tokens || 0,
116
+ output_tokens: rm.usage.output_tokens || rm.usage.completion_tokens || 0,
117
+ };
118
+ }
119
+ if (rm?.tokenUsage) {
120
+ return {
121
+ input_tokens: rm.tokenUsage.promptTokens || 0,
122
+ output_tokens: rm.tokenUsage.completionTokens || 0,
123
+ };
124
+ }
125
+ return null;
126
+ };
127
+ /**
128
+ * Accumulates token usage from a streaming chunk into a running total. LangChain
129
+ * typically attaches `usage_metadata` to the final chunk; earlier chunks carry no
130
+ * usage and are no-ops here.
131
+ */
132
+ const accumulateChunkUsage = (acc, chunk) => {
133
+ if (!chunk)
134
+ return acc;
135
+ const um = chunk.usage_metadata;
136
+ if (um) {
137
+ acc.input_tokens += um.input_tokens || 0;
138
+ acc.output_tokens += um.output_tokens || 0;
139
+ return acc;
140
+ }
141
+ const rm = chunk.response_metadata;
142
+ if (rm?.usage) {
143
+ acc.input_tokens += rm.usage.input_tokens || rm.usage.prompt_tokens || 0;
144
+ acc.output_tokens +=
145
+ rm.usage.output_tokens || rm.usage.completion_tokens || 0;
146
+ }
147
+ return acc;
148
+ };
149
+ /**
150
+ * Sums token usage across every message in an agent invocation response. Each
151
+ * AIMessage in `response.messages` may carry its own `usage_metadata` (one per
152
+ * LLM round-trip the agent made).
153
+ */
154
+ const sumAgentResponseUsage = (response) => {
155
+ const messages = response?.messages;
156
+ if (!Array.isArray(messages) || messages.length === 0)
157
+ return null;
158
+ const total = { input_tokens: 0, output_tokens: 0 };
159
+ let found = false;
160
+ for (const msg of messages) {
161
+ const um = msg?.usage_metadata;
162
+ if (um) {
163
+ total.input_tokens += um.input_tokens || 0;
164
+ total.output_tokens += um.output_tokens || 0;
165
+ found = true;
166
+ }
167
+ }
168
+ return found ? total : null;
169
+ };
25
170
  /**
26
171
  * Converts a Zod validation error object into a human-readable multi-line string.
27
172
  * Each failing field is described with its dot-notation path and a contextual message
@@ -377,12 +522,15 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
377
522
  },
378
523
  ]
379
524
  : undefined;
525
+ assertQuotaAvailable();
380
526
  const response = await agent.invoke({
381
527
  messages: [{ role: "user", content: prompt }],
382
528
  }, {
383
529
  recursionLimit: config.recursionLimit || 25,
384
530
  ...(callbacks ? { callbacks } : {}),
385
531
  });
532
+ const modelName = agent.options?.model?.modelName || agent.options?.model?.model || "";
533
+ recordCost(calculateTextCost(modelName, sumAgentResponseUsage(response), config));
386
534
  const endTime = Date.now();
387
535
  const duration = endTime - startTime;
388
536
  logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -589,8 +737,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
589
737
  // overshoots. This avoids the magic "expected length" constant — longer responses
590
738
  // simply slow the curve down rather than exceeding the range.
591
739
  const expectedDurationMs = 15_000; // Tune: expected typical response time
740
+ const streamUsage = { input_tokens: 0, output_tokens: 0 };
741
+ assertQuotaAvailable();
592
742
  const stream = await llm.stream(messagesToSend);
593
743
  for await (const chunk of stream) {
744
+ accumulateChunkUsage(streamUsage, chunk);
594
745
  const content = chunk?.content || chunk;
595
746
  if (typeof content === "string") {
596
747
  rawContent += content;
@@ -608,6 +759,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
608
759
  }
609
760
  }
610
761
  }
762
+ recordCost(calculateTextCost(modelName, streamUsage, config));
611
763
  await onProgressReport({
612
764
  message: "Processing complete",
613
765
  progress: maxPercent,
@@ -694,7 +846,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
694
846
  messagesToSend = messages;
695
847
  }
696
848
  // Simply invoke without streaming
849
+ assertQuotaAvailable();
697
850
  const response = await llm.invoke(messagesToSend);
851
+ recordCost(calculateTextCost(modelName, extractUsageFromInvoke(response), config));
698
852
  const rawContent = response?.content || response;
699
853
  // If not expecting JSON, return raw content directly
700
854
  if (!expectsJsonResponse) {
@@ -877,7 +1031,9 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
877
1031
  requestParams.style = style;
878
1032
  }
879
1033
  }
1034
+ assertQuotaAvailable();
880
1035
  const response = await openai.images.generate(requestParams);
1036
+ recordCost(calculateImageCost(modelName, response.data?.length || n, config));
881
1037
  // Format response based on number of images
882
1038
  if (n === 1) {
883
1039
  const imageData = response.data[0];
@@ -975,6 +1131,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
975
1131
  safetySettings: safetySettings,
976
1132
  },
977
1133
  };
1134
+ assertQuotaAvailable();
978
1135
  const response = await ai.models.generateContent(req);
979
1136
  // Extract images from response
980
1137
  const images = [];
@@ -993,6 +1150,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
993
1150
  if (images.length === 0) {
994
1151
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
995
1152
  }
1153
+ recordCost(calculateImageCost(modelName, images.length, config));
996
1154
  if (numberOfImages === 1 || images.length === 1) {
997
1155
  return images[0];
998
1156
  }
@@ -1,6 +1,7 @@
1
1
  declare const _default: {
2
2
  HTTP_CODES: {
3
3
  BAD_REQUEST: number;
4
+ PAYMENT_REQUIRED: number;
4
5
  UNPROCESSABLE_ENTITY: number;
5
6
  INTERNAL_SERVER_ERROR: number;
6
7
  BAD_GATEWAY: number;
@@ -9,6 +10,7 @@ declare const _default: {
9
10
  UNABLE_TO_GENERATE_CONTENT: string;
10
11
  UNEXPECTED_ERROR: string;
11
12
  UNSUPPORTED_MODEL: string;
13
+ QUOTA_EXHAUSTED: string;
12
14
  };
13
15
  };
14
16
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../src/const.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,wBAaE"}
1
+ {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../src/const.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,wBAgBE"}
package/dist/esm/const.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export default {
2
2
  HTTP_CODES: {
3
3
  BAD_REQUEST: 400,
4
+ PAYMENT_REQUIRED: 402,
4
5
  UNPROCESSABLE_ENTITY: 422,
5
6
  INTERNAL_SERVER_ERROR: 500,
6
7
  BAD_GATEWAY: 502,
@@ -9,5 +10,6 @@ export default {
9
10
  UNABLE_TO_GENERATE_CONTENT: "Unable to generate content",
10
11
  UNEXPECTED_ERROR: "An unexpected error occured. If the issue persists please contact the StackFactor support team at support@stackfactor.ai",
11
12
  UNSUPPORTED_MODEL: "The specified model is not supported",
13
+ QUOTA_EXHAUSTED: "Agent session quota exhausted: no remaining budget for additional LLM calls.",
12
14
  },
13
15
  };
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";wBAgWQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAmfF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCAxYO,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDA6rBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CA7vB8B,GAAG,KAAG,MAAM;;AAwzBzD,wBAOE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":";wBAogBQ,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;wCAmgBF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,KACpB,GAAG;oCAhZO,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDA4sBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CA5wB8B,GAAG,KAAG,MAAM;;AAu0BzD,wBAOE"}
@@ -11,12 +11,157 @@ import { zodToJsonSchema } from "zod-to-json-schema";
11
11
  const JSON_ESCAPE_INSTRUCTION = `
12
12
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
13
13
  - Newlines → \\n
14
- - Tabs → \\t
14
+ - Tabs → \\t
15
15
  - Carriage returns → \\r
16
16
  - Double quotes inside strings → \\"
17
17
  - Backslashes → \\\\
18
18
  Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
19
19
  `.trim();
20
+ /**
21
+ * Reads `global.quota` (set by the StackFactor host before invoking `main()`) and
22
+ * throws a `PAYMENT_REQUIRED` error when the session has no remaining budget. No-op
23
+ * when `global.quota` is absent (e.g. local tests) or `remaining` is not a number,
24
+ * which keeps the library usable outside the StackFactor runtime.
25
+ */
26
+ const assertQuotaAvailable = () => {
27
+ const quota = globalThis.quota;
28
+ if (!quota || typeof quota.remaining !== "number")
29
+ return;
30
+ if (quota.remaining <= 0) {
31
+ throw errorHandlingHelper.create(constants.HTTP_CODES.PAYMENT_REQUIRED, constants.ERROR.QUOTA_EXHAUSTED);
32
+ }
33
+ };
34
+ /**
35
+ * Subtracts the given USD cost from `global.quota.remaining` and adds it to
36
+ * `global.quota.usedThisSession`. Silently no-ops when the quota globals are not
37
+ * present so callers do not need to branch. Negative, zero, or non-finite costs
38
+ * are ignored to keep counters monotonic.
39
+ */
40
+ const recordCost = (cost) => {
41
+ if (!cost || cost <= 0 || !Number.isFinite(cost))
42
+ return;
43
+ const quota = globalThis.quota;
44
+ if (!quota)
45
+ return;
46
+ if (typeof quota.remaining === "number") {
47
+ quota.remaining -= cost;
48
+ }
49
+ if (typeof quota.usedThisSession === "number") {
50
+ quota.usedThisSession += cost;
51
+ }
52
+ else {
53
+ quota.usedThisSession = cost;
54
+ }
55
+ };
56
+ /**
57
+ * Looks up the per-model pricing entry from `config.modelPricing`. Returns `null`
58
+ * when pricing is not configured for the model, in which case cost recording
59
+ * becomes a no-op (the call still succeeds — pricing data is the host's
60
+ * responsibility, not the agent's).
61
+ */
62
+ const getModelPrice = (modelName, config) => {
63
+ const pricing = config?.modelPricing;
64
+ if (!pricing)
65
+ return null;
66
+ return pricing[modelName] || null;
67
+ };
68
+ /**
69
+ * Computes USD cost for a text LLM call. `config.modelPricing[modelName]` is
70
+ * expected to provide `input` and `output` rates in dollars per million tokens.
71
+ */
72
+ const calculateTextCost = (modelName, usage, config) => {
73
+ if (!usage)
74
+ return 0;
75
+ const price = getModelPrice(modelName, config);
76
+ if (!price)
77
+ return 0;
78
+ const inputCost = ((usage.input_tokens || 0) / 1_000_000) * (price.input || 0);
79
+ const outputCost = ((usage.output_tokens || 0) / 1_000_000) * (price.output || 0);
80
+ return inputCost + outputCost;
81
+ };
82
+ /**
83
+ * Computes USD cost for an image generation call. `config.modelPricing[modelName]`
84
+ * is expected to provide a `perImage` rate in dollars.
85
+ */
86
+ const calculateImageCost = (modelName, numImages, config) => {
87
+ const price = getModelPrice(modelName, config);
88
+ if (!price)
89
+ return 0;
90
+ return (numImages || 0) * (price.perImage || 0);
91
+ };
92
+ /**
93
+ * Extracts a normalized token-usage object from a single LangChain `invoke()`
94
+ * response. Reads from `usage_metadata` first (standardized in LangChain v1),
95
+ * then falls back to provider-specific shapes under `response_metadata`.
96
+ */
97
+ const extractUsageFromInvoke = (response) => {
98
+ if (!response)
99
+ return null;
100
+ const um = response.usage_metadata;
101
+ if (um) {
102
+ return {
103
+ input_tokens: um.input_tokens || 0,
104
+ output_tokens: um.output_tokens || 0,
105
+ };
106
+ }
107
+ const rm = response.response_metadata;
108
+ if (rm?.usage) {
109
+ return {
110
+ input_tokens: rm.usage.input_tokens || rm.usage.prompt_tokens || 0,
111
+ output_tokens: rm.usage.output_tokens || rm.usage.completion_tokens || 0,
112
+ };
113
+ }
114
+ if (rm?.tokenUsage) {
115
+ return {
116
+ input_tokens: rm.tokenUsage.promptTokens || 0,
117
+ output_tokens: rm.tokenUsage.completionTokens || 0,
118
+ };
119
+ }
120
+ return null;
121
+ };
122
+ /**
123
+ * Accumulates token usage from a streaming chunk into a running total. LangChain
124
+ * typically attaches `usage_metadata` to the final chunk; earlier chunks carry no
125
+ * usage and are no-ops here.
126
+ */
127
+ const accumulateChunkUsage = (acc, chunk) => {
128
+ if (!chunk)
129
+ return acc;
130
+ const um = chunk.usage_metadata;
131
+ if (um) {
132
+ acc.input_tokens += um.input_tokens || 0;
133
+ acc.output_tokens += um.output_tokens || 0;
134
+ return acc;
135
+ }
136
+ const rm = chunk.response_metadata;
137
+ if (rm?.usage) {
138
+ acc.input_tokens += rm.usage.input_tokens || rm.usage.prompt_tokens || 0;
139
+ acc.output_tokens +=
140
+ rm.usage.output_tokens || rm.usage.completion_tokens || 0;
141
+ }
142
+ return acc;
143
+ };
144
+ /**
145
+ * Sums token usage across every message in an agent invocation response. Each
146
+ * AIMessage in `response.messages` may carry its own `usage_metadata` (one per
147
+ * LLM round-trip the agent made).
148
+ */
149
+ const sumAgentResponseUsage = (response) => {
150
+ const messages = response?.messages;
151
+ if (!Array.isArray(messages) || messages.length === 0)
152
+ return null;
153
+ const total = { input_tokens: 0, output_tokens: 0 };
154
+ let found = false;
155
+ for (const msg of messages) {
156
+ const um = msg?.usage_metadata;
157
+ if (um) {
158
+ total.input_tokens += um.input_tokens || 0;
159
+ total.output_tokens += um.output_tokens || 0;
160
+ found = true;
161
+ }
162
+ }
163
+ return found ? total : null;
164
+ };
20
165
  /**
21
166
  * Converts a Zod validation error object into a human-readable multi-line string.
22
167
  * Each failing field is described with its dot-notation path and a contextual message
@@ -372,12 +517,15 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
372
517
  },
373
518
  ]
374
519
  : undefined;
520
+ assertQuotaAvailable();
375
521
  const response = await agent.invoke({
376
522
  messages: [{ role: "user", content: prompt }],
377
523
  }, {
378
524
  recursionLimit: config.recursionLimit || 25,
379
525
  ...(callbacks ? { callbacks } : {}),
380
526
  });
527
+ const modelName = agent.options?.model?.modelName || agent.options?.model?.model || "";
528
+ recordCost(calculateTextCost(modelName, sumAgentResponseUsage(response), config));
381
529
  const endTime = Date.now();
382
530
  const duration = endTime - startTime;
383
531
  logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -584,8 +732,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
584
732
  // overshoots. This avoids the magic "expected length" constant — longer responses
585
733
  // simply slow the curve down rather than exceeding the range.
586
734
  const expectedDurationMs = 15_000; // Tune: expected typical response time
735
+ const streamUsage = { input_tokens: 0, output_tokens: 0 };
736
+ assertQuotaAvailable();
587
737
  const stream = await llm.stream(messagesToSend);
588
738
  for await (const chunk of stream) {
739
+ accumulateChunkUsage(streamUsage, chunk);
589
740
  const content = chunk?.content || chunk;
590
741
  if (typeof content === "string") {
591
742
  rawContent += content;
@@ -603,6 +754,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
603
754
  }
604
755
  }
605
756
  }
757
+ recordCost(calculateTextCost(modelName, streamUsage, config));
606
758
  await onProgressReport({
607
759
  message: "Processing complete",
608
760
  progress: maxPercent,
@@ -689,7 +841,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
689
841
  messagesToSend = messages;
690
842
  }
691
843
  // Simply invoke without streaming
844
+ assertQuotaAvailable();
692
845
  const response = await llm.invoke(messagesToSend);
846
+ recordCost(calculateTextCost(modelName, extractUsageFromInvoke(response), config));
693
847
  const rawContent = response?.content || response;
694
848
  // If not expecting JSON, return raw content directly
695
849
  if (!expectsJsonResponse) {
@@ -872,7 +1026,9 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
872
1026
  requestParams.style = style;
873
1027
  }
874
1028
  }
1029
+ assertQuotaAvailable();
875
1030
  const response = await openai.images.generate(requestParams);
1031
+ recordCost(calculateImageCost(modelName, response.data?.length || n, config));
876
1032
  // Format response based on number of images
877
1033
  if (n === 1) {
878
1034
  const imageData = response.data[0];
@@ -970,6 +1126,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
970
1126
  safetySettings: safetySettings,
971
1127
  },
972
1128
  };
1129
+ assertQuotaAvailable();
973
1130
  const response = await ai.models.generateContent(req);
974
1131
  // Extract images from response
975
1132
  const images = [];
@@ -988,6 +1145,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
988
1145
  if (images.length === 0) {
989
1146
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
990
1147
  }
1148
+ recordCost(calculateImageCost(modelName, images.length, config));
991
1149
  if (numberOfImages === 1 || images.length === 1) {
992
1150
  return images[0];
993
1151
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "restricted"
5
5
  },
6
- "version": "1.0.19",
6
+ "version": "1.0.20",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",