@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.
- package/dist/cjs/const.d.ts +2 -0
- package/dist/cjs/const.d.ts.map +1 -1
- package/dist/cjs/const.js +2 -0
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +159 -1
- package/dist/esm/const.d.ts +2 -0
- package/dist/esm/const.d.ts.map +1 -1
- package/dist/esm/const.js +2 -0
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +159 -1
- package/package.json +1 -1
package/dist/cjs/const.d.ts
CHANGED
|
@@ -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;
|
package/dist/cjs/const.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../src/const.ts"],"names":[],"mappings":"
|
|
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":";
|
|
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"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -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
|
}
|
package/dist/esm/const.d.ts
CHANGED
|
@@ -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;
|
package/dist/esm/const.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../src/const.ts"],"names":[],"mappings":"
|
|
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":";
|
|
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"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -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
|
}
|