@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 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
  ---
@@ -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 };
@@ -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"}
@@ -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":";0CAiD6C,GAAG,KAAG,IAAI;wBA0hB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;oCAqXF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDAosBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAn+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAwgCT,wBAQE"}
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"}
@@ -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 `null` when the constant is
74
- * absent or not numeric, in which case cost recording becomes a no-op (the
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 null;
42
+ return 0;
80
43
  const rate = Number(config[`${modelName}-${kind}-costs`]);
81
- return Number.isFinite(rate) ? rate : null;
44
+ return Number.isFinite(rate) ? rate : 0;
82
45
  };
83
46
  /**
84
- * Accumulates the per-model usage breakdown on `global.quota.byModel` so the
85
- * agent can report input/output token counts and image spend per model to the
86
- * UI. No-ops when the quota global is absent.
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 recordModelBreakdown = (modelName, inputCost, outputCost, imageCost, inputTokens = 0, outputTokens = 0) => {
89
- const quota = globalThis.quota;
90
- if (!quota || !modelName)
51
+ const updateUsageTracker = (tracker, modelName, usage, config) => {
52
+ if (!tracker || !usage || !modelName)
91
53
  return;
92
- if (!quota.byModel || typeof quota.byModel !== "object")
93
- quota.byModel = {};
94
- const entry = (quota.byModel[modelName] ||= {
95
- inputCost: 0,
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 inputCost = (inputTokens / 1_000_000) * inputRate;
132
- const outputCost = (outputTokens / 1_000_000) * outputRate;
133
- recordCost(inputCost + outputCost);
134
- recordModelBreakdown(modelName, inputCost, outputCost, 0, inputTokens, outputTokens);
135
- };
136
- /**
137
- * Computes and records the USD cost of an image generation call from the
138
- * `<model>-image-costs` constant (dollars per generated image). Updates both
139
- * the session total and the per-model breakdown.
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. Cost is only recorded on a
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
- recordTextCost(modelName, sumAgentResponseUsage(response), config);
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 is billed
1016
- // independently via recordCost so retry cost is still visible in telemetry.
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
- // Cost is only recorded on a successful stream — partial streams that error
1038
- // out with a rate limit are not billed. A 429 fired mid-stream simply
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
- recordTextCost(modelName, streamUsage, config);
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 is billed independently via recordCost so retry cost is visible.
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
- recordTextCost(modelName, extractUsageFromInvoke(response), config);
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
  }
@@ -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 };
@@ -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"}
@@ -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":";0CAiD6C,GAAG,KAAG,IAAI;wBA0hB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAyBG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,KAC1B,OAAO,CAAC,GAAG,CAAC;oCAqXF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,KACX,OAAO,CAAC,GAAG,CAAC;sDAosBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,KACX,OAAO,CAAC,GAAG,CAAC;0CAn+B8B,GAAG,KAAG,MAAM;mCAoBxC,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAwgCT,wBAQE"}
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"}
@@ -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 `null` when the constant is
69
- * absent or not numeric, in which case cost recording becomes a no-op (the
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 null;
37
+ return 0;
75
38
  const rate = Number(config[`${modelName}-${kind}-costs`]);
76
- return Number.isFinite(rate) ? rate : null;
39
+ return Number.isFinite(rate) ? rate : 0;
77
40
  };
78
41
  /**
79
- * Accumulates the per-model usage breakdown on `global.quota.byModel` so the
80
- * agent can report input/output token counts and image spend per model to the
81
- * UI. No-ops when the quota global is absent.
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 recordModelBreakdown = (modelName, inputCost, outputCost, imageCost, inputTokens = 0, outputTokens = 0) => {
84
- const quota = globalThis.quota;
85
- if (!quota || !modelName)
46
+ const updateUsageTracker = (tracker, modelName, usage, config) => {
47
+ if (!tracker || !usage || !modelName)
86
48
  return;
87
- if (!quota.byModel || typeof quota.byModel !== "object")
88
- quota.byModel = {};
89
- const entry = (quota.byModel[modelName] ||= {
90
- inputCost: 0,
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 inputCost = (inputTokens / 1_000_000) * inputRate;
127
- const outputCost = (outputTokens / 1_000_000) * outputRate;
128
- recordCost(inputCost + outputCost);
129
- recordModelBreakdown(modelName, inputCost, outputCost, 0, inputTokens, outputTokens);
130
- };
131
- /**
132
- * Computes and records the USD cost of an image generation call from the
133
- * `<model>-image-costs` constant (dollars per generated image). Updates both
134
- * the session total and the per-model breakdown.
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. Cost is only recorded on a
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
- recordTextCost(modelName, sumAgentResponseUsage(response), config);
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 is billed
1011
- // independently via recordCost so retry cost is still visible in telemetry.
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
- // Cost is only recorded on a successful stream — partial streams that error
1033
- // out with a rate limit are not billed. A 429 fired mid-stream simply
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
- recordTextCost(modelName, streamUsage, config);
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 is billed independently via recordCost so retry cost is visible.
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
- recordTextCost(modelName, extractUsageFromInvoke(response), config);
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
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.6",
6
+ "version": "1.2.7",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",