@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.
@@ -1 +1 @@
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"}
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"}
@@ -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 outputTokens = usage.output_tokens || 0;
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
- const imageInputRate = getModelRate(modelName, config, "image-input-token");
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 = { input_tokens: 0, output_tokens: 0 };
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 = { input_tokens: 0, output_tokens: 0 };
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
- * and `topP`. Safety settings are disabled for image generation. Throws an error if no
1344
- * images are returned.
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`, `numberOfImages`, and
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)
@@ -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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAuKP,CAAC"}
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. Throws {@link UnauthenticatedError}
206
- * when the token is missing or rejected.
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
- try {
214
- return await client_api_1.session.getSession(token);
215
- }
216
- catch (error) {
217
- throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
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
- logger_js_1.default.log(null, logger_js_1.default.levels.warn, `agent Execute refused: ${message}`);
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: grpc.status.UNAUTHENTICATED,
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":"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"}
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"}
@@ -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 outputTokens = usage.output_tokens || 0;
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
- const imageInputRate = getModelRate(modelName, config, "image-input-token");
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 = { input_tokens: 0, output_tokens: 0 };
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 = { input_tokens: 0, output_tokens: 0 };
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
- * and `topP`. Safety settings are disabled for image generation. Throws an error if no
1339
- * images are returned.
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`, `numberOfImages`, and
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)
@@ -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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAuKP,CAAC"}
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. Throws {@link UnauthenticatedError}
167
- * when the token is missing or rejected.
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
- try {
175
- return await clientSession.getSession(token);
176
- }
177
- catch (error) {
178
- throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
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
- logger.log(null, logger.levels.warn, `agent Execute refused: ${message}`);
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: grpc.status.UNAUTHENTICATED,
305
+ code: unavailable
306
+ ? grpc.status.UNAVAILABLE
307
+ : grpc.status.UNAUTHENTICATED,
250
308
  details: message,
251
309
  });
252
310
  return;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.8",
6
+ "version": "1.2.11",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",