visual-ai-assertions 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -387,11 +387,14 @@ The `VisualAIKnownError` union and `isVisualAIKnownError()` helper are useful wh
387
387
 
388
388
  ### Optional Configuration
389
389
 
390
- | Variable | Description |
391
- | ----------------------- | ---------------------------------------------------------------------------------------------- |
392
- | `VISUAL_AI_MODEL` | Default model when `model` is not set in config. Overrides the provider's default model. |
393
- | `VISUAL_AI_DEBUG` | Enable debug logging when `debug` is not set in config. Use `"true"` or `"1"` to enable. |
394
- | `VISUAL_AI_TRACK_USAGE` | Enable usage tracking when `trackUsage` is not set in config. Use `"true"` or `"1"` to enable. |
390
+ | Variable | Description |
391
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
392
+ | `VISUAL_AI_MODEL` | Default model when `model` is not set in config. Overrides the provider's default model. |
393
+ | `VISUAL_AI_DEBUG` | Enable error diagnostic logging to stderr. Does **not** enable prompt/response logging. Use `"true"` or `"1"`. |
394
+ | `VISUAL_AI_DEBUG_PROMPT` | Enable prompt-only debug logging to stderr. Use `"true"` or `"1"`. |
395
+ | `VISUAL_AI_DEBUG_RESPONSE` | Enable response-only debug logging to stderr. Use `"true"` or `"1"`. |
396
+ | `VISUAL_AI_REASONING_EFFORT` | Default reasoning effort when `reasoningEffort` is not set in config. Use `"low"`, `"medium"`, `"high"`, or `"xhigh"`. |
397
+ | `VISUAL_AI_TRACK_USAGE` | Enable usage tracking (token counts and cost) to stderr. Use `"true"` or `"1"`. |
395
398
 
396
399
  ## Configuration
397
400
 
@@ -399,7 +402,9 @@ The `VisualAIKnownError` union and `isVisualAIKnownError()` helper are useful wh
399
402
  | ----------------- | ------- | ---------------- | ----------------------------------------------------------------------------- |
400
403
  | `apiKey` | string | env var | API key for the provider |
401
404
  | `model` | string | provider default | Model to use |
402
- | `debug` | boolean | `false` | Log prompts/responses to stderr |
405
+ | `debug` | boolean | `false` | Enable error diagnostic logging to stderr |
406
+ | `debugPrompt` | boolean | `false` | Log prompts to stderr |
407
+ | `debugResponse` | boolean | `false` | Log responses to stderr |
403
408
  | `maxTokens` | number | `4096` | Max tokens for AI response |
404
409
  | `reasoningEffort` | string | `undefined` | `"low"` `"medium"` `"high"` `"xhigh"` — controls how deeply the model reasons |
405
410
  | `trackUsage` | boolean | `false` | Log token usage and estimated cost to stderr |
package/dist/index.cjs CHANGED
@@ -100,6 +100,11 @@ var MODEL_TO_PROVIDER = new Map([
100
100
  ...Object.values(Model.Google).map((m) => [m, Provider.GOOGLE])
101
101
  ]);
102
102
  var VALID_PROVIDERS = Object.values(Provider);
103
+ var PROVIDER_DEFAULT_REASONING = {
104
+ openai: "medium",
105
+ anthropic: "off",
106
+ google: "off"
107
+ };
103
108
  var Content = {
104
109
  /** Detects Lorem ipsum, TODO, TBD, and similar placeholder text */
105
110
  PLACEHOLDER_TEXT: "placeholder-text",
@@ -752,16 +757,38 @@ function parseBooleanEnv(envName, value) {
752
757
  `Invalid ${envName} value: "${value}". Use "true", "1", "false", or "0".`
753
758
  );
754
759
  }
760
+ var VALID_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
761
+ function parseReasoningEffortEnv(envName, value) {
762
+ if (value === void 0 || value === "") return void 0;
763
+ const lower = value.toLowerCase();
764
+ if (VALID_REASONING_EFFORTS.includes(lower)) return lower;
765
+ throw new VisualAIConfigError(
766
+ `Invalid ${envName} value: "${value}". Use "low", "medium", "high", or "xhigh".`
767
+ );
768
+ }
769
+ var debugDeprecationWarned = false;
755
770
  function resolveConfig(config) {
756
771
  const provider = resolveProvider(config);
757
772
  const model = config.model ?? process.env.VISUAL_AI_MODEL ?? DEFAULT_MODELS[provider];
773
+ const debug = config.debug ?? parseBooleanEnv("VISUAL_AI_DEBUG", process.env.VISUAL_AI_DEBUG) ?? false;
774
+ const debugPrompt = config.debugPrompt ?? parseBooleanEnv("VISUAL_AI_DEBUG_PROMPT", process.env.VISUAL_AI_DEBUG_PROMPT) ?? false;
775
+ const debugResponse = config.debugResponse ?? parseBooleanEnv("VISUAL_AI_DEBUG_RESPONSE", process.env.VISUAL_AI_DEBUG_RESPONSE) ?? false;
776
+ if (debug && !debugPrompt && !debugResponse && !debugDeprecationWarned) {
777
+ debugDeprecationWarned = true;
778
+ process.stderr.write(
779
+ `[visual-ai-assertions] Warning: VISUAL_AI_DEBUG no longer enables prompt/response logging. Use VISUAL_AI_DEBUG_PROMPT=true and/or VISUAL_AI_DEBUG_RESPONSE=true instead.
780
+ `
781
+ );
782
+ }
758
783
  return {
759
784
  provider,
760
785
  apiKey: config.apiKey,
761
786
  model,
762
787
  maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
763
- reasoningEffort: config.reasoningEffort,
764
- debug: config.debug ?? parseBooleanEnv("VISUAL_AI_DEBUG", process.env.VISUAL_AI_DEBUG) ?? false,
788
+ reasoningEffort: config.reasoningEffort ?? parseReasoningEffortEnv("VISUAL_AI_REASONING_EFFORT", process.env.VISUAL_AI_REASONING_EFFORT),
789
+ debug,
790
+ debugPrompt,
791
+ debugResponse,
765
792
  trackUsage: config.trackUsage ?? parseBooleanEnv("VISUAL_AI_TRACK_USAGE", process.env.VISUAL_AI_TRACK_USAGE) ?? false
766
793
  };
767
794
  }
@@ -822,8 +849,9 @@ function calculateCost(provider, model, inputTokens, outputTokens) {
822
849
  }
823
850
 
824
851
  // src/core/debug.ts
825
- function debugLog(config, label, data) {
826
- if (config.debug) {
852
+ function debugLog(config, label, data, kind = "error") {
853
+ const enabled = kind === "prompt" ? config.debugPrompt : kind === "response" ? config.debugResponse : config.debug;
854
+ if (enabled) {
827
855
  process.stderr.write(`[visual-ai-assertions] ${label}: ${data}
828
856
  `);
829
857
  }
@@ -831,8 +859,9 @@ function debugLog(config, label, data) {
831
859
  function usageLog(config, method, usage) {
832
860
  if (!config.trackUsage) return;
833
861
  const costStr = usage.estimatedCost !== void 0 ? `$${usage.estimatedCost.toFixed(6)}` : "unknown";
862
+ const reasoningStr = config.reasoningEffort ? `reasoning: ${config.reasoningEffort}` : `reasoning: ${PROVIDER_DEFAULT_REASONING[config.provider]} (provider default)`;
834
863
  process.stderr.write(
835
- `[visual-ai-assertions] ${method} usage: ${usage.inputTokens} input + ${usage.outputTokens} output tokens (${costStr}) in ${usage.durationSeconds?.toFixed(3) ?? "0.000"}s [${config.model}]
864
+ `[visual-ai-assertions] ${method} usage: ${usage.inputTokens} input + ${usage.outputTokens} output tokens (${costStr}) in ${usage.durationSeconds?.toFixed(3) ?? "0.000"}s [${config.model}, ${reasoningStr}]
836
865
  `
837
866
  );
838
867
  }
@@ -848,6 +877,28 @@ function processUsage(method, rawUsage, durationSeconds, config) {
848
877
  usageLog(config, method, usage);
849
878
  return usage;
850
879
  }
880
+ var MAX_RAW_RESPONSE_PREVIEW = 500;
881
+ function formatError(error) {
882
+ if (error instanceof VisualAIResponseParseError) {
883
+ const truncated = error.rawResponse.length > MAX_RAW_RESPONSE_PREVIEW ? error.rawResponse.slice(0, MAX_RAW_RESPONSE_PREVIEW) + "..." : error.rawResponse;
884
+ return `${error.name} (${error.code}): ${error.message}. Raw (truncated): ${truncated}`;
885
+ }
886
+ if (error instanceof VisualAIError) {
887
+ return `${error.name} (${error.code}): ${error.message}`;
888
+ }
889
+ if (error instanceof Error) {
890
+ return `${error.name}: ${error.message}`;
891
+ }
892
+ return String(error);
893
+ }
894
+ async function withErrorDebug(config, method, fn) {
895
+ try {
896
+ return await fn();
897
+ } catch (error) {
898
+ debugLog(config, `${method} error`, formatError(error), "error");
899
+ throw error;
900
+ }
901
+ }
851
902
  async function timedSendMessage(driver, images, prompt) {
852
903
  const start = performance.now();
853
904
  const response = await driver.sendMessage(images, prompt);
@@ -1176,16 +1227,18 @@ function visualAI(config = {}) {
1176
1227
  if (elements.length === 0) {
1177
1228
  throw new VisualAIConfigError(`At least one element is required for ${methodName}()`);
1178
1229
  }
1179
- const img = await normalizeImage(image);
1180
- const prompt = buildElementsVisibilityPrompt(elements, visible, options);
1181
- debugLog(resolvedConfig, `${methodName} prompt`, prompt);
1182
- const response = await timedSendMessage(driver, [img], prompt);
1183
- debugLog(resolvedConfig, `${methodName} response`, response.text);
1184
- const result = parseCheckResponse(response.text);
1185
- return {
1186
- ...result,
1187
- usage: processUsage(methodName, response.usage, response.durationSeconds, resolvedConfig)
1188
- };
1230
+ return withErrorDebug(resolvedConfig, methodName, async () => {
1231
+ const img = await normalizeImage(image);
1232
+ const prompt = buildElementsVisibilityPrompt(elements, visible, options);
1233
+ debugLog(resolvedConfig, `${methodName} prompt`, prompt, "prompt");
1234
+ const response = await timedSendMessage(driver, [img], prompt);
1235
+ debugLog(resolvedConfig, `${methodName} response`, response.text, "response");
1236
+ const result = parseCheckResponse(response.text);
1237
+ return {
1238
+ ...result,
1239
+ usage: processUsage(methodName, response.usage, response.durationSeconds, resolvedConfig)
1240
+ };
1241
+ });
1189
1242
  }
1190
1243
  return {
1191
1244
  async check(image, statements, options) {
@@ -1193,61 +1246,64 @@ function visualAI(config = {}) {
1193
1246
  if (stmts.length === 0) {
1194
1247
  throw new VisualAIConfigError("At least one statement is required for check()");
1195
1248
  }
1196
- const img = await normalizeImage(image);
1197
- const prompt = buildCheckPrompt(stmts, { instructions: options?.instructions });
1198
- debugLog(resolvedConfig, "check prompt", prompt);
1199
- const response = await timedSendMessage(driver, [img], prompt);
1200
- debugLog(resolvedConfig, "check response", response.text);
1201
- const result = parseCheckResponse(response.text);
1202
- return {
1203
- ...result,
1204
- usage: processUsage("check", response.usage, response.durationSeconds, resolvedConfig)
1205
- };
1249
+ return withErrorDebug(resolvedConfig, "check", async () => {
1250
+ const img = await normalizeImage(image);
1251
+ const prompt = buildCheckPrompt(stmts, { instructions: options?.instructions });
1252
+ debugLog(resolvedConfig, "check prompt", prompt, "prompt");
1253
+ const response = await timedSendMessage(driver, [img], prompt);
1254
+ debugLog(resolvedConfig, "check response", response.text, "response");
1255
+ const result = parseCheckResponse(response.text);
1256
+ return {
1257
+ ...result,
1258
+ usage: processUsage("check", response.usage, response.durationSeconds, resolvedConfig)
1259
+ };
1260
+ });
1206
1261
  },
1207
1262
  async ask(image, userPrompt, options) {
1208
- const img = await normalizeImage(image);
1209
- const prompt = buildAskPrompt(userPrompt, { instructions: options?.instructions });
1210
- debugLog(resolvedConfig, "ask prompt", prompt);
1211
- const response = await timedSendMessage(driver, [img], prompt);
1212
- debugLog(resolvedConfig, "ask response", response.text);
1213
- const result = parseAskResponse(response.text);
1214
- return {
1215
- ...result,
1216
- usage: processUsage("ask", response.usage, response.durationSeconds, resolvedConfig)
1217
- };
1263
+ return withErrorDebug(resolvedConfig, "ask", async () => {
1264
+ const img = await normalizeImage(image);
1265
+ const prompt = buildAskPrompt(userPrompt, { instructions: options?.instructions });
1266
+ debugLog(resolvedConfig, "ask prompt", prompt, "prompt");
1267
+ const response = await timedSendMessage(driver, [img], prompt);
1268
+ debugLog(resolvedConfig, "ask response", response.text, "response");
1269
+ const result = parseAskResponse(response.text);
1270
+ return {
1271
+ ...result,
1272
+ usage: processUsage("ask", response.usage, response.durationSeconds, resolvedConfig)
1273
+ };
1274
+ });
1218
1275
  },
1219
1276
  async compare(imageA, imageB, options) {
1220
- const [imgA, imgB] = await Promise.all([normalizeImage(imageA), normalizeImage(imageB)]);
1221
- const prompt = buildComparePrompt({
1222
- userPrompt: options?.prompt,
1223
- instructions: options?.instructions
1224
- });
1225
- debugLog(resolvedConfig, "compare prompt", prompt);
1226
- const response = await timedSendMessage(driver, [imgA, imgB], prompt);
1227
- debugLog(resolvedConfig, "compare response", response.text);
1228
- const supportsAnnotatedDiff = resolvedConfig.provider === "google" && resolvedConfig.model === Model.Google.GEMINI_3_FLASH_PREVIEW;
1229
- const effectiveDiffImage = options?.diffImage ?? (supportsAnnotatedDiff ? true : false);
1230
- let diffImage;
1231
- if (effectiveDiffImage) {
1232
- try {
1233
- diffImage = await generateAiDiff(imgA, imgB, resolvedConfig.model, driver);
1234
- } catch (err) {
1235
- const msg = err instanceof Error ? err.message : String(err);
1236
- debugLog(resolvedConfig, "ai diff error", msg);
1237
- if (!resolvedConfig.debug) {
1277
+ return withErrorDebug(resolvedConfig, "compare", async () => {
1278
+ const [imgA, imgB] = await Promise.all([normalizeImage(imageA), normalizeImage(imageB)]);
1279
+ const prompt = buildComparePrompt({
1280
+ userPrompt: options?.prompt,
1281
+ instructions: options?.instructions
1282
+ });
1283
+ debugLog(resolvedConfig, "compare prompt", prompt, "prompt");
1284
+ const response = await timedSendMessage(driver, [imgA, imgB], prompt);
1285
+ debugLog(resolvedConfig, "compare response", response.text, "response");
1286
+ const supportsAnnotatedDiff = resolvedConfig.provider === "google" && resolvedConfig.model === Model.Google.GEMINI_3_FLASH_PREVIEW;
1287
+ const effectiveDiffImage = options?.diffImage ?? (supportsAnnotatedDiff ? true : false);
1288
+ let diffImage;
1289
+ if (effectiveDiffImage) {
1290
+ try {
1291
+ diffImage = await generateAiDiff(imgA, imgB, resolvedConfig.model, driver);
1292
+ } catch (err) {
1293
+ const msg = err instanceof Error ? err.message : String(err);
1238
1294
  process.stderr.write(
1239
1295
  `[visual-ai-assertions] warning: diff generation failed: ${msg}
1240
1296
  `
1241
1297
  );
1242
1298
  }
1243
1299
  }
1244
- }
1245
- const result = parseCompareResponse(response.text);
1246
- return {
1247
- ...result,
1248
- ...diffImage ? { diffImage } : {},
1249
- usage: processUsage("compare", response.usage, response.durationSeconds, resolvedConfig)
1250
- };
1300
+ const result = parseCompareResponse(response.text);
1301
+ return {
1302
+ ...result,
1303
+ ...diffImage ? { diffImage } : {},
1304
+ usage: processUsage("compare", response.usage, response.durationSeconds, resolvedConfig)
1305
+ };
1306
+ });
1251
1307
  },
1252
1308
  elementsVisible(image, elements, options) {
1253
1309
  return checkElementsVisibility(image, elements, true, options);
@@ -1256,57 +1312,65 @@ function visualAI(config = {}) {
1256
1312
  return checkElementsVisibility(image, elements, false, options);
1257
1313
  },
1258
1314
  async accessibility(image, options) {
1259
- const img = await normalizeImage(image);
1260
- const prompt = buildAccessibilityPrompt(options);
1261
- debugLog(resolvedConfig, "accessibility prompt", prompt);
1262
- const response = await timedSendMessage(driver, [img], prompt);
1263
- debugLog(resolvedConfig, "accessibility response", response.text);
1264
- const result = parseCheckResponse(response.text);
1265
- return {
1266
- ...result,
1267
- usage: processUsage(
1268
- "accessibility",
1269
- response.usage,
1270
- response.durationSeconds,
1271
- resolvedConfig
1272
- )
1273
- };
1315
+ return withErrorDebug(resolvedConfig, "accessibility", async () => {
1316
+ const img = await normalizeImage(image);
1317
+ const prompt = buildAccessibilityPrompt(options);
1318
+ debugLog(resolvedConfig, "accessibility prompt", prompt, "prompt");
1319
+ const response = await timedSendMessage(driver, [img], prompt);
1320
+ debugLog(resolvedConfig, "accessibility response", response.text, "response");
1321
+ const result = parseCheckResponse(response.text);
1322
+ return {
1323
+ ...result,
1324
+ usage: processUsage(
1325
+ "accessibility",
1326
+ response.usage,
1327
+ response.durationSeconds,
1328
+ resolvedConfig
1329
+ )
1330
+ };
1331
+ });
1274
1332
  },
1275
1333
  async layout(image, options) {
1276
- const img = await normalizeImage(image);
1277
- const prompt = buildLayoutPrompt(options);
1278
- debugLog(resolvedConfig, "layout prompt", prompt);
1279
- const response = await timedSendMessage(driver, [img], prompt);
1280
- debugLog(resolvedConfig, "layout response", response.text);
1281
- const result = parseCheckResponse(response.text);
1282
- return {
1283
- ...result,
1284
- usage: processUsage("layout", response.usage, response.durationSeconds, resolvedConfig)
1285
- };
1334
+ return withErrorDebug(resolvedConfig, "layout", async () => {
1335
+ const img = await normalizeImage(image);
1336
+ const prompt = buildLayoutPrompt(options);
1337
+ debugLog(resolvedConfig, "layout prompt", prompt, "prompt");
1338
+ const response = await timedSendMessage(driver, [img], prompt);
1339
+ debugLog(resolvedConfig, "layout response", response.text, "response");
1340
+ const result = parseCheckResponse(response.text);
1341
+ return {
1342
+ ...result,
1343
+ usage: processUsage("layout", response.usage, response.durationSeconds, resolvedConfig)
1344
+ };
1345
+ });
1286
1346
  },
1287
1347
  async pageLoad(image, options) {
1288
- const img = await normalizeImage(image);
1289
- const prompt = buildPageLoadPrompt(options);
1290
- debugLog(resolvedConfig, "pageLoad prompt", prompt);
1291
- const response = await timedSendMessage(driver, [img], prompt);
1292
- debugLog(resolvedConfig, "pageLoad response", response.text);
1293
- const result = parseCheckResponse(response.text);
1294
- return {
1295
- ...result,
1296
- usage: processUsage("pageLoad", response.usage, response.durationSeconds, resolvedConfig)
1297
- };
1348
+ return withErrorDebug(resolvedConfig, "pageLoad", async () => {
1349
+ const img = await normalizeImage(image);
1350
+ const prompt = buildPageLoadPrompt(options);
1351
+ debugLog(resolvedConfig, "pageLoad prompt", prompt, "prompt");
1352
+ const response = await timedSendMessage(driver, [img], prompt);
1353
+ debugLog(resolvedConfig, "pageLoad response", response.text, "response");
1354
+ const result = parseCheckResponse(response.text);
1355
+ return {
1356
+ ...result,
1357
+ usage: processUsage("pageLoad", response.usage, response.durationSeconds, resolvedConfig)
1358
+ };
1359
+ });
1298
1360
  },
1299
1361
  async content(image, options) {
1300
- const img = await normalizeImage(image);
1301
- const prompt = buildContentPrompt(options);
1302
- debugLog(resolvedConfig, "content prompt", prompt);
1303
- const response = await timedSendMessage(driver, [img], prompt);
1304
- debugLog(resolvedConfig, "content response", response.text);
1305
- const result = parseCheckResponse(response.text);
1306
- return {
1307
- ...result,
1308
- usage: processUsage("content", response.usage, response.durationSeconds, resolvedConfig)
1309
- };
1362
+ return withErrorDebug(resolvedConfig, "content", async () => {
1363
+ const img = await normalizeImage(image);
1364
+ const prompt = buildContentPrompt(options);
1365
+ debugLog(resolvedConfig, "content prompt", prompt, "prompt");
1366
+ const response = await timedSendMessage(driver, [img], prompt);
1367
+ debugLog(resolvedConfig, "content response", response.text, "response");
1368
+ const result = parseCheckResponse(response.text);
1369
+ return {
1370
+ ...result,
1371
+ usage: processUsage("content", response.usage, response.durationSeconds, resolvedConfig)
1372
+ };
1373
+ });
1310
1374
  }
1311
1375
  };
1312
1376
  }