visual-ai-assertions 0.21.0 → 0.22.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 +46 -11
- package/dist/index.cjs +121 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +121 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ const ai = visualAI({
|
|
|
104
104
|
debug: true, // optional, logs prompts/responses to stderr
|
|
105
105
|
maxTokens: 4096, // optional, default 4096
|
|
106
106
|
reasoningEffort: "high", // optional, "low" | "medium" | "high" | "xhigh"
|
|
107
|
+
timeout: 120_000, // optional, ms — defaults to the provider SDK's own timeout
|
|
107
108
|
trackUsage: false, // optional, defaults to false — usage stats to stderr
|
|
108
109
|
});
|
|
109
110
|
|
|
@@ -255,6 +256,33 @@ await ai.elementsVisible(screenshot, ["Submit button", "Nav bar", "Footer"]);
|
|
|
255
256
|
// Check that UI elements are hidden
|
|
256
257
|
await ai.elementsHidden(screenshot, ["Loading spinner", "Error modal"]);
|
|
257
258
|
|
|
259
|
+
// Clipping is judged the way a tester would. An element cut off at the trailing
|
|
260
|
+
// edge of a scrollable row or feed passes, because scrolling reaches it. One
|
|
261
|
+
// sliced by the screen edge or by fixed chrome such as the status bar or a
|
|
262
|
+
// sticky nav fails, because scrolling cannot. An element you cannot see at all
|
|
263
|
+
// fails: only what the screenshot shows is judged.
|
|
264
|
+
//
|
|
265
|
+
// Overlays are judged by what they say about the element's state, not by how
|
|
266
|
+
// much they cover. A badge, favourite icon or price pill coexists with finished
|
|
267
|
+
// content and leaves the element visible. A loading spinner, skeleton, progress
|
|
268
|
+
// bar or error overlay says it is not ready, so the check fails even though you
|
|
269
|
+
// can still see what is underneath. A modal, dialog or cookie banner that a user
|
|
270
|
+
// could not read or use past also fails.
|
|
271
|
+
//
|
|
272
|
+
// Pass finalState: false when the screenshot was deliberately captured mid-load,
|
|
273
|
+
// so loading chrome is expected rather than a defect. The finished-state rules
|
|
274
|
+
// are left out; presence, clipping and blocking overlays are still judged.
|
|
275
|
+
await ai.elementsVisible(screenshot, ["Product image"], { finalState: false });
|
|
276
|
+
|
|
277
|
+
// By default this is a presence check: an element counts as visible if it is
|
|
278
|
+
// there at all, whatever it looks like. Pass requireCorrectRendering: true to
|
|
279
|
+
// also fail an element that is present but clearly badly rendered — unreadable
|
|
280
|
+
// contrast, overlapping or misaligned elements, text cut off mid-word — with
|
|
281
|
+
// the model saying the element is present before naming the defect. Opt in per
|
|
282
|
+
// assertion: measured on the bench, it catches exactly that case and makes
|
|
283
|
+
// models flakier on plain presence questions.
|
|
284
|
+
await ai.elementsVisible(screenshot, ["Promo banner"], { requireCorrectRendering: true });
|
|
285
|
+
|
|
258
286
|
// Accessibility checks (contrast, readability, interactive visibility, color blindness, color-alone meaning)
|
|
259
287
|
await ai.accessibility(screenshot);
|
|
260
288
|
await ai.accessibility(screenshot, {
|
|
@@ -463,16 +491,17 @@ The `VisualAIKnownError` union and `isVisualAIKnownError()` helper are useful wh
|
|
|
463
491
|
|
|
464
492
|
## Configuration
|
|
465
493
|
|
|
466
|
-
| Option | Type | Default | Description
|
|
467
|
-
| ----------------- | ------- | ---------------- |
|
|
468
|
-
| `apiKey` | string | env var | API key for the provider
|
|
469
|
-
| `model` | string | provider default | Model to use
|
|
470
|
-
| `debug` | boolean | `false` | Enable error diagnostic logging to stderr
|
|
471
|
-
| `debugPrompt` | boolean | `false` | Log prompts to stderr
|
|
472
|
-
| `debugResponse` | boolean | `false` | Log responses to stderr
|
|
473
|
-
| `maxTokens` | number | `4096` | Max tokens for AI response
|
|
474
|
-
| `reasoningEffort` | string | `undefined` | `"low"` `"medium"` `"high"` `"xhigh"` — controls how deeply the model reasons
|
|
475
|
-
| `
|
|
494
|
+
| Option | Type | Default | Description |
|
|
495
|
+
| ----------------- | ------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
496
|
+
| `apiKey` | string | env var | API key for the provider |
|
|
497
|
+
| `model` | string | provider default | Model to use |
|
|
498
|
+
| `debug` | boolean | `false` | Enable error diagnostic logging to stderr |
|
|
499
|
+
| `debugPrompt` | boolean | `false` | Log prompts to stderr |
|
|
500
|
+
| `debugResponse` | boolean | `false` | Log responses to stderr |
|
|
501
|
+
| `maxTokens` | number | `4096` | Max tokens for AI response |
|
|
502
|
+
| `reasoningEffort` | string | `undefined` | `"low"` `"medium"` `"high"` `"xhigh"` — controls how deeply the model reasons |
|
|
503
|
+
| `timeout` | number | SDK default | Per-request timeout in ms. Unset leaves each SDK's own default (OpenAI/OpenRouter 10 min, Google 1 min). SDKs retry timeouts, so total wall time can be a multiple of this. |
|
|
504
|
+
| `trackUsage` | boolean | `false` | Log token usage and estimated cost to stderr |
|
|
476
505
|
|
|
477
506
|
## Exported Types
|
|
478
507
|
|
|
@@ -535,7 +564,8 @@ All listed models support image/vision input. Pass any model ID to the `model` c
|
|
|
535
564
|
|
|
536
565
|
| Model | Model ID | Input $/MTok | Output $/MTok | Notes |
|
|
537
566
|
| ----------------- | ------------------- | ------------ | ------------- | ------------------------------------------- |
|
|
538
|
-
| Claude Fable 5
|
|
567
|
+
| Claude Fable 5.1 | `claude-fable-5-1` | $10 | $50 | Most capable; long-horizon agentic work |
|
|
568
|
+
| Claude Fable 5 | `claude-fable-5` | $10 | $50 | Predecessor to Fable 5.1, same price |
|
|
539
569
|
| Claude Opus 4.8 | `claude-opus-4-8` | $5 | $25 | Most capable Opus tier; supports `xhigh` |
|
|
540
570
|
| Claude Opus 4.7 | `claude-opus-4-7` | $5 | $25 | Previous Opus; supports `xhigh` effort tier |
|
|
541
571
|
| Claude Opus 4.6 | `claude-opus-4-6` | $5 | $25 | Previous flagship, 128K max output |
|
|
@@ -547,6 +577,7 @@ All listed models support image/vision input. Pass any model ID to the `model` c
|
|
|
547
577
|
|
|
548
578
|
| Model | Model ID | Input $/MTok | Output $/MTok | Notes |
|
|
549
579
|
| ------------- | --------------- | ------------ | ------------- | -------------------------------------- |
|
|
580
|
+
| GPT-6 Astra | `gpt-6-astra` | $10 | $50 | Most capable; restricted access¹ |
|
|
550
581
|
| GPT-5.6 Sol | `gpt-5.6-sol` | $5 | $30 | Newest flagship, frontier tier |
|
|
551
582
|
| GPT-5.6 Terra | `gpt-5.6-terra` | $2 | $12 | Newest balanced, everyday tier |
|
|
552
583
|
| GPT-5.6 Luna | `gpt-5.6-luna` | $0.20 | $1.20 | **Default** — newest, fastest/cheapest |
|
|
@@ -558,6 +589,10 @@ All listed models support image/vision input. Pass any model ID to the `model` c
|
|
|
558
589
|
| GPT-5.4 nano | `gpt-5.4-nano` | $0.20 | $1.25 | Cheapest older-generation option |
|
|
559
590
|
| GPT-5 mini | `gpt-5-mini` | $0.25 | $2 | Fast and cheap |
|
|
560
591
|
|
|
592
|
+
¹ GPT-6 Astra is rolling out through OpenAI's Trusted Access Program, so many API keys cannot reach it yet — expect a `VisualAIProviderError` naming the model until your account is enabled.
|
|
593
|
+
|
|
594
|
+
Astra reasons heavily enough to spend the entire 4096-token default output budget before emitting an answer, so **it is given a 32768-token budget automatically**, at every reasoning effort rather than only at `high`/`xhigh` like other OpenAI models. That follows OpenAI's guidance to reserve at least 25,000 tokens for reasoning and output. Its output length is erratic — identical calls have used anywhere from 0 to 16384+ reasoning tokens — so a large budget reduces truncation without eliminating it; a call that exhausts the budget still bills for the tokens it burned. Passing `maxTokens` explicitly still wins. It also accepts a fifth reasoning level, `max`, above `xhigh`; this library's `reasoningEffort` stops at `xhigh`, which is passed through unchanged, so `max` is not currently reachable.
|
|
595
|
+
|
|
561
596
|
### Google
|
|
562
597
|
|
|
563
598
|
| Model | Model ID | Input $/MTok | Output $/MTok | Notes |
|
package/dist/index.cjs
CHANGED
|
@@ -89,6 +89,7 @@ var Provider = {
|
|
|
89
89
|
};
|
|
90
90
|
var Model = {
|
|
91
91
|
Anthropic: {
|
|
92
|
+
FABLE_5_1: "claude-fable-5-1",
|
|
92
93
|
FABLE_5: "claude-fable-5",
|
|
93
94
|
OPUS_5: "claude-opus-5",
|
|
94
95
|
OPUS_4_8: "claude-opus-4-8",
|
|
@@ -99,6 +100,7 @@ var Model = {
|
|
|
99
100
|
HAIKU_4_5: "claude-haiku-4-5"
|
|
100
101
|
},
|
|
101
102
|
OpenAI: {
|
|
103
|
+
GPT_6_ASTRA: "gpt-6-astra",
|
|
102
104
|
GPT_5_6_SOL: "gpt-5.6-sol",
|
|
103
105
|
GPT_5_6_TERRA: "gpt-5.6-terra",
|
|
104
106
|
GPT_5_6_LUNA: "gpt-5.6-luna",
|
|
@@ -144,6 +146,10 @@ var DEFAULT_MODELS = {
|
|
|
144
146
|
};
|
|
145
147
|
var DEFAULT_MAX_TOKENS = 4096;
|
|
146
148
|
var OPENAI_REASONING_MAX_TOKENS = 16384;
|
|
149
|
+
var OPENAI_HEAVY_REASONING_MAX_TOKENS = 32768;
|
|
150
|
+
var MODELS_REQUIRING_LARGE_OUTPUT_BUDGET = /* @__PURE__ */ new Set([
|
|
151
|
+
Model.OpenAI.GPT_6_ASTRA
|
|
152
|
+
]);
|
|
147
153
|
var MODEL_TO_PROVIDER = new Map([
|
|
148
154
|
...Object.values(Model.Anthropic).map((m) => [m, Provider.ANTHROPIC]),
|
|
149
155
|
...Object.values(Model.OpenAI).map((m) => [m, Provider.OPENAI]),
|
|
@@ -488,22 +494,50 @@ function buildComparePrompt(options) {
|
|
|
488
494
|
}
|
|
489
495
|
|
|
490
496
|
// src/templates/elements-visibility.ts
|
|
491
|
-
var ELEMENTS_VISIBLE_ROLE = "Check whether specific UI elements are present and fully visible in this screenshot.";
|
|
492
497
|
var ELEMENTS_HIDDEN_ROLE = "Check whether specific UI elements are absent or hidden in this screenshot.";
|
|
493
|
-
|
|
494
|
-
|
|
498
|
+
function joinClauses(clauses) {
|
|
499
|
+
if (clauses.length <= 1) return clauses[0] ?? "";
|
|
500
|
+
if (clauses.length === 2) return `${clauses[0]} and ${clauses[1]}`;
|
|
501
|
+
return `${clauses.slice(0, -1).join(", ")}, and ${clauses[clauses.length - 1]}`;
|
|
502
|
+
}
|
|
503
|
+
function visibleRole(finalState, requireCorrectRendering) {
|
|
504
|
+
const clauses = ["present", "properly visible"];
|
|
505
|
+
if (requireCorrectRendering) clauses.push("correctly rendered");
|
|
506
|
+
if (finalState) clauses.push("in their finished state");
|
|
507
|
+
return `Check whether specific UI elements are ${joinClauses(clauses)} in this screenshot.`;
|
|
508
|
+
}
|
|
509
|
+
var ELEMENTS_VISIBLE_CLIPPING_RULES = [
|
|
510
|
+
"When an element is partly rendered but cut off at an edge, decide whether ordinary scrolling would bring it fully into view. For example, a card peeking past the end of a horizontal carousel, a filter chip in a row that continues past the screen edge, or a list item partly below the bottom of a scrolling feed is reachable that way, so the check for that element PASSES. Say in your reasoning that it is reached by scrolling.",
|
|
511
|
+
"An element that scrolling cannot bring into view is NOT properly visible: one sliced by the screen edge itself, or cut off or overlapped by fixed chrome such as the status bar, a notch, a home indicator, a sticky header, or a fixed bottom navigation bar. That is a layout fault, so the check for that element FAILS. Describe the clipping in your reasoning.",
|
|
512
|
+
"An element you cannot see at all is not visible, even if the page might reveal it after scrolling. Judge only what this screenshot actually shows."
|
|
495
513
|
];
|
|
496
|
-
var
|
|
497
|
-
|
|
514
|
+
var ELEMENTS_VISIBLE_FINAL_STATE_RULE = "Judge each element in its finished, presented state. Things a design draws on top of an element \u2014 a badge, a favourite icon, a duration or price pill, a gradient scrim \u2014 coexist with finished content and leave it visible. An overlay that says the element is NOT ready \u2014 a loading spinner, a skeleton placeholder, a shimmer, a progress bar, an error or retry overlay \u2014 means the element is not properly visible even when you can still make out what sits underneath, so the check for that element FAILS. Name which of the two you are seeing in your reasoning.";
|
|
515
|
+
var ELEMENTS_VISIBLE_CORRECT_RENDERING_RULE = "An element that is present but clearly defective in how it is rendered is NOT properly visible: text at contrast too low to read, elements overlapping or colliding with one another, an element visibly out of alignment with the siblings it should line up with, or text cut off mid-word inside its own container. The check for that element FAILS. In your reasoning, say that the element is present and then name the defect. Only clear, unambiguous defects count: do not fail an element for tight spacing, stylistic choices, or anything you would have to argue for.";
|
|
516
|
+
var ELEMENTS_VISIBLE_OCCLUSION_RULE = "An element a user could not read or use because a modal, dialog, cookie banner, toast or similar overlay covers it is NOT visible: the check for that element FAILS.";
|
|
517
|
+
function visibleRules(finalState, requireCorrectRendering) {
|
|
518
|
+
return [
|
|
519
|
+
...ELEMENTS_VISIBLE_CLIPPING_RULES,
|
|
520
|
+
...finalState ? [ELEMENTS_VISIBLE_FINAL_STATE_RULE] : [],
|
|
521
|
+
...requireCorrectRendering ? [ELEMENTS_VISIBLE_CORRECT_RENDERING_RULE] : [],
|
|
522
|
+
ELEMENTS_VISIBLE_OCCLUSION_RULE
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
var ELEMENTS_HIDDEN_BASE_RULES = [
|
|
526
|
+
"An element that is rendered at all, even partly, is not hidden, so the check for that element FAILS. This includes one peeking past the edge of a scrollable row or feed, which the user reaches by scrolling normally. Note the partial visibility in your reasoning.",
|
|
527
|
+
"An element that appears nowhere in this screenshot counts as hidden, even if the page might reveal it after scrolling. Judge only what this screenshot actually shows."
|
|
498
528
|
];
|
|
529
|
+
var ELEMENTS_HIDDEN_FINAL_STATE_RULE = "An element sitting under a loading spinner, skeleton, progress bar or error overlay is still rendered, so it is not hidden and the check for that element FAILS. It is not properly visible either; that is what the visible check is for.";
|
|
530
|
+
function hiddenRules(finalState) {
|
|
531
|
+
return finalState ? [...ELEMENTS_HIDDEN_BASE_RULES, ELEMENTS_HIDDEN_FINAL_STATE_RULE] : ELEMENTS_HIDDEN_BASE_RULES;
|
|
532
|
+
}
|
|
499
533
|
function buildElementsVisibilityPrompt(elements, visible, options) {
|
|
500
|
-
const statements = visible ? elements.map((el) => `The element "${el}" is
|
|
501
|
-
const
|
|
534
|
+
const statements = visible ? elements.map((el) => `The element "${el}" is visible on the page`) : elements.map((el) => `The element "${el}" is NOT visible on the page`);
|
|
535
|
+
const finalState = options?.finalState ?? true;
|
|
536
|
+
const correctRendering = visible && (options?.requireCorrectRendering ?? false);
|
|
537
|
+
const defaultRules = visible ? visibleRules(finalState, correctRendering) : hiddenRules(finalState);
|
|
502
538
|
const instructions = options?.instructions ? [...defaultRules, ...options.instructions] : defaultRules;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
instructions
|
|
506
|
-
});
|
|
539
|
+
const role = visible ? visibleRole(finalState, correctRendering) : ELEMENTS_HIDDEN_ROLE;
|
|
540
|
+
return buildCheckPrompt(statements, { role, instructions });
|
|
507
541
|
}
|
|
508
542
|
|
|
509
543
|
// src/templates/accessibility.ts
|
|
@@ -613,6 +647,7 @@ function parseRetryAfter(value) {
|
|
|
613
647
|
|
|
614
648
|
// src/providers/anthropic.ts
|
|
615
649
|
var XHIGH_CAPABLE_MODELS = /* @__PURE__ */ new Set([
|
|
650
|
+
Model.Anthropic.FABLE_5_1,
|
|
616
651
|
Model.Anthropic.FABLE_5,
|
|
617
652
|
Model.Anthropic.OPUS_5,
|
|
618
653
|
Model.Anthropic.OPUS_4_8,
|
|
@@ -636,12 +671,14 @@ var AnthropicDriver = class {
|
|
|
636
671
|
maxTokens;
|
|
637
672
|
apiKeyOrEnv;
|
|
638
673
|
reasoningEffort;
|
|
674
|
+
timeout;
|
|
639
675
|
constructor(config) {
|
|
640
676
|
this.model = config.model;
|
|
641
677
|
this.maxTokens = config.maxTokens;
|
|
642
678
|
this.client = null;
|
|
643
679
|
this.apiKeyOrEnv = config.apiKey;
|
|
644
680
|
this.reasoningEffort = config.reasoningEffort;
|
|
681
|
+
this.timeout = config.timeout;
|
|
645
682
|
}
|
|
646
683
|
async getClient() {
|
|
647
684
|
if (this.client) return this.client;
|
|
@@ -660,7 +697,10 @@ var AnthropicDriver = class {
|
|
|
660
697
|
"Anthropic API key not found. Set ANTHROPIC_API_KEY or pass apiKey in config."
|
|
661
698
|
);
|
|
662
699
|
}
|
|
663
|
-
this.client = new Anthropic({
|
|
700
|
+
this.client = new Anthropic({
|
|
701
|
+
apiKey,
|
|
702
|
+
...this.timeout !== void 0 && { timeout: this.timeout }
|
|
703
|
+
});
|
|
664
704
|
return this.client;
|
|
665
705
|
}
|
|
666
706
|
async sendMessage(images, prompt, _options) {
|
|
@@ -758,6 +798,7 @@ var GoogleDriver = class {
|
|
|
758
798
|
apiKeyOrEnv;
|
|
759
799
|
reasoningEffort;
|
|
760
800
|
imageDetail;
|
|
801
|
+
timeout;
|
|
761
802
|
constructor(config) {
|
|
762
803
|
this.model = config.model;
|
|
763
804
|
this.maxTokens = config.maxTokens;
|
|
@@ -765,6 +806,7 @@ var GoogleDriver = class {
|
|
|
765
806
|
this.apiKeyOrEnv = config.apiKey;
|
|
766
807
|
this.reasoningEffort = config.reasoningEffort;
|
|
767
808
|
this.imageDetail = config.imageDetail;
|
|
809
|
+
this.timeout = config.timeout;
|
|
768
810
|
}
|
|
769
811
|
toGeminiParts(images) {
|
|
770
812
|
return images.map((img) => ({
|
|
@@ -788,7 +830,10 @@ var GoogleDriver = class {
|
|
|
788
830
|
"Google API key not found. Set GOOGLE_API_KEY or pass apiKey in config."
|
|
789
831
|
);
|
|
790
832
|
}
|
|
791
|
-
this.client = new GoogleGenAI({
|
|
833
|
+
this.client = new GoogleGenAI({
|
|
834
|
+
apiKey,
|
|
835
|
+
...this.timeout !== void 0 && { httpOptions: { timeout: this.timeout } }
|
|
836
|
+
});
|
|
792
837
|
return this.client;
|
|
793
838
|
}
|
|
794
839
|
async sendMessage(images, prompt, _options) {
|
|
@@ -874,6 +919,7 @@ var OpenAIDriver = class {
|
|
|
874
919
|
apiKeyOrEnv;
|
|
875
920
|
reasoningEffort;
|
|
876
921
|
imageDetail;
|
|
922
|
+
timeout;
|
|
877
923
|
constructor(config) {
|
|
878
924
|
this.model = config.model;
|
|
879
925
|
this.maxTokens = config.maxTokens;
|
|
@@ -881,6 +927,7 @@ var OpenAIDriver = class {
|
|
|
881
927
|
this.apiKeyOrEnv = config.apiKey;
|
|
882
928
|
this.reasoningEffort = config.reasoningEffort;
|
|
883
929
|
this.imageDetail = config.imageDetail;
|
|
930
|
+
this.timeout = config.timeout;
|
|
884
931
|
}
|
|
885
932
|
async getClient() {
|
|
886
933
|
if (this.client) return this.client;
|
|
@@ -897,7 +944,10 @@ var OpenAIDriver = class {
|
|
|
897
944
|
"OpenAI API key not found. Set OPENAI_API_KEY or pass apiKey in config."
|
|
898
945
|
);
|
|
899
946
|
}
|
|
900
|
-
this.client = new OpenAI({
|
|
947
|
+
this.client = new OpenAI({
|
|
948
|
+
apiKey,
|
|
949
|
+
...this.timeout !== void 0 && { timeout: this.timeout }
|
|
950
|
+
});
|
|
901
951
|
return this.client;
|
|
902
952
|
}
|
|
903
953
|
async sendMessage(images, prompt, options) {
|
|
@@ -972,6 +1022,7 @@ var OpenRouterDriver = class {
|
|
|
972
1022
|
apiKeyOrEnv;
|
|
973
1023
|
reasoningEffort;
|
|
974
1024
|
imageDetail;
|
|
1025
|
+
timeout;
|
|
975
1026
|
constructor(config) {
|
|
976
1027
|
this.model = config.model;
|
|
977
1028
|
this.maxTokens = config.maxTokens;
|
|
@@ -979,6 +1030,7 @@ var OpenRouterDriver = class {
|
|
|
979
1030
|
this.apiKeyOrEnv = config.apiKey;
|
|
980
1031
|
this.reasoningEffort = config.reasoningEffort;
|
|
981
1032
|
this.imageDetail = config.imageDetail;
|
|
1033
|
+
this.timeout = config.timeout;
|
|
982
1034
|
}
|
|
983
1035
|
async getClient() {
|
|
984
1036
|
if (this.client) return this.client;
|
|
@@ -997,7 +1049,11 @@ var OpenRouterDriver = class {
|
|
|
997
1049
|
"OpenRouter API key not found. Set OPENROUTER_API_KEY or pass apiKey in config."
|
|
998
1050
|
);
|
|
999
1051
|
}
|
|
1000
|
-
this.client = new OpenAI({
|
|
1052
|
+
this.client = new OpenAI({
|
|
1053
|
+
apiKey,
|
|
1054
|
+
baseURL: OPENROUTER_BASE_URL,
|
|
1055
|
+
...this.timeout !== void 0 && { timeout: this.timeout }
|
|
1056
|
+
});
|
|
1001
1057
|
return this.client;
|
|
1002
1058
|
}
|
|
1003
1059
|
async sendMessage(images, prompt, options) {
|
|
@@ -1127,13 +1183,21 @@ function resolveConfig(config) {
|
|
|
1127
1183
|
`
|
|
1128
1184
|
);
|
|
1129
1185
|
}
|
|
1186
|
+
if (config.timeout !== void 0 && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
|
|
1187
|
+
throw new VisualAIConfigError(
|
|
1188
|
+
`Invalid timeout: ${config.timeout}. Must be a positive number of milliseconds.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1130
1191
|
const userSetMaxTokens = config.maxTokens !== void 0;
|
|
1131
1192
|
let maxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
1132
|
-
|
|
1133
|
-
|
|
1193
|
+
const effortNeedsLargeBudget = config.reasoningEffort === "high" || config.reasoningEffort === "xhigh";
|
|
1194
|
+
const modelNeedsLargeBudget = MODELS_REQUIRING_LARGE_OUTPUT_BUDGET.has(model);
|
|
1195
|
+
if (!userSetMaxTokens && (provider === "openai" || provider === "openrouter") && (effortNeedsLargeBudget || modelNeedsLargeBudget)) {
|
|
1196
|
+
maxTokens = modelNeedsLargeBudget ? OPENAI_HEAVY_REASONING_MAX_TOKENS : OPENAI_REASONING_MAX_TOKENS;
|
|
1134
1197
|
if (debug) {
|
|
1198
|
+
const reason = modelNeedsLargeBudget ? `model "${model}", which exhausts smaller budgets on reasoning at any effort` : `provider "${provider}" with reasoningEffort "${config.reasoningEffort}"`;
|
|
1135
1199
|
process.stderr.write(
|
|
1136
|
-
`[visual-ai-assertions] Auto-increased maxTokens from ${DEFAULT_MAX_TOKENS} to ${
|
|
1200
|
+
`[visual-ai-assertions] Auto-increased maxTokens from ${DEFAULT_MAX_TOKENS} to ${maxTokens} for ${reason}.
|
|
1137
1201
|
`
|
|
1138
1202
|
);
|
|
1139
1203
|
}
|
|
@@ -1146,6 +1210,7 @@ function resolveConfig(config) {
|
|
|
1146
1210
|
reasoningEffort: config.reasoningEffort,
|
|
1147
1211
|
maxImageDimension: config.maxImageDimension ?? DEFAULT_MAX_IMAGE_DIMENSION,
|
|
1148
1212
|
imageDetail: config.imageDetail ?? DEFAULT_IMAGE_DETAIL,
|
|
1213
|
+
timeout: config.timeout,
|
|
1149
1214
|
debug,
|
|
1150
1215
|
debugPrompt,
|
|
1151
1216
|
debugResponse,
|
|
@@ -1156,6 +1221,11 @@ function resolveConfig(config) {
|
|
|
1156
1221
|
// src/core/pricing.ts
|
|
1157
1222
|
var PER_MILLION = 1e6;
|
|
1158
1223
|
var PRICING_TABLE = {
|
|
1224
|
+
// Fable 5.1 is priced identically to Fable 5, the tier it succeeds.
|
|
1225
|
+
[`${Provider.ANTHROPIC}:${Model.Anthropic.FABLE_5_1}`]: {
|
|
1226
|
+
inputPricePerToken: 10 / PER_MILLION,
|
|
1227
|
+
outputPricePerToken: 50 / PER_MILLION
|
|
1228
|
+
},
|
|
1159
1229
|
[`${Provider.ANTHROPIC}:${Model.Anthropic.FABLE_5}`]: {
|
|
1160
1230
|
inputPricePerToken: 10 / PER_MILLION,
|
|
1161
1231
|
outputPricePerToken: 50 / PER_MILLION
|
|
@@ -1188,6 +1258,12 @@ var PRICING_TABLE = {
|
|
|
1188
1258
|
inputPricePerToken: 1 / PER_MILLION,
|
|
1189
1259
|
outputPricePerToken: 5 / PER_MILLION
|
|
1190
1260
|
},
|
|
1261
|
+
// Cached input is $1/MTok and cache writes $12.50/MTok; neither is modelled
|
|
1262
|
+
// here, since `calculateCost` applies no cache discount on any provider.
|
|
1263
|
+
[`${Provider.OPENAI}:${Model.OpenAI.GPT_6_ASTRA}`]: {
|
|
1264
|
+
inputPricePerToken: 10 / PER_MILLION,
|
|
1265
|
+
outputPricePerToken: 50 / PER_MILLION
|
|
1266
|
+
},
|
|
1191
1267
|
[`${Provider.OPENAI}:${Model.OpenAI.GPT_5_6_SOL}`]: {
|
|
1192
1268
|
inputPricePerToken: 5 / PER_MILLION,
|
|
1193
1269
|
outputPricePerToken: 30 / PER_MILLION
|
|
@@ -2196,10 +2272,34 @@ function stripCodeFences(text) {
|
|
|
2196
2272
|
var CheckResponseSchema = CheckResultSchema.omit({ usage: true });
|
|
2197
2273
|
var AskResponseSchema = AskResultSchema.omit({ usage: true });
|
|
2198
2274
|
var CompareResponseSchema = CompareResultSchema.omit({ usage: true });
|
|
2275
|
+
var STRAY_CONTROL_CHARS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
2276
|
+
function parseJson(text) {
|
|
2277
|
+
try {
|
|
2278
|
+
return JSON.parse(text);
|
|
2279
|
+
} catch (first) {
|
|
2280
|
+
try {
|
|
2281
|
+
return JSON.parse(text.replace(/[\u0000-\u001f]/g, " "));
|
|
2282
|
+
} catch {
|
|
2283
|
+
throw first;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
function stripControlCharacters(value) {
|
|
2288
|
+
if (typeof value === "string") return value.replace(STRAY_CONTROL_CHARS, "");
|
|
2289
|
+
if (Array.isArray(value)) return value.map((item) => stripControlCharacters(item));
|
|
2290
|
+
if (value !== null && typeof value === "object") {
|
|
2291
|
+
const out = {};
|
|
2292
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2293
|
+
out[key] = stripControlCharacters(item);
|
|
2294
|
+
}
|
|
2295
|
+
return out;
|
|
2296
|
+
}
|
|
2297
|
+
return value;
|
|
2298
|
+
}
|
|
2199
2299
|
function parseResponse(raw, schema) {
|
|
2200
2300
|
let parsed;
|
|
2201
2301
|
try {
|
|
2202
|
-
parsed =
|
|
2302
|
+
parsed = stripControlCharacters(parseJson(stripCodeFences(raw)));
|
|
2203
2303
|
} catch {
|
|
2204
2304
|
throw new VisualAIResponseParseError(
|
|
2205
2305
|
`Failed to parse AI response as JSON: ${raw.slice(0, 200)}`,
|
|
@@ -2294,7 +2394,8 @@ function visualAI(config = {}) {
|
|
|
2294
2394
|
model: resolvedConfig.model,
|
|
2295
2395
|
maxTokens: resolvedConfig.maxTokens,
|
|
2296
2396
|
reasoningEffort: resolvedConfig.reasoningEffort,
|
|
2297
|
-
imageDetail: resolvedConfig.imageDetail
|
|
2397
|
+
imageDetail: resolvedConfig.imageDetail,
|
|
2398
|
+
timeout: resolvedConfig.timeout
|
|
2298
2399
|
};
|
|
2299
2400
|
const driver = createDriver(resolvedConfig.provider, driverConfig);
|
|
2300
2401
|
const maxImageDimension = resolvedConfig.maxImageDimension;
|