@stackfactor/agent-utils 1.2.8 → 1.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +53 -11
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +67 -9
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +53 -11
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +67 -9
- package/package.json +1 -1
|
@@ -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":"AAgCA;;;;;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;wBA6lB/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;sDAstBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA1/B8B,GAAG,KAAG,MAAM;+CA/jB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCA+kBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAiiCT,wBASE"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -56,7 +56,19 @@ const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
|
56
56
|
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
57
57
|
tracker.tokens = {};
|
|
58
58
|
const inputTokens = usage.input_tokens || 0;
|
|
59
|
-
const
|
|
59
|
+
const visibleOutputTokens = usage.output_tokens || 0;
|
|
60
|
+
const totalTokens = usage.total_tokens || 0;
|
|
61
|
+
// Recover reasoning/"thinking" tokens the provider leaves out of
|
|
62
|
+
// `output_tokens`. Gemini reports its thoughts only in `totalTokenCount`, so
|
|
63
|
+
// `total - input - visibleOutput` is the thinking output the caller was still
|
|
64
|
+
// billed for. Providers that already fold reasoning into `output_tokens`
|
|
65
|
+
// (Anthropic, OpenAI) report `total == input + output`, so this adds 0.
|
|
66
|
+
// Reasoning is charged at the output rate, so we treat it as output for both
|
|
67
|
+
// the token counters and the cost.
|
|
68
|
+
const reasoningTokens = totalTokens > 0
|
|
69
|
+
? Math.max(0, totalTokens - inputTokens - visibleOutputTokens)
|
|
70
|
+
: 0;
|
|
71
|
+
const outputTokens = visibleOutputTokens + reasoningTokens;
|
|
60
72
|
const inputRate = getModelRate(modelName, config, "input-token");
|
|
61
73
|
const outputRate = getModelRate(modelName, config, "output-token");
|
|
62
74
|
const addedCost = (inputTokens / 1_000_000) * inputRate +
|
|
@@ -122,7 +134,12 @@ const updateImageUsageTracker = (tracker, modelName, response, config, provider)
|
|
|
122
134
|
imageOutputTokens = Number(um.candidatesTokenCount) || 0;
|
|
123
135
|
}
|
|
124
136
|
const textInputRate = getModelRate(modelName, config, "input-token");
|
|
125
|
-
|
|
137
|
+
// Reference-image input tokens are billed at the model's input rate. Configs
|
|
138
|
+
// that don't define a dedicated `<model>-image-input-token-costs` (the common
|
|
139
|
+
// case — image models charge all input at one rate and only differ on output)
|
|
140
|
+
// fall back to the text input rate rather than silently billing image input
|
|
141
|
+
// at $0.
|
|
142
|
+
const imageInputRate = getModelRate(modelName, config, "image-input-token") || textInputRate;
|
|
126
143
|
const imageOutputRate = getModelRate(modelName, config, "image-output-token");
|
|
127
144
|
const addedCost = (textInputTokens / 1_000_000) * textInputRate +
|
|
128
145
|
(imageInputTokens / 1_000_000) * imageInputRate +
|
|
@@ -176,6 +193,7 @@ const extractUsageFromInvoke = (response) => {
|
|
|
176
193
|
return {
|
|
177
194
|
input_tokens: um.input_tokens || 0,
|
|
178
195
|
output_tokens: um.output_tokens || 0,
|
|
196
|
+
total_tokens: um.total_tokens || 0,
|
|
179
197
|
};
|
|
180
198
|
}
|
|
181
199
|
const rm = response.response_metadata;
|
|
@@ -183,12 +201,14 @@ const extractUsageFromInvoke = (response) => {
|
|
|
183
201
|
return {
|
|
184
202
|
input_tokens: rm.usage.input_tokens || rm.usage.prompt_tokens || 0,
|
|
185
203
|
output_tokens: rm.usage.output_tokens || rm.usage.completion_tokens || 0,
|
|
204
|
+
total_tokens: rm.usage.total_tokens || rm.usage.total_token_count || 0,
|
|
186
205
|
};
|
|
187
206
|
}
|
|
188
207
|
if (rm?.tokenUsage) {
|
|
189
208
|
return {
|
|
190
209
|
input_tokens: rm.tokenUsage.promptTokens || 0,
|
|
191
210
|
output_tokens: rm.tokenUsage.completionTokens || 0,
|
|
211
|
+
total_tokens: rm.tokenUsage.totalTokens || 0,
|
|
192
212
|
};
|
|
193
213
|
}
|
|
194
214
|
return null;
|
|
@@ -205,6 +225,7 @@ const accumulateChunkUsage = (acc, chunk) => {
|
|
|
205
225
|
if (um) {
|
|
206
226
|
acc.input_tokens += um.input_tokens || 0;
|
|
207
227
|
acc.output_tokens += um.output_tokens || 0;
|
|
228
|
+
acc.total_tokens += um.total_tokens || 0;
|
|
208
229
|
return acc;
|
|
209
230
|
}
|
|
210
231
|
const rm = chunk.response_metadata;
|
|
@@ -212,6 +233,8 @@ const accumulateChunkUsage = (acc, chunk) => {
|
|
|
212
233
|
acc.input_tokens += rm.usage.input_tokens || rm.usage.prompt_tokens || 0;
|
|
213
234
|
acc.output_tokens +=
|
|
214
235
|
rm.usage.output_tokens || rm.usage.completion_tokens || 0;
|
|
236
|
+
acc.total_tokens +=
|
|
237
|
+
rm.usage.total_tokens || rm.usage.total_token_count || 0;
|
|
215
238
|
}
|
|
216
239
|
return acc;
|
|
217
240
|
};
|
|
@@ -224,13 +247,18 @@ const sumAgentResponseUsage = (response) => {
|
|
|
224
247
|
const messages = response?.messages;
|
|
225
248
|
if (!Array.isArray(messages) || messages.length === 0)
|
|
226
249
|
return null;
|
|
227
|
-
const total = {
|
|
250
|
+
const total = {
|
|
251
|
+
input_tokens: 0,
|
|
252
|
+
output_tokens: 0,
|
|
253
|
+
total_tokens: 0,
|
|
254
|
+
};
|
|
228
255
|
let found = false;
|
|
229
256
|
for (const msg of messages) {
|
|
230
257
|
const um = msg?.usage_metadata;
|
|
231
258
|
if (um) {
|
|
232
259
|
total.input_tokens += um.input_tokens || 0;
|
|
233
260
|
total.output_tokens += um.output_tokens || 0;
|
|
261
|
+
total.total_tokens += um.total_tokens || 0;
|
|
234
262
|
found = true;
|
|
235
263
|
}
|
|
236
264
|
}
|
|
@@ -1054,7 +1082,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1054
1082
|
while (true) {
|
|
1055
1083
|
let rawContent = "";
|
|
1056
1084
|
let chunkCount = 0;
|
|
1057
|
-
let streamUsage = {
|
|
1085
|
+
let streamUsage = {
|
|
1086
|
+
input_tokens: 0,
|
|
1087
|
+
output_tokens: 0,
|
|
1088
|
+
total_tokens: 0,
|
|
1089
|
+
};
|
|
1058
1090
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1059
1091
|
// Usage is only recorded on a successful stream — partial streams that
|
|
1060
1092
|
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
@@ -1063,7 +1095,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1063
1095
|
while (true) {
|
|
1064
1096
|
rawContent = "";
|
|
1065
1097
|
chunkCount = 0;
|
|
1066
|
-
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1098
|
+
streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
1067
1099
|
try {
|
|
1068
1100
|
// Honour caller cancellation: passing the signal tears down the
|
|
1069
1101
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
@@ -1339,20 +1371,21 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options, usage
|
|
|
1339
1371
|
* Google models do not support alpha channels. Validates the `aspectRatio` against
|
|
1340
1372
|
* allowed values (`"1:1"`, `"3:4"`, `"4:3"`, `"9:16"`, `"16:9"`). Applies model-
|
|
1341
1373
|
* specific generation config: Imagen models receive `numberOfImages`, `aspectRatio`,
|
|
1342
|
-
* `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1374
|
+
* `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`,
|
|
1375
|
+
* `topP`, and an `imageConfig` carrying `aspectRatio` plus an optional `imageSize`
|
|
1376
|
+
* resolution tier (e.g. "1K"/"2K"/"4K", honoured by gemini-3-pro-image). Safety
|
|
1377
|
+
* settings are disabled for image generation. Throws an error if no images are returned.
|
|
1345
1378
|
* @param modelName - The Google model identifier (e.g. `"imagen-4.0-generate-001"`,
|
|
1346
1379
|
* `"gemini-3.0-pro-image"`)
|
|
1347
1380
|
* @param config - Configuration object; must include `googleAPIKey`
|
|
1348
1381
|
* @param prompt - The text prompt describing the image to generate
|
|
1349
|
-
* @param options - Generation options including `aspectRatio`, `
|
|
1350
|
-
* optional `negativePrompt` (Imagen only)
|
|
1382
|
+
* @param options - Generation options including `aspectRatio`, optional `imageSize`
|
|
1383
|
+
* (Gemini resolution tier), `numberOfImages`, and optional `negativePrompt` (Imagen only)
|
|
1351
1384
|
* @returns A single image descriptor `{ b64_json, mimeType }` when one image is
|
|
1352
1385
|
* requested, or `{ images: [...] }` for multiple images
|
|
1353
1386
|
*/
|
|
1354
1387
|
const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
|
|
1355
|
-
const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
|
|
1388
|
+
const { aspectRatio = "1:1", imageSize, numberOfImages = 1, negativePrompt = "", } = options;
|
|
1356
1389
|
if (!config.googleAPIKey) {
|
|
1357
1390
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
|
|
1358
1391
|
}
|
|
@@ -1389,6 +1422,15 @@ const generateImageWithGoogle = async (modelName, config, prompt, options, usage
|
|
|
1389
1422
|
// Gemini-specific config
|
|
1390
1423
|
temperature: 1,
|
|
1391
1424
|
topP: 0.95,
|
|
1425
|
+
// Gemini controls output dimensions through `imageConfig`, not the
|
|
1426
|
+
// top-level `aspectRatio` field that Imagen uses. `imageSize`
|
|
1427
|
+
// (e.g. "1K"/"2K"/"4K") is only honoured by resolution-capable
|
|
1428
|
+
// models such as gemini-3-pro-image, so it is passed through only
|
|
1429
|
+
// when the caller explicitly requests it.
|
|
1430
|
+
imageConfig: {
|
|
1431
|
+
aspectRatio: aspectRatio,
|
|
1432
|
+
...(imageSize ? { imageSize: imageSize } : {}),
|
|
1433
|
+
},
|
|
1392
1434
|
}),
|
|
1393
1435
|
};
|
|
1394
1436
|
// Safety settings (disable for image generation)
|
package/dist/cjs/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA4KP,CAAC"}
|
package/dist/cjs/serve.js
CHANGED
|
@@ -199,23 +199,78 @@ const runSelfCheck = async (checkCode, config, session) => {
|
|
|
199
199
|
/** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
|
|
200
200
|
class UnauthenticatedError extends Error {
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Marks a *transient* failure to reach the StackFactor session-validation
|
|
204
|
+
* endpoint (5xx / request timeout / transport error). Unlike
|
|
205
|
+
* {@link UnauthenticatedError} the token was never actually rejected — the
|
|
206
|
+
* backend was momentarily unreachable — so the handler maps this to the
|
|
207
|
+
* retryable gRPC UNAVAILABLE instead of UNAUTHENTICATED, and the message no
|
|
208
|
+
* longer slanders a perfectly valid token.
|
|
209
|
+
*/
|
|
210
|
+
class BackendUnavailableError extends Error {
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Session-validation retry policy. A GKE front-door blip (a backend pod briefly
|
|
214
|
+
* refusing connections during a restart or an event-loop stall) surfaces here as
|
|
215
|
+
* a 502/503 or a transport error, not a 401 — and retrying on a fresh connection
|
|
216
|
+
* almost always lands on a healthy replica. Env-overridable for prod tuning.
|
|
217
|
+
*/
|
|
218
|
+
const AUTH_MAX_ATTEMPTS = Math.max(1, Number(process.env.STACKFACTOR_AUTH_MAX_ATTEMPTS) || 4);
|
|
219
|
+
const AUTH_RETRY_BASE_MS = Math.max(0, Number(process.env.STACKFACTOR_AUTH_RETRY_BASE_MS) || 500);
|
|
220
|
+
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
221
|
+
/** A 401/403 is the backend genuinely rejecting the token. */
|
|
222
|
+
const isAuthRejection = (error) => {
|
|
223
|
+
const status = error?.response?.status;
|
|
224
|
+
return status === 401 || status === 403;
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* True when the failure is the validation endpoint being unreachable rather than
|
|
228
|
+
* the token being bad: a 5xx/408/429 HTTP response, or a transport-level error
|
|
229
|
+
* (no HTTP response but an axios/errno signature). Plain programming errors
|
|
230
|
+
* (no response and no axios/errno marker) are NOT treated as transient.
|
|
231
|
+
*/
|
|
232
|
+
const isTransient = (error) => {
|
|
233
|
+
const status = error?.response?.status;
|
|
234
|
+
if (status)
|
|
235
|
+
return status >= 500 || status === 408 || status === 429;
|
|
236
|
+
return Boolean(error?.isAxiosError || error?.code);
|
|
237
|
+
};
|
|
202
238
|
/**
|
|
203
239
|
* Default authenticator: require a token on the request and resolve it to a
|
|
204
240
|
* session via the StackFactor API. Requires `BACKEND_URL` (or `REACT_APP_NODE_ENV`)
|
|
205
|
-
* to point client-api at the right backend.
|
|
206
|
-
*
|
|
241
|
+
* to point client-api at the right backend.
|
|
242
|
+
*
|
|
243
|
+
* A 401/403 (or any non-transient error) throws {@link UnauthenticatedError}
|
|
244
|
+
* immediately. A transient failure (5xx / timeout / network) is retried with
|
|
245
|
+
* exponential backoff; if every attempt fails it throws
|
|
246
|
+
* {@link BackendUnavailableError} so the caller sees a retryable UNAVAILABLE
|
|
247
|
+
* rather than a misleading "invalid token".
|
|
207
248
|
*/
|
|
208
249
|
const defaultAuthenticate = async (request) => {
|
|
209
250
|
const token = request?.authToken ?? request?.authorization;
|
|
210
251
|
if (!token) {
|
|
211
252
|
throw new UnauthenticatedError("Missing StackFactor auth token");
|
|
212
253
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
254
|
+
let lastError;
|
|
255
|
+
for (let attempt = 1; attempt <= AUTH_MAX_ATTEMPTS; attempt++) {
|
|
256
|
+
try {
|
|
257
|
+
return await client_api_1.session.getSession(token);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
lastError = error;
|
|
261
|
+
// Real rejection, or an unexpected non-transient error: fail fast.
|
|
262
|
+
if (isAuthRejection(error) || !isTransient(error)) {
|
|
263
|
+
throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
|
|
264
|
+
}
|
|
265
|
+
// Transient: the endpoint is momentarily unreachable, not the token bad.
|
|
266
|
+
if (attempt < AUTH_MAX_ATTEMPTS) {
|
|
267
|
+
const backoff = AUTH_RETRY_BASE_MS * 2 ** (attempt - 1);
|
|
268
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `session validation transient failure (attempt ${attempt}/${AUTH_MAX_ATTEMPTS}); retrying in ${backoff}ms: ${error?.message ?? error}`);
|
|
269
|
+
await delay(backoff);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
218
272
|
}
|
|
273
|
+
throw new BackendUnavailableError(`StackFactor session validation unavailable after ${AUTH_MAX_ATTEMPTS} attempts: ${lastError?.message ?? lastError}`);
|
|
219
274
|
};
|
|
220
275
|
const safeParse = (value, fallback) => {
|
|
221
276
|
if (!value)
|
|
@@ -283,9 +338,12 @@ const serve = (main, options = {}) => {
|
|
|
283
338
|
}
|
|
284
339
|
catch (error) {
|
|
285
340
|
const message = error?.message ?? "Unauthenticated";
|
|
286
|
-
|
|
341
|
+
const unavailable = error instanceof BackendUnavailableError;
|
|
342
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `agent Execute refused (${unavailable ? "unavailable" : "unauthenticated"}): ${message}`);
|
|
287
343
|
call.emit("error", {
|
|
288
|
-
code:
|
|
344
|
+
code: unavailable
|
|
345
|
+
? grpc.status.UNAVAILABLE
|
|
346
|
+
: grpc.status.UNAUTHENTICATED,
|
|
289
347
|
details: message,
|
|
290
348
|
});
|
|
291
349
|
return;
|
|
@@ -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":"AAgCA;;;;;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;wBA6lB/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;sDAstBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA1/B8B,GAAG,KAAG,MAAM;+CA/jB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCA+kBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAiiCT,wBASE"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -51,7 +51,19 @@ const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
|
51
51
|
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
52
52
|
tracker.tokens = {};
|
|
53
53
|
const inputTokens = usage.input_tokens || 0;
|
|
54
|
-
const
|
|
54
|
+
const visibleOutputTokens = usage.output_tokens || 0;
|
|
55
|
+
const totalTokens = usage.total_tokens || 0;
|
|
56
|
+
// Recover reasoning/"thinking" tokens the provider leaves out of
|
|
57
|
+
// `output_tokens`. Gemini reports its thoughts only in `totalTokenCount`, so
|
|
58
|
+
// `total - input - visibleOutput` is the thinking output the caller was still
|
|
59
|
+
// billed for. Providers that already fold reasoning into `output_tokens`
|
|
60
|
+
// (Anthropic, OpenAI) report `total == input + output`, so this adds 0.
|
|
61
|
+
// Reasoning is charged at the output rate, so we treat it as output for both
|
|
62
|
+
// the token counters and the cost.
|
|
63
|
+
const reasoningTokens = totalTokens > 0
|
|
64
|
+
? Math.max(0, totalTokens - inputTokens - visibleOutputTokens)
|
|
65
|
+
: 0;
|
|
66
|
+
const outputTokens = visibleOutputTokens + reasoningTokens;
|
|
55
67
|
const inputRate = getModelRate(modelName, config, "input-token");
|
|
56
68
|
const outputRate = getModelRate(modelName, config, "output-token");
|
|
57
69
|
const addedCost = (inputTokens / 1_000_000) * inputRate +
|
|
@@ -117,7 +129,12 @@ const updateImageUsageTracker = (tracker, modelName, response, config, provider)
|
|
|
117
129
|
imageOutputTokens = Number(um.candidatesTokenCount) || 0;
|
|
118
130
|
}
|
|
119
131
|
const textInputRate = getModelRate(modelName, config, "input-token");
|
|
120
|
-
|
|
132
|
+
// Reference-image input tokens are billed at the model's input rate. Configs
|
|
133
|
+
// that don't define a dedicated `<model>-image-input-token-costs` (the common
|
|
134
|
+
// case — image models charge all input at one rate and only differ on output)
|
|
135
|
+
// fall back to the text input rate rather than silently billing image input
|
|
136
|
+
// at $0.
|
|
137
|
+
const imageInputRate = getModelRate(modelName, config, "image-input-token") || textInputRate;
|
|
121
138
|
const imageOutputRate = getModelRate(modelName, config, "image-output-token");
|
|
122
139
|
const addedCost = (textInputTokens / 1_000_000) * textInputRate +
|
|
123
140
|
(imageInputTokens / 1_000_000) * imageInputRate +
|
|
@@ -171,6 +188,7 @@ const extractUsageFromInvoke = (response) => {
|
|
|
171
188
|
return {
|
|
172
189
|
input_tokens: um.input_tokens || 0,
|
|
173
190
|
output_tokens: um.output_tokens || 0,
|
|
191
|
+
total_tokens: um.total_tokens || 0,
|
|
174
192
|
};
|
|
175
193
|
}
|
|
176
194
|
const rm = response.response_metadata;
|
|
@@ -178,12 +196,14 @@ const extractUsageFromInvoke = (response) => {
|
|
|
178
196
|
return {
|
|
179
197
|
input_tokens: rm.usage.input_tokens || rm.usage.prompt_tokens || 0,
|
|
180
198
|
output_tokens: rm.usage.output_tokens || rm.usage.completion_tokens || 0,
|
|
199
|
+
total_tokens: rm.usage.total_tokens || rm.usage.total_token_count || 0,
|
|
181
200
|
};
|
|
182
201
|
}
|
|
183
202
|
if (rm?.tokenUsage) {
|
|
184
203
|
return {
|
|
185
204
|
input_tokens: rm.tokenUsage.promptTokens || 0,
|
|
186
205
|
output_tokens: rm.tokenUsage.completionTokens || 0,
|
|
206
|
+
total_tokens: rm.tokenUsage.totalTokens || 0,
|
|
187
207
|
};
|
|
188
208
|
}
|
|
189
209
|
return null;
|
|
@@ -200,6 +220,7 @@ const accumulateChunkUsage = (acc, chunk) => {
|
|
|
200
220
|
if (um) {
|
|
201
221
|
acc.input_tokens += um.input_tokens || 0;
|
|
202
222
|
acc.output_tokens += um.output_tokens || 0;
|
|
223
|
+
acc.total_tokens += um.total_tokens || 0;
|
|
203
224
|
return acc;
|
|
204
225
|
}
|
|
205
226
|
const rm = chunk.response_metadata;
|
|
@@ -207,6 +228,8 @@ const accumulateChunkUsage = (acc, chunk) => {
|
|
|
207
228
|
acc.input_tokens += rm.usage.input_tokens || rm.usage.prompt_tokens || 0;
|
|
208
229
|
acc.output_tokens +=
|
|
209
230
|
rm.usage.output_tokens || rm.usage.completion_tokens || 0;
|
|
231
|
+
acc.total_tokens +=
|
|
232
|
+
rm.usage.total_tokens || rm.usage.total_token_count || 0;
|
|
210
233
|
}
|
|
211
234
|
return acc;
|
|
212
235
|
};
|
|
@@ -219,13 +242,18 @@ const sumAgentResponseUsage = (response) => {
|
|
|
219
242
|
const messages = response?.messages;
|
|
220
243
|
if (!Array.isArray(messages) || messages.length === 0)
|
|
221
244
|
return null;
|
|
222
|
-
const total = {
|
|
245
|
+
const total = {
|
|
246
|
+
input_tokens: 0,
|
|
247
|
+
output_tokens: 0,
|
|
248
|
+
total_tokens: 0,
|
|
249
|
+
};
|
|
223
250
|
let found = false;
|
|
224
251
|
for (const msg of messages) {
|
|
225
252
|
const um = msg?.usage_metadata;
|
|
226
253
|
if (um) {
|
|
227
254
|
total.input_tokens += um.input_tokens || 0;
|
|
228
255
|
total.output_tokens += um.output_tokens || 0;
|
|
256
|
+
total.total_tokens += um.total_tokens || 0;
|
|
229
257
|
found = true;
|
|
230
258
|
}
|
|
231
259
|
}
|
|
@@ -1049,7 +1077,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1049
1077
|
while (true) {
|
|
1050
1078
|
let rawContent = "";
|
|
1051
1079
|
let chunkCount = 0;
|
|
1052
|
-
let streamUsage = {
|
|
1080
|
+
let streamUsage = {
|
|
1081
|
+
input_tokens: 0,
|
|
1082
|
+
output_tokens: 0,
|
|
1083
|
+
total_tokens: 0,
|
|
1084
|
+
};
|
|
1053
1085
|
// Inner loop: wait + retry on 429 around stream setup and consumption.
|
|
1054
1086
|
// Usage is only recorded on a successful stream — partial streams that
|
|
1055
1087
|
// error out with a rate limit are not counted. A 429 fired mid-stream
|
|
@@ -1058,7 +1090,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1058
1090
|
while (true) {
|
|
1059
1091
|
rawContent = "";
|
|
1060
1092
|
chunkCount = 0;
|
|
1061
|
-
streamUsage = { input_tokens: 0, output_tokens: 0 };
|
|
1093
|
+
streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
1062
1094
|
try {
|
|
1063
1095
|
// Honour caller cancellation: passing the signal tears down the
|
|
1064
1096
|
// upstream HTTP request so a cancelled call stops billing tokens.
|
|
@@ -1334,20 +1366,21 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options, usage
|
|
|
1334
1366
|
* Google models do not support alpha channels. Validates the `aspectRatio` against
|
|
1335
1367
|
* allowed values (`"1:1"`, `"3:4"`, `"4:3"`, `"9:16"`, `"16:9"`). Applies model-
|
|
1336
1368
|
* specific generation config: Imagen models receive `numberOfImages`, `aspectRatio`,
|
|
1337
|
-
* `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1369
|
+
* `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`,
|
|
1370
|
+
* `topP`, and an `imageConfig` carrying `aspectRatio` plus an optional `imageSize`
|
|
1371
|
+
* resolution tier (e.g. "1K"/"2K"/"4K", honoured by gemini-3-pro-image). Safety
|
|
1372
|
+
* settings are disabled for image generation. Throws an error if no images are returned.
|
|
1340
1373
|
* @param modelName - The Google model identifier (e.g. `"imagen-4.0-generate-001"`,
|
|
1341
1374
|
* `"gemini-3.0-pro-image"`)
|
|
1342
1375
|
* @param config - Configuration object; must include `googleAPIKey`
|
|
1343
1376
|
* @param prompt - The text prompt describing the image to generate
|
|
1344
|
-
* @param options - Generation options including `aspectRatio`, `
|
|
1345
|
-
* optional `negativePrompt` (Imagen only)
|
|
1377
|
+
* @param options - Generation options including `aspectRatio`, optional `imageSize`
|
|
1378
|
+
* (Gemini resolution tier), `numberOfImages`, and optional `negativePrompt` (Imagen only)
|
|
1346
1379
|
* @returns A single image descriptor `{ b64_json, mimeType }` when one image is
|
|
1347
1380
|
* requested, or `{ images: [...] }` for multiple images
|
|
1348
1381
|
*/
|
|
1349
1382
|
const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
|
|
1350
|
-
const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
|
|
1383
|
+
const { aspectRatio = "1:1", imageSize, numberOfImages = 1, negativePrompt = "", } = options;
|
|
1351
1384
|
if (!config.googleAPIKey) {
|
|
1352
1385
|
throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
|
|
1353
1386
|
}
|
|
@@ -1384,6 +1417,15 @@ const generateImageWithGoogle = async (modelName, config, prompt, options, usage
|
|
|
1384
1417
|
// Gemini-specific config
|
|
1385
1418
|
temperature: 1,
|
|
1386
1419
|
topP: 0.95,
|
|
1420
|
+
// Gemini controls output dimensions through `imageConfig`, not the
|
|
1421
|
+
// top-level `aspectRatio` field that Imagen uses. `imageSize`
|
|
1422
|
+
// (e.g. "1K"/"2K"/"4K") is only honoured by resolution-capable
|
|
1423
|
+
// models such as gemini-3-pro-image, so it is passed through only
|
|
1424
|
+
// when the caller explicitly requests it.
|
|
1425
|
+
imageConfig: {
|
|
1426
|
+
aspectRatio: aspectRatio,
|
|
1427
|
+
...(imageSize ? { imageSize: imageSize } : {}),
|
|
1428
|
+
},
|
|
1387
1429
|
}),
|
|
1388
1430
|
};
|
|
1389
1431
|
// Safety settings (disable for image generation)
|
package/dist/esm/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA4KP,CAAC"}
|
package/dist/esm/serve.js
CHANGED
|
@@ -160,23 +160,78 @@ const runSelfCheck = async (checkCode, config, session) => {
|
|
|
160
160
|
/** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
|
|
161
161
|
class UnauthenticatedError extends Error {
|
|
162
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Marks a *transient* failure to reach the StackFactor session-validation
|
|
165
|
+
* endpoint (5xx / request timeout / transport error). Unlike
|
|
166
|
+
* {@link UnauthenticatedError} the token was never actually rejected — the
|
|
167
|
+
* backend was momentarily unreachable — so the handler maps this to the
|
|
168
|
+
* retryable gRPC UNAVAILABLE instead of UNAUTHENTICATED, and the message no
|
|
169
|
+
* longer slanders a perfectly valid token.
|
|
170
|
+
*/
|
|
171
|
+
class BackendUnavailableError extends Error {
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Session-validation retry policy. A GKE front-door blip (a backend pod briefly
|
|
175
|
+
* refusing connections during a restart or an event-loop stall) surfaces here as
|
|
176
|
+
* a 502/503 or a transport error, not a 401 — and retrying on a fresh connection
|
|
177
|
+
* almost always lands on a healthy replica. Env-overridable for prod tuning.
|
|
178
|
+
*/
|
|
179
|
+
const AUTH_MAX_ATTEMPTS = Math.max(1, Number(process.env.STACKFACTOR_AUTH_MAX_ATTEMPTS) || 4);
|
|
180
|
+
const AUTH_RETRY_BASE_MS = Math.max(0, Number(process.env.STACKFACTOR_AUTH_RETRY_BASE_MS) || 500);
|
|
181
|
+
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
182
|
+
/** A 401/403 is the backend genuinely rejecting the token. */
|
|
183
|
+
const isAuthRejection = (error) => {
|
|
184
|
+
const status = error?.response?.status;
|
|
185
|
+
return status === 401 || status === 403;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* True when the failure is the validation endpoint being unreachable rather than
|
|
189
|
+
* the token being bad: a 5xx/408/429 HTTP response, or a transport-level error
|
|
190
|
+
* (no HTTP response but an axios/errno signature). Plain programming errors
|
|
191
|
+
* (no response and no axios/errno marker) are NOT treated as transient.
|
|
192
|
+
*/
|
|
193
|
+
const isTransient = (error) => {
|
|
194
|
+
const status = error?.response?.status;
|
|
195
|
+
if (status)
|
|
196
|
+
return status >= 500 || status === 408 || status === 429;
|
|
197
|
+
return Boolean(error?.isAxiosError || error?.code);
|
|
198
|
+
};
|
|
163
199
|
/**
|
|
164
200
|
* Default authenticator: require a token on the request and resolve it to a
|
|
165
201
|
* session via the StackFactor API. Requires `BACKEND_URL` (or `REACT_APP_NODE_ENV`)
|
|
166
|
-
* to point client-api at the right backend.
|
|
167
|
-
*
|
|
202
|
+
* to point client-api at the right backend.
|
|
203
|
+
*
|
|
204
|
+
* A 401/403 (or any non-transient error) throws {@link UnauthenticatedError}
|
|
205
|
+
* immediately. A transient failure (5xx / timeout / network) is retried with
|
|
206
|
+
* exponential backoff; if every attempt fails it throws
|
|
207
|
+
* {@link BackendUnavailableError} so the caller sees a retryable UNAVAILABLE
|
|
208
|
+
* rather than a misleading "invalid token".
|
|
168
209
|
*/
|
|
169
210
|
const defaultAuthenticate = async (request) => {
|
|
170
211
|
const token = request?.authToken ?? request?.authorization;
|
|
171
212
|
if (!token) {
|
|
172
213
|
throw new UnauthenticatedError("Missing StackFactor auth token");
|
|
173
214
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
215
|
+
let lastError;
|
|
216
|
+
for (let attempt = 1; attempt <= AUTH_MAX_ATTEMPTS; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
return await clientSession.getSession(token);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
lastError = error;
|
|
222
|
+
// Real rejection, or an unexpected non-transient error: fail fast.
|
|
223
|
+
if (isAuthRejection(error) || !isTransient(error)) {
|
|
224
|
+
throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
|
|
225
|
+
}
|
|
226
|
+
// Transient: the endpoint is momentarily unreachable, not the token bad.
|
|
227
|
+
if (attempt < AUTH_MAX_ATTEMPTS) {
|
|
228
|
+
const backoff = AUTH_RETRY_BASE_MS * 2 ** (attempt - 1);
|
|
229
|
+
logger.log(null, logger.levels.warn, `session validation transient failure (attempt ${attempt}/${AUTH_MAX_ATTEMPTS}); retrying in ${backoff}ms: ${error?.message ?? error}`);
|
|
230
|
+
await delay(backoff);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
179
233
|
}
|
|
234
|
+
throw new BackendUnavailableError(`StackFactor session validation unavailable after ${AUTH_MAX_ATTEMPTS} attempts: ${lastError?.message ?? lastError}`);
|
|
180
235
|
};
|
|
181
236
|
const safeParse = (value, fallback) => {
|
|
182
237
|
if (!value)
|
|
@@ -244,9 +299,12 @@ export const serve = (main, options = {}) => {
|
|
|
244
299
|
}
|
|
245
300
|
catch (error) {
|
|
246
301
|
const message = error?.message ?? "Unauthenticated";
|
|
247
|
-
|
|
302
|
+
const unavailable = error instanceof BackendUnavailableError;
|
|
303
|
+
logger.log(null, logger.levels.warn, `agent Execute refused (${unavailable ? "unavailable" : "unauthenticated"}): ${message}`);
|
|
248
304
|
call.emit("error", {
|
|
249
|
-
code:
|
|
305
|
+
code: unavailable
|
|
306
|
+
? grpc.status.UNAVAILABLE
|
|
307
|
+
: grpc.status.UNAUTHENTICATED,
|
|
250
308
|
details: message,
|
|
251
309
|
});
|
|
252
310
|
return;
|