@wrenautomation/models 0.0.0-stage → 0.7.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/LICENSE.md +91 -0
- package/README.md +105 -2
- package/dist/command.d.ts +44 -0
- package/dist/command.js +151 -0
- package/dist/command.js.map +1 -0
- package/dist/errors.d.ts +62 -0
- package/dist/errors.js +95 -0
- package/dist/errors.js.map +1 -0
- package/dist/fake.d.ts +5 -0
- package/dist/fake.js +37 -0
- package/dist/fake.js.map +1 -0
- package/dist/hear.d.ts +65 -0
- package/dist/hear.js +106 -0
- package/dist/hear.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +36 -0
- package/dist/keys.js +110 -0
- package/dist/keys.js.map +1 -0
- package/dist/model.d.ts +21 -0
- package/dist/model.js +68 -0
- package/dist/model.js.map +1 -0
- package/dist/parse.d.ts +15 -0
- package/dist/parse.js +66 -0
- package/dist/parse.js.map +1 -0
- package/dist/prices.d.ts +50 -0
- package/dist/prices.js +153 -0
- package/dist/prices.js.map +1 -0
- package/dist/providers.d.ts +83 -0
- package/dist/providers.js +171 -0
- package/dist/providers.js.map +1 -0
- package/dist/sdk.d.ts +34 -0
- package/dist/sdk.js +170 -0
- package/dist/sdk.js.map +1 -0
- package/dist/spec.d.ts +11 -0
- package/dist/spec.js +70 -0
- package/dist/spec.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -3
package/dist/hear.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type RotateOptions } from "./keys.js";
|
|
2
|
+
export interface Audio {
|
|
3
|
+
data: Uint8Array;
|
|
4
|
+
/** `audio/wav`, `audio/mpeg`. */
|
|
5
|
+
mediaType: string;
|
|
6
|
+
filename?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HearOptions {
|
|
9
|
+
/** ISO 639-1. Default `en`. */
|
|
10
|
+
language?: string;
|
|
11
|
+
/** The audio's length, when known: it prices the call. */
|
|
12
|
+
seconds?: number;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
export interface Heard {
|
|
16
|
+
text: string;
|
|
17
|
+
/** The model that heard it. */
|
|
18
|
+
model: string;
|
|
19
|
+
ms: number;
|
|
20
|
+
/** 0 on a free key; absent when the model or the length has no price. */
|
|
21
|
+
usd?: number;
|
|
22
|
+
free?: boolean;
|
|
23
|
+
listUsd?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface Ear {
|
|
26
|
+
/** As spends name it (`groq:whisper-large-v3-turbo`). */
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly model: string;
|
|
29
|
+
hear(audio: Audio, o?: HearOptions): Promise<Heard>;
|
|
30
|
+
}
|
|
31
|
+
export interface EarOptions {
|
|
32
|
+
/** The full `/audio/transcriptions` URL. */
|
|
33
|
+
url: string;
|
|
34
|
+
model: string;
|
|
35
|
+
/** None for a server with no auth. */
|
|
36
|
+
key?: string | readonly string[];
|
|
37
|
+
/** The vendor, for the id. Default: the URL's host. */
|
|
38
|
+
vendor?: string;
|
|
39
|
+
free?: boolean;
|
|
40
|
+
/** Send `response_format=json` (OpenAI's field). Default true; Cohere's route takes no such field. */
|
|
41
|
+
jsonFormat?: boolean;
|
|
42
|
+
/** Bytes the vendor takes at most; a larger file throws before the call. */
|
|
43
|
+
maxBytes?: number;
|
|
44
|
+
limitMs?: number;
|
|
45
|
+
rotate?: RotateOptions;
|
|
46
|
+
fetch?: typeof fetch;
|
|
47
|
+
}
|
|
48
|
+
/** Any OpenAI-compatible transcription endpoint. Keys travel in a header, never in an error. */
|
|
49
|
+
export declare function compatibleEar(o: EarOptions): Ear;
|
|
50
|
+
export declare const COHERE_SPEECH_MODEL = "cohere-transcribe-03-2026";
|
|
51
|
+
/** Cohere's transcription on the free key ring. It takes 25 MB at most. */
|
|
52
|
+
export declare const cohereEar: (o: {
|
|
53
|
+
key: string | readonly string[];
|
|
54
|
+
rotate?: RotateOptions;
|
|
55
|
+
fetch?: typeof fetch;
|
|
56
|
+
}) => Ear;
|
|
57
|
+
/** Whisper on Groq's free key ring. */
|
|
58
|
+
export declare const groqEar: (o: {
|
|
59
|
+
key: string | readonly string[];
|
|
60
|
+
model?: string;
|
|
61
|
+
rotate?: RotateOptions;
|
|
62
|
+
fetch?: typeof fetch;
|
|
63
|
+
}) => Ear;
|
|
64
|
+
/** An ear for tests and dry runs: no network, no keys. Answers each call from `respond`. */
|
|
65
|
+
export declare function fakeEar(respond?: string | ((audio: Audio, o: HearOptions) => string | Promise<string>), id?: string): Ear;
|
package/dist/hear.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech to text: the `Ear` door. Every vendor here takes the OpenAI-style multipart POST to
|
|
3
|
+
* `/audio/transcriptions` (Groq, Cohere, a self-hosted whisper server). No `node:` import: the
|
|
4
|
+
* portal Worker uses it.
|
|
5
|
+
*/
|
|
6
|
+
import { httpError, transport } from "./errors.js";
|
|
7
|
+
import { cursor, overRing } from "./keys.js";
|
|
8
|
+
import { speechUsd } from "./prices.js";
|
|
9
|
+
const EXT = {
|
|
10
|
+
"audio/wav": "wav",
|
|
11
|
+
"audio/x-wav": "wav",
|
|
12
|
+
"audio/mpeg": "mp3",
|
|
13
|
+
"audio/mp4": "m4a",
|
|
14
|
+
"audio/webm": "webm",
|
|
15
|
+
"audio/ogg": "ogg",
|
|
16
|
+
"audio/flac": "flac",
|
|
17
|
+
};
|
|
18
|
+
/** Any OpenAI-compatible transcription endpoint. Keys travel in a header, never in an error. */
|
|
19
|
+
export function compatibleEar(o) {
|
|
20
|
+
const vendor = o.vendor ?? new URL(o.url).hostname;
|
|
21
|
+
const id = `${vendor}:${o.model}`;
|
|
22
|
+
const keys = o.key === undefined ? [undefined] : typeof o.key === "string" ? [o.key] : o.key;
|
|
23
|
+
if (!keys.length)
|
|
24
|
+
throw new Error(`${id} needs at least one key`);
|
|
25
|
+
const go = o.fetch ?? fetch;
|
|
26
|
+
const next = cursor();
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
model: o.model,
|
|
30
|
+
async hear(audio, h = {}) {
|
|
31
|
+
if (o.maxBytes && audio.data.byteLength > o.maxBytes)
|
|
32
|
+
throw new Error(`${id}: ${(audio.data.byteLength / 1e6).toFixed(1)} MB of audio, over its ${o.maxBytes / 1e6} MB`);
|
|
33
|
+
const started = performance.now();
|
|
34
|
+
const once = async (key) => {
|
|
35
|
+
const form = new FormData();
|
|
36
|
+
const name = audio.filename ?? `audio.${EXT[audio.mediaType] ?? "bin"}`;
|
|
37
|
+
form.set("file", new Blob([audio.data], { type: audio.mediaType }), name);
|
|
38
|
+
form.set("model", o.model);
|
|
39
|
+
if (o.jsonFormat !== false)
|
|
40
|
+
form.set("response_format", "json");
|
|
41
|
+
form.set("language", h.language ?? "en");
|
|
42
|
+
const timeout = AbortSignal.timeout(o.limitMs ?? 120_000);
|
|
43
|
+
let res;
|
|
44
|
+
try {
|
|
45
|
+
res = await go(o.url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: key ? { authorization: `Bearer ${key}` } : {},
|
|
48
|
+
body: form,
|
|
49
|
+
signal: h.signal ? AbortSignal.any([h.signal, timeout]) : timeout,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (h.signal?.aborted)
|
|
54
|
+
throw err;
|
|
55
|
+
throw transport(id, err);
|
|
56
|
+
}
|
|
57
|
+
if (!res.ok)
|
|
58
|
+
throw httpError(id, res, (await res.text().catch(() => "")).slice(0, 200));
|
|
59
|
+
const body = (await res.json().catch(() => ({})));
|
|
60
|
+
return typeof body.text === "string" ? body.text.trim() : "";
|
|
61
|
+
};
|
|
62
|
+
const text = await overRing(keys, once, next, o.rotate);
|
|
63
|
+
const list = h.seconds === undefined ? undefined : speechUsd(o.model, h.seconds);
|
|
64
|
+
const cost = o.free
|
|
65
|
+
? { usd: 0, free: true, ...(list !== undefined ? { listUsd: list } : {}) }
|
|
66
|
+
: list !== undefined
|
|
67
|
+
? { usd: list }
|
|
68
|
+
: {};
|
|
69
|
+
return { text, model: o.model, ms: Math.round(performance.now() - started), ...cost };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export const COHERE_SPEECH_MODEL = "cohere-transcribe-03-2026";
|
|
74
|
+
/** Cohere's transcription on the free key ring. It takes 25 MB at most. */
|
|
75
|
+
export const cohereEar = (o) => compatibleEar({
|
|
76
|
+
url: "https://api.cohere.com/v2/audio/transcriptions",
|
|
77
|
+
model: COHERE_SPEECH_MODEL,
|
|
78
|
+
vendor: "cohere",
|
|
79
|
+
free: true,
|
|
80
|
+
jsonFormat: false,
|
|
81
|
+
maxBytes: 25_000_000,
|
|
82
|
+
...o,
|
|
83
|
+
});
|
|
84
|
+
/** Whisper on Groq's free key ring. */
|
|
85
|
+
export const groqEar = (o) => compatibleEar({
|
|
86
|
+
url: "https://api.groq.com/openai/v1/audio/transcriptions",
|
|
87
|
+
model: o.model ?? "whisper-large-v3-turbo",
|
|
88
|
+
vendor: "groq",
|
|
89
|
+
free: true,
|
|
90
|
+
maxBytes: 25_000_000,
|
|
91
|
+
key: o.key,
|
|
92
|
+
...(o.rotate ? { rotate: o.rotate } : {}),
|
|
93
|
+
...(o.fetch ? { fetch: o.fetch } : {}),
|
|
94
|
+
});
|
|
95
|
+
/** An ear for tests and dry runs: no network, no keys. Answers each call from `respond`. */
|
|
96
|
+
export function fakeEar(respond = "", id = "fake:ear") {
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
model: id,
|
|
100
|
+
async hear(audio, o = {}) {
|
|
101
|
+
const text = typeof respond === "string" ? respond : await respond(audio, o);
|
|
102
|
+
return { text, model: id, ms: 0, usd: 0 };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=hear.js.map
|
package/dist/hear.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hear.js","sourceRoot":"","sources":["../src/hear.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one door to model calls: every Wren repo calls a model through this package. A `Model`
|
|
3
|
+
* takes text and media parts and answers text, tokens, USD and why it stopped; `ask` parses the
|
|
4
|
+
* answer to a zod shape. No `node:` import behind this door: it runs in a Worker too.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./errors.js";
|
|
7
|
+
export { fakeModel, scriptedModel } from "./fake.js";
|
|
8
|
+
export { cursor, type EnvMap, fallback, fleetKeys, overRing, type RotateOptions, rotating, } from "./keys.js";
|
|
9
|
+
export { askWith, asRequest, checkParts, modelOf, promptText, TEXT_ONLY } from "./model.js";
|
|
10
|
+
export { NoAnswer, parseAnswer, wellFormed } from "./parse.js";
|
|
11
|
+
export { addUsage, priced } from "./prices.js";
|
|
12
|
+
export * from "./providers.js";
|
|
13
|
+
export { CALL_LIMIT_MS, DEFAULT_MAX_TOKENS, type SdkModelOptions, sdkModel } from "./sdk.js";
|
|
14
|
+
export type * from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one door to model calls: every Wren repo calls a model through this package. A `Model`
|
|
3
|
+
* takes text and media parts and answers text, tokens, USD and why it stopped; `ask` parses the
|
|
4
|
+
* answer to a zod shape. No `node:` import behind this door: it runs in a Worker too.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./errors.js";
|
|
7
|
+
export { fakeModel, scriptedModel } from "./fake.js";
|
|
8
|
+
export { cursor, fallback, fleetKeys, overRing, rotating, } from "./keys.js";
|
|
9
|
+
export { askWith, asRequest, checkParts, modelOf, promptText, TEXT_ONLY } from "./model.js";
|
|
10
|
+
export { NoAnswer, parseAnswer, wellFormed } from "./parse.js";
|
|
11
|
+
export { addUsage, priced } from "./prices.js";
|
|
12
|
+
export * from "./providers.js";
|
|
13
|
+
export { CALL_LIMIT_MS, DEFAULT_MAX_TOKENS, sdkModel } from "./sdk.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EACL,MAAM,EAEN,QAAQ,EACR,SAAS,EACT,QAAQ,EAER,QAAQ,GACT,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5F,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/C,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAwB,QAAQ,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Model } from "./types.js";
|
|
2
|
+
export type EnvMap = Readonly<Record<string, string | undefined>>;
|
|
3
|
+
/**
|
|
4
|
+
* A vendor's keys, model-keys-file style: `NUM_<V>` plus `<V>_API_KEY_1..N` (gaps passed over),
|
|
5
|
+
* else one `<V>_API_KEY`. A line may carry an inline note after the key.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fleetKeys(env: EnvMap, vendor: string): string[];
|
|
8
|
+
export interface RotateOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Passes over the whole ring while keys only say "later" (429, 5xx). Default 1: the caller
|
|
11
|
+
* owns waiting. More passes wait between them, doubling from `waitMs` up to `maxWaitMs`.
|
|
12
|
+
*/
|
|
13
|
+
passes?: number;
|
|
14
|
+
waitMs?: number;
|
|
15
|
+
maxWaitMs?: number;
|
|
16
|
+
sleep?: (ms: number) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Call over a ring of keys, round robin from `start()`. A refused key (401, 402, 403) hands the
|
|
20
|
+
* call to the next at once; a busy one (429, 5xx, a dropped connection) does too, and when the
|
|
21
|
+
* whole ring is busy the pass ends. Any other error is the call's and is thrown as is.
|
|
22
|
+
*/
|
|
23
|
+
export declare function overRing<K, T>(ring: readonly K[], call: (k: K) => Promise<T>, start: () => number, o?: RotateOptions): Promise<T>;
|
|
24
|
+
/** A round-robin cursor: each call returns the next index. */
|
|
25
|
+
export declare const cursor: () => () => number;
|
|
26
|
+
/**
|
|
27
|
+
* One model over several keys (one model per key), by `overRing`. A key refused once (401, 402,
|
|
28
|
+
* 403: suspended, revoked) is passed over on later calls; when every key has been refused, the
|
|
29
|
+
* whole ring is tried again so the error is a live one.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rotating(models: readonly Model[], o?: RotateOptions): Model;
|
|
32
|
+
/**
|
|
33
|
+
* Models in order of preference (`gemini-3.8-flash`, then `gemini-3.5-flash`): each call starts at
|
|
34
|
+
* the first and moves down past a busy or refused one. The reply's `model` names who answered.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fallback(models: readonly Model[], o?: RotateOptions): Model;
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** API keys from the environment and one rotation over them. */
|
|
2
|
+
import { isRefused, isTransient } from "./errors.js";
|
|
3
|
+
import { modelOf } from "./model.js";
|
|
4
|
+
/**
|
|
5
|
+
* A vendor's keys, model-keys-file style: `NUM_<V>` plus `<V>_API_KEY_1..N` (gaps passed over),
|
|
6
|
+
* else one `<V>_API_KEY`. A line may carry an inline note after the key.
|
|
7
|
+
*/
|
|
8
|
+
export function fleetKeys(env, vendor) {
|
|
9
|
+
const upper = vendor.toUpperCase();
|
|
10
|
+
const first = (v) => v?.trim().split(/\s/)[0] || undefined;
|
|
11
|
+
const count = Number.parseInt(env[`NUM_${upper}`] ?? "0", 10);
|
|
12
|
+
const keys = [];
|
|
13
|
+
for (let n = 1; n <= count; n++) {
|
|
14
|
+
const key = first(env[`${upper}_API_KEY_${n}`]);
|
|
15
|
+
if (key)
|
|
16
|
+
keys.push(key);
|
|
17
|
+
}
|
|
18
|
+
const single = first(env[`${upper}_API_KEY`]);
|
|
19
|
+
if (!keys.length && single)
|
|
20
|
+
keys.push(single);
|
|
21
|
+
return keys;
|
|
22
|
+
}
|
|
23
|
+
const nap = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
/**
|
|
25
|
+
* Call over a ring of keys, round robin from `start()`. A refused key (401, 402, 403) hands the
|
|
26
|
+
* call to the next at once; a busy one (429, 5xx, a dropped connection) does too, and when the
|
|
27
|
+
* whole ring is busy the pass ends. Any other error is the call's and is thrown as is.
|
|
28
|
+
*/
|
|
29
|
+
export async function overRing(ring, call, start, o = {}) {
|
|
30
|
+
const { passes = 1, waitMs = 2000, maxWaitMs = 30_000, sleep = nap } = o;
|
|
31
|
+
let last;
|
|
32
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
33
|
+
if (pass > 0)
|
|
34
|
+
await sleep(Math.min(waitMs * 2 ** (pass - 1), maxWaitMs));
|
|
35
|
+
let busy = false;
|
|
36
|
+
for (let tried = 0; tried < ring.length; tried++) {
|
|
37
|
+
try {
|
|
38
|
+
return await call(ring[start() % ring.length]);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (!isRefused(err) && !isTransient(err))
|
|
42
|
+
throw err;
|
|
43
|
+
if (isTransient(err))
|
|
44
|
+
busy = true;
|
|
45
|
+
last = err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!busy)
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
throw last;
|
|
52
|
+
}
|
|
53
|
+
/** A round-robin cursor: each call returns the next index. */
|
|
54
|
+
export const cursor = () => {
|
|
55
|
+
let n = 0;
|
|
56
|
+
return () => n++;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* One model over several keys (one model per key), by `overRing`. A key refused once (401, 402,
|
|
60
|
+
* 403: suspended, revoked) is passed over on later calls; when every key has been refused, the
|
|
61
|
+
* whole ring is tried again so the error is a live one.
|
|
62
|
+
*/
|
|
63
|
+
export function rotating(models, o = {}) {
|
|
64
|
+
const first = models[0];
|
|
65
|
+
if (!first)
|
|
66
|
+
throw new Error("rotating needs at least one model");
|
|
67
|
+
if (models.length === 1 && (o.passes ?? 1) <= 1)
|
|
68
|
+
return first;
|
|
69
|
+
const next = cursor();
|
|
70
|
+
const dead = new Set();
|
|
71
|
+
const call = (req) => async (m) => {
|
|
72
|
+
try {
|
|
73
|
+
return await m.complete(req);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (isRefused(err))
|
|
77
|
+
dead.add(m);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return modelOf({
|
|
82
|
+
id: first.id,
|
|
83
|
+
provider: first.provider,
|
|
84
|
+
takes: first.takes,
|
|
85
|
+
complete: (req) => {
|
|
86
|
+
const live = models.filter((m) => !dead.has(m));
|
|
87
|
+
return overRing(live.length ? live : models, call(req), next, o);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Models in order of preference (`gemini-3.8-flash`, then `gemini-3.5-flash`): each call starts at
|
|
93
|
+
* the first and moves down past a busy or refused one. The reply's `model` names who answered.
|
|
94
|
+
*/
|
|
95
|
+
export function fallback(models, o = {}) {
|
|
96
|
+
const first = models[0];
|
|
97
|
+
if (!first)
|
|
98
|
+
throw new Error("fallback needs at least one model");
|
|
99
|
+
const takes = new Set(models.flatMap((m) => [...m.takes]).filter((k) => models.every((m) => m.takes.has(k))));
|
|
100
|
+
return modelOf({
|
|
101
|
+
id: first.id,
|
|
102
|
+
provider: first.provider,
|
|
103
|
+
takes,
|
|
104
|
+
complete: (req) => {
|
|
105
|
+
const next = cursor();
|
|
106
|
+
return overRing(models, (m) => m.complete(req), next, o);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A `Model` from one `complete`: the request checks and the parse-and-repair `ask` live here once. */
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
import type { Answer, AskOptions, Model, Part, PartKind, Reply, Request } from "./types.js";
|
|
4
|
+
export declare const TEXT_ONLY: ReadonlySet<PartKind>;
|
|
5
|
+
export declare const asRequest: (r: Request | string) => Request;
|
|
6
|
+
/** The prompt as plain text when it is only text parts; null when it holds media. */
|
|
7
|
+
export declare function promptText(prompt: string | readonly Part[]): string | null;
|
|
8
|
+
/** Throws `Unsupported` for the first part the model does not take. */
|
|
9
|
+
export declare function checkParts(id: string, takes: ReadonlySet<PartKind>, prompt: Request["prompt"]): void;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a reply to the shape; on a miss, ask again with why, up to `repairs` times (default 1).
|
|
12
|
+
* The usage of every call is summed into the answer.
|
|
13
|
+
*/
|
|
14
|
+
export declare function askWith<T>(complete: (r: Request) => Promise<Reply>, prompt: string | readonly Part[], shape: ZodType<T>, o?: AskOptions): Promise<Answer<T>>;
|
|
15
|
+
/** A full `Model` from its id, vendor, part kinds and one `complete`. */
|
|
16
|
+
export declare function modelOf(o: {
|
|
17
|
+
id: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
takes?: ReadonlySet<PartKind>;
|
|
20
|
+
complete: (r: Request) => Promise<Reply>;
|
|
21
|
+
}): Model;
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Unsupported } from "./errors.js";
|
|
2
|
+
import { NoAnswer, parseAnswer } from "./parse.js";
|
|
3
|
+
import { addUsage } from "./prices.js";
|
|
4
|
+
export const TEXT_ONLY = new Set(["text"]);
|
|
5
|
+
export const asRequest = (r) => typeof r === "string" ? { prompt: r } : r;
|
|
6
|
+
/** The prompt as plain text when it is only text parts; null when it holds media. */
|
|
7
|
+
export function promptText(prompt) {
|
|
8
|
+
if (typeof prompt === "string")
|
|
9
|
+
return prompt;
|
|
10
|
+
const texts = [];
|
|
11
|
+
for (const p of prompt) {
|
|
12
|
+
if (p.type !== "text")
|
|
13
|
+
return null;
|
|
14
|
+
texts.push(p.text);
|
|
15
|
+
}
|
|
16
|
+
return texts.join("\n\n");
|
|
17
|
+
}
|
|
18
|
+
/** Throws `Unsupported` for the first part the model does not take. */
|
|
19
|
+
export function checkParts(id, takes, prompt) {
|
|
20
|
+
if (typeof prompt === "string")
|
|
21
|
+
return;
|
|
22
|
+
for (const p of prompt)
|
|
23
|
+
if (!takes.has(p.type))
|
|
24
|
+
throw new Unsupported(id, p.type);
|
|
25
|
+
}
|
|
26
|
+
const repairNote = (issues) => `Your last answer was not one JSON object of the asked shape${issues ? ` (${issues})` : ""}. Answer again with only that JSON object.`;
|
|
27
|
+
/**
|
|
28
|
+
* Parse a reply to the shape; on a miss, ask again with why, up to `repairs` times (default 1).
|
|
29
|
+
* The usage of every call is summed into the answer.
|
|
30
|
+
*/
|
|
31
|
+
export async function askWith(complete, prompt, shape, o = {}) {
|
|
32
|
+
const { repairs = 1, ...rest } = o;
|
|
33
|
+
let usage;
|
|
34
|
+
let current = prompt;
|
|
35
|
+
for (let attempt = 0;; attempt++) {
|
|
36
|
+
const reply = await complete({ ...rest, prompt: current });
|
|
37
|
+
usage = addUsage(usage, reply.usage);
|
|
38
|
+
try {
|
|
39
|
+
return { value: parseAnswer(reply.text, shape), ...(usage ? { usage } : {}) };
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (!(err instanceof NoAnswer) || attempt >= repairs)
|
|
43
|
+
throw err;
|
|
44
|
+
const note = `${reply.text}\n\n${repairNote(err.issues)}`;
|
|
45
|
+
current =
|
|
46
|
+
typeof prompt === "string"
|
|
47
|
+
? `${prompt}\n\nYour last answer:\n${note}`
|
|
48
|
+
: [...prompt, { type: "text", text: `Your last answer:\n${note}` }];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** A full `Model` from its id, vendor, part kinds and one `complete`. */
|
|
53
|
+
export function modelOf(o) {
|
|
54
|
+
const takes = o.takes ?? TEXT_ONLY;
|
|
55
|
+
const complete = async (r) => {
|
|
56
|
+
const req = asRequest(r);
|
|
57
|
+
checkParts(o.id, takes, req.prompt);
|
|
58
|
+
return o.complete(req);
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
id: o.id,
|
|
62
|
+
provider: o.provider,
|
|
63
|
+
takes,
|
|
64
|
+
complete,
|
|
65
|
+
ask: (prompt, shape, opts) => askWith(complete, prompt, shape, opts),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The one reader of a model's JSON answer. */
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
export declare class NoAnswer extends Error {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
/** Why the closest candidate failed the shape; empty when no JSON object parsed. */
|
|
6
|
+
readonly issues: string;
|
|
7
|
+
constructor(text: string, issues: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The first JSON object of the shape in a model's text, found by brace balance: fences, prose
|
|
11
|
+
* and stray braces around it are passed over. Throws `NoAnswer` when nothing parses.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseAnswer<T>(text: string, shape: ZodType<T>): T;
|
|
14
|
+
/** Lone UTF-16 surrogates out: a vendor's JSON parser refuses them. */
|
|
15
|
+
export declare const wellFormed: (s: string) => string;
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Every balanced `{...}` in a text, by start position: an outer object comes before its inner ones. */
|
|
2
|
+
function* objects(text) {
|
|
3
|
+
for (let start = text.indexOf("{"); start >= 0; start = text.indexOf("{", start + 1)) {
|
|
4
|
+
let depth = 0;
|
|
5
|
+
let inString = false;
|
|
6
|
+
let escaped = false;
|
|
7
|
+
for (let i = start; i < text.length; i++) {
|
|
8
|
+
const c = text[i];
|
|
9
|
+
if (inString) {
|
|
10
|
+
if (escaped)
|
|
11
|
+
escaped = false;
|
|
12
|
+
else if (c === "\\")
|
|
13
|
+
escaped = true;
|
|
14
|
+
else if (c === '"')
|
|
15
|
+
inString = false;
|
|
16
|
+
}
|
|
17
|
+
else if (c === '"')
|
|
18
|
+
inString = true;
|
|
19
|
+
else if (c === "{")
|
|
20
|
+
depth++;
|
|
21
|
+
else if (c === "}" && --depth === 0) {
|
|
22
|
+
yield text.slice(start, i + 1);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class NoAnswer extends Error {
|
|
29
|
+
text;
|
|
30
|
+
/** Why the closest candidate failed the shape; empty when no JSON object parsed. */
|
|
31
|
+
issues;
|
|
32
|
+
constructor(text, issues) {
|
|
33
|
+
super(`no answer of the expected shape${issues ? ` (${issues})` : ""} in: ${text.slice(0, 400)}`);
|
|
34
|
+
this.name = "NoAnswer";
|
|
35
|
+
this.text = text;
|
|
36
|
+
this.issues = issues;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The first JSON object of the shape in a model's text, found by brace balance: fences, prose
|
|
41
|
+
* and stray braces around it are passed over. Throws `NoAnswer` when nothing parses.
|
|
42
|
+
*/
|
|
43
|
+
export function parseAnswer(text, shape) {
|
|
44
|
+
let issues = "";
|
|
45
|
+
for (const candidate of objects(text)) {
|
|
46
|
+
let json;
|
|
47
|
+
try {
|
|
48
|
+
json = JSON.parse(candidate);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const out = shape.safeParse(json);
|
|
54
|
+
if (out.success)
|
|
55
|
+
return out.data;
|
|
56
|
+
if (!issues)
|
|
57
|
+
issues = out.error.issues
|
|
58
|
+
.slice(0, 5)
|
|
59
|
+
.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
60
|
+
.join("; ");
|
|
61
|
+
}
|
|
62
|
+
throw new NoAnswer(text, issues);
|
|
63
|
+
}
|
|
64
|
+
/** Lone UTF-16 surrogates out: a vendor's JSON parser refuses them. */
|
|
65
|
+
export const wellFormed = (s) => s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
|
66
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
|
package/dist/prices.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a model call costs, in USD, from the tokens it reports: the one price table. Rates are
|
|
3
|
+
* USD per million tokens (per hour for speech), by model id; a dated id
|
|
4
|
+
* (`claude-haiku-5-5-20261001`) takes its base's row. A request is priced on its own prompt:
|
|
5
|
+
* every input token, cache reads and writes included, picks the tier. A model with no row gets
|
|
6
|
+
* no `usd`: the spend keeps the id and the tokens, so a price can be applied later. A free key
|
|
7
|
+
* costs 0 and keeps the list price as `listUsd`.
|
|
8
|
+
*
|
|
9
|
+
* Sources, read 2026-10-27: Anthropic from the Claude API docs (cache writes 1.25x input for 5
|
|
10
|
+
* minutes, 2x for 1 hour; reads 0.1x, Opus 5.5 0.05x); Cohere, Groq and Gemini from
|
|
11
|
+
* third-party price lists that quote the vendor pages. Check a vendor page before a budget.
|
|
12
|
+
*/
|
|
13
|
+
import type { Usage } from "./types.js";
|
|
14
|
+
export declare const PRICES_AS_OF = "2026-10-27";
|
|
15
|
+
export interface Rate {
|
|
16
|
+
input: number;
|
|
17
|
+
output: number;
|
|
18
|
+
cacheRead?: number;
|
|
19
|
+
cacheWrite5m?: number;
|
|
20
|
+
cacheWrite1h?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ModelPrice {
|
|
23
|
+
/** The model id without a date suffix. */
|
|
24
|
+
model: string;
|
|
25
|
+
/** Said to a person. */
|
|
26
|
+
name: string;
|
|
27
|
+
vendor: string;
|
|
28
|
+
/** Rates by prompt length, shortest first; `upTo` is the longest prompt a tier takes (null: any). */
|
|
29
|
+
tiers: readonly {
|
|
30
|
+
upTo: number | null;
|
|
31
|
+
rate: Rate;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
export declare const MODEL_PRICES: readonly ModelPrice[];
|
|
35
|
+
/** Speech to text, USD per hour of audio. */
|
|
36
|
+
export declare const SPEECH_PRICES: Readonly<Record<string, number>>;
|
|
37
|
+
/** The price row for a model id; a dated id takes its base's. */
|
|
38
|
+
export declare function priceOf(model: string): ModelPrice | null;
|
|
39
|
+
/** The call's cost in USD, or undefined when the model has no row. Never throws. */
|
|
40
|
+
export declare function priceUsd(model: string, u: Usage): number | undefined;
|
|
41
|
+
/** The usage with its price. A free call says `usd: 0`, `free: true` and its list price. */
|
|
42
|
+
export declare function priced(model: string, usage: Usage, o?: {
|
|
43
|
+
free?: boolean;
|
|
44
|
+
}): Usage;
|
|
45
|
+
/** What `seconds` of speech cost on a speech model, or undefined with no row. */
|
|
46
|
+
export declare function speechUsd(model: string, seconds: number): number | undefined;
|
|
47
|
+
/** Two usages added, each field summed where either side has it. */
|
|
48
|
+
export declare function addUsage(a: Usage | undefined, b: Usage | undefined): Usage | undefined;
|
|
49
|
+
/** "Opus 5.5 $4.00 in, $20.00 out; ... per million tokens". */
|
|
50
|
+
export declare function pricesText(vendor?: string): string;
|