@stackfactor/agent-utils 1.2.6 → 1.2.8
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 +16 -3
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +138 -119
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/langChain.d.ts +16 -3
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +138 -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,10 +1,23 @@
|
|
|
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>;
|
|
6
|
-
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: 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>;
|
|
18
|
+
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
|
|
7
19
|
throwErrorIfNotSuccessful: (response: any) => string;
|
|
20
|
+
updateUsageTrackerForCharacters: (tracker: UsageTracker | null | undefined, modelName: string, characterCount: number, config: any) => void;
|
|
8
21
|
validateModel: (selectedModel: string, supportedModels: string[]) => string;
|
|
9
22
|
};
|
|
10
23
|
export default _default;
|
|
@@ -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;wBA+jB/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;sDAusBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA3+B8B,GAAG,KAAG,MAAM;+CApjB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAokBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAkhCT,wBASE"}
|
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,137 @@ 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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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;
|
|
54
|
+
if (typeof tracker.cost !== "number")
|
|
55
|
+
tracker.cost = 0;
|
|
56
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
57
|
+
tracker.tokens = {};
|
|
58
|
+
const inputTokens = usage.input_tokens || 0;
|
|
59
|
+
const outputTokens = usage.output_tokens || 0;
|
|
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;
|
|
116
70
|
};
|
|
117
71
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* (`
|
|
72
|
+
* Adds a single image-generation call's usage to the caller-supplied tracker.
|
|
73
|
+
* Extracts token counts from provider-specific response shapes: OpenAI returns
|
|
74
|
+
* `response.usage` with `input_tokens_details.{text_tokens,image_tokens}` and
|
|
75
|
+
* `output_tokens`; Google returns `response.usageMetadata` with
|
|
76
|
+
* `promptTokensDetails` (modality-keyed) and `candidatesTokenCount` for the
|
|
77
|
+
* generated image. Reads rates from `<model>-input-token-costs`,
|
|
78
|
+
* `<model>-image-input-token-costs`, and `<model>-image-output-token-costs`.
|
|
79
|
+
* No-ops when the tracker or response usage info is absent.
|
|
123
80
|
*/
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
81
|
+
const updateImageUsageTracker = (tracker, modelName, response, config, provider) => {
|
|
82
|
+
if (!tracker || !modelName || !response)
|
|
126
83
|
return;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
84
|
+
if (typeof tracker.cost !== "number")
|
|
85
|
+
tracker.cost = 0;
|
|
86
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
87
|
+
tracker.tokens = {};
|
|
88
|
+
let textInputTokens = 0;
|
|
89
|
+
let imageInputTokens = 0;
|
|
90
|
+
let imageOutputTokens = 0;
|
|
91
|
+
if (provider === "openai") {
|
|
92
|
+
const usage = response.usage;
|
|
93
|
+
if (!usage)
|
|
94
|
+
return;
|
|
95
|
+
const details = usage.input_tokens_details || {};
|
|
96
|
+
textInputTokens = details.text_tokens || 0;
|
|
97
|
+
imageInputTokens = details.image_tokens || 0;
|
|
98
|
+
// Older shapes report a single input_tokens without modality breakdown.
|
|
99
|
+
if (!textInputTokens && !imageInputTokens && usage.input_tokens) {
|
|
100
|
+
textInputTokens = usage.input_tokens;
|
|
101
|
+
}
|
|
102
|
+
imageOutputTokens = usage.output_tokens || 0;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const um = response.usageMetadata;
|
|
106
|
+
if (!um)
|
|
107
|
+
return;
|
|
108
|
+
const promptDetails = Array.isArray(um.promptTokensDetails)
|
|
109
|
+
? um.promptTokensDetails
|
|
110
|
+
: [];
|
|
111
|
+
for (const d of promptDetails) {
|
|
112
|
+
const modality = String(d?.modality || "").toUpperCase();
|
|
113
|
+
const count = Number(d?.tokenCount) || 0;
|
|
114
|
+
if (modality === "IMAGE")
|
|
115
|
+
imageInputTokens += count;
|
|
116
|
+
else
|
|
117
|
+
textInputTokens += count;
|
|
118
|
+
}
|
|
119
|
+
if (!textInputTokens && !imageInputTokens) {
|
|
120
|
+
textInputTokens = Number(um.promptTokenCount) || 0;
|
|
121
|
+
}
|
|
122
|
+
imageOutputTokens = Number(um.candidatesTokenCount) || 0;
|
|
123
|
+
}
|
|
124
|
+
const textInputRate = getModelRate(modelName, config, "input-token");
|
|
125
|
+
const imageInputRate = getModelRate(modelName, config, "image-input-token");
|
|
126
|
+
const imageOutputRate = getModelRate(modelName, config, "image-output-token");
|
|
127
|
+
const addedCost = (textInputTokens / 1_000_000) * textInputRate +
|
|
128
|
+
(imageInputTokens / 1_000_000) * imageInputRate +
|
|
129
|
+
(imageOutputTokens / 1_000_000) * imageOutputRate;
|
|
130
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
131
|
+
tracker.cost += addedCost;
|
|
132
|
+
if (textInputTokens > 0) {
|
|
133
|
+
const key = `${modelName}_inputTokens`;
|
|
134
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + textInputTokens;
|
|
135
|
+
}
|
|
136
|
+
if (imageInputTokens > 0) {
|
|
137
|
+
const key = `${modelName}_imageInputTokens`;
|
|
138
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageInputTokens;
|
|
139
|
+
}
|
|
140
|
+
if (imageOutputTokens > 0) {
|
|
141
|
+
const key = `${modelName}_imageOutputTokens`;
|
|
142
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageOutputTokens;
|
|
143
|
+
}
|
|
135
144
|
};
|
|
136
145
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* the
|
|
146
|
+
* Adds character-billed usage (e.g. ElevenLabs TTS) to the caller-supplied
|
|
147
|
+
* tracker. There is no provider response to parse — billing is deterministic
|
|
148
|
+
* from the input character count. Reads the rate from
|
|
149
|
+
* `<model>-character-costs` (USD per million characters) and accumulates
|
|
150
|
+
* under `<model>_characters`. No-ops on missing tracker / model / count.
|
|
140
151
|
*/
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, config) => {
|
|
153
|
+
if (!tracker || !modelName || !characterCount)
|
|
154
|
+
return;
|
|
155
|
+
if (typeof tracker.cost !== "number")
|
|
156
|
+
tracker.cost = 0;
|
|
157
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
158
|
+
tracker.tokens = {};
|
|
159
|
+
const rate = getModelRate(modelName, config, "character");
|
|
160
|
+
const addedCost = (characterCount / 1_000_000) * rate;
|
|
161
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
162
|
+
tracker.cost += addedCost;
|
|
163
|
+
const key = `${modelName}_characters`;
|
|
164
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
|
|
146
165
|
};
|
|
147
166
|
/**
|
|
148
167
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -541,9 +560,10 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
541
560
|
* number of agent steps (defaults to `25` when not specified)
|
|
542
561
|
* @param onProgress - Optional callback invoked with progress event objects throughout
|
|
543
562
|
* agent execution
|
|
563
|
+
* @param usageTracker - Optional usage accumulator updated on successful completion
|
|
544
564
|
* @returns The raw response object returned by the agent's `invoke` method
|
|
545
565
|
*/
|
|
546
|
-
const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
566
|
+
const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null) => {
|
|
547
567
|
const startTime = Date.now();
|
|
548
568
|
const callbacks = onProgress
|
|
549
569
|
? [
|
|
@@ -578,13 +598,12 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
578
598
|
// many internal LLM calls, but a rate limit surfaces as a thrown error from
|
|
579
599
|
// the wrapping invoke. On retry we restart the agent run from scratch — any
|
|
580
600
|
// partial progress (tool calls, intermediate messages) is discarded, since
|
|
581
|
-
// the agent state isn't externally checkpointed.
|
|
601
|
+
// the agent state isn't externally checkpointed. Usage is only recorded on a
|
|
582
602
|
// successful completion. Matches the behavior of the non-agentic paths.
|
|
583
603
|
let response;
|
|
584
604
|
let rateLimitAttempt = 0;
|
|
585
605
|
while (true) {
|
|
586
606
|
try {
|
|
587
|
-
assertQuotaAvailable();
|
|
588
607
|
response = await agent.invoke({
|
|
589
608
|
messages: [{ role: "user", content: prompt }],
|
|
590
609
|
}, {
|
|
@@ -611,7 +630,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
611
630
|
throw err;
|
|
612
631
|
}
|
|
613
632
|
}
|
|
614
|
-
|
|
633
|
+
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
615
634
|
const endTime = Date.now();
|
|
616
635
|
const duration = endTime - startTime;
|
|
617
636
|
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 +885,13 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
866
885
|
* `"StackFactor"`
|
|
867
886
|
* @param tools - Additional LangChain tools available to the agent in agentic mode;
|
|
868
887
|
* defaults to an empty array
|
|
888
|
+
* @param usageTracker - Optional accumulator updated with `cost` and per-model token
|
|
889
|
+
* counts for every successful LLM call. Callers should initialise as `{}` before
|
|
890
|
+
* the run and read the populated values after.
|
|
869
891
|
* @returns The LLM response as a JSON string (when `expectsJsonResponse` is `true`) or
|
|
870
892
|
* as raw content (when `false`)
|
|
871
893
|
*/
|
|
872
|
-
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = []) => {
|
|
894
|
+
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = [], usageTracker = null) => {
|
|
873
895
|
// Agentic mode: use an agent with tools instead of simple LLM invocation
|
|
874
896
|
if (config.agentic === true) {
|
|
875
897
|
// Extract system prompt and user prompt from the prompt parameter
|
|
@@ -903,7 +925,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
903
925
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
904
926
|
config);
|
|
905
927
|
// Run the agent with progress callback
|
|
906
|
-
const response = await runAgent(agent, userPrompt, config, onProgressReport || null);
|
|
928
|
+
const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
|
|
907
929
|
// Extract content from agent response
|
|
908
930
|
const messages = response?.messages || [];
|
|
909
931
|
if (messages.length === 0) {
|
|
@@ -1012,8 +1034,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1012
1034
|
// callback — same "Generating content..." message and the progress curve
|
|
1013
1035
|
// continues from the original startTime so the bar doesn't visibly reset.
|
|
1014
1036
|
// 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
|
|
1037
|
+
// server-side schema bug, not a model output issue). Each attempt accumulates
|
|
1038
|
+
// independently into usageTracker so retry usage is still visible.
|
|
1017
1039
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1018
1040
|
const progressReportInterval = 10; // Report every N chunks
|
|
1019
1041
|
const overallStartTime = Date.now();
|
|
@@ -1034,16 +1056,15 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1034
1056
|
let chunkCount = 0;
|
|
1035
1057
|
let streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1036
1058
|
// 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.
|
|
1059
|
+
// Usage is only recorded on a successful stream — partial streams that
|
|
1060
|
+
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
1061
|
+
// simply discards the partial output and restarts cleanly.
|
|
1040
1062
|
let rateLimitAttempt = 0;
|
|
1041
1063
|
while (true) {
|
|
1042
1064
|
rawContent = "";
|
|
1043
1065
|
chunkCount = 0;
|
|
1044
1066
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1045
1067
|
try {
|
|
1046
|
-
assertQuotaAvailable();
|
|
1047
1068
|
// Honour caller cancellation: passing the signal tears down the
|
|
1048
1069
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1049
1070
|
const abortSignal = (0, runtimeContext_js_1.getAbortSignal)();
|
|
@@ -1086,7 +1107,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1086
1107
|
throw err;
|
|
1087
1108
|
}
|
|
1088
1109
|
}
|
|
1089
|
-
|
|
1110
|
+
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1090
1111
|
if (!rawContent) {
|
|
1091
1112
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1092
1113
|
}
|
|
@@ -1159,7 +1180,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1159
1180
|
messagesToSend = messages;
|
|
1160
1181
|
}
|
|
1161
1182
|
// Retry once on schema validation failure (see streaming branch for rationale).
|
|
1162
|
-
// Each attempt
|
|
1183
|
+
// Each attempt accumulates independently into usageTracker.
|
|
1163
1184
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1164
1185
|
let activeMessages = messagesToSend;
|
|
1165
1186
|
let attempt = 0;
|
|
@@ -1170,7 +1191,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1170
1191
|
let rateLimitAttempt = 0;
|
|
1171
1192
|
while (true) {
|
|
1172
1193
|
try {
|
|
1173
|
-
assertQuotaAvailable();
|
|
1174
1194
|
response = await llm.invoke(activeMessages);
|
|
1175
1195
|
break;
|
|
1176
1196
|
}
|
|
@@ -1186,7 +1206,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1186
1206
|
throw err;
|
|
1187
1207
|
}
|
|
1188
1208
|
}
|
|
1189
|
-
|
|
1209
|
+
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1190
1210
|
const rawContent = response?.content || response;
|
|
1191
1211
|
// If not expecting JSON, return raw content directly
|
|
1192
1212
|
if (!expectsJsonResponse) {
|
|
@@ -1245,7 +1265,7 @@ const getImageModelProvider = (modelName) => {
|
|
|
1245
1265
|
* @returns An object with `url`, `b64_json`, and `revisedPrompt` for a single image,
|
|
1246
1266
|
* or `{ images: [...] }` for multiple images
|
|
1247
1267
|
*/
|
|
1248
|
-
const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
1268
|
+
const generateImageWithOpenAI = async (modelName, config, prompt, options, usageTracker = null) => {
|
|
1249
1269
|
const { size = "1024x1024", style = "vivid", responseFormat = "url", n = 1, } = options;
|
|
1250
1270
|
if (!config.openAIAPIKey) {
|
|
1251
1271
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "OpenAI API key is required for OpenAI image generation");
|
|
@@ -1292,9 +1312,8 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1292
1312
|
requestParams.style = style;
|
|
1293
1313
|
}
|
|
1294
1314
|
}
|
|
1295
|
-
assertQuotaAvailable();
|
|
1296
1315
|
const response = await openai.images.generate(requestParams);
|
|
1297
|
-
|
|
1316
|
+
updateImageUsageTracker(usageTracker, modelName, response, config, "openai");
|
|
1298
1317
|
// Format response based on number of images
|
|
1299
1318
|
if (n === 1) {
|
|
1300
1319
|
const imageData = response.data[0];
|
|
@@ -1332,7 +1351,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1332
1351
|
* @returns A single image descriptor `{ b64_json, mimeType }` when one image is
|
|
1333
1352
|
* requested, or `{ images: [...] }` for multiple images
|
|
1334
1353
|
*/
|
|
1335
|
-
const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
1354
|
+
const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
|
|
1336
1355
|
const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
|
|
1337
1356
|
if (!config.googleAPIKey) {
|
|
1338
1357
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
|
|
@@ -1392,8 +1411,8 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1392
1411
|
safetySettings: safetySettings,
|
|
1393
1412
|
},
|
|
1394
1413
|
};
|
|
1395
|
-
assertQuotaAvailable();
|
|
1396
1414
|
const response = await ai.models.generateContent(req);
|
|
1415
|
+
updateImageUsageTracker(usageTracker, modelName, response, config, "google");
|
|
1397
1416
|
// Extract images from response
|
|
1398
1417
|
const images = [];
|
|
1399
1418
|
const candidates = response.candidates || [];
|
|
@@ -1411,7 +1430,6 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1411
1430
|
if (images.length === 0) {
|
|
1412
1431
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
|
|
1413
1432
|
}
|
|
1414
|
-
recordImageCost(modelName, images.length, config);
|
|
1415
1433
|
if (numberOfImages === 1 || images.length === 1) {
|
|
1416
1434
|
return images[0];
|
|
1417
1435
|
}
|
|
@@ -1437,7 +1455,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1437
1455
|
* and `generateImageWithGoogle` for full option sets); defaults to `{}`
|
|
1438
1456
|
* @returns The generated image data object returned by the provider-specific function
|
|
1439
1457
|
*/
|
|
1440
|
-
const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}) => {
|
|
1458
|
+
const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
|
|
1441
1459
|
const provider = getImageModelProvider(modelName);
|
|
1442
1460
|
if (!provider) {
|
|
1443
1461
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, `Unable to determine provider for model: ${modelName}. Model name should start with 'gpt-image-', 'gemini-', or 'imagen-'.`);
|
|
@@ -1446,10 +1464,10 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
|
|
|
1446
1464
|
try {
|
|
1447
1465
|
let result;
|
|
1448
1466
|
if (provider === "openai") {
|
|
1449
|
-
result = await generateImageWithOpenAI(modelName, config, prompt, options);
|
|
1467
|
+
result = await generateImageWithOpenAI(modelName, config, prompt, options, usageTracker);
|
|
1450
1468
|
}
|
|
1451
1469
|
else if (provider === "google") {
|
|
1452
|
-
result = await generateImageWithGoogle(modelName, config, prompt, options);
|
|
1470
|
+
result = await generateImageWithGoogle(modelName, config, prompt, options, usageTracker);
|
|
1453
1471
|
}
|
|
1454
1472
|
const endTime = Date.now();
|
|
1455
1473
|
const duration = endTime - startTime;
|
|
@@ -1477,5 +1495,6 @@ exports.default = {
|
|
|
1477
1495
|
runPromptWithModel,
|
|
1478
1496
|
runPromptWithModelForImageGeneration,
|
|
1479
1497
|
throwErrorIfNotSuccessful,
|
|
1498
|
+
updateUsageTrackerForCharacters,
|
|
1480
1499
|
validateModel,
|
|
1481
1500
|
};
|
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,10 +1,23 @@
|
|
|
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>;
|
|
6
|
-
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: 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>;
|
|
18
|
+
runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
|
|
7
19
|
throwErrorIfNotSuccessful: (response: any) => string;
|
|
20
|
+
updateUsageTrackerForCharacters: (tracker: UsageTracker | null | undefined, modelName: string, characterCount: number, config: any) => void;
|
|
8
21
|
validateModel: (selectedModel: string, supportedModels: string[]) => string;
|
|
9
22
|
};
|
|
10
23
|
export default _default;
|
|
@@ -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;wBA+jB/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;sDAusBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA3+B8B,GAAG,KAAG,MAAM;+CApjB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAokBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAkhCT,wBASE"}
|
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,137 @@ 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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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;
|
|
49
|
+
if (typeof tracker.cost !== "number")
|
|
50
|
+
tracker.cost = 0;
|
|
51
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
52
|
+
tracker.tokens = {};
|
|
53
|
+
const inputTokens = usage.input_tokens || 0;
|
|
54
|
+
const outputTokens = usage.output_tokens || 0;
|
|
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;
|
|
111
65
|
};
|
|
112
66
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* (`
|
|
67
|
+
* Adds a single image-generation call's usage to the caller-supplied tracker.
|
|
68
|
+
* Extracts token counts from provider-specific response shapes: OpenAI returns
|
|
69
|
+
* `response.usage` with `input_tokens_details.{text_tokens,image_tokens}` and
|
|
70
|
+
* `output_tokens`; Google returns `response.usageMetadata` with
|
|
71
|
+
* `promptTokensDetails` (modality-keyed) and `candidatesTokenCount` for the
|
|
72
|
+
* generated image. Reads rates from `<model>-input-token-costs`,
|
|
73
|
+
* `<model>-image-input-token-costs`, and `<model>-image-output-token-costs`.
|
|
74
|
+
* No-ops when the tracker or response usage info is absent.
|
|
118
75
|
*/
|
|
119
|
-
const
|
|
120
|
-
if (!
|
|
76
|
+
const updateImageUsageTracker = (tracker, modelName, response, config, provider) => {
|
|
77
|
+
if (!tracker || !modelName || !response)
|
|
121
78
|
return;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
79
|
+
if (typeof tracker.cost !== "number")
|
|
80
|
+
tracker.cost = 0;
|
|
81
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
82
|
+
tracker.tokens = {};
|
|
83
|
+
let textInputTokens = 0;
|
|
84
|
+
let imageInputTokens = 0;
|
|
85
|
+
let imageOutputTokens = 0;
|
|
86
|
+
if (provider === "openai") {
|
|
87
|
+
const usage = response.usage;
|
|
88
|
+
if (!usage)
|
|
89
|
+
return;
|
|
90
|
+
const details = usage.input_tokens_details || {};
|
|
91
|
+
textInputTokens = details.text_tokens || 0;
|
|
92
|
+
imageInputTokens = details.image_tokens || 0;
|
|
93
|
+
// Older shapes report a single input_tokens without modality breakdown.
|
|
94
|
+
if (!textInputTokens && !imageInputTokens && usage.input_tokens) {
|
|
95
|
+
textInputTokens = usage.input_tokens;
|
|
96
|
+
}
|
|
97
|
+
imageOutputTokens = usage.output_tokens || 0;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const um = response.usageMetadata;
|
|
101
|
+
if (!um)
|
|
102
|
+
return;
|
|
103
|
+
const promptDetails = Array.isArray(um.promptTokensDetails)
|
|
104
|
+
? um.promptTokensDetails
|
|
105
|
+
: [];
|
|
106
|
+
for (const d of promptDetails) {
|
|
107
|
+
const modality = String(d?.modality || "").toUpperCase();
|
|
108
|
+
const count = Number(d?.tokenCount) || 0;
|
|
109
|
+
if (modality === "IMAGE")
|
|
110
|
+
imageInputTokens += count;
|
|
111
|
+
else
|
|
112
|
+
textInputTokens += count;
|
|
113
|
+
}
|
|
114
|
+
if (!textInputTokens && !imageInputTokens) {
|
|
115
|
+
textInputTokens = Number(um.promptTokenCount) || 0;
|
|
116
|
+
}
|
|
117
|
+
imageOutputTokens = Number(um.candidatesTokenCount) || 0;
|
|
118
|
+
}
|
|
119
|
+
const textInputRate = getModelRate(modelName, config, "input-token");
|
|
120
|
+
const imageInputRate = getModelRate(modelName, config, "image-input-token");
|
|
121
|
+
const imageOutputRate = getModelRate(modelName, config, "image-output-token");
|
|
122
|
+
const addedCost = (textInputTokens / 1_000_000) * textInputRate +
|
|
123
|
+
(imageInputTokens / 1_000_000) * imageInputRate +
|
|
124
|
+
(imageOutputTokens / 1_000_000) * imageOutputRate;
|
|
125
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
126
|
+
tracker.cost += addedCost;
|
|
127
|
+
if (textInputTokens > 0) {
|
|
128
|
+
const key = `${modelName}_inputTokens`;
|
|
129
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + textInputTokens;
|
|
130
|
+
}
|
|
131
|
+
if (imageInputTokens > 0) {
|
|
132
|
+
const key = `${modelName}_imageInputTokens`;
|
|
133
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageInputTokens;
|
|
134
|
+
}
|
|
135
|
+
if (imageOutputTokens > 0) {
|
|
136
|
+
const key = `${modelName}_imageOutputTokens`;
|
|
137
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageOutputTokens;
|
|
138
|
+
}
|
|
130
139
|
};
|
|
131
140
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* the
|
|
141
|
+
* Adds character-billed usage (e.g. ElevenLabs TTS) to the caller-supplied
|
|
142
|
+
* tracker. There is no provider response to parse — billing is deterministic
|
|
143
|
+
* from the input character count. Reads the rate from
|
|
144
|
+
* `<model>-character-costs` (USD per million characters) and accumulates
|
|
145
|
+
* under `<model>_characters`. No-ops on missing tracker / model / count.
|
|
135
146
|
*/
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, config) => {
|
|
148
|
+
if (!tracker || !modelName || !characterCount)
|
|
149
|
+
return;
|
|
150
|
+
if (typeof tracker.cost !== "number")
|
|
151
|
+
tracker.cost = 0;
|
|
152
|
+
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
153
|
+
tracker.tokens = {};
|
|
154
|
+
const rate = getModelRate(modelName, config, "character");
|
|
155
|
+
const addedCost = (characterCount / 1_000_000) * rate;
|
|
156
|
+
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
157
|
+
tracker.cost += addedCost;
|
|
158
|
+
const key = `${modelName}_characters`;
|
|
159
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
|
|
141
160
|
};
|
|
142
161
|
/**
|
|
143
162
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -536,9 +555,10 @@ const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat,
|
|
|
536
555
|
* number of agent steps (defaults to `25` when not specified)
|
|
537
556
|
* @param onProgress - Optional callback invoked with progress event objects throughout
|
|
538
557
|
* agent execution
|
|
558
|
+
* @param usageTracker - Optional usage accumulator updated on successful completion
|
|
539
559
|
* @returns The raw response object returned by the agent's `invoke` method
|
|
540
560
|
*/
|
|
541
|
-
const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
561
|
+
const runAgent = async (agent, prompt, config, onProgress = null, usageTracker = null) => {
|
|
542
562
|
const startTime = Date.now();
|
|
543
563
|
const callbacks = onProgress
|
|
544
564
|
? [
|
|
@@ -573,13 +593,12 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
573
593
|
// many internal LLM calls, but a rate limit surfaces as a thrown error from
|
|
574
594
|
// the wrapping invoke. On retry we restart the agent run from scratch — any
|
|
575
595
|
// partial progress (tool calls, intermediate messages) is discarded, since
|
|
576
|
-
// the agent state isn't externally checkpointed.
|
|
596
|
+
// the agent state isn't externally checkpointed. Usage is only recorded on a
|
|
577
597
|
// successful completion. Matches the behavior of the non-agentic paths.
|
|
578
598
|
let response;
|
|
579
599
|
let rateLimitAttempt = 0;
|
|
580
600
|
while (true) {
|
|
581
601
|
try {
|
|
582
|
-
assertQuotaAvailable();
|
|
583
602
|
response = await agent.invoke({
|
|
584
603
|
messages: [{ role: "user", content: prompt }],
|
|
585
604
|
}, {
|
|
@@ -606,7 +625,7 @@ const runAgent = async (agent, prompt, config, onProgress = null) => {
|
|
|
606
625
|
throw err;
|
|
607
626
|
}
|
|
608
627
|
}
|
|
609
|
-
|
|
628
|
+
updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
|
|
610
629
|
const endTime = Date.now();
|
|
611
630
|
const duration = endTime - startTime;
|
|
612
631
|
logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
|
|
@@ -861,10 +880,13 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
861
880
|
* `"StackFactor"`
|
|
862
881
|
* @param tools - Additional LangChain tools available to the agent in agentic mode;
|
|
863
882
|
* defaults to an empty array
|
|
883
|
+
* @param usageTracker - Optional accumulator updated with `cost` and per-model token
|
|
884
|
+
* counts for every successful LLM call. Callers should initialise as `{}` before
|
|
885
|
+
* the run and read the populated values after.
|
|
864
886
|
* @returns The LLM response as a JSON string (when `expectsJsonResponse` is `true`) or
|
|
865
887
|
* as raw content (when `false`)
|
|
866
888
|
*/
|
|
867
|
-
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = []) => {
|
|
889
|
+
const runPromptWithModel = async (modelName, config, prompt, onProgressReport, minPercent = 0, maxPercent = 100, expectsJsonResponse = true, schema = null, agentName = "StackFactor", tools = [], usageTracker = null) => {
|
|
868
890
|
// Agentic mode: use an agent with tools instead of simple LLM invocation
|
|
869
891
|
if (config.agentic === true) {
|
|
870
892
|
// Extract system prompt and user prompt from the prompt parameter
|
|
@@ -898,7 +920,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
898
920
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
899
921
|
config);
|
|
900
922
|
// Run the agent with progress callback
|
|
901
|
-
const response = await runAgent(agent, userPrompt, config, onProgressReport || null);
|
|
923
|
+
const response = await runAgent(agent, userPrompt, config, onProgressReport || null, usageTracker);
|
|
902
924
|
// Extract content from agent response
|
|
903
925
|
const messages = response?.messages || [];
|
|
904
926
|
if (messages.length === 0) {
|
|
@@ -1007,8 +1029,8 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1007
1029
|
// callback — same "Generating content..." message and the progress curve
|
|
1008
1030
|
// continues from the original startTime so the bar doesn't visibly reset.
|
|
1009
1031
|
// 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
|
|
1032
|
+
// server-side schema bug, not a model output issue). Each attempt accumulates
|
|
1033
|
+
// independently into usageTracker so retry usage is still visible.
|
|
1012
1034
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1013
1035
|
const progressReportInterval = 10; // Report every N chunks
|
|
1014
1036
|
const overallStartTime = Date.now();
|
|
@@ -1029,16 +1051,15 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1029
1051
|
let chunkCount = 0;
|
|
1030
1052
|
let streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1031
1053
|
// 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.
|
|
1054
|
+
// Usage is only recorded on a successful stream — partial streams that
|
|
1055
|
+
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
1056
|
+
// simply discards the partial output and restarts cleanly.
|
|
1035
1057
|
let rateLimitAttempt = 0;
|
|
1036
1058
|
while (true) {
|
|
1037
1059
|
rawContent = "";
|
|
1038
1060
|
chunkCount = 0;
|
|
1039
1061
|
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1040
1062
|
try {
|
|
1041
|
-
assertQuotaAvailable();
|
|
1042
1063
|
// Honour caller cancellation: passing the signal tears down the
|
|
1043
1064
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
1044
1065
|
const abortSignal = getAbortSignal();
|
|
@@ -1081,7 +1102,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1081
1102
|
throw err;
|
|
1082
1103
|
}
|
|
1083
1104
|
}
|
|
1084
|
-
|
|
1105
|
+
updateUsageTracker(usageTracker, modelName, streamUsage, config);
|
|
1085
1106
|
if (!rawContent) {
|
|
1086
1107
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
|
|
1087
1108
|
}
|
|
@@ -1154,7 +1175,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1154
1175
|
messagesToSend = messages;
|
|
1155
1176
|
}
|
|
1156
1177
|
// Retry once on schema validation failure (see streaming branch for rationale).
|
|
1157
|
-
// Each attempt
|
|
1178
|
+
// Each attempt accumulates independently into usageTracker.
|
|
1158
1179
|
const canRetryOnValidation = expectsJsonResponse && !!schema && !useNativeSchema;
|
|
1159
1180
|
let activeMessages = messagesToSend;
|
|
1160
1181
|
let attempt = 0;
|
|
@@ -1165,7 +1186,6 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1165
1186
|
let rateLimitAttempt = 0;
|
|
1166
1187
|
while (true) {
|
|
1167
1188
|
try {
|
|
1168
|
-
assertQuotaAvailable();
|
|
1169
1189
|
response = await llm.invoke(activeMessages);
|
|
1170
1190
|
break;
|
|
1171
1191
|
}
|
|
@@ -1181,7 +1201,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1181
1201
|
throw err;
|
|
1182
1202
|
}
|
|
1183
1203
|
}
|
|
1184
|
-
|
|
1204
|
+
updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
|
|
1185
1205
|
const rawContent = response?.content || response;
|
|
1186
1206
|
// If not expecting JSON, return raw content directly
|
|
1187
1207
|
if (!expectsJsonResponse) {
|
|
@@ -1240,7 +1260,7 @@ const getImageModelProvider = (modelName) => {
|
|
|
1240
1260
|
* @returns An object with `url`, `b64_json`, and `revisedPrompt` for a single image,
|
|
1241
1261
|
* or `{ images: [...] }` for multiple images
|
|
1242
1262
|
*/
|
|
1243
|
-
const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
1263
|
+
const generateImageWithOpenAI = async (modelName, config, prompt, options, usageTracker = null) => {
|
|
1244
1264
|
const { size = "1024x1024", style = "vivid", responseFormat = "url", n = 1, } = options;
|
|
1245
1265
|
if (!config.openAIAPIKey) {
|
|
1246
1266
|
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "OpenAI API key is required for OpenAI image generation");
|
|
@@ -1287,9 +1307,8 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1287
1307
|
requestParams.style = style;
|
|
1288
1308
|
}
|
|
1289
1309
|
}
|
|
1290
|
-
assertQuotaAvailable();
|
|
1291
1310
|
const response = await openai.images.generate(requestParams);
|
|
1292
|
-
|
|
1311
|
+
updateImageUsageTracker(usageTracker, modelName, response, config, "openai");
|
|
1293
1312
|
// Format response based on number of images
|
|
1294
1313
|
if (n === 1) {
|
|
1295
1314
|
const imageData = response.data[0];
|
|
@@ -1327,7 +1346,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
|
|
|
1327
1346
|
* @returns A single image descriptor `{ b64_json, mimeType }` when one image is
|
|
1328
1347
|
* requested, or `{ images: [...] }` for multiple images
|
|
1329
1348
|
*/
|
|
1330
|
-
const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
1349
|
+
const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
|
|
1331
1350
|
const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
|
|
1332
1351
|
if (!config.googleAPIKey) {
|
|
1333
1352
|
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
|
|
@@ -1387,8 +1406,8 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1387
1406
|
safetySettings: safetySettings,
|
|
1388
1407
|
},
|
|
1389
1408
|
};
|
|
1390
|
-
assertQuotaAvailable();
|
|
1391
1409
|
const response = await ai.models.generateContent(req);
|
|
1410
|
+
updateImageUsageTracker(usageTracker, modelName, response, config, "google");
|
|
1392
1411
|
// Extract images from response
|
|
1393
1412
|
const images = [];
|
|
1394
1413
|
const candidates = response.candidates || [];
|
|
@@ -1406,7 +1425,6 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1406
1425
|
if (images.length === 0) {
|
|
1407
1426
|
throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `No images were generated by ${modelName}`);
|
|
1408
1427
|
}
|
|
1409
|
-
recordImageCost(modelName, images.length, config);
|
|
1410
1428
|
if (numberOfImages === 1 || images.length === 1) {
|
|
1411
1429
|
return images[0];
|
|
1412
1430
|
}
|
|
@@ -1432,7 +1450,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
|
|
|
1432
1450
|
* and `generateImageWithGoogle` for full option sets); defaults to `{}`
|
|
1433
1451
|
* @returns The generated image data object returned by the provider-specific function
|
|
1434
1452
|
*/
|
|
1435
|
-
const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}) => {
|
|
1453
|
+
const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
|
|
1436
1454
|
const provider = getImageModelProvider(modelName);
|
|
1437
1455
|
if (!provider) {
|
|
1438
1456
|
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, `Unable to determine provider for model: ${modelName}. Model name should start with 'gpt-image-', 'gemini-', or 'imagen-'.`);
|
|
@@ -1441,10 +1459,10 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
|
|
|
1441
1459
|
try {
|
|
1442
1460
|
let result;
|
|
1443
1461
|
if (provider === "openai") {
|
|
1444
|
-
result = await generateImageWithOpenAI(modelName, config, prompt, options);
|
|
1462
|
+
result = await generateImageWithOpenAI(modelName, config, prompt, options, usageTracker);
|
|
1445
1463
|
}
|
|
1446
1464
|
else if (provider === "google") {
|
|
1447
|
-
result = await generateImageWithGoogle(modelName, config, prompt, options);
|
|
1465
|
+
result = await generateImageWithGoogle(modelName, config, prompt, options, usageTracker);
|
|
1448
1466
|
}
|
|
1449
1467
|
const endTime = Date.now();
|
|
1450
1468
|
const duration = endTime - startTime;
|
|
@@ -1472,5 +1490,6 @@ export default {
|
|
|
1472
1490
|
runPromptWithModel,
|
|
1473
1491
|
runPromptWithModelForImageGeneration,
|
|
1474
1492
|
throwErrorIfNotSuccessful,
|
|
1493
|
+
updateUsageTrackerForCharacters,
|
|
1475
1494
|
validateModel,
|
|
1476
1495
|
};
|