@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.
- package/dist/cjs/langChain.d.ts +2 -1
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +119 -11
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +67 -9
- package/dist/esm/langChain.d.ts +2 -1
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +119 -11
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +67 -9
- package/package.json +1 -1
package/dist/cjs/langChain.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -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
|
-
*
|
|
1248
|
-
*
|
|
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`, `
|
|
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
|
};
|
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;
|
package/dist/esm/langChain.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -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
|
-
*
|
|
1243
|
-
*
|
|
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`, `
|
|
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
|
};
|
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;
|