@stackfactor/agent-utils 1.2.7 → 1.2.10

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.
@@ -15,8 +15,9 @@ declare const _default: {
15
15
  createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
16
16
  runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null, usageTracker?: UsageTracker | null) => Promise<any>;
17
17
  runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[], usageTracker?: UsageTracker | null) => Promise<any>;
18
- runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any) => Promise<any>;
18
+ runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
19
19
  throwErrorIfNotSuccessful: (response: any) => string;
20
+ updateUsageTrackerForCharacters: (tracker: UsageTracker | null | undefined, modelName: string, characterCount: number, config: any) => void;
20
21
  validateModel: (selectedModel: string, supportedModels: string[]) => string;
21
22
  };
22
23
  export default _default;
@@ -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;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"}
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;sDAktBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CAt/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;;AA6hCT,wBASE"}
@@ -68,6 +68,101 @@ const updateUsageTracker = (tracker, modelName, usage, config) => {
68
68
  tracker.tokens[inputKey] = (tracker.tokens[inputKey] || 0) + inputTokens;
69
69
  tracker.tokens[outputKey] = (tracker.tokens[outputKey] || 0) + outputTokens;
70
70
  };
71
+ /**
72
+ * Adds a single image-generation call's usage to the caller-supplied tracker.
73
+ * Extracts token counts from provider-specific response shapes: OpenAI returns
74
+ * `response.usage` with `input_tokens_details.{text_tokens,image_tokens}` and
75
+ * `output_tokens`; Google returns `response.usageMetadata` with
76
+ * `promptTokensDetails` (modality-keyed) and `candidatesTokenCount` for the
77
+ * generated image. Reads rates from `<model>-input-token-costs`,
78
+ * `<model>-image-input-token-costs`, and `<model>-image-output-token-costs`.
79
+ * No-ops when the tracker or response usage info is absent.
80
+ */
81
+ const updateImageUsageTracker = (tracker, modelName, response, config, provider) => {
82
+ if (!tracker || !modelName || !response)
83
+ return;
84
+ if (typeof tracker.cost !== "number")
85
+ tracker.cost = 0;
86
+ if (!tracker.tokens || typeof tracker.tokens !== "object")
87
+ tracker.tokens = {};
88
+ let textInputTokens = 0;
89
+ let imageInputTokens = 0;
90
+ let imageOutputTokens = 0;
91
+ if (provider === "openai") {
92
+ const usage = response.usage;
93
+ if (!usage)
94
+ return;
95
+ const details = usage.input_tokens_details || {};
96
+ textInputTokens = details.text_tokens || 0;
97
+ imageInputTokens = details.image_tokens || 0;
98
+ // Older shapes report a single input_tokens without modality breakdown.
99
+ if (!textInputTokens && !imageInputTokens && usage.input_tokens) {
100
+ textInputTokens = usage.input_tokens;
101
+ }
102
+ imageOutputTokens = usage.output_tokens || 0;
103
+ }
104
+ else {
105
+ const um = response.usageMetadata;
106
+ if (!um)
107
+ return;
108
+ const promptDetails = Array.isArray(um.promptTokensDetails)
109
+ ? um.promptTokensDetails
110
+ : [];
111
+ for (const d of promptDetails) {
112
+ const modality = String(d?.modality || "").toUpperCase();
113
+ const count = Number(d?.tokenCount) || 0;
114
+ if (modality === "IMAGE")
115
+ imageInputTokens += count;
116
+ else
117
+ textInputTokens += count;
118
+ }
119
+ if (!textInputTokens && !imageInputTokens) {
120
+ textInputTokens = Number(um.promptTokenCount) || 0;
121
+ }
122
+ imageOutputTokens = Number(um.candidatesTokenCount) || 0;
123
+ }
124
+ const textInputRate = getModelRate(modelName, config, "input-token");
125
+ const imageInputRate = getModelRate(modelName, config, "image-input-token");
126
+ const imageOutputRate = getModelRate(modelName, config, "image-output-token");
127
+ const addedCost = (textInputTokens / 1_000_000) * textInputRate +
128
+ (imageInputTokens / 1_000_000) * imageInputRate +
129
+ (imageOutputTokens / 1_000_000) * imageOutputRate;
130
+ if (Number.isFinite(addedCost) && addedCost > 0)
131
+ tracker.cost += addedCost;
132
+ if (textInputTokens > 0) {
133
+ const key = `${modelName}_inputTokens`;
134
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + textInputTokens;
135
+ }
136
+ if (imageInputTokens > 0) {
137
+ const key = `${modelName}_imageInputTokens`;
138
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + imageInputTokens;
139
+ }
140
+ if (imageOutputTokens > 0) {
141
+ const key = `${modelName}_imageOutputTokens`;
142
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + imageOutputTokens;
143
+ }
144
+ };
145
+ /**
146
+ * Adds character-billed usage (e.g. ElevenLabs TTS) to the caller-supplied
147
+ * tracker. There is no provider response to parse — billing is deterministic
148
+ * from the input character count. Reads the rate from
149
+ * `<model>-character-costs` (USD per million characters) and accumulates
150
+ * under `<model>_characters`. No-ops on missing tracker / model / count.
151
+ */
152
+ const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, config) => {
153
+ if (!tracker || !modelName || !characterCount)
154
+ return;
155
+ if (typeof tracker.cost !== "number")
156
+ tracker.cost = 0;
157
+ if (!tracker.tokens || typeof tracker.tokens !== "object")
158
+ tracker.tokens = {};
159
+ const rate = getModelRate(modelName, config, "character");
160
+ const addedCost = (characterCount / 1_000_000) * rate;
161
+ if (Number.isFinite(addedCost) && addedCost > 0)
162
+ tracker.cost += addedCost;
163
+ const key = `${modelName}_characters`;
164
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
165
+ };
71
166
  /**
72
167
  * Extracts a normalized token-usage object from a single LangChain `invoke()`
73
168
  * response. Reads from `usage_metadata` first (standardized in LangChain v1),
@@ -1170,7 +1265,7 @@ const getImageModelProvider = (modelName) => {
1170
1265
  * @returns An object with `url`, `b64_json`, and `revisedPrompt` for a single image,
1171
1266
  * or `{ images: [...] }` for multiple images
1172
1267
  */
1173
- const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1268
+ const generateImageWithOpenAI = async (modelName, config, prompt, options, usageTracker = null) => {
1174
1269
  const { size = "1024x1024", style = "vivid", responseFormat = "url", n = 1, } = options;
1175
1270
  if (!config.openAIAPIKey) {
1176
1271
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "OpenAI API key is required for OpenAI image generation");
@@ -1218,6 +1313,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1218
1313
  }
1219
1314
  }
1220
1315
  const response = await openai.images.generate(requestParams);
1316
+ updateImageUsageTracker(usageTracker, modelName, response, config, "openai");
1221
1317
  // Format response based on number of images
1222
1318
  if (n === 1) {
1223
1319
  const imageData = response.data[0];
@@ -1243,20 +1339,21 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1243
1339
  * Google models do not support alpha channels. Validates the `aspectRatio` against
1244
1340
  * allowed values (`"1:1"`, `"3:4"`, `"4:3"`, `"9:16"`, `"16:9"`). Applies model-
1245
1341
  * specific generation config: Imagen models receive `numberOfImages`, `aspectRatio`,
1246
- * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`
1247
- * and `topP`. Safety settings are disabled for image generation. Throws an error if no
1248
- * images are returned.
1342
+ * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`,
1343
+ * `topP`, and an `imageConfig` carrying `aspectRatio` plus an optional `imageSize`
1344
+ * resolution tier (e.g. "1K"/"2K"/"4K", honoured by gemini-3-pro-image). Safety
1345
+ * settings are disabled for image generation. Throws an error if no images are returned.
1249
1346
  * @param modelName - The Google model identifier (e.g. `"imagen-4.0-generate-001"`,
1250
1347
  * `"gemini-3.0-pro-image"`)
1251
1348
  * @param config - Configuration object; must include `googleAPIKey`
1252
1349
  * @param prompt - The text prompt describing the image to generate
1253
- * @param options - Generation options including `aspectRatio`, `numberOfImages`, and
1254
- * optional `negativePrompt` (Imagen only)
1350
+ * @param options - Generation options including `aspectRatio`, optional `imageSize`
1351
+ * (Gemini resolution tier), `numberOfImages`, and optional `negativePrompt` (Imagen only)
1255
1352
  * @returns A single image descriptor `{ b64_json, mimeType }` when one image is
1256
1353
  * requested, or `{ images: [...] }` for multiple images
1257
1354
  */
1258
- const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1259
- const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
1355
+ const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
1356
+ const { aspectRatio = "1:1", imageSize, numberOfImages = 1, negativePrompt = "", } = options;
1260
1357
  if (!config.googleAPIKey) {
1261
1358
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
1262
1359
  }
@@ -1293,6 +1390,15 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1293
1390
  // Gemini-specific config
1294
1391
  temperature: 1,
1295
1392
  topP: 0.95,
1393
+ // Gemini controls output dimensions through `imageConfig`, not the
1394
+ // top-level `aspectRatio` field that Imagen uses. `imageSize`
1395
+ // (e.g. "1K"/"2K"/"4K") is only honoured by resolution-capable
1396
+ // models such as gemini-3-pro-image, so it is passed through only
1397
+ // when the caller explicitly requests it.
1398
+ imageConfig: {
1399
+ aspectRatio: aspectRatio,
1400
+ ...(imageSize ? { imageSize: imageSize } : {}),
1401
+ },
1296
1402
  }),
1297
1403
  };
1298
1404
  // Safety settings (disable for image generation)
@@ -1316,6 +1422,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1316
1422
  },
1317
1423
  };
1318
1424
  const response = await ai.models.generateContent(req);
1425
+ updateImageUsageTracker(usageTracker, modelName, response, config, "google");
1319
1426
  // Extract images from response
1320
1427
  const images = [];
1321
1428
  const candidates = response.candidates || [];
@@ -1358,7 +1465,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1358
1465
  * and `generateImageWithGoogle` for full option sets); defaults to `{}`
1359
1466
  * @returns The generated image data object returned by the provider-specific function
1360
1467
  */
1361
- const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}) => {
1468
+ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1362
1469
  const provider = getImageModelProvider(modelName);
1363
1470
  if (!provider) {
1364
1471
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, `Unable to determine provider for model: ${modelName}. Model name should start with 'gpt-image-', 'gemini-', or 'imagen-'.`);
@@ -1367,10 +1474,10 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1367
1474
  try {
1368
1475
  let result;
1369
1476
  if (provider === "openai") {
1370
- result = await generateImageWithOpenAI(modelName, config, prompt, options);
1477
+ result = await generateImageWithOpenAI(modelName, config, prompt, options, usageTracker);
1371
1478
  }
1372
1479
  else if (provider === "google") {
1373
- result = await generateImageWithGoogle(modelName, config, prompt, options);
1480
+ result = await generateImageWithGoogle(modelName, config, prompt, options, usageTracker);
1374
1481
  }
1375
1482
  const endTime = Date.now();
1376
1483
  const duration = endTime - startTime;
@@ -1398,5 +1505,6 @@ exports.default = {
1398
1505
  runPromptWithModel,
1399
1506
  runPromptWithModelForImageGeneration,
1400
1507
  throwErrorIfNotSuccessful,
1508
+ updateUsageTrackerForCharacters,
1401
1509
  validateModel,
1402
1510
  };
@@ -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;
@@ -15,8 +15,9 @@ declare const _default: {
15
15
  createAgent: (name: string, modelName: string, systemPrompt: string, tools: any[], responseFormat: any, config: any) => any;
16
16
  runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null, usageTracker?: UsageTracker | null) => Promise<any>;
17
17
  runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[], usageTracker?: UsageTracker | null) => Promise<any>;
18
- runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any) => Promise<any>;
18
+ runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
19
19
  throwErrorIfNotSuccessful: (response: any) => string;
20
+ updateUsageTrackerForCharacters: (tracker: UsageTracker | null | undefined, modelName: string, characterCount: number, config: any) => void;
20
21
  validateModel: (selectedModel: string, supportedModels: string[]) => string;
21
22
  };
22
23
  export default _default;
@@ -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;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"}
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;sDAktBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CAt/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;;AA6hCT,wBASE"}
@@ -63,6 +63,101 @@ const updateUsageTracker = (tracker, modelName, usage, config) => {
63
63
  tracker.tokens[inputKey] = (tracker.tokens[inputKey] || 0) + inputTokens;
64
64
  tracker.tokens[outputKey] = (tracker.tokens[outputKey] || 0) + outputTokens;
65
65
  };
66
+ /**
67
+ * Adds a single image-generation call's usage to the caller-supplied tracker.
68
+ * Extracts token counts from provider-specific response shapes: OpenAI returns
69
+ * `response.usage` with `input_tokens_details.{text_tokens,image_tokens}` and
70
+ * `output_tokens`; Google returns `response.usageMetadata` with
71
+ * `promptTokensDetails` (modality-keyed) and `candidatesTokenCount` for the
72
+ * generated image. Reads rates from `<model>-input-token-costs`,
73
+ * `<model>-image-input-token-costs`, and `<model>-image-output-token-costs`.
74
+ * No-ops when the tracker or response usage info is absent.
75
+ */
76
+ const updateImageUsageTracker = (tracker, modelName, response, config, provider) => {
77
+ if (!tracker || !modelName || !response)
78
+ return;
79
+ if (typeof tracker.cost !== "number")
80
+ tracker.cost = 0;
81
+ if (!tracker.tokens || typeof tracker.tokens !== "object")
82
+ tracker.tokens = {};
83
+ let textInputTokens = 0;
84
+ let imageInputTokens = 0;
85
+ let imageOutputTokens = 0;
86
+ if (provider === "openai") {
87
+ const usage = response.usage;
88
+ if (!usage)
89
+ return;
90
+ const details = usage.input_tokens_details || {};
91
+ textInputTokens = details.text_tokens || 0;
92
+ imageInputTokens = details.image_tokens || 0;
93
+ // Older shapes report a single input_tokens without modality breakdown.
94
+ if (!textInputTokens && !imageInputTokens && usage.input_tokens) {
95
+ textInputTokens = usage.input_tokens;
96
+ }
97
+ imageOutputTokens = usage.output_tokens || 0;
98
+ }
99
+ else {
100
+ const um = response.usageMetadata;
101
+ if (!um)
102
+ return;
103
+ const promptDetails = Array.isArray(um.promptTokensDetails)
104
+ ? um.promptTokensDetails
105
+ : [];
106
+ for (const d of promptDetails) {
107
+ const modality = String(d?.modality || "").toUpperCase();
108
+ const count = Number(d?.tokenCount) || 0;
109
+ if (modality === "IMAGE")
110
+ imageInputTokens += count;
111
+ else
112
+ textInputTokens += count;
113
+ }
114
+ if (!textInputTokens && !imageInputTokens) {
115
+ textInputTokens = Number(um.promptTokenCount) || 0;
116
+ }
117
+ imageOutputTokens = Number(um.candidatesTokenCount) || 0;
118
+ }
119
+ const textInputRate = getModelRate(modelName, config, "input-token");
120
+ const imageInputRate = getModelRate(modelName, config, "image-input-token");
121
+ const imageOutputRate = getModelRate(modelName, config, "image-output-token");
122
+ const addedCost = (textInputTokens / 1_000_000) * textInputRate +
123
+ (imageInputTokens / 1_000_000) * imageInputRate +
124
+ (imageOutputTokens / 1_000_000) * imageOutputRate;
125
+ if (Number.isFinite(addedCost) && addedCost > 0)
126
+ tracker.cost += addedCost;
127
+ if (textInputTokens > 0) {
128
+ const key = `${modelName}_inputTokens`;
129
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + textInputTokens;
130
+ }
131
+ if (imageInputTokens > 0) {
132
+ const key = `${modelName}_imageInputTokens`;
133
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + imageInputTokens;
134
+ }
135
+ if (imageOutputTokens > 0) {
136
+ const key = `${modelName}_imageOutputTokens`;
137
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + imageOutputTokens;
138
+ }
139
+ };
140
+ /**
141
+ * Adds character-billed usage (e.g. ElevenLabs TTS) to the caller-supplied
142
+ * tracker. There is no provider response to parse — billing is deterministic
143
+ * from the input character count. Reads the rate from
144
+ * `<model>-character-costs` (USD per million characters) and accumulates
145
+ * under `<model>_characters`. No-ops on missing tracker / model / count.
146
+ */
147
+ const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, config) => {
148
+ if (!tracker || !modelName || !characterCount)
149
+ return;
150
+ if (typeof tracker.cost !== "number")
151
+ tracker.cost = 0;
152
+ if (!tracker.tokens || typeof tracker.tokens !== "object")
153
+ tracker.tokens = {};
154
+ const rate = getModelRate(modelName, config, "character");
155
+ const addedCost = (characterCount / 1_000_000) * rate;
156
+ if (Number.isFinite(addedCost) && addedCost > 0)
157
+ tracker.cost += addedCost;
158
+ const key = `${modelName}_characters`;
159
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
160
+ };
66
161
  /**
67
162
  * Extracts a normalized token-usage object from a single LangChain `invoke()`
68
163
  * response. Reads from `usage_metadata` first (standardized in LangChain v1),
@@ -1165,7 +1260,7 @@ const getImageModelProvider = (modelName) => {
1165
1260
  * @returns An object with `url`, `b64_json`, and `revisedPrompt` for a single image,
1166
1261
  * or `{ images: [...] }` for multiple images
1167
1262
  */
1168
- const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1263
+ const generateImageWithOpenAI = async (modelName, config, prompt, options, usageTracker = null) => {
1169
1264
  const { size = "1024x1024", style = "vivid", responseFormat = "url", n = 1, } = options;
1170
1265
  if (!config.openAIAPIKey) {
1171
1266
  throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "OpenAI API key is required for OpenAI image generation");
@@ -1213,6 +1308,7 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1213
1308
  }
1214
1309
  }
1215
1310
  const response = await openai.images.generate(requestParams);
1311
+ updateImageUsageTracker(usageTracker, modelName, response, config, "openai");
1216
1312
  // Format response based on number of images
1217
1313
  if (n === 1) {
1218
1314
  const imageData = response.data[0];
@@ -1238,20 +1334,21 @@ const generateImageWithOpenAI = async (modelName, config, prompt, options) => {
1238
1334
  * Google models do not support alpha channels. Validates the `aspectRatio` against
1239
1335
  * allowed values (`"1:1"`, `"3:4"`, `"4:3"`, `"9:16"`, `"16:9"`). Applies model-
1240
1336
  * specific generation config: Imagen models receive `numberOfImages`, `aspectRatio`,
1241
- * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`
1242
- * and `topP`. Safety settings are disabled for image generation. Throws an error if no
1243
- * images are returned.
1337
+ * `outputMimeType`, and optional `negativePrompt`; Gemini models receive `temperature`,
1338
+ * `topP`, and an `imageConfig` carrying `aspectRatio` plus an optional `imageSize`
1339
+ * resolution tier (e.g. "1K"/"2K"/"4K", honoured by gemini-3-pro-image). Safety
1340
+ * settings are disabled for image generation. Throws an error if no images are returned.
1244
1341
  * @param modelName - The Google model identifier (e.g. `"imagen-4.0-generate-001"`,
1245
1342
  * `"gemini-3.0-pro-image"`)
1246
1343
  * @param config - Configuration object; must include `googleAPIKey`
1247
1344
  * @param prompt - The text prompt describing the image to generate
1248
- * @param options - Generation options including `aspectRatio`, `numberOfImages`, and
1249
- * optional `negativePrompt` (Imagen only)
1345
+ * @param options - Generation options including `aspectRatio`, optional `imageSize`
1346
+ * (Gemini resolution tier), `numberOfImages`, and optional `negativePrompt` (Imagen only)
1250
1347
  * @returns A single image descriptor `{ b64_json, mimeType }` when one image is
1251
1348
  * requested, or `{ images: [...] }` for multiple images
1252
1349
  */
1253
- const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1254
- const { aspectRatio = "1:1", numberOfImages = 1, negativePrompt = "", } = options;
1350
+ const generateImageWithGoogle = async (modelName, config, prompt, options, usageTracker = null) => {
1351
+ const { aspectRatio = "1:1", imageSize, numberOfImages = 1, negativePrompt = "", } = options;
1255
1352
  if (!config.googleAPIKey) {
1256
1353
  throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Google API key is required for Google image generation");
1257
1354
  }
@@ -1288,6 +1385,15 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1288
1385
  // Gemini-specific config
1289
1386
  temperature: 1,
1290
1387
  topP: 0.95,
1388
+ // Gemini controls output dimensions through `imageConfig`, not the
1389
+ // top-level `aspectRatio` field that Imagen uses. `imageSize`
1390
+ // (e.g. "1K"/"2K"/"4K") is only honoured by resolution-capable
1391
+ // models such as gemini-3-pro-image, so it is passed through only
1392
+ // when the caller explicitly requests it.
1393
+ imageConfig: {
1394
+ aspectRatio: aspectRatio,
1395
+ ...(imageSize ? { imageSize: imageSize } : {}),
1396
+ },
1291
1397
  }),
1292
1398
  };
1293
1399
  // Safety settings (disable for image generation)
@@ -1311,6 +1417,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1311
1417
  },
1312
1418
  };
1313
1419
  const response = await ai.models.generateContent(req);
1420
+ updateImageUsageTracker(usageTracker, modelName, response, config, "google");
1314
1421
  // Extract images from response
1315
1422
  const images = [];
1316
1423
  const candidates = response.candidates || [];
@@ -1353,7 +1460,7 @@ const generateImageWithGoogle = async (modelName, config, prompt, options) => {
1353
1460
  * and `generateImageWithGoogle` for full option sets); defaults to `{}`
1354
1461
  * @returns The generated image data object returned by the provider-specific function
1355
1462
  */
1356
- const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}) => {
1463
+ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1357
1464
  const provider = getImageModelProvider(modelName);
1358
1465
  if (!provider) {
1359
1466
  throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, `Unable to determine provider for model: ${modelName}. Model name should start with 'gpt-image-', 'gemini-', or 'imagen-'.`);
@@ -1362,10 +1469,10 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1362
1469
  try {
1363
1470
  let result;
1364
1471
  if (provider === "openai") {
1365
- result = await generateImageWithOpenAI(modelName, config, prompt, options);
1472
+ result = await generateImageWithOpenAI(modelName, config, prompt, options, usageTracker);
1366
1473
  }
1367
1474
  else if (provider === "google") {
1368
- result = await generateImageWithGoogle(modelName, config, prompt, options);
1475
+ result = await generateImageWithGoogle(modelName, config, prompt, options, usageTracker);
1369
1476
  }
1370
1477
  const endTime = Date.now();
1371
1478
  const duration = endTime - startTime;
@@ -1393,5 +1500,6 @@ export default {
1393
1500
  runPromptWithModel,
1394
1501
  runPromptWithModelForImageGeneration,
1395
1502
  throwErrorIfNotSuccessful,
1503
+ updateUsageTrackerForCharacters,
1396
1504
  validateModel,
1397
1505
  };
@@ -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.7",
6
+ "version": "1.2.10",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",