@tanstack-ai-browser/prompt-api 0.0.0-stage → 0.2.1
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 +21 -0
- package/README.md +96 -2
- package/dist/adapter.d.ts +160 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +414 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/limit.d.ts +119 -0
- package/dist/limit.d.ts.map +1 -0
- package/dist/limit.js +203 -0
- package/dist/messages.d.ts +20 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +95 -0
- package/dist/model-meta.d.ts +21 -0
- package/dist/model-meta.d.ts.map +1 -0
- package/dist/model-meta.js +18 -0
- package/dist/model.d.ts +28 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +53 -0
- package/dist/reasoning.d.ts +42 -0
- package/dist/reasoning.d.ts.map +1 -0
- package/dist/reasoning.js +113 -0
- package/dist/sessions.d.ts +48 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +275 -0
- package/dist/shared-load.d.ts +48 -0
- package/dist/shared-load.d.ts.map +1 -0
- package/dist/shared-load.js +103 -0
- package/dist/store.d.ts +8 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +17 -0
- package/dist/tools.d.ts +50 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +314 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/package.json +60 -4
- package/src/adapter.test.ts +571 -0
- package/src/adapter.ts +522 -0
- package/src/index.ts +13 -0
- package/src/messages.ts +102 -0
- package/src/model-meta.ts +35 -0
- package/src/model.ts +75 -0
- package/src/tools.test.ts +129 -0
- package/src/tools.ts +311 -0
- package/src/types.ts +110 -0
package/dist/limit.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/** `maxCompletionTokens`, validated like the other adapters validate it. */
|
|
2
|
+
export function outputTokenLimit(options) {
|
|
3
|
+
const limit = options?.maxCompletionTokens;
|
|
4
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
5
|
+
throw new RangeError("maxCompletionTokens must be a nonnegative safe integer.");
|
|
6
|
+
}
|
|
7
|
+
return limit;
|
|
8
|
+
}
|
|
9
|
+
export const outputLimitUsage = (limit, reached) => limit === undefined
|
|
10
|
+
? undefined
|
|
11
|
+
: {
|
|
12
|
+
outputTokenLimit: limit,
|
|
13
|
+
outputTokenMeasurement: "estimate-then-context-usage",
|
|
14
|
+
outputTokenLimitEnforcement: "best-effort",
|
|
15
|
+
outputTokenLimitReached: reached
|
|
16
|
+
};
|
|
17
|
+
/** UTF-8 bytes of a string: an upper bound on its tokens. */
|
|
18
|
+
function utf8Bytes(text) {
|
|
19
|
+
let bytes = 0;
|
|
20
|
+
for (let i = 0; i < text.length; i++) {
|
|
21
|
+
const code = text.charCodeAt(i);
|
|
22
|
+
if (code < 0x80)
|
|
23
|
+
bytes += 1;
|
|
24
|
+
else if (code < 0x800)
|
|
25
|
+
bytes += 2;
|
|
26
|
+
else if (code >= 0xd800 && code < 0xdc00 && i + 1 < text.length) {
|
|
27
|
+
bytes += 4;
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
bytes += 3;
|
|
32
|
+
}
|
|
33
|
+
return bytes;
|
|
34
|
+
}
|
|
35
|
+
/** The text of a prompt input (or of messages), without images or audio. */
|
|
36
|
+
export function textOfInput(input) {
|
|
37
|
+
if (typeof input === "string")
|
|
38
|
+
return input;
|
|
39
|
+
return input
|
|
40
|
+
.map((message) => typeof message.content === "string"
|
|
41
|
+
? message.content
|
|
42
|
+
: message.content
|
|
43
|
+
.map((part) => (part.type === "text" && typeof part.value === "string" ? part.value : ""))
|
|
44
|
+
.join(""))
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join("\n");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Live emission stops once the estimate passes this share of the limit, or
|
|
50
|
+
* once the text passes the limit in UTF-8 bytes. Text sent live can't be
|
|
51
|
+
* taken back, so it must stay within the limit whatever the estimate says:
|
|
52
|
+
* no token is shorter than a byte, so text of at most `limit` bytes holds at
|
|
53
|
+
* most `limit` tokens, however much denser the reply is than the calibration
|
|
54
|
+
* text. The share keeps longer limits from streaming up to the last token
|
|
55
|
+
* live, where a low estimate would leave nothing to trim.
|
|
56
|
+
*/
|
|
57
|
+
export const LIVE_FRACTION = 0.75;
|
|
58
|
+
/**
|
|
59
|
+
* Generation is stopped once the estimate passes STOP_FACTOR × limit +
|
|
60
|
+
* STOP_SLACK tokens: past the limit, so the exact trim afterwards finds the
|
|
61
|
+
* limit's worth of text unless the reply holds a fifth fewer tokens per byte
|
|
62
|
+
* than the calibration text (the estimate is then high by more than 1.25×).
|
|
63
|
+
* The slack covers short limits, where a chunk or two of noise is a large
|
|
64
|
+
* share; 8 tokens is about 0.15 s of Gemini Nano's decoding.
|
|
65
|
+
*/
|
|
66
|
+
export const STOP_FACTOR = 1.25;
|
|
67
|
+
export const STOP_SLACK = 8;
|
|
68
|
+
/** Calibration text shorter than this (UTF-8 bytes) is too noisy alone. */
|
|
69
|
+
const MIN_SAMPLE_BYTES = 64;
|
|
70
|
+
/** Calibration measures at most this many characters of text. */
|
|
71
|
+
const MAX_SAMPLE_CHARS = 4000;
|
|
72
|
+
/**
|
|
73
|
+
* Calibrates when the conversation has almost no text ("hi"): measured with
|
|
74
|
+
* the model's own tokenizer like any other text, never a fixed ratio.
|
|
75
|
+
*/
|
|
76
|
+
const REFERENCE_TEXT = "The quick brown fox jumps over the lazy dog, then rests in the warm afternoon sun while the farmer counts his sheep.";
|
|
77
|
+
/**
|
|
78
|
+
* The text a limit's tokens-per-byte ratio is calibrated on: the request's
|
|
79
|
+
* own text first, then earlier messages (most recent first) while it's
|
|
80
|
+
* under 64 bytes, then a fixed reference sentence if it still is.
|
|
81
|
+
*/
|
|
82
|
+
export function calibrationSample(texts) {
|
|
83
|
+
let sample = "";
|
|
84
|
+
for (const text of texts) {
|
|
85
|
+
if (!text)
|
|
86
|
+
continue;
|
|
87
|
+
if (sample && utf8Bytes(sample) >= MIN_SAMPLE_BYTES)
|
|
88
|
+
break;
|
|
89
|
+
sample = sample ? `${sample}\n${text}` : text;
|
|
90
|
+
}
|
|
91
|
+
if (utf8Bytes(sample) < MIN_SAMPLE_BYTES) {
|
|
92
|
+
sample = sample ? `${sample}\n${REFERENCE_TEXT}` : REFERENCE_TEXT;
|
|
93
|
+
}
|
|
94
|
+
if (sample.length <= MAX_SAMPLE_CHARS)
|
|
95
|
+
return sample;
|
|
96
|
+
const end = sample.charCodeAt(MAX_SAMPLE_CHARS - 1);
|
|
97
|
+
return sample.slice(0, end >= 0xd800 && end < 0xdc00 ? MAX_SAMPLE_CHARS - 1 : MAX_SAMPLE_CHARS);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A limiter for one generation. Chrome runs one operation at a time per
|
|
101
|
+
* model: `measureContextUsage` (on any session or clone) and `clone()` wait
|
|
102
|
+
* until a generation in progress finishes, so nothing can be measured while
|
|
103
|
+
* the model writes. Instead, before generating, `sample` (the request's
|
|
104
|
+
* text) is measured to calibrate a bytes-per-token ratio; while generating,
|
|
105
|
+
* tokens are estimated from UTF-8 bytes with it; once the generation has
|
|
106
|
+
* stopped, `finish()` measures exactly and trims.
|
|
107
|
+
*
|
|
108
|
+
* Bytes per token, not characters: it varies much less across scripts (a
|
|
109
|
+
* CJK character is 3 bytes and about a token; English runs about 4 bytes a
|
|
110
|
+
* token), and no token is shorter than a byte, so the ratio's floor of 1 is
|
|
111
|
+
* the hard bound tokens ≤ bytes.
|
|
112
|
+
*/
|
|
113
|
+
export async function tokenLimiter(session, limit, sample, signal) {
|
|
114
|
+
const options = signal ? [{ signal }] : [];
|
|
115
|
+
let overhead;
|
|
116
|
+
const measure = async (text) => {
|
|
117
|
+
signal?.throwIfAborted();
|
|
118
|
+
if (!text)
|
|
119
|
+
return 0;
|
|
120
|
+
overhead ??= await session.measureContextUsage("", ...options);
|
|
121
|
+
return Math.max(0, (await session.measureContextUsage(text, ...options)) - overhead);
|
|
122
|
+
};
|
|
123
|
+
const sampleTokens = limit > 0 ? await measure(sample) : 0;
|
|
124
|
+
const bytesPerToken = Math.max(1, utf8Bytes(sample) / Math.max(1, sampleTokens));
|
|
125
|
+
const liveLine = LIVE_FRACTION * limit;
|
|
126
|
+
const stopLine = STOP_FACTOR * limit + STOP_SLACK;
|
|
127
|
+
let text = "";
|
|
128
|
+
let emitted = "";
|
|
129
|
+
let holding = false;
|
|
130
|
+
let bytes = 0;
|
|
131
|
+
let stopped = limit === 0;
|
|
132
|
+
const estimate = () => bytes / bytesPerToken;
|
|
133
|
+
const check = () => {
|
|
134
|
+
if (estimate() >= stopLine)
|
|
135
|
+
stopped = true;
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
get text() { return text; },
|
|
139
|
+
get stopped() { return stopped; },
|
|
140
|
+
push(delta) {
|
|
141
|
+
if (!delta)
|
|
142
|
+
return "";
|
|
143
|
+
text += delta;
|
|
144
|
+
bytes += utf8Bytes(delta);
|
|
145
|
+
check();
|
|
146
|
+
if (!holding && estimate() <= liveLine && bytes <= limit) {
|
|
147
|
+
emitted += delta;
|
|
148
|
+
return delta;
|
|
149
|
+
}
|
|
150
|
+
holding = true;
|
|
151
|
+
return "";
|
|
152
|
+
},
|
|
153
|
+
async finish() {
|
|
154
|
+
const tokens = await measure(text);
|
|
155
|
+
if (tokens <= limit) {
|
|
156
|
+
return { text, held: text.slice(emitted.length), tokens, reached: stopped || tokens >= limit };
|
|
157
|
+
}
|
|
158
|
+
// Over the limit: keep the longest prefix within it, searching by code
|
|
159
|
+
// point from what was sent live. Token counts need not be monotonic in
|
|
160
|
+
// the prefix, so the chosen prefix is verified afterwards.
|
|
161
|
+
const points = Array.from(text);
|
|
162
|
+
const start = Array.from(emitted).length;
|
|
163
|
+
let low = start;
|
|
164
|
+
let high = points.length - 1;
|
|
165
|
+
let accepted = emitted;
|
|
166
|
+
while (low <= high) {
|
|
167
|
+
const middle = Math.floor((low + high) / 2);
|
|
168
|
+
const prefix = points.slice(0, middle).join("");
|
|
169
|
+
if ((await measure(prefix)) <= limit) {
|
|
170
|
+
accepted = prefix;
|
|
171
|
+
low = middle + 1;
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
high = middle - 1;
|
|
175
|
+
}
|
|
176
|
+
let acceptedPoints = Array.from(accepted);
|
|
177
|
+
let count = await measure(accepted);
|
|
178
|
+
while (count > limit && acceptedPoints.length > start) {
|
|
179
|
+
acceptedPoints = acceptedPoints.slice(0, -1);
|
|
180
|
+
accepted = acceptedPoints.join("");
|
|
181
|
+
count = await measure(accepted);
|
|
182
|
+
}
|
|
183
|
+
return { text: accepted, held: accepted.slice(emitted.length), tokens: count, reached: true };
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* An abort signal for one generation that follows the caller's (with its
|
|
189
|
+
* reason) and that `stop()` aborts on its own, never touching the caller's.
|
|
190
|
+
*/
|
|
191
|
+
export function internalAbort(signal, reason) {
|
|
192
|
+
const internal = new AbortController();
|
|
193
|
+
const cancel = () => internal.abort(signal?.reason);
|
|
194
|
+
if (signal?.aborted)
|
|
195
|
+
cancel();
|
|
196
|
+
else
|
|
197
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
198
|
+
return {
|
|
199
|
+
signal: internal.signal,
|
|
200
|
+
stop: () => internal.abort(new DOMException(reason, "AbortError")),
|
|
201
|
+
dispose: () => signal?.removeEventListener("abort", cancel)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation as the Prompt API reads it: user messages with their
|
|
3
|
+
* images and audio (loaded as Blobs), the model's messages with their tool
|
|
4
|
+
* calls as ```tool_call fences, and tool results as user messages in a
|
|
5
|
+
* ```tool_result fence.
|
|
6
|
+
*/
|
|
7
|
+
import type { ModelMessage } from "@tanstack/ai";
|
|
8
|
+
import type { MediaInput } from "./model.js";
|
|
9
|
+
import type { LanguageModelMessage } from "./types.js";
|
|
10
|
+
/** The text of a message's content, without its images or audio. */
|
|
11
|
+
export declare function contentText(content: ModelMessage["content"]): string;
|
|
12
|
+
/** The kinds of media in a conversation, which its session must expect. */
|
|
13
|
+
export declare function mediaIn(messages: ReadonlyArray<ModelMessage>): Array<MediaInput>;
|
|
14
|
+
/**
|
|
15
|
+
* The conversation as Prompt API messages, from `start` on (a kept session
|
|
16
|
+
* holds the rest); a step's tool results share one message. Tool names come
|
|
17
|
+
* from the whole conversation.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toLanguageModelMessages(messages: ReadonlyArray<ModelMessage>, start?: number): Promise<Array<LanguageModelMessage>>;
|
|
20
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,cAAc,CAAA;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,KAAK,EAAwB,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAE5E,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,UAG3D;AAED,2EAA2E;AAC3E,wBAAgB,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAShF;AAgCD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,EACrC,KAAK,SAAI,GACR,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAiCtC"}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { toolCallFence, toolResultFence, toolResultLine } from "./tools.js";
|
|
2
|
+
/** The text of a message's content, without its images or audio. */
|
|
3
|
+
export function contentText(content) {
|
|
4
|
+
if (typeof content === "string")
|
|
5
|
+
return content;
|
|
6
|
+
return (content ?? []).map((part) => (part.type === "text" ? part.content : "")).join("");
|
|
7
|
+
}
|
|
8
|
+
/** The kinds of media in a conversation, which its session must expect. */
|
|
9
|
+
export function mediaIn(messages) {
|
|
10
|
+
const kinds = new Set();
|
|
11
|
+
for (const m of messages) {
|
|
12
|
+
if (m.role !== "user" || typeof m.content === "string")
|
|
13
|
+
continue;
|
|
14
|
+
for (const part of m.content ?? []) {
|
|
15
|
+
if (part.type === "image" || part.type === "audio")
|
|
16
|
+
kinds.add(part.type);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...kinds].sort();
|
|
20
|
+
}
|
|
21
|
+
function base64Blob(value, mimeType) {
|
|
22
|
+
const data = value.startsWith("data:") ? value.slice(value.indexOf(",") + 1) : value;
|
|
23
|
+
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
|
24
|
+
return new Blob([bytes], { type: mimeType ?? "" });
|
|
25
|
+
}
|
|
26
|
+
async function loadMedia(type, source) {
|
|
27
|
+
if (source.type === "data")
|
|
28
|
+
return base64Blob(source.value, source.mimeType);
|
|
29
|
+
if (source.type !== "url") {
|
|
30
|
+
throw new Error(`The Prompt API takes ${type} as data or a URL, not a ${source.type} source.`);
|
|
31
|
+
}
|
|
32
|
+
const response = await fetch(source.value);
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
throw new Error(`${source.value.slice(0, 120)}: HTTP ${response.status}`);
|
|
35
|
+
return response.blob();
|
|
36
|
+
}
|
|
37
|
+
async function userContent(content) {
|
|
38
|
+
if (typeof content === "string" || !content)
|
|
39
|
+
return content ?? "";
|
|
40
|
+
if (content.every((part) => part.type === "text"))
|
|
41
|
+
return contentText(content);
|
|
42
|
+
return Promise.all(content.map(async (part) => {
|
|
43
|
+
if (part.type === "text")
|
|
44
|
+
return { type: "text", value: part.content };
|
|
45
|
+
if (part.type === "image" || part.type === "audio") {
|
|
46
|
+
return { type: part.type, value: await loadMedia(part.type, part.source) };
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`The Prompt API takes text, images and audio, not ${part.type}.`);
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The conversation as Prompt API messages, from `start` on (a kept session
|
|
53
|
+
* holds the rest); a step's tool results share one message. Tool names come
|
|
54
|
+
* from the whole conversation.
|
|
55
|
+
*/
|
|
56
|
+
export async function toLanguageModelMessages(messages, start = 0) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const toolNames = new Map();
|
|
59
|
+
for (const m of messages.slice(0, start)) {
|
|
60
|
+
for (const call of m.toolCalls ?? [])
|
|
61
|
+
toolNames.set(call.id, call.function.name);
|
|
62
|
+
}
|
|
63
|
+
let results;
|
|
64
|
+
for (const m of messages.slice(start)) {
|
|
65
|
+
if (m.role === "tool") {
|
|
66
|
+
const id = m.toolCallId ?? "";
|
|
67
|
+
const line = toolResultLine(id, toolNames.get(id) ?? "tool", contentText(m.content), m.error);
|
|
68
|
+
if (results) {
|
|
69
|
+
results.push(line);
|
|
70
|
+
out[out.length - 1] = { role: "user", content: toolResultFence(results) };
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
results = [line];
|
|
74
|
+
out.push({ role: "user", content: toolResultFence(results) });
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
results = undefined;
|
|
79
|
+
if (m.role === "assistant") {
|
|
80
|
+
for (const call of m.toolCalls ?? [])
|
|
81
|
+
toolNames.set(call.id, call.function.name);
|
|
82
|
+
const content = [contentText(m.content), ...(m.toolCalls?.length ? [toolCallFence(m.toolCalls)] : [])]
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join("\n");
|
|
85
|
+
if (content)
|
|
86
|
+
out.push({ role: "assistant", content });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const content = await userContent(m.content);
|
|
90
|
+
if (content.length > 0)
|
|
91
|
+
out.push({ role: "user", content });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model the Prompt API gives: the browser's own text model ("text", as other Prompt API libraries name it), Gemini
|
|
3
|
+
* Nano in Chrome and Phi-4-mini in Edge, with what it supports.
|
|
4
|
+
*/
|
|
5
|
+
/** What the browser's model supports. */
|
|
6
|
+
export type PromptApiModelMeta = {
|
|
7
|
+
/** The model's name for `promptApiText`: "text". */
|
|
8
|
+
name: string;
|
|
9
|
+
supports: {
|
|
10
|
+
input: ReadonlyArray<"text" | "image" | "audio">;
|
|
11
|
+
output: ReadonlyArray<"text">;
|
|
12
|
+
capabilities: ReadonlyArray<"tools">;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/** The model: whichever one the browser provides. */
|
|
16
|
+
export declare const PROMPT_API_CHAT_MODELS: readonly ["text"];
|
|
17
|
+
/** The model: whichever one the browser provides. */
|
|
18
|
+
export type PromptApiChatModel = (typeof PROMPT_API_CHAT_MODELS)[number];
|
|
19
|
+
/** The model's metadata, by name. */
|
|
20
|
+
export declare const PROMPT_API_MODEL_META: Readonly<Record<PromptApiChatModel, PromptApiModelMeta>>;
|
|
21
|
+
//# sourceMappingURL=model-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-meta.d.ts","sourceRoot":"","sources":["../src/model-meta.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,yCAAyC;AACzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE;QACR,KAAK,EAAE,aAAa,CAAC,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,CAAA;QAChD,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;QAC7B,YAAY,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;KACrC,CAAA;CACF,CAAA;AAWD,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,mBAA2B,CAAA;AAE9D,qDAAqD;AACrD,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAA;AAExE,qCAAqC;AACrC,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAE1F,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model the Prompt API gives: the browser's own text model ("text", as other Prompt API libraries name it), Gemini
|
|
3
|
+
* Nano in Chrome and Phi-4-mini in Edge, with what it supports.
|
|
4
|
+
*/
|
|
5
|
+
const BUILT_IN = {
|
|
6
|
+
name: "text",
|
|
7
|
+
supports: {
|
|
8
|
+
input: ["text", "image", "audio"],
|
|
9
|
+
output: ["text"],
|
|
10
|
+
capabilities: ["tools"]
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/** The model: whichever one the browser provides. */
|
|
14
|
+
export const PROMPT_API_CHAT_MODELS = [BUILT_IN.name];
|
|
15
|
+
/** The model's metadata, by name. */
|
|
16
|
+
export const PROMPT_API_MODEL_META = {
|
|
17
|
+
[BUILT_IN.name]: BUILT_IN
|
|
18
|
+
};
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Availability, DownloadProgressCallback, LanguageModelCreateOptions, LanguageModelSession } from "./types.js";
|
|
2
|
+
export type { Availability, SamplingMode } from "./types.js";
|
|
3
|
+
/** Kinds of input besides text a session can be told to expect. */
|
|
4
|
+
export type MediaInput = "image" | "audio";
|
|
5
|
+
/** Every kind of input besides text, in the order `availableInputModalities` asks. */
|
|
6
|
+
export declare const MEDIA_INPUTS: ReadonlyArray<MediaInput>;
|
|
7
|
+
/** Languages the model is told to expect, in and out, by default. */
|
|
8
|
+
export declare const DEFAULT_LANGUAGES: readonly ["en"];
|
|
9
|
+
/**
|
|
10
|
+
* What every session expects: text in and out in the languages given (Chrome
|
|
11
|
+
* checks its safety filters for the output language), and images or audio
|
|
12
|
+
* in if the conversation has them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function baseOptions(languages?: ReadonlyArray<string>, media?: ReadonlyArray<MediaInput>): LanguageModelCreateOptions;
|
|
15
|
+
/**
|
|
16
|
+
* Whether this browser can run its built-in model with these options:
|
|
17
|
+
* "available" (on the device), "downloadable" or "downloading" (it
|
|
18
|
+
* downloads when a session is created), or "unavailable".
|
|
19
|
+
*/
|
|
20
|
+
export declare function promptApiAvailability(options: LanguageModelCreateOptions): Promise<Availability>;
|
|
21
|
+
/**
|
|
22
|
+
* A new session. `onDownloadProgress` hears a download, 0 to 1 (Chrome
|
|
23
|
+
* reports 0 and 1 on every create, so only the steps between are passed
|
|
24
|
+
* on), then 1 once the session has started. Aborting `signal` stops the
|
|
25
|
+
* create and destroys the session.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSession(options: LanguageModelCreateOptions, signal?: AbortSignal, onDownloadProgress?: DownloadProgressCallback): Promise<LanguageModelSession>;
|
|
28
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EAExB,0BAA0B,EAC1B,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE5D,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,OAAO,CAAA;AAE1C,sFAAsF;AACtF,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,UAAU,CAAsB,CAAA;AAEzE,qEAAqE;AACrE,eAAO,MAAM,iBAAiB,YAAI,IAAI,CAAU,CAAA;AAEhD;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,SAAS,GAAE,aAAa,CAAC,MAAM,CAAqB,EACpD,KAAK,GAAE,aAAa,CAAC,UAAU,CAAM,GACpC,0BAA0B,CAK5B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,YAAY,CAAC,CAEtG;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,0BAA0B,EACnC,MAAM,CAAC,EAAE,WAAW,EACpB,kBAAkB,CAAC,EAAE,wBAAwB,GAC5C,OAAO,CAAC,oBAAoB,CAAC,CAe/B"}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's built-in model: whether this browser has one, and its
|
|
3
|
+
* sessions. The browser downloads the model once for all sites (Chrome
|
|
4
|
+
* keeps it in its profile) and loads it when a session is created.
|
|
5
|
+
*/
|
|
6
|
+
import { languageModel } from "./types.js";
|
|
7
|
+
/** Every kind of input besides text, in the order `availableInputModalities` asks. */
|
|
8
|
+
export const MEDIA_INPUTS = ["image", "audio"];
|
|
9
|
+
/** Languages the model is told to expect, in and out, by default. */
|
|
10
|
+
export const DEFAULT_LANGUAGES = ["en"];
|
|
11
|
+
/**
|
|
12
|
+
* What every session expects: text in and out in the languages given (Chrome
|
|
13
|
+
* checks its safety filters for the output language), and images or audio
|
|
14
|
+
* in if the conversation has them.
|
|
15
|
+
*/
|
|
16
|
+
export function baseOptions(languages = DEFAULT_LANGUAGES, media = []) {
|
|
17
|
+
return {
|
|
18
|
+
expectedInputs: [{ type: "text", languages: [...languages] }, ...media.map((type) => ({ type }))],
|
|
19
|
+
expectedOutputs: [{ type: "text", languages: [...languages] }]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether this browser can run its built-in model with these options:
|
|
24
|
+
* "available" (on the device), "downloadable" or "downloading" (it
|
|
25
|
+
* downloads when a session is created), or "unavailable".
|
|
26
|
+
*/
|
|
27
|
+
export async function promptApiAvailability(options) {
|
|
28
|
+
return (await languageModel()?.availability(options)) ?? "unavailable";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A new session. `onDownloadProgress` hears a download, 0 to 1 (Chrome
|
|
32
|
+
* reports 0 and 1 on every create, so only the steps between are passed
|
|
33
|
+
* on), then 1 once the session has started. Aborting `signal` stops the
|
|
34
|
+
* create and destroys the session.
|
|
35
|
+
*/
|
|
36
|
+
export async function createSession(options, signal, onDownloadProgress) {
|
|
37
|
+
const api = languageModel();
|
|
38
|
+
if (!api)
|
|
39
|
+
throw new Error("This browser has no Prompt API (LanguageModel).");
|
|
40
|
+
const session = await api.create({
|
|
41
|
+
...options,
|
|
42
|
+
...(signal && { signal }),
|
|
43
|
+
monitor(monitor) {
|
|
44
|
+
monitor.addEventListener("downloadprogress", (event) => {
|
|
45
|
+
const { loaded } = event;
|
|
46
|
+
if (loaded > 0 && loaded < 1)
|
|
47
|
+
onDownloadProgress?.(loaded);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
onDownloadProgress?.(1);
|
|
52
|
+
return session;
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ModelMessage, ReasoningRequest } from "@tanstack/ai";
|
|
2
|
+
import type { LanguageModelInput, LanguageModelMessage, LanguageModelSession } from "./types.js";
|
|
3
|
+
export type PromptApiReasoning = {
|
|
4
|
+
levels: "off" | "high";
|
|
5
|
+
budget: true;
|
|
6
|
+
};
|
|
7
|
+
export type PromptApiReasoningUsage = {
|
|
8
|
+
reasoningEmulated: true;
|
|
9
|
+
reasoningTokenMeasurement: "context-usage";
|
|
10
|
+
reasoningBudgetEnforcement: "best-effort";
|
|
11
|
+
thinkingTokens: number;
|
|
12
|
+
thinkingBudgetTokens?: number;
|
|
13
|
+
thinkingBudgetExhausted: boolean;
|
|
14
|
+
reasoningMs: number;
|
|
15
|
+
};
|
|
16
|
+
export type DraftReasoningResult = {
|
|
17
|
+
notes: string;
|
|
18
|
+
usage: PromptApiReasoningUsage;
|
|
19
|
+
capReached: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function promptReasoningOptions(request: ReasoningRequest | undefined): {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
budget: number | undefined;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The draft's instruction, quoting the user's latest message: after a tool
|
|
27
|
+
* result, the newest user message is this instruction itself, and small
|
|
28
|
+
* models took it for the message to reply to.
|
|
29
|
+
*/
|
|
30
|
+
export declare function draftInstruction(messages: ReadonlyArray<ModelMessage>): LanguageModelMessage;
|
|
31
|
+
export declare function withWorkingNotes(input: LanguageModelInput, notes: string): LanguageModelInput;
|
|
32
|
+
/**
|
|
33
|
+
* Two-pass adapter emulation, not hidden native model reasoning or native
|
|
34
|
+
* token control. The draft stops at the thinking `budget` and at `cap`, the
|
|
35
|
+
* output tokens left for the whole reply (`maxCompletionTokens`, which counts
|
|
36
|
+
* the draft too), whichever comes first; `capReached` says the cap stopped it.
|
|
37
|
+
* Under a limit, the draft is held to it as `tokenLimiter` holds any reply:
|
|
38
|
+
* estimated while it streams, stopped a little past the limit, then measured
|
|
39
|
+
* and trimmed exactly (the rest of the notes streams once the draft stops).
|
|
40
|
+
*/
|
|
41
|
+
export declare function draftReasoning(session: LanguageModelSession, input: LanguageModelInput, conversation: ReadonlyArray<ModelMessage>, signal: AbortSignal | undefined, budget?: number, cap?: number): AsyncGenerator<string, DraftReasoningResult>;
|
|
42
|
+
//# sourceMappingURL=reasoning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning.d.ts","sourceRoot":"","sources":["../src/reasoning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAIlE,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEhG,MAAM,MAAM,kBAAkB,GAAG;IAAE,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAAC,MAAM,EAAE,IAAI,CAAA;CAAE,CAAA;AACzE,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,IAAI,CAAA;IACvB,yBAAyB,EAAE,eAAe,CAAA;IAC1C,0BAA0B,EAAE,aAAa,CAAA;IACzC,cAAc,EAAE,MAAM,CAAA;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,uBAAuB,EAAE,OAAO,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AACD,MAAM,MAAM,oBAAoB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,uBAAuB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzG,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS;;;EAK3E;AAKD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,oBAAoB,CAQ5F;AACD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAI7F;AAED;;;;;;;;GAQG;AACH,wBAAuB,cAAc,CAAC,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,aAAa,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAkDvP"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { calibrationSample, internalAbort, smallest, textOfInput, tokenLimiter } from "./limit.js";
|
|
2
|
+
import { contentText } from "./messages.js";
|
|
3
|
+
export function promptReasoningOptions(request) {
|
|
4
|
+
const budget = request?.budgetTokens;
|
|
5
|
+
if (budget !== undefined && (!Number.isSafeInteger(budget) || budget < 0))
|
|
6
|
+
throw new RangeError("reasoning.budgetTokens must be a nonnegative safe integer.");
|
|
7
|
+
if (request?.level !== undefined && request.level !== "off" && request.level !== "high")
|
|
8
|
+
throw new RangeError("Prompt API emulated reasoning supports off or high.");
|
|
9
|
+
return { enabled: request?.level === "high", budget };
|
|
10
|
+
}
|
|
11
|
+
/** The longest quote of the user's message the draft instruction carries. */
|
|
12
|
+
const QUOTE_CHARS = 500;
|
|
13
|
+
/**
|
|
14
|
+
* The draft's instruction, quoting the user's latest message: after a tool
|
|
15
|
+
* result, the newest user message is this instruction itself, and small
|
|
16
|
+
* models took it for the message to reply to.
|
|
17
|
+
*/
|
|
18
|
+
export function draftInstruction(messages) {
|
|
19
|
+
const latest = [...messages].reverse().find((m) => m.role === "user");
|
|
20
|
+
const text = latest ? contentText(latest.content).trim().replace(/\s+/g, " ") : "";
|
|
21
|
+
const quote = text.length > QUOTE_CHARS ? `${text.slice(0, QUOTE_CHARS - 1)}…` : text;
|
|
22
|
+
return {
|
|
23
|
+
role: "user",
|
|
24
|
+
content: `Now write notes for yourself before replying to the user's latest message${quote ? `, "${quote}"` : ""}: what's needed, what you already have, and what to do next. Don't call tools or write the reply yet.`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function withWorkingNotes(input, notes) {
|
|
28
|
+
if (!notes)
|
|
29
|
+
return input;
|
|
30
|
+
const messages = typeof input === "string" ? [{ role: "user", content: input }] : input;
|
|
31
|
+
return [...messages, { role: "user", content: `Working notes to verify against the conversation:\n${notes}\nNow produce the final response or required tool call.` }];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Two-pass adapter emulation, not hidden native model reasoning or native
|
|
35
|
+
* token control. The draft stops at the thinking `budget` and at `cap`, the
|
|
36
|
+
* output tokens left for the whole reply (`maxCompletionTokens`, which counts
|
|
37
|
+
* the draft too), whichever comes first; `capReached` says the cap stopped it.
|
|
38
|
+
* Under a limit, the draft is held to it as `tokenLimiter` holds any reply:
|
|
39
|
+
* estimated while it streams, stopped a little past the limit, then measured
|
|
40
|
+
* and trimmed exactly (the rest of the notes streams once the draft stops).
|
|
41
|
+
*/
|
|
42
|
+
export async function* draftReasoning(session, input, conversation, signal, budget, cap) {
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
if (budget !== undefined && (!Number.isSafeInteger(budget) || budget < 0))
|
|
45
|
+
throw new RangeError("reasoning.budgetTokens must be a nonnegative safe integer.");
|
|
46
|
+
const began = performance.now();
|
|
47
|
+
const limit = smallest(budget, cap);
|
|
48
|
+
const usage = (tokens, exhausted) => ({ reasoningEmulated: true, reasoningTokenMeasurement: "context-usage", reasoningBudgetEnforcement: "best-effort", thinkingTokens: tokens, ...(budget !== undefined && { thinkingBudgetTokens: budget }), thinkingBudgetExhausted: exhausted, reasoningMs: performance.now() - began });
|
|
49
|
+
if (limit === 0)
|
|
50
|
+
return { notes: "", usage: usage(0, budget === 0), capReached: cap === 0 };
|
|
51
|
+
let draft;
|
|
52
|
+
let limiter;
|
|
53
|
+
const stopper = internalAbort(signal, "Emulated reasoning budget reached");
|
|
54
|
+
try {
|
|
55
|
+
draft = await session.clone({ signal });
|
|
56
|
+
const messages = typeof input === "string" ? [{ role: "user", content: input }] : input;
|
|
57
|
+
const prompt = [...messages, draftInstruction(conversation)];
|
|
58
|
+
// Calibrated while the model is idle; nothing is measured mid-draft.
|
|
59
|
+
if (limit !== undefined)
|
|
60
|
+
limiter = await tokenLimiter(session, limit, calibrationSample([textOfInput(prompt)]), signal);
|
|
61
|
+
const stream = draft.promptStreaming(prompt, { signal: stopper.signal });
|
|
62
|
+
let notes = "";
|
|
63
|
+
try {
|
|
64
|
+
for await (const piece of stream) {
|
|
65
|
+
signal?.throwIfAborted();
|
|
66
|
+
const parts = Array.isArray(piece) ? piece : [piece];
|
|
67
|
+
const delta = parts.map(part => typeof part === "string" ? part : part.type === "text" ? String(part.value) : "").join("");
|
|
68
|
+
if (!delta)
|
|
69
|
+
continue;
|
|
70
|
+
if (!limiter) {
|
|
71
|
+
notes += delta;
|
|
72
|
+
yield delta;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const live = limiter.push(delta);
|
|
76
|
+
if (limiter.stopped)
|
|
77
|
+
stopper.stop();
|
|
78
|
+
if (live)
|
|
79
|
+
yield live;
|
|
80
|
+
if (limiter.stopped)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
signal?.throwIfAborted();
|
|
86
|
+
if (!limiter?.stopped)
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
if (!limiter)
|
|
91
|
+
return { notes, usage: usage(await notesTokens(session, notes, signal), false), capReached: false };
|
|
92
|
+
// The draft has stopped: the model is idle, so the exact count is instant.
|
|
93
|
+
const kept = await limiter.finish();
|
|
94
|
+
if (kept.held)
|
|
95
|
+
yield kept.held;
|
|
96
|
+
return { notes: kept.text, usage: usage(kept.tokens, kept.reached && limit === budget), capReached: kept.reached && limit === cap };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
stopper.dispose();
|
|
100
|
+
// Stops a draft left unread.
|
|
101
|
+
if (!stopper.signal.aborted)
|
|
102
|
+
stopper.stop();
|
|
103
|
+
draft?.destroy();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Tokens of finished notes, less the empty message's overhead. */
|
|
107
|
+
async function notesTokens(session, notes, signal) {
|
|
108
|
+
if (!notes)
|
|
109
|
+
return 0;
|
|
110
|
+
const options = signal ? [{ signal }] : [];
|
|
111
|
+
const overhead = await session.measureContextUsage("", ...options);
|
|
112
|
+
return Math.max(0, (await session.measureContextUsage(notes, ...options)) - overhead);
|
|
113
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PromptMessage } from "./messages.js";
|
|
2
|
+
import type { LanguageModelCreateOptions, LanguageModelSession } from "./types.js";
|
|
3
|
+
/** A session lent to one model call. */
|
|
4
|
+
export type Lease = {
|
|
5
|
+
session: LanguageModelSession;
|
|
6
|
+
/** Tokens the session had already read and didn't read again. */
|
|
7
|
+
cachedTokens: number;
|
|
8
|
+
/** Time spent creating the session (reading the conversation), in ms. */
|
|
9
|
+
createMs: number;
|
|
10
|
+
/** Why the thread's kept session couldn't be used, if it had one. */
|
|
11
|
+
miss?: string;
|
|
12
|
+
/** Whether this call reused a previously prepared input prefix. */
|
|
13
|
+
reused: boolean;
|
|
14
|
+
/** Whether the browser dropped old turns to make room since the lease began. */
|
|
15
|
+
overflowed: () => boolean;
|
|
16
|
+
/** Gives the session back, having read `history` (after the system prompt). */
|
|
17
|
+
release: (history: Array<PromptMessage>) => void;
|
|
18
|
+
/** Frees this session; for an isolated call, only its working clone is freed. */
|
|
19
|
+
discard: () => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A session for a conversation that has read `system` and `prior`: the
|
|
23
|
+
* thread's own if it has read a start of them (the rest is appended), else
|
|
24
|
+
* a new one. `media` caches the images and audio loaded for the call.
|
|
25
|
+
*/
|
|
26
|
+
type SessionOptions = {
|
|
27
|
+
threadId: string | undefined;
|
|
28
|
+
system: string;
|
|
29
|
+
prior: Array<PromptMessage>;
|
|
30
|
+
options: LanguageModelCreateOptions;
|
|
31
|
+
signal: AbortSignal | undefined;
|
|
32
|
+
media?: Map<string, Promise<Blob>>;
|
|
33
|
+
/** Generate on a disposable clone of a cached, unmodified input-prefix session. */
|
|
34
|
+
isolated?: boolean;
|
|
35
|
+
/** Disable managed cross-request reuse and retention for this lease only. */
|
|
36
|
+
retain?: boolean;
|
|
37
|
+
/** The browser's download of its model, 0 to 1, if a new session starts it. */
|
|
38
|
+
onDownloadProgress?: (progress: number) => void;
|
|
39
|
+
};
|
|
40
|
+
export declare function leaseSession(options: SessionOptions): Promise<Lease>;
|
|
41
|
+
/**
|
|
42
|
+
* Frees every session kept, as when the page no longer needs the model.
|
|
43
|
+
* Sessions a call holds, or is still creating, finish that call and are
|
|
44
|
+
* freed when it ends instead of going back in the cache.
|
|
45
|
+
*/
|
|
46
|
+
export declare function destroyPromptApiSessions(): void;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=sessions.d.ts.map
|