decision-api 1.2.0 → 2.0.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/dist/eval.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ import type { DecisionResult } from './create-decision-api.ts';
2
+ import type { DecisionModel } from './models.ts';
3
+ import type { DecisionAnswer, DecisionQuestion } from './questions.ts';
4
+ /** One call and the answers a careful human would give to its questions. */
5
+ export type DecisionCase = {
6
+ name: string;
7
+ state: unknown;
8
+ questions: Record<string, DecisionQuestion>;
9
+ /** By question key: a noul's truth, a choice's option, a score's level. */
10
+ expected: Record<string, boolean | string | number>;
11
+ };
12
+ export type GradedAnswer = {
13
+ expected: boolean | string | number;
14
+ value: string;
15
+ /** At 0.5 for a noul; `report` regrades nouls at your thresholds. */
16
+ correct: boolean;
17
+ /** The probability the model put on the expected answer. */
18
+ pCorrect: number;
19
+ /** A noul's probability, kept for the threshold sweep and calibration. */
20
+ noul?: number;
21
+ };
22
+ export type CaseLog = {
23
+ model: DecisionModel;
24
+ caseName: string;
25
+ run: number;
26
+ latencyMs: number;
27
+ inputTokens?: number;
28
+ costUsd?: number;
29
+ /** `kind: message` of a failed call. */
30
+ error?: string;
31
+ answers?: Record<string, GradedAnswer>;
32
+ };
33
+ /** Right or wrong, plus the probability the model put on the right answer. */
34
+ export declare function grade(answer: DecisionAnswer, expected: boolean | string | number): GradedAnswer;
35
+ /** What `runCases` needs of a client: `decide` from `createDecisionApi` fits. */
36
+ type CaseClient = {
37
+ result(input: {
38
+ state: unknown;
39
+ questions: Record<string, DecisionQuestion>;
40
+ }, model?: DecisionModel): Promise<DecisionResult<Record<string, DecisionQuestion>>>;
41
+ };
42
+ /**
43
+ * Asks every case `runs` times on each of `models` (the client's default
44
+ * model when left out). Models run side by side; each model's calls run one
45
+ * at a time, so they never queue behind each other. `context` is the usage
46
+ * context the client requires, if it requires one.
47
+ */
48
+ export declare function runCases(input: {
49
+ decide: CaseClient;
50
+ cases: DecisionCase[];
51
+ runs?: number;
52
+ models?: DecisionModel[];
53
+ context?: object;
54
+ onCall?: (log: CaseLog) => void;
55
+ }): Promise<CaseLog[]>;
56
+ export declare function percentile(values: number[], p: number): number;
57
+ export declare const mean: (values: number[]) => number;
58
+ /**
59
+ * Per model and question: accuracy at its threshold (`thresholds[key]`, 0.5
60
+ * when unset; a noul is positive at or above it), and for nouls the misses
61
+ * and false positives there and at every threshold from 0.05 to 0.95, the
62
+ * margin between the lowest positive and the highest negative (below zero
63
+ * means no threshold separates them), the Brier score and a 10-bin
64
+ * reliability table. `ok` is false when any call failed or any answer was
65
+ * wrong: `if (!summary.ok) process.exitCode = 1`.
66
+ */
67
+ export declare function report(logs: CaseLog[], options?: {
68
+ thresholds?: Record<string, number>;
69
+ }): {
70
+ ok: boolean;
71
+ models: {
72
+ model: "jev" | "clef" | "clef-flash" | "gpt-6-luna" | "microsoft/microsoft-decision-1" | "typesafe/jev-1.13";
73
+ calls: number;
74
+ failed: number;
75
+ wrong: number;
76
+ brier: number;
77
+ latencyP50: number;
78
+ latencyP90: number;
79
+ inputTokens: number;
80
+ costUsd: number;
81
+ questions: ({
82
+ key: string;
83
+ answers: number;
84
+ threshold: undefined;
85
+ wrong: number;
86
+ accuracy: number;
87
+ noul: undefined;
88
+ } | {
89
+ key: string;
90
+ answers: number;
91
+ threshold: number;
92
+ wrong: number;
93
+ accuracy: number;
94
+ noul: {
95
+ misses: number;
96
+ falsePositives: number;
97
+ margin: number;
98
+ brier: number;
99
+ sweep: {
100
+ threshold: number;
101
+ misses: number;
102
+ falsePositives: number;
103
+ }[];
104
+ reliability: {
105
+ from: number;
106
+ to: number;
107
+ answers: number;
108
+ meanP: number;
109
+ observed: number;
110
+ }[];
111
+ };
112
+ })[];
113
+ }[];
114
+ };
115
+ export {};
package/dist/eval.js ADDED
@@ -0,0 +1,182 @@
1
+ // Labelled cases against a live client: run them, then read accuracy at the
2
+ // thresholds the app ships, a threshold sweep and calibration per question.
3
+ // The app brings the cases; nothing here knows its domain.
4
+ /** Right or wrong, plus the probability the model put on the right answer. */
5
+ export function grade(answer, expected) {
6
+ if (answer.type === 'noul') {
7
+ const pCorrect = expected ? answer.noul : 1 - answer.noul;
8
+ return {
9
+ expected,
10
+ value: answer.noul.toFixed(2),
11
+ correct: pCorrect >= 0.5,
12
+ pCorrect,
13
+ noul: answer.noul
14
+ };
15
+ }
16
+ const pCorrect = answer.probabilities[String(expected)] ?? 0;
17
+ if (answer.type === 'choice') {
18
+ return {
19
+ expected,
20
+ value: answer.choice,
21
+ correct: answer.choice === expected,
22
+ pCorrect
23
+ };
24
+ }
25
+ return {
26
+ expected,
27
+ value: answer.score.toFixed(2),
28
+ correct: Math.round(answer.score) === expected,
29
+ pCorrect
30
+ };
31
+ }
32
+ /**
33
+ * Asks every case `runs` times on each of `models` (the client's default
34
+ * model when left out). Models run side by side; each model's calls run one
35
+ * at a time, so they never queue behind each other. `context` is the usage
36
+ * context the client requires, if it requires one.
37
+ */
38
+ export async function runCases(input) {
39
+ const runs = input.runs ?? 1;
40
+ const perModel = await Promise.all((input.models ?? [undefined]).map(async (model) => {
41
+ const logs = [];
42
+ for (let run = 1; run <= runs; run++) {
43
+ for (const decisionCase of input.cases) {
44
+ const outcome = await input.decide.result({
45
+ ...input.context,
46
+ state: decisionCase.state,
47
+ questions: decisionCase.questions
48
+ }, model);
49
+ const log = outcome.ok
50
+ ? {
51
+ model: outcome.usage?.model ?? model ?? 'jev',
52
+ caseName: decisionCase.name,
53
+ run,
54
+ latencyMs: outcome.usage?.latencyMs ?? 0,
55
+ inputTokens: outcome.usage?.inputTokens,
56
+ costUsd: outcome.usage?.costUsd,
57
+ answers: Object.fromEntries(Object.entries(decisionCase.expected).map(([key, expected]) => [
58
+ key,
59
+ grade(outcome.answers[key], expected)
60
+ ]))
61
+ }
62
+ : {
63
+ model: outcome.error.model,
64
+ caseName: decisionCase.name,
65
+ run,
66
+ latencyMs: outcome.error.latencyMs,
67
+ error: `${outcome.error.kind}: ${outcome.error.message}`
68
+ };
69
+ input.onCall?.(log);
70
+ logs.push(log);
71
+ }
72
+ }
73
+ return logs;
74
+ }));
75
+ return perModel.flat();
76
+ }
77
+ export function percentile(values, p) {
78
+ if (values.length === 0)
79
+ return Number.NaN;
80
+ const sorted = [...values].sort((a, b) => a - b);
81
+ return sorted[Math.max(0, Math.ceil(p * sorted.length) - 1)];
82
+ }
83
+ export const mean = (values) => values.length === 0
84
+ ? Number.NaN
85
+ : values.reduce((sum, value) => sum + value, 0) / values.length;
86
+ const SWEEP = Array.from({ length: 19 }, (_, index) => (index + 1) / 20);
87
+ /**
88
+ * Per model and question: accuracy at its threshold (`thresholds[key]`, 0.5
89
+ * when unset; a noul is positive at or above it), and for nouls the misses
90
+ * and false positives there and at every threshold from 0.05 to 0.95, the
91
+ * margin between the lowest positive and the highest negative (below zero
92
+ * means no threshold separates them), the Brier score and a 10-bin
93
+ * reliability table. `ok` is false when any call failed or any answer was
94
+ * wrong: `if (!summary.ok) process.exitCode = 1`.
95
+ */
96
+ export function report(logs, options = {}) {
97
+ const models = [...new Set(logs.map((log) => log.model))];
98
+ const perModel = models.map((model) => {
99
+ const own = logs.filter((log) => log.model === model);
100
+ const answered = own.filter((log) => log.answers);
101
+ const keys = [
102
+ ...new Set(answered.flatMap((log) => Object.keys(log.answers ?? {})))
103
+ ];
104
+ const questions = keys.map((key) => summarizeQuestion(key, answered.flatMap((log) => log.answers?.[key] ?? []), options.thresholds?.[key] ?? 0.5));
105
+ const nouls = answered.flatMap((log) => Object.values(log.answers ?? {}).filter((a) => a.noul !== undefined));
106
+ const latencies = answered.map((log) => log.latencyMs);
107
+ return {
108
+ model,
109
+ calls: own.length,
110
+ failed: own.length - answered.length,
111
+ wrong: questions.reduce((sum, question) => sum + question.wrong, 0),
112
+ brier: brier(nouls),
113
+ latencyP50: percentile(latencies, 0.5),
114
+ latencyP90: percentile(latencies, 0.9),
115
+ inputTokens: sum(answered.map((log) => log.inputTokens ?? 0)),
116
+ costUsd: sum(answered.map((log) => log.costUsd ?? 0)),
117
+ questions
118
+ };
119
+ });
120
+ return {
121
+ ok: perModel.every((row) => row.failed === 0 && row.wrong === 0),
122
+ models: perModel
123
+ };
124
+ }
125
+ function summarizeQuestion(key, graded, threshold) {
126
+ const nouls = graded.filter((answer) => answer.noul !== undefined);
127
+ if (nouls.length === 0 || nouls.length !== graded.length) {
128
+ const wrong = graded.filter((answer) => !answer.correct).length;
129
+ return {
130
+ key,
131
+ answers: graded.length,
132
+ threshold: undefined,
133
+ wrong,
134
+ accuracy: 1 - wrong / graded.length,
135
+ noul: undefined
136
+ };
137
+ }
138
+ const errorsAt = (at) => ({
139
+ threshold: at,
140
+ misses: nouls.filter((a) => a.expected && a.noul < at).length,
141
+ falsePositives: nouls.filter((a) => !a.expected && a.noul >= at).length
142
+ });
143
+ const at = errorsAt(threshold);
144
+ const positives = nouls.filter((a) => a.expected).map((a) => a.noul);
145
+ const negatives = nouls.filter((a) => !a.expected).map((a) => a.noul);
146
+ const wrong = at.misses + at.falsePositives;
147
+ return {
148
+ key,
149
+ answers: graded.length,
150
+ threshold,
151
+ wrong,
152
+ accuracy: 1 - wrong / graded.length,
153
+ noul: {
154
+ misses: at.misses,
155
+ falsePositives: at.falsePositives,
156
+ margin: positives.length && negatives.length
157
+ ? Math.min(...positives) - Math.max(...negatives)
158
+ : Number.NaN,
159
+ brier: brier(nouls),
160
+ sweep: SWEEP.map(errorsAt),
161
+ reliability: reliability(nouls)
162
+ }
163
+ };
164
+ }
165
+ const sum = (values) => values.reduce((total, v) => total + v, 0);
166
+ /** Mean squared distance from the truth: 0 is perfect, 0.25 is a coin. */
167
+ function brier(nouls) {
168
+ return mean(nouls.map((a) => ((a.noul ?? 0) - (a.expected ? 1 : 0)) ** 2));
169
+ }
170
+ /** Answers binned by probability: a calibrated model's `observed` tracks `meanP`. */
171
+ function reliability(nouls) {
172
+ return Array.from({ length: 10 }, (_, bin) => {
173
+ const inBin = nouls.filter((a) => Math.min(9, Math.floor(a.noul * 10)) === bin);
174
+ return {
175
+ from: bin / 10,
176
+ to: (bin + 1) / 10,
177
+ answers: inBin.length,
178
+ meanP: mean(inBin.map((a) => a.noul)),
179
+ observed: mean(inBin.map((a) => (a.expected ? 1 : 0)))
180
+ };
181
+ }).filter((row) => row.answers > 0);
182
+ }
@@ -0,0 +1,58 @@
1
+ export type DecisionImageFormat = 'png' | 'jpg';
2
+ /**
3
+ * A PNG or JPEG image, created by `image()`. Put it wherever text can go: as
4
+ * (part of) the state, a question's instructions or a criterion. Inside a
5
+ * sentence, write it into a `withImages` template.
6
+ */
7
+ export declare class DecisionImage {
8
+ readonly type = "image";
9
+ /** The text the image is sent under; what `String(image)` and `JSON.stringify` write. */
10
+ readonly reference: string;
11
+ readonly format: DecisionImageFormat;
12
+ private bytes;
13
+ private encoded;
14
+ constructor(data: ArrayBuffer | ArrayBufferView | string);
15
+ /** The file in standard base64, encoded once on first use. */
16
+ get base64(): string;
17
+ toString(): string;
18
+ toJSON(): string;
19
+ }
20
+ /** Text from a `withImages` template: the images in it travel with the request. */
21
+ export declare class DecisionImageText {
22
+ readonly text: string;
23
+ readonly images: readonly DecisionImage[];
24
+ constructor(text: string, images: readonly DecisionImage[]);
25
+ toString(): string;
26
+ toJSON(): string;
27
+ }
28
+ /** Text, an image on its own, or text with images in it: whatever a model may read. */
29
+ export type DecisionText = string | DecisionImage | DecisionImageText;
30
+ /**
31
+ * A PNG or JPEG image to put in a call, from its file bytes or base64 (a
32
+ * `data:` URL works too). Only models with `images: true` in
33
+ * `DECISION_MODELS` read images.
34
+ *
35
+ * Throws when the data is not a PNG or JPEG file. The provider rejects, never
36
+ * resizes, an image outside its limits: for Jev, 280 to 1568 px a side and at
37
+ * most 16 MiB, with EXIF orientation ignored, so rotate and scale first.
38
+ */
39
+ export declare function image(data: ArrayBuffer | ArrayBufferView | string): DecisionImage;
40
+ /**
41
+ * Tag a template whose text names images, so they are sent with it:
42
+ * withImages`Is the person in \`door\` the one on this badge: ${badge}?`
43
+ * A plain template would carry the reference without the image, and the call
44
+ * answers `null` (`image-detached`) rather than ask about a bare string.
45
+ */
46
+ export declare function withImages(strings: TemplateStringsArray, ...values: unknown[]): DecisionImageText;
47
+ /** Every image in a value, once each, keyed by its reference. Object keys stay text. */
48
+ export declare function collectImages(value: unknown, into?: Map<string, DecisionImage>): Map<string, DecisionImage>;
49
+ /** A reference written into the text whose image was not sent with it, if any. */
50
+ export declare function detachedReference(text: string, images: ReadonlyMap<string, DecisionImage>): string | undefined;
51
+ /** The `assets` map TypeSafe's API (and OpenRouter's, for Jev) reads. */
52
+ export declare function toAssets(images: Iterable<DecisionImage>): {
53
+ [k: string]: {
54
+ type: string;
55
+ format: DecisionImageFormat;
56
+ data_base64: string;
57
+ };
58
+ };
package/dist/images.js ADDED
@@ -0,0 +1,160 @@
1
+ // Images in a decision call. Each image is sent once, base64-encoded in the
2
+ // request's top-level `assets` map, and stands in the state, an instruction or
3
+ // a criterion as its reference string:
4
+ // https://docs.typesafe.ai/images
5
+ /** Every reference starts with this, so a reference without its image can be caught. */
6
+ const REFERENCE_PREFIX = 'decision-api-image-';
7
+ const REFERENCE_PATTERN = new RegExp(`${REFERENCE_PREFIX}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`, 'g');
8
+ /**
9
+ * A PNG or JPEG image, created by `image()`. Put it wherever text can go: as
10
+ * (part of) the state, a question's instructions or a criterion. Inside a
11
+ * sentence, write it into a `withImages` template.
12
+ */
13
+ export class DecisionImage {
14
+ type = 'image';
15
+ /** The text the image is sent under; what `String(image)` and `JSON.stringify` write. */
16
+ reference = `${REFERENCE_PREFIX}${uuid()}`;
17
+ format;
18
+ bytes;
19
+ encoded;
20
+ constructor(data) {
21
+ if (typeof data === 'string') {
22
+ // A data URL's prefix is dropped; the API wants the bare base64.
23
+ const base64 = data.replace(/^data:image\/[a-z]+;base64,/, '');
24
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64) || base64.length % 4 !== 0) {
25
+ throw new TypeError('image() needs padded standard base64');
26
+ }
27
+ this.encoded = base64;
28
+ this.format = formatOfBase64(base64);
29
+ }
30
+ else {
31
+ this.bytes = ArrayBuffer.isView(data)
32
+ ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
33
+ : new Uint8Array(data);
34
+ this.format = formatOfBytes(this.bytes);
35
+ }
36
+ }
37
+ /** The file in standard base64, encoded once on first use. */
38
+ get base64() {
39
+ if (this.encoded === undefined && this.bytes) {
40
+ this.encoded = toBase64(this.bytes);
41
+ this.bytes = undefined;
42
+ }
43
+ return this.encoded ?? '';
44
+ }
45
+ toString() {
46
+ return this.reference;
47
+ }
48
+ toJSON() {
49
+ return this.reference;
50
+ }
51
+ }
52
+ /** Text from a `withImages` template: the images in it travel with the request. */
53
+ export class DecisionImageText {
54
+ text;
55
+ images;
56
+ constructor(text, images) {
57
+ this.text = text;
58
+ this.images = images;
59
+ }
60
+ toString() {
61
+ return this.text;
62
+ }
63
+ toJSON() {
64
+ return this.text;
65
+ }
66
+ }
67
+ /**
68
+ * A PNG or JPEG image to put in a call, from its file bytes or base64 (a
69
+ * `data:` URL works too). Only models with `images: true` in
70
+ * `DECISION_MODELS` read images.
71
+ *
72
+ * Throws when the data is not a PNG or JPEG file. The provider rejects, never
73
+ * resizes, an image outside its limits: for Jev, 280 to 1568 px a side and at
74
+ * most 16 MiB, with EXIF orientation ignored, so rotate and scale first.
75
+ */
76
+ export function image(data) {
77
+ return new DecisionImage(data);
78
+ }
79
+ /**
80
+ * Tag a template whose text names images, so they are sent with it:
81
+ * withImages`Is the person in \`door\` the one on this badge: ${badge}?`
82
+ * A plain template would carry the reference without the image, and the call
83
+ * answers `null` (`image-detached`) rather than ask about a bare string.
84
+ */
85
+ export function withImages(strings, ...values) {
86
+ const images = [];
87
+ let text = strings[0] ?? '';
88
+ values.forEach((value, index) => {
89
+ if (value instanceof DecisionImage)
90
+ images.push(value);
91
+ if (value instanceof DecisionImageText)
92
+ images.push(...value.images);
93
+ text += String(value) + (strings[index + 1] ?? '');
94
+ });
95
+ return new DecisionImageText(text, images);
96
+ }
97
+ /** Every image in a value, once each, keyed by its reference. Object keys stay text. */
98
+ export function collectImages(value, into = new Map()) {
99
+ if (value instanceof DecisionImage) {
100
+ into.set(value.reference, value);
101
+ }
102
+ else if (value instanceof DecisionImageText) {
103
+ for (const item of value.images)
104
+ into.set(item.reference, item);
105
+ }
106
+ else if (Array.isArray(value)) {
107
+ for (const item of value)
108
+ collectImages(item, into);
109
+ }
110
+ else if (value && typeof value === 'object') {
111
+ for (const item of Object.values(value))
112
+ collectImages(item, into);
113
+ }
114
+ return into;
115
+ }
116
+ /** A reference written into the text whose image was not sent with it, if any. */
117
+ export function detachedReference(text, images) {
118
+ if (!text.includes(REFERENCE_PREFIX))
119
+ return undefined;
120
+ return text.match(REFERENCE_PATTERN)?.find((found) => !images.has(found));
121
+ }
122
+ /** The `assets` map TypeSafe's API (and OpenRouter's, for Jev) reads. */
123
+ export function toAssets(images) {
124
+ return Object.fromEntries(Array.from(images, (item) => [
125
+ item.reference,
126
+ { type: 'image', format: item.format, data_base64: item.base64 }
127
+ ]));
128
+ }
129
+ function formatOfBytes(bytes) {
130
+ const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
131
+ if (png.every((byte, index) => bytes[index] === byte))
132
+ return 'png';
133
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
134
+ return 'jpg';
135
+ throw new TypeError('image() takes PNG or JPEG files only');
136
+ }
137
+ /** The same magic numbers, as they start a base64 string. */
138
+ function formatOfBase64(base64) {
139
+ if (base64.startsWith('iVBORw0KGgo'))
140
+ return 'png';
141
+ if (base64.startsWith('/9j/'))
142
+ return 'jpg';
143
+ throw new TypeError('image() takes PNG or JPEG files only');
144
+ }
145
+ function toBase64(bytes) {
146
+ // In slices, because spreading megabytes into one call overflows the stack.
147
+ let binary = '';
148
+ for (let start = 0; start < bytes.length; start += 0x8000) {
149
+ binary += String.fromCharCode(...bytes.subarray(start, start + 0x8000));
150
+ }
151
+ return btoa(binary);
152
+ }
153
+ /** `crypto.randomUUID` where the runtime has it (Node 18 and React Native may not). */
154
+ function uuid() {
155
+ const random = globalThis.crypto;
156
+ if (random?.randomUUID)
157
+ return random.randomUUID();
158
+ const hex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
159
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20)}`;
160
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { createDecisionApi, DECISION_MAX_STATE_CHARS, type DecisionApi, DecisionError, type DecisionErrorKind, type DecisionFetch, type DecisionUsage } from './create-decision-api.ts';
2
- export { DECISION_MODELS, type DecisionModel, type DecisionProvider, type DecisionProviderKeys, type ModelsFor } from './models.ts';
3
- export type { DecisionAnswers, DecisionChoice, DecisionNoul, DecisionQuestion, DecisionScore } from './questions.ts';
1
+ export { createDecisionApi, DECISION_MAX_REQUEST_CHARS, type DecisionApi, DecisionError, type DecisionErrorKind, type DecisionFetch, type DecisionResult, type DecisionUsage } from './create-decision-api.ts';
2
+ export { type DecisionImage, type DecisionImageFormat, type DecisionImageText, type DecisionText, image, withImages } from './images.ts';
3
+ export { DECISION_MODELS, type DecisionImageModel, type DecisionModel, type DecisionProvider, type DecisionProviderKeys, type ModelsFor } from './models.ts';
4
+ export { type DecisionAnswer, type DecisionAnswers, type DecisionChoice, type DecisionChoiceAnswer, type DecisionNoul, type DecisionNoulAnswer, type DecisionQuestion, type DecisionScore, type DecisionScoreAnswer, perItem } from './questions.ts';
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
- export { createDecisionApi, DECISION_MAX_STATE_CHARS, DecisionError } from "./create-decision-api.js";
1
+ export { createDecisionApi, DECISION_MAX_REQUEST_CHARS, DecisionError } from "./create-decision-api.js";
2
+ export { image, withImages } from "./images.js";
2
3
  export { DECISION_MODELS } from "./models.js";
4
+ export { perItem } from "./questions.js";
package/dist/models.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { type DecisionImage } from './images.ts';
2
3
  import { type DecisionQuestion } from './questions.ts';
3
4
  /** The credentials each provider needs; pass only the providers you use. */
4
5
  export type DecisionProviderKeys = {
@@ -13,38 +14,65 @@ export type DecisionProviderKeys = {
13
14
  };
14
15
  export type DecisionProvider = keyof DecisionProviderKeys;
15
16
  /**
16
- * USD per million input tokens; output is free on every provider, because an
17
- * answer is a few tokens of enum rather than generated prose. Jev's rate is
18
- * from TypeSafe's launch material (the docs publish no price sheet); the rest
19
- * are the providers' list prices (2026-10).
17
+ * `inputUsdPerMTok` is USD per million input tokens; output is free on every
18
+ * provider, because an answer is a few tokens of enum rather than generated
19
+ * prose. Jev's rate is from TypeSafe's launch material (the docs publish no
20
+ * price sheet); the rest are the providers' list prices (2026-10). Images are
21
+ * billed as input tokens at the same rate.
22
+ *
23
+ * `images` marks the models this client sends images to. Jev reads them
24
+ * anywhere in the state, instructions and criteria (1.13 and later:
25
+ * https://docs.typesafe.ai/images). Clef and gpt-6-luna read images too, but
26
+ * in wire formats this client does not speak yet.
27
+ *
28
+ * `maxQuestions`, `maxChoices` (options in one choice) and `maxLevels` (levels
29
+ * in one score) are the limits a call is checked against before it is sent,
30
+ * where a provider documents or was measured to enforce one. Jev's come from
31
+ * its API schema (https://docs.typesafe.ai/api); Clef's question limit was
32
+ * measured (it refuses more than 64).
20
33
  */
21
34
  export declare const DECISION_MODELS: {
22
35
  readonly jev: {
23
36
  readonly provider: "typesafe";
24
37
  readonly inputUsdPerMTok: 0.042;
38
+ readonly images: true;
39
+ readonly maxChoices: 255;
40
+ readonly maxLevels: 10;
25
41
  };
26
42
  readonly clef: {
27
43
  readonly provider: "cloudflare";
28
44
  readonly inputUsdPerMTok: 0.24;
45
+ readonly images: false;
46
+ readonly maxQuestions: 64;
29
47
  };
30
48
  readonly 'clef-flash': {
31
49
  readonly provider: "cloudflare";
32
50
  readonly inputUsdPerMTok: 0.09;
51
+ readonly images: false;
33
52
  };
34
53
  readonly 'gpt-6-luna': {
35
54
  readonly provider: "openai";
36
55
  readonly inputUsdPerMTok: 0.1;
56
+ readonly images: false;
37
57
  };
38
58
  readonly 'microsoft/microsoft-decision-1': {
39
59
  readonly provider: "openrouter";
40
60
  readonly inputUsdPerMTok: 0.042;
61
+ readonly images: false;
41
62
  };
42
63
  readonly 'typesafe/jev-1.13': {
43
64
  readonly provider: "openrouter";
44
65
  readonly inputUsdPerMTok: 0.042;
66
+ readonly images: true;
67
+ readonly maxChoices: 255;
68
+ readonly maxLevels: 10;
45
69
  };
46
70
  };
47
71
  export type DecisionModel = keyof typeof DECISION_MODELS;
72
+ /** The models that read images. */
73
+ export type DecisionImageModel = {
74
+ [Model in DecisionModel]: (typeof DECISION_MODELS)[Model]['images'] extends true ? Model : never;
75
+ }[DecisionModel];
48
76
  type ModelsOf<Provider> = {
49
77
  [Model in DecisionModel]: (typeof DECISION_MODELS)[Model]['provider'] extends Provider ? Model : never;
50
78
  }[DecisionModel];
@@ -52,9 +80,13 @@ type ModelsOf<Provider> = {
52
80
  export type ModelsFor<Keys extends DecisionProviderKeys> = ModelsOf<{
53
81
  [Provider in keyof Keys]-?: undefined extends Keys[Provider] ? never : Provider;
54
82
  }[keyof Keys]>;
83
+ /** Why a model would refuse these questions, or undefined when it takes them. */
84
+ export declare function overModelLimit(model: DecisionModel, questions: Record<string, DecisionQuestion>): string | undefined;
55
85
  type RequestInput = {
56
86
  state: unknown;
57
87
  questions: Record<string, DecisionQuestion>;
88
+ /** Sent only to models with `images: true`; the caller checks. */
89
+ images: DecisionImage[];
58
90
  };
59
91
  /** One POST: where it goes, and the headers and body it carries. */
60
92
  export declare function buildDecisionRequest(model: DecisionModel, input: RequestInput, keys: DecisionProviderKeys): {
@@ -85,5 +117,6 @@ export declare function readDecisionResponse(model: DecisionModel, json: unknown
85
117
  input_tokens: number;
86
118
  output_tokens: number;
87
119
  };
120
+ model?: string | undefined;
88
121
  }>;
89
122
  export {};