@stackfactor/agent-utils 1.2.6 → 1.2.7
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 +19 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/langChain.d.ts +14 -2
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +40 -119
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/langChain.d.ts +14 -2
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +40 -119
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
|
|
28
28
|
Unified interface for running LLM prompts, managing LangChain agents, and generating images across OpenAI, Anthropic, Google, DeepSeek, Kimi (Moonshot), and GLM (Zhipu) providers.
|
|
29
29
|
|
|
30
|
-
### `langChain.runPromptWithModel(modelName, config, prompt, onProgressReport?, minPercent?, maxPercent?, expectsJsonResponse?, schema?, agentName?, tools?)`
|
|
30
|
+
### `langChain.runPromptWithModel(modelName, config, prompt, onProgressReport?, minPercent?, maxPercent?, expectsJsonResponse?, schema?, agentName?, tools?, usageTracker?)`
|
|
31
31
|
|
|
32
32
|
Sends a prompt to an LLM and returns the response. Supports three execution modes:
|
|
33
33
|
|
|
@@ -49,15 +49,33 @@ When `expectsJsonResponse` is `true` (the default), JSON escape instructions are
|
|
|
49
49
|
| `schema` | `ZodSchema \| null` | `null` | Zod schema for response validation |
|
|
50
50
|
| `agentName` | `string` | `"StackFactor"` | Display name for the agent (agentic mode) |
|
|
51
51
|
| `tools` | `any[]` | `[]` | LangChain tools available to the agent (agentic mode) |
|
|
52
|
+
| `usageTracker` | `UsageTracker \| null` | `null` | Optional accumulator updated on every successful LLM call with `cost` (USD) and per-model token counts. |
|
|
52
53
|
|
|
53
54
|
**Returns:** JSON string (when `expectsJsonResponse` is `true`) or raw content string.
|
|
54
55
|
|
|
55
56
|
```typescript
|
|
57
|
+
const usageTracker = {};
|
|
56
58
|
const result = await langChain.runPromptWithModel(
|
|
57
59
|
"gpt-4o",
|
|
58
60
|
{ openAIAPIKey: "sk-..." },
|
|
59
61
|
"Generate a summary of this document.",
|
|
62
|
+
null,
|
|
63
|
+
0,
|
|
64
|
+
100,
|
|
65
|
+
true,
|
|
66
|
+
null,
|
|
67
|
+
"StackFactor",
|
|
68
|
+
[],
|
|
69
|
+
usageTracker,
|
|
60
70
|
);
|
|
71
|
+
// usageTracker is now:
|
|
72
|
+
// {
|
|
73
|
+
// cost: 0.00123,
|
|
74
|
+
// tokens: {
|
|
75
|
+
// "gpt-4o_inputTokens": 412,
|
|
76
|
+
// "gpt-4o_outputTokens": 87,
|
|
77
|
+
// }
|
|
78
|
+
// }
|
|
61
79
|
```
|
|
62
80
|
|
|
63
81
|
---
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { constants };
|
|
|
9
9
|
export { errorHandling, AppError };
|
|
10
10
|
export type { ParsedError } from "./errorHandling.js";
|
|
11
11
|
export { langChain };
|
|
12
|
+
export type { UsageTracker } from "./langChain.js";
|
|
12
13
|
export { logger };
|
|
13
14
|
export { serve };
|
|
14
15
|
export { callAgent };
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/cjs/langChain.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accumulated usage and cost for a single agent run. Callers initialize an
|
|
3
|
+
* empty object (`{}`) and pass it through `runPromptWithModel`; helpers
|
|
4
|
+
* populate `cost` (USD total across all models) and per-model
|
|
5
|
+
* `<model>_inputTokens` / `<model>_outputTokens` counters under `tokens`.
|
|
6
|
+
*/
|
|
7
|
+
export type UsageTracker = {
|
|
8
|
+
cost: number;
|
|
9
|
+
tokens: {
|
|
10
|
+
[tokenKey: string]: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
1
13
|
declare const _default: {
|
|
2
14
|
checkIfAIProviderConfigured: (config: any) => void;
|
|
3
15
|
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
|
|
4
|
-
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null) => Promise<any>;
|
|
5
|
-
runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[]) => Promise<any>;
|
|
16
|
+
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null, usageTracker?: UsageTracker | null) => Promise<any>;
|
|
17
|
+
runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[], usageTracker?: UsageTracker | null) => Promise<any>;
|
|
6
18
|
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any) => Promise<any>;
|
|
7
19
|
throwErrorIfNotSuccessful: (response: any) => string;
|
|
8
20
|
validateModel: (selectedModel: string, supportedModels: string[]) => string;
|
|
@@ -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":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBAkd/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAmsBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAt+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA2gCT,wBAQE"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -23,20 +23,6 @@ CRITICAL - Your response must be valid JSON. Escape ALL special characters in st
|
|
|
23
23
|
- Backslashes → \\\\
|
|
24
24
|
Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
|
|
25
25
|
`.trim();
|
|
26
|
-
/**
|
|
27
|
-
* Reads `global.quota` (set by the StackFactor host before invoking `main()`) and
|
|
28
|
-
* throws a `PAYMENT_REQUIRED` error when the session has no remaining budget. No-op
|
|
29
|
-
* when `global.quota` is absent (e.g. local tests) or `remaining` is not a number,
|
|
30
|
-
* which keeps the library usable outside the StackFactor runtime.
|
|
31
|
-
*/
|
|
32
|
-
const assertQuotaAvailable = () => {
|
|
33
|
-
const quota = globalThis.quota;
|
|
34
|
-
if (!quota || typeof quota.remaining !== "number")
|
|
35
|
-
return;
|
|
36
|
-
if (quota.remaining <= 0) {
|
|
37
|
-
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.PAYMENT_REQUIRED, const_js_1.default.ERROR.QUOTA_EXHAUSTED);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
26
|
const checkIfAIProviderConfigured = (config) => {
|
|
41
27
|
if (!config ||
|
|
42
28
|
!config.openAIAPIKey ||
|
|
@@ -45,104 +31,42 @@ const checkIfAIProviderConfigured = (config) => {
|
|
|
45
31
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "The integration is not properly configured with all AI providers.");
|
|
46
32
|
}
|
|
47
33
|
};
|
|
48
|
-
/**
|
|
49
|
-
* Subtracts the given USD cost from `global.quota.remaining` and adds it to
|
|
50
|
-
* `global.quota.usedThisSession`. Silently no-ops when the quota globals are not
|
|
51
|
-
* present so callers do not need to branch. Negative, zero, or non-finite costs
|
|
52
|
-
* are ignored to keep counters monotonic.
|
|
53
|
-
*/
|
|
54
|
-
const recordCost = (cost) => {
|
|
55
|
-
if (!cost || cost <= 0 || !Number.isFinite(cost))
|
|
56
|
-
return;
|
|
57
|
-
const quota = globalThis.quota;
|
|
58
|
-
if (!quota)
|
|
59
|
-
return;
|
|
60
|
-
if (typeof quota.remaining === "number") {
|
|
61
|
-
quota.remaining -= cost;
|
|
62
|
-
}
|
|
63
|
-
if (typeof quota.usedThisSession === "number") {
|
|
64
|
-
quota.usedThisSession += cost;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
quota.usedThisSession = cost;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
34
|
/**
|
|
71
35
|
* Reads a per-model rate from the flat cost constants the integration defines
|
|
72
36
|
* in its `config.yaml` (exposed on the config object), e.g.
|
|
73
|
-
* `claude-opus-4-7-input-token-costs`. Returns `
|
|
74
|
-
* absent or not numeric,
|
|
75
|
-
* call still succeeds — pricing is the integration's configuration concern).
|
|
37
|
+
* `claude-opus-4-7-input-token-costs`. Returns `0` when the constant is
|
|
38
|
+
* absent or not numeric, which leaves cost contribution zero.
|
|
76
39
|
*/
|
|
77
40
|
const getModelRate = (modelName, config, kind) => {
|
|
78
41
|
if (!config || !modelName)
|
|
79
|
-
return
|
|
42
|
+
return 0;
|
|
80
43
|
const rate = Number(config[`${modelName}-${kind}-costs`]);
|
|
81
|
-
return Number.isFinite(rate) ? rate :
|
|
44
|
+
return Number.isFinite(rate) ? rate : 0;
|
|
82
45
|
};
|
|
83
46
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
47
|
+
* Adds a single call's usage to the caller-supplied tracker. Initializes
|
|
48
|
+
* `cost` and `tokens` lazily so callers can pass `{}`. No-ops when the
|
|
49
|
+
* tracker is null/undefined.
|
|
87
50
|
*/
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
if (!quota || !modelName)
|
|
51
|
+
const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
52
|
+
if (!tracker || !usage || !modelName)
|
|
91
53
|
return;
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
outputCost: 0,
|
|
97
|
-
imageCost: 0,
|
|
98
|
-
inputTokens: 0,
|
|
99
|
-
outputTokens: 0,
|
|
100
|
-
});
|
|
101
|
-
// Entries created by older versions of this module lack the token counters.
|
|
102
|
-
if (typeof entry.inputTokens !== "number")
|
|
103
|
-
entry.inputTokens = 0;
|
|
104
|
-
if (typeof entry.outputTokens !== "number")
|
|
105
|
-
entry.outputTokens = 0;
|
|
106
|
-
if (Number.isFinite(inputCost) && inputCost > 0)
|
|
107
|
-
entry.inputCost += inputCost;
|
|
108
|
-
if (Number.isFinite(outputCost) && outputCost > 0)
|
|
109
|
-
entry.outputCost += outputCost;
|
|
110
|
-
if (Number.isFinite(imageCost) && imageCost > 0)
|
|
111
|
-
entry.imageCost += imageCost;
|
|
112
|
-
if (Number.isFinite(inputTokens) && inputTokens > 0)
|
|
113
|
-
entry.inputTokens += inputTokens;
|
|
114
|
-
if (Number.isFinite(outputTokens) && outputTokens > 0)
|
|
115
|
-
entry.outputTokens += outputTokens;
|
|
116
|
-
};
|
|
117
|
-
/**
|
|
118
|
-
* Computes and records the USD cost of a text LLM call from the
|
|
119
|
-
* `<model>-input-token-costs` / `<model>-output-token-costs` constants
|
|
120
|
-
* (dollars per million tokens). Updates both the session total
|
|
121
|
-
* (`quota.usedThisSession` / `quota.remaining`) and the per-model breakdown
|
|
122
|
-
* (`quota.byModel`).
|
|
123
|
-
*/
|
|
124
|
-
const recordTextCost = (modelName, usage, config) => {
|
|
125
|
-
if (!usage)
|
|
126
|
-
return;
|
|
127
|
-
const inputRate = getModelRate(modelName, config, "input-token") || 0;
|
|
128
|
-
const outputRate = getModelRate(modelName, config, "output-token") || 0;
|
|
54
|
+
if (typeof tracker.cost !== "number")
|
|
55
|
+
tracker.cost = 0;
|
|
56
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
57
|
+
tracker.tokens = {};
|
|
129
58
|
const inputTokens = usage.input_tokens || 0;
|
|
130
59
|
const outputTokens = usage.output_tokens || 0;
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const recordImageCost = (modelName, numImages, config) => {
|
|
142
|
-
const rate = getModelRate(modelName, config, "image") || 0;
|
|
143
|
-
const imageCost = (numImages || 0) * rate;
|
|
144
|
-
recordCost(imageCost);
|
|
145
|
-
recordModelBreakdown(modelName, 0, 0, imageCost);
|
|
60
|
+
const inputRate = getModelRate(modelName, config, "input-token");
|
|
61
|
+
const outputRate = getModelRate(modelName, config, "output-token");
|
|
62
|
+
const addedCost = (inputTokens / 1_000_000) * inputRate +
|
|
63
|
+
(outputTokens / 1_000_000) * outputRate;
|
|
64
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
65
|
+
tracker.cost += addedCost;
|
|
66
|
+
const inputKey = `${modelName}_inputTokens`;
|
|
67
|
+
const outputKey = `${modelName}_outputTokens`;
|
|
68
|
+
tracker.tokens[inputKey] = (tracker.tokens[inputKey] || 0) + inputTokens;
|
|
69
|
+
tracker.tokens[outputKey] = (tracker.tokens[outputKey] || 0) + outputTokens;
|
|
146
70
|
};
|
|
147
71
|
/**
|
|
148
72
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -541,9 +465,10 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
541
465
|
* number of agent steps (defaults to `25` when not specified)
|
|
542
466
|
* @param onProgress - Optional callback invoked with progress event objects throughout
|
|
543
467
|
* agent execution
|
|
468
|
+
* @param usageTracker - Optional usage accumulator updated on successful completion
|
|
544
469
|
* @returns The raw response object returned by the agent's `invoke` method
|
|
545
470
|
*/
|
|
546
|
-
const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
471
|
+
const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null) => {
|
|
547
472
|
const startTime = Date.now();
|
|
548
473
|
const callbacks = onProgress
|
|
549
474
|
? [
|
|
@@ -578,13 +503,12 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
578
503
|
// many internal LLM calls, but a rate limit surfaces as a thrown error from
|
|
579
504
|
// the wrapping invoke. On retry we restart the agent run from scratch — any
|
|
580
505
|
// partial progress (tool calls, intermediate messages) is discarded, since
|
|
581
|
-
// the agent state isn't externally checkpointed.
|
|
506
|
+
// the agent state isn't externally checkpointed. Usage is only recorded on a
|
|
582
507
|
// successful completion. Matches the behavior of the non-agentic paths.
|
|
583
508
|
let response;
|
|
584
509
|
let rateLimitAttempt = 0;
|
|
585
510
|
while (true) {
|
|
586
511
|
try {
|
|
587
|
-
assertQuotaAvailable();
|
|
588
512
|
response = await agent.invoke({
|
|
589
513
|
messages: [{ role: "user", content: prompt }],
|
|
590
514
|
}, {
|
|
@@ -611,7 +535,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
611
535
|
throw err;
|
|
612
536
|
}
|
|
613
537
|
}
|
|
614
|
-
|
|
538
|
+
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
615
539
|
const endTime = Date.now();
|
|
616
540
|
const duration = endTime - startTime;
|
|
617
541
|
logger_js_1.default.log(null, logger_js_1.default.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -866,10 +790,13 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
866
790
|
* `"StackFactor"`
|
|
867
791
|
* @param tools - Additional LangChain tools available to the agent in agentic mode;
|
|
868
792
|
* defaults to an empty array
|
|
793
|
+
* @param usageTracker - Optional accumulator updated with `cost` and per-model token
|
|
794
|
+
* counts for every successful LLM call. Callers should initialise as `{}` before
|
|
795
|
+
* the run and read the populated values after.
|
|
869
796
|
* @returns The LLM response as a JSON string (when `expectsJsonResponse` is `true`) or
|
|
870
797
|
* as raw content (when `false`)
|
|
871
798
|
*/
|
|
872
|
-
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = []) => {
|
|
799
|
+
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = [], usageTracker = null) => {
|
|
873
800
|
// Agentic mode: use an agent with tools instead of simple LLM invocation
|
|
874
801
|
if (config.agentic === true) {
|
|
875
802
|
// Extract system prompt and user prompt from the prompt parameter
|
|
@@ -903,7 +830,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
903
830
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
904
831
|
config);
|
|
905
832
|
// Run the agent with progress callback
|
|
906
|
-
const response = await runAgent(agent, userPrompt, config, onProgressReport || null);
|
|
833
|
+
const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
|
|
907
834
|
// Extract content from agent response
|
|
908
835
|
const messages = response?.messages || [];
|
|
909
836
|
if (messages.length === 0) {
|
|
@@ -1012,8 +939,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1012
939
|
// callback — same "Generating content..." message and the progress curve
|
|
1013
940
|
// continues from the original startTime so the bar doesn't visibly reset.
|
|
1014
941
|
// Skipped when OpenAI native response_format is in use (a 422 there is a
|
|
1015
|
-
// server-side schema bug, not a model output issue). Each attempt
|
|
1016
|
-
// independently
|
|
942
|
+
// server-side schema bug, not a model output issue). Each attempt accumulates
|
|
943
|
+
// independently into usageTracker so retry usage is still visible.
|
|
1017
944
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1018
945
|
const progressReportInterval = 10; // Report every N chunks
|
|
1019
946
|
const overallStartTime = Date.now();
|
|
@@ -1034,16 +961,15 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1034
961
|
let chunkCount = 0;
|
|
1035
962
|
let streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1036
963
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1037
|
-
//
|
|
1038
|
-
// out with a rate limit are not
|
|
1039
|
-
// discards the partial output and restarts cleanly.
|
|
964
|
+
// Usage is only recorded on a successful stream — partial streams that
|
|
965
|
+
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
966
|
+
// simply discards the partial output and restarts cleanly.
|
|
1040
967
|
let rateLimitAttempt = 0;
|
|
1041
968
|
while (true) {
|
|
1042
969
|
rawContent = "";
|
|
1043
970
|
chunkCount = 0;
|
|
1044
971
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1045
972
|
try {
|
|
1046
|
-
assertQuotaAvailable();
|
|
1047
973
|
// Honour caller cancellation: passing the signal tears down the
|
|
1048
974
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1049
975
|
const abortSignal = (0, runtimeContext_js_1.getAbortSignal)();
|
|
@@ -1086,7 +1012,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1086
1012
|
throw err;
|
|
1087
1013
|
}
|
|
1088
1014
|
}
|
|
1089
|
-
|
|
1015
|
+
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1090
1016
|
if (!rawContent) {
|
|
1091
1017
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1092
1018
|
}
|
|
@@ -1159,7 +1085,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1159
1085
|
messagesToSend = messages;
|
|
1160
1086
|
}
|
|
1161
1087
|
// Retry once on schema validation failure (see streaming branch for rationale).
|
|
1162
|
-
// Each attempt
|
|
1088
|
+
// Each attempt accumulates independently into usageTracker.
|
|
1163
1089
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1164
1090
|
let activeMessages = messagesToSend;
|
|
1165
1091
|
let attempt = 0;
|
|
@@ -1170,7 +1096,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1170
1096
|
let rateLimitAttempt = 0;
|
|
1171
1097
|
while (true) {
|
|
1172
1098
|
try {
|
|
1173
|
-
assertQuotaAvailable();
|
|
1174
1099
|
response = await llm.invoke(activeMessages);
|
|
1175
1100
|
break;
|
|
1176
1101
|
}
|
|
@@ -1186,7 +1111,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1186
1111
|
throw err;
|
|
1187
1112
|
}
|
|
1188
1113
|
}
|
|
1189
|
-
|
|
1114
|
+
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1190
1115
|
const rawContent = response?.content || response;
|
|
1191
1116
|
// If not expecting JSON, return raw content directly
|
|
1192
1117
|
if (!expectsJsonResponse) {
|
|
@@ -1292,9 +1217,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1292
1217
|
requestParams.style = style;
|
|
1293
1218
|
}
|
|
1294
1219
|
}
|
|
1295
|
-
assertQuotaAvailable();
|
|
1296
1220
|
const response = await openai.images.generate(requestParams);
|
|
1297
|
-
recordImageCost(modelName, response.data?.length || n, config);
|
|
1298
1221
|
// Format response based on number of images
|
|
1299
1222
|
if (n === 1) {
|
|
1300
1223
|
const imageData = response.data[0];
|
|
@@ -1392,7 +1315,6 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1392
1315
|
safetySettings: safetySettings,
|
|
1393
1316
|
},
|
|
1394
1317
|
};
|
|
1395
|
-
assertQuotaAvailable();
|
|
1396
1318
|
const response = await ai.models.generateContent(req);
|
|
1397
1319
|
// Extract images from response
|
|
1398
1320
|
const images = [];
|
|
@@ -1411,7 +1333,6 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1411
1333
|
if (images.length === 0) {
|
|
1412
1334
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
|
|
1413
1335
|
}
|
|
1414
|
-
recordImageCost(modelName, images.length, config);
|
|
1415
1336
|
if (numberOfImages === 1 || images.length === 1) {
|
|
1416
1337
|
return images[0];
|
|
1417
1338
|
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { constants };
|
|
|
9
9
|
export { errorHandling, AppError };
|
|
10
10
|
export type { ParsedError } from "./errorHandling.js";
|
|
11
11
|
export { langChain };
|
|
12
|
+
export type { UsageTracker } from "./langChain.js";
|
|
12
13
|
export { logger };
|
|
13
14
|
export { serve };
|
|
14
15
|
export { callAgent };
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/esm/langChain.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accumulated usage and cost for a single agent run. Callers initialize an
|
|
3
|
+
* empty object (`{}`) and pass it through `runPromptWithModel`; helpers
|
|
4
|
+
* populate `cost` (USD total across all models) and per-model
|
|
5
|
+
* `<model>_inputTokens` / `<model>_outputTokens` counters under `tokens`.
|
|
6
|
+
*/
|
|
7
|
+
export type UsageTracker = {
|
|
8
|
+
cost: number;
|
|
9
|
+
tokens: {
|
|
10
|
+
[tokenKey: string]: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
1
13
|
declare const _default: {
|
|
2
14
|
checkIfAIProviderConfigured: (config: any) => void;
|
|
3
15
|
createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
|
|
4
|
-
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null) => Promise<any>;
|
|
5
|
-
runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[]) => Promise<any>;
|
|
16
|
+
runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null, usageTracker?: UsageTracker | null) => Promise<any>;
|
|
17
|
+
runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[], usageTracker?: UsageTracker | null) => Promise<any>;
|
|
6
18
|
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any) => Promise<any>;
|
|
7
19
|
throwErrorIfNotSuccessful: (response: any) => string;
|
|
8
20
|
validateModel: (selectedModel: string, supportedModels: string[]) => string;
|
|
@@ -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":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBAkd/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAmsBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAt+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA2gCT,wBAQE"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -18,20 +18,6 @@ CRITICAL - Your response must be valid JSON. Escape ALL special characters in st
|
|
|
18
18
|
- Backslashes → \\\\
|
|
19
19
|
Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
|
|
20
20
|
`.trim();
|
|
21
|
-
/**
|
|
22
|
-
* Reads `global.quota` (set by the StackFactor host before invoking `main()`) and
|
|
23
|
-
* throws a `PAYMENT_REQUIRED` error when the session has no remaining budget. No-op
|
|
24
|
-
* when `global.quota` is absent (e.g. local tests) or `remaining` is not a number,
|
|
25
|
-
* which keeps the library usable outside the StackFactor runtime.
|
|
26
|
-
*/
|
|
27
|
-
const assertQuotaAvailable = () => {
|
|
28
|
-
const quota = globalThis.quota;
|
|
29
|
-
if (!quota || typeof quota.remaining !== "number")
|
|
30
|
-
return;
|
|
31
|
-
if (quota.remaining <= 0) {
|
|
32
|
-
throw errorHandlingHelper.create(constants.HTTP_CODES.PAYMENT_REQUIRED, constants.ERROR.QUOTA_EXHAUSTED);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
21
|
const checkIfAIProviderConfigured = (config) => {
|
|
36
22
|
if (!config ||
|
|
37
23
|
!config.openAIAPIKey ||
|
|
@@ -40,104 +26,42 @@ const checkIfAIProviderConfigured = (config) => {
|
|
|
40
26
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "The integration is not properly configured with all AI providers.");
|
|
41
27
|
}
|
|
42
28
|
};
|
|
43
|
-
/**
|
|
44
|
-
* Subtracts the given USD cost from `global.quota.remaining` and adds it to
|
|
45
|
-
* `global.quota.usedThisSession`. Silently no-ops when the quota globals are not
|
|
46
|
-
* present so callers do not need to branch. Negative, zero, or non-finite costs
|
|
47
|
-
* are ignored to keep counters monotonic.
|
|
48
|
-
*/
|
|
49
|
-
const recordCost = (cost) => {
|
|
50
|
-
if (!cost || cost <= 0 || !Number.isFinite(cost))
|
|
51
|
-
return;
|
|
52
|
-
const quota = globalThis.quota;
|
|
53
|
-
if (!quota)
|
|
54
|
-
return;
|
|
55
|
-
if (typeof quota.remaining === "number") {
|
|
56
|
-
quota.remaining -= cost;
|
|
57
|
-
}
|
|
58
|
-
if (typeof quota.usedThisSession === "number") {
|
|
59
|
-
quota.usedThisSession += cost;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
quota.usedThisSession = cost;
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
29
|
/**
|
|
66
30
|
* Reads a per-model rate from the flat cost constants the integration defines
|
|
67
31
|
* in its `config.yaml` (exposed on the config object), e.g.
|
|
68
|
-
* `claude-opus-4-7-input-token-costs`. Returns `
|
|
69
|
-
* absent or not numeric,
|
|
70
|
-
* call still succeeds — pricing is the integration's configuration concern).
|
|
32
|
+
* `claude-opus-4-7-input-token-costs`. Returns `0` when the constant is
|
|
33
|
+
* absent or not numeric, which leaves cost contribution zero.
|
|
71
34
|
*/
|
|
72
35
|
const getModelRate = (modelName, config, kind) => {
|
|
73
36
|
if (!config || !modelName)
|
|
74
|
-
return
|
|
37
|
+
return 0;
|
|
75
38
|
const rate = Number(config[`${modelName}-${kind}-costs`]);
|
|
76
|
-
return Number.isFinite(rate) ? rate :
|
|
39
|
+
return Number.isFinite(rate) ? rate : 0;
|
|
77
40
|
};
|
|
78
41
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
42
|
+
* Adds a single call's usage to the caller-supplied tracker. Initializes
|
|
43
|
+
* `cost` and `tokens` lazily so callers can pass `{}`. No-ops when the
|
|
44
|
+
* tracker is null/undefined.
|
|
82
45
|
*/
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
if (!quota || !modelName)
|
|
46
|
+
const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
47
|
+
if (!tracker || !usage || !modelName)
|
|
86
48
|
return;
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
outputCost: 0,
|
|
92
|
-
imageCost: 0,
|
|
93
|
-
inputTokens: 0,
|
|
94
|
-
outputTokens: 0,
|
|
95
|
-
});
|
|
96
|
-
// Entries created by older versions of this module lack the token counters.
|
|
97
|
-
if (typeof entry.inputTokens !== "number")
|
|
98
|
-
entry.inputTokens = 0;
|
|
99
|
-
if (typeof entry.outputTokens !== "number")
|
|
100
|
-
entry.outputTokens = 0;
|
|
101
|
-
if (Number.isFinite(inputCost) && inputCost > 0)
|
|
102
|
-
entry.inputCost += inputCost;
|
|
103
|
-
if (Number.isFinite(outputCost) && outputCost > 0)
|
|
104
|
-
entry.outputCost += outputCost;
|
|
105
|
-
if (Number.isFinite(imageCost) && imageCost > 0)
|
|
106
|
-
entry.imageCost += imageCost;
|
|
107
|
-
if (Number.isFinite(inputTokens) && inputTokens > 0)
|
|
108
|
-
entry.inputTokens += inputTokens;
|
|
109
|
-
if (Number.isFinite(outputTokens) && outputTokens > 0)
|
|
110
|
-
entry.outputTokens += outputTokens;
|
|
111
|
-
};
|
|
112
|
-
/**
|
|
113
|
-
* Computes and records the USD cost of a text LLM call from the
|
|
114
|
-
* `<model>-input-token-costs` / `<model>-output-token-costs` constants
|
|
115
|
-
* (dollars per million tokens). Updates both the session total
|
|
116
|
-
* (`quota.usedThisSession` / `quota.remaining`) and the per-model breakdown
|
|
117
|
-
* (`quota.byModel`).
|
|
118
|
-
*/
|
|
119
|
-
const recordTextCost = (modelName, usage, config) => {
|
|
120
|
-
if (!usage)
|
|
121
|
-
return;
|
|
122
|
-
const inputRate = getModelRate(modelName, config, "input-token") || 0;
|
|
123
|
-
const outputRate = getModelRate(modelName, config, "output-token") || 0;
|
|
49
|
+
if (typeof tracker.cost !== "number")
|
|
50
|
+
tracker.cost = 0;
|
|
51
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
52
|
+
tracker.tokens = {};
|
|
124
53
|
const inputTokens = usage.input_tokens || 0;
|
|
125
54
|
const outputTokens = usage.output_tokens || 0;
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const recordImageCost = (modelName, numImages, config) => {
|
|
137
|
-
const rate = getModelRate(modelName, config, "image") || 0;
|
|
138
|
-
const imageCost = (numImages || 0) * rate;
|
|
139
|
-
recordCost(imageCost);
|
|
140
|
-
recordModelBreakdown(modelName, 0, 0, imageCost);
|
|
55
|
+
const inputRate = getModelRate(modelName, config, "input-token");
|
|
56
|
+
const outputRate = getModelRate(modelName, config, "output-token");
|
|
57
|
+
const addedCost = (inputTokens / 1_000_000) * inputRate +
|
|
58
|
+
(outputTokens / 1_000_000) * outputRate;
|
|
59
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
60
|
+
tracker.cost += addedCost;
|
|
61
|
+
const inputKey = `${modelName}_inputTokens`;
|
|
62
|
+
const outputKey = `${modelName}_outputTokens`;
|
|
63
|
+
tracker.tokens[inputKey] = (tracker.tokens[inputKey] || 0) + inputTokens;
|
|
64
|
+
tracker.tokens[outputKey] = (tracker.tokens[outputKey] || 0) + outputTokens;
|
|
141
65
|
};
|
|
142
66
|
/**
|
|
143
67
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -536,9 +460,10 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
536
460
|
* number of agent steps (defaults to `25` when not specified)
|
|
537
461
|
* @param onProgress - Optional callback invoked with progress event objects throughout
|
|
538
462
|
* agent execution
|
|
463
|
+
* @param usageTracker - Optional usage accumulator updated on successful completion
|
|
539
464
|
* @returns The raw response object returned by the agent's `invoke` method
|
|
540
465
|
*/
|
|
541
|
-
const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
466
|
+
const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null) => {
|
|
542
467
|
const startTime = Date.now();
|
|
543
468
|
const callbacks = onProgress
|
|
544
469
|
? [
|
|
@@ -573,13 +498,12 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
573
498
|
// many internal LLM calls, but a rate limit surfaces as a thrown error from
|
|
574
499
|
// the wrapping invoke. On retry we restart the agent run from scratch — any
|
|
575
500
|
// partial progress (tool calls, intermediate messages) is discarded, since
|
|
576
|
-
// the agent state isn't externally checkpointed.
|
|
501
|
+
// the agent state isn't externally checkpointed. Usage is only recorded on a
|
|
577
502
|
// successful completion. Matches the behavior of the non-agentic paths.
|
|
578
503
|
let response;
|
|
579
504
|
let rateLimitAttempt = 0;
|
|
580
505
|
while (true) {
|
|
581
506
|
try {
|
|
582
|
-
assertQuotaAvailable();
|
|
583
507
|
response = await agent.invoke({
|
|
584
508
|
messages: [{ role: "user", content: prompt }],
|
|
585
509
|
}, {
|
|
@@ -606,7 +530,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
606
530
|
throw err;
|
|
607
531
|
}
|
|
608
532
|
}
|
|
609
|
-
|
|
533
|
+
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
610
534
|
const endTime = Date.now();
|
|
611
535
|
const duration = endTime - startTime;
|
|
612
536
|
logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -861,10 +785,13 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
861
785
|
* `"StackFactor"`
|
|
862
786
|
* @param tools - Additional LangChain tools available to the agent in agentic mode;
|
|
863
787
|
* defaults to an empty array
|
|
788
|
+
* @param usageTracker - Optional accumulator updated with `cost` and per-model token
|
|
789
|
+
* counts for every successful LLM call. Callers should initialise as `{}` before
|
|
790
|
+
* the run and read the populated values after.
|
|
864
791
|
* @returns The LLM response as a JSON string (when `expectsJsonResponse` is `true`) or
|
|
865
792
|
* as raw content (when `false`)
|
|
866
793
|
*/
|
|
867
|
-
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = []) => {
|
|
794
|
+
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = [], usageTracker = null) => {
|
|
868
795
|
// Agentic mode: use an agent with tools instead of simple LLM invocation
|
|
869
796
|
if (config.agentic === true) {
|
|
870
797
|
// Extract system prompt and user prompt from the prompt parameter
|
|
@@ -898,7 +825,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
898
825
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
899
826
|
config);
|
|
900
827
|
// Run the agent with progress callback
|
|
901
|
-
const response = await runAgent(agent, userPrompt, config, onProgressReport || null);
|
|
828
|
+
const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
|
|
902
829
|
// Extract content from agent response
|
|
903
830
|
const messages = response?.messages || [];
|
|
904
831
|
if (messages.length === 0) {
|
|
@@ -1007,8 +934,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1007
934
|
// callback — same "Generating content..." message and the progress curve
|
|
1008
935
|
// continues from the original startTime so the bar doesn't visibly reset.
|
|
1009
936
|
// Skipped when OpenAI native response_format is in use (a 422 there is a
|
|
1010
|
-
// server-side schema bug, not a model output issue). Each attempt
|
|
1011
|
-
// independently
|
|
937
|
+
// server-side schema bug, not a model output issue). Each attempt accumulates
|
|
938
|
+
// independently into usageTracker so retry usage is still visible.
|
|
1012
939
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1013
940
|
const progressReportInterval = 10; // Report every N chunks
|
|
1014
941
|
const overallStartTime = Date.now();
|
|
@@ -1029,16 +956,15 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1029
956
|
let chunkCount = 0;
|
|
1030
957
|
let streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1031
958
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1032
|
-
//
|
|
1033
|
-
// out with a rate limit are not
|
|
1034
|
-
// discards the partial output and restarts cleanly.
|
|
959
|
+
// Usage is only recorded on a successful stream — partial streams that
|
|
960
|
+
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
961
|
+
// simply discards the partial output and restarts cleanly.
|
|
1035
962
|
let rateLimitAttempt = 0;
|
|
1036
963
|
while (true) {
|
|
1037
964
|
rawContent = "";
|
|
1038
965
|
chunkCount = 0;
|
|
1039
966
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1040
967
|
try {
|
|
1041
|
-
assertQuotaAvailable();
|
|
1042
968
|
// Honour caller cancellation: passing the signal tears down the
|
|
1043
969
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1044
970
|
const abortSignal = getAbortSignal();
|
|
@@ -1081,7 +1007,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1081
1007
|
throw err;
|
|
1082
1008
|
}
|
|
1083
1009
|
}
|
|
1084
|
-
|
|
1010
|
+
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1085
1011
|
if (!rawContent) {
|
|
1086
1012
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1087
1013
|
}
|
|
@@ -1154,7 +1080,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1154
1080
|
messagesToSend = messages;
|
|
1155
1081
|
}
|
|
1156
1082
|
// Retry once on schema validation failure (see streaming branch for rationale).
|
|
1157
|
-
// Each attempt
|
|
1083
|
+
// Each attempt accumulates independently into usageTracker.
|
|
1158
1084
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1159
1085
|
let activeMessages = messagesToSend;
|
|
1160
1086
|
let attempt = 0;
|
|
@@ -1165,7 +1091,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1165
1091
|
let rateLimitAttempt = 0;
|
|
1166
1092
|
while (true) {
|
|
1167
1093
|
try {
|
|
1168
|
-
assertQuotaAvailable();
|
|
1169
1094
|
response = await llm.invoke(activeMessages);
|
|
1170
1095
|
break;
|
|
1171
1096
|
}
|
|
@@ -1181,7 +1106,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1181
1106
|
throw err;
|
|
1182
1107
|
}
|
|
1183
1108
|
}
|
|
1184
|
-
|
|
1109
|
+
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1185
1110
|
const rawContent = response?.content || response;
|
|
1186
1111
|
// If not expecting JSON, return raw content directly
|
|
1187
1112
|
if (!expectsJsonResponse) {
|
|
@@ -1287,9 +1212,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1287
1212
|
requestParams.style = style;
|
|
1288
1213
|
}
|
|
1289
1214
|
}
|
|
1290
|
-
assertQuotaAvailable();
|
|
1291
1215
|
const response = await openai.images.generate(requestParams);
|
|
1292
|
-
recordImageCost(modelName, response.data?.length || n, config);
|
|
1293
1216
|
// Format response based on number of images
|
|
1294
1217
|
if (n === 1) {
|
|
1295
1218
|
const imageData = response.data[0];
|
|
@@ -1387,7 +1310,6 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1387
1310
|
safetySettings: safetySettings,
|
|
1388
1311
|
},
|
|
1389
1312
|
};
|
|
1390
|
-
assertQuotaAvailable();
|
|
1391
1313
|
const response = await ai.models.generateContent(req);
|
|
1392
1314
|
// Extract images from response
|
|
1393
1315
|
const images = [];
|
|
@@ -1406,7 +1328,6 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1406
1328
|
if (images.length === 0) {
|
|
1407
1329
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
|
|
1408
1330
|
}
|
|
1409
|
-
recordImageCost(modelName, images.length, config);
|
|
1410
1331
|
if (numberOfImages === 1 || images.length === 1) {
|
|
1411
1332
|
return images[0];
|
|
1412
1333
|
}
|