prompt-api-polyfill 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -66
- package/backends/base.js +59 -0
- package/backends/defaults.js +9 -0
- package/backends/firebase.js +45 -0
- package/backends/gemini.js +48 -0
- package/backends/openai.js +340 -0
- package/backends/transformers.js +106 -0
- package/json-schema-converter.js +3 -1
- package/multimodal-converter.js +138 -12
- package/package.json +21 -5
- package/prompt-api-polyfill.js +482 -444
package/prompt-api-polyfill.js
CHANGED
|
@@ -1,548 +1,586 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Polyfill for the Prompt API (`LanguageModel`)
|
|
3
|
-
*
|
|
3
|
+
* Backends:
|
|
4
|
+
* - Firebase AI Logic (via `firebase/ai`)
|
|
5
|
+
* - Google Gemini API (via `@google/generative-ai`)
|
|
6
|
+
* - OpenAI API (via `openai`)
|
|
7
|
+
*
|
|
4
8
|
* Spec: https://github.com/webmachinelearning/prompt-api/blob/main/README.md
|
|
5
9
|
*
|
|
6
|
-
*
|
|
10
|
+
* Instructions:
|
|
7
11
|
* 1. Include this script in your HTML type="module".
|
|
8
|
-
* 2.
|
|
12
|
+
* 2. Configure the backend:
|
|
13
|
+
* - For Firebase: Define `window.FIREBASE_CONFIG`.
|
|
14
|
+
* - For Gemini: Define `window.GEMINI_CONFIG`.
|
|
15
|
+
* - For OpenAI: Define `window.OPENAI_CONFIG`.
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
getAI,
|
|
14
|
-
getGenerativeModel,
|
|
15
|
-
GoogleAIBackend,
|
|
16
|
-
InferenceMode,
|
|
17
|
-
} from 'https://esm.run/firebase/ai';
|
|
18
|
-
|
|
19
|
-
import './async-iterator-polyfill.js'; // Still needed for Safari 26.2.
|
|
18
|
+
import './async-iterator-polyfill.js';
|
|
20
19
|
import MultimodalConverter from './multimodal-converter.js';
|
|
21
20
|
import { convertJsonSchemaToVertexSchema } from './json-schema-converter.js';
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// --- Helper to convert initial History ---
|
|
23
|
+
async function convertToHistory(prompts) {
|
|
24
|
+
const history = [];
|
|
25
|
+
for (const p of prompts) {
|
|
26
|
+
const role = p.role === 'assistant' ? 'model' : 'user';
|
|
27
|
+
let parts = [];
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(p.content)) {
|
|
30
|
+
// Mixed content
|
|
31
|
+
for (const item of p.content) {
|
|
32
|
+
if (item.type === 'text') {
|
|
33
|
+
parts.push({ text: item.value || item.text || '' });
|
|
34
|
+
} else {
|
|
35
|
+
const part = await MultimodalConverter.convert(item.type, item.value);
|
|
36
|
+
parts.push(part);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
// Simple string
|
|
41
|
+
parts.push({ text: p.content });
|
|
42
|
+
}
|
|
43
|
+
history.push({ role, parts });
|
|
26
44
|
}
|
|
45
|
+
return history;
|
|
46
|
+
}
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Main LanguageModel Class
|
|
50
|
+
*/
|
|
51
|
+
export class LanguageModel extends EventTarget {
|
|
52
|
+
#backend;
|
|
53
|
+
#model;
|
|
54
|
+
#history;
|
|
55
|
+
#options;
|
|
56
|
+
#inCloudParams;
|
|
57
|
+
#destroyed;
|
|
58
|
+
#inputUsage;
|
|
59
|
+
#topK;
|
|
60
|
+
#temperature;
|
|
61
|
+
#onquotaoverflow;
|
|
62
|
+
|
|
63
|
+
constructor(backend, model, initialHistory, options = {}, inCloudParams) {
|
|
64
|
+
super();
|
|
65
|
+
this.#backend = backend;
|
|
66
|
+
this.#model = model;
|
|
67
|
+
this.#history = initialHistory || [];
|
|
68
|
+
this.#options = options;
|
|
69
|
+
this.#inCloudParams = inCloudParams;
|
|
70
|
+
this.#destroyed = false;
|
|
71
|
+
this.#inputUsage = 0;
|
|
72
|
+
|
|
73
|
+
this.#topK = options.topK;
|
|
74
|
+
this.#temperature = options.temperature;
|
|
34
75
|
}
|
|
35
76
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (Array.isArray(p.content)) {
|
|
49
|
-
// Mixed content
|
|
50
|
-
for (const item of p.content) {
|
|
51
|
-
if (item.type === 'text') {
|
|
52
|
-
parts.push({ text: item.value || item.text || '' });
|
|
53
|
-
} else {
|
|
54
|
-
const part = await MultimodalConverter.convert(
|
|
55
|
-
item.type,
|
|
56
|
-
item.value
|
|
57
|
-
);
|
|
58
|
-
parts.push(part);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
// Simple string
|
|
63
|
-
parts.push({ text: p.content });
|
|
64
|
-
}
|
|
65
|
-
history.push({ role, parts });
|
|
66
|
-
}
|
|
67
|
-
return history;
|
|
77
|
+
get inputUsage() {
|
|
78
|
+
return this.#inputUsage;
|
|
79
|
+
}
|
|
80
|
+
get inputQuota() {
|
|
81
|
+
return 1000000;
|
|
82
|
+
}
|
|
83
|
+
get topK() {
|
|
84
|
+
return this.#topK;
|
|
85
|
+
}
|
|
86
|
+
get temperature() {
|
|
87
|
+
return this.#temperature;
|
|
68
88
|
}
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class LanguageModel extends EventTarget {
|
|
74
|
-
#model;
|
|
75
|
-
#history;
|
|
76
|
-
#options;
|
|
77
|
-
#inCloudParams;
|
|
78
|
-
#destroyed;
|
|
79
|
-
#inputUsage;
|
|
80
|
-
#topK;
|
|
81
|
-
#temperature;
|
|
82
|
-
#onquotaoverflow;
|
|
83
|
-
|
|
84
|
-
constructor(model, initialHistory, options = {}, inCloudParams) {
|
|
85
|
-
super();
|
|
86
|
-
this.#model = model;
|
|
87
|
-
this.#history = initialHistory || [];
|
|
88
|
-
this.#options = options;
|
|
89
|
-
this.#inCloudParams = inCloudParams;
|
|
90
|
-
this.#destroyed = false;
|
|
91
|
-
this.#inputUsage = 0;
|
|
92
|
-
|
|
93
|
-
this.#topK = options.topK;
|
|
94
|
-
this.#temperature = options.temperature;
|
|
95
|
-
}
|
|
90
|
+
get onquotaoverflow() {
|
|
91
|
+
return this.#onquotaoverflow;
|
|
92
|
+
}
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
set onquotaoverflow(handler) {
|
|
95
|
+
if (this.#onquotaoverflow) {
|
|
96
|
+
this.removeEventListener('quotaoverflow', this.#onquotaoverflow);
|
|
99
97
|
}
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
this.#onquotaoverflow = handler;
|
|
99
|
+
if (typeof handler === 'function') {
|
|
100
|
+
this.addEventListener('quotaoverflow', handler);
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static async availability(options = {}) {
|
|
105
|
+
await LanguageModel.#validateOptions(options);
|
|
106
|
+
const backendClass = await LanguageModel.#getBackendClass();
|
|
107
|
+
return backendClass.availability(options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static #backends = [
|
|
111
|
+
{
|
|
112
|
+
config: 'FIREBASE_CONFIG',
|
|
113
|
+
path: './backends/firebase.js',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
config: 'GEMINI_CONFIG',
|
|
117
|
+
path: './backends/gemini.js',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
config: 'OPENAI_CONFIG',
|
|
121
|
+
path: './backends/openai.js',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
config: 'TRANSFORMERS_CONFIG',
|
|
125
|
+
path: './backends/transformers.js',
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
static #getBackendInfo() {
|
|
130
|
+
for (const b of LanguageModel.#backends) {
|
|
131
|
+
const config = window[b.config];
|
|
132
|
+
if (config && config.apiKey) {
|
|
133
|
+
return { ...b, configValue: config };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new DOMException(
|
|
137
|
+
'Prompt API Polyfill: No backend configuration found. Please set window.FIREBASE_CONFIG, window.GEMINI_CONFIG, or window.OPENAI_CONFIG.',
|
|
138
|
+
'NotSupportedError'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static async #getBackendClass() {
|
|
143
|
+
const info = LanguageModel.#getBackendInfo();
|
|
144
|
+
return (await import(/* @vite-ignore */ info.path)).default;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static async #validateOptions(options = {}) {
|
|
148
|
+
const { maxTemperature, maxTopK } = await LanguageModel.params();
|
|
149
|
+
|
|
150
|
+
const hasTemperature = Object.prototype.hasOwnProperty.call(
|
|
151
|
+
options,
|
|
152
|
+
'temperature'
|
|
153
|
+
);
|
|
154
|
+
const hasTopK = Object.prototype.hasOwnProperty.call(options, 'topK');
|
|
155
|
+
|
|
156
|
+
if (hasTemperature !== hasTopK) {
|
|
157
|
+
throw new DOMException(
|
|
158
|
+
'Initializing a new session must either specify both topK and temperature, or neither of them.',
|
|
159
|
+
'NotSupportedError'
|
|
160
|
+
);
|
|
105
161
|
}
|
|
106
|
-
|
|
107
|
-
|
|
162
|
+
|
|
163
|
+
// If neither temperature nor topK are provided, nothing to validate.
|
|
164
|
+
if (!hasTemperature && !hasTopK) {
|
|
165
|
+
return;
|
|
108
166
|
}
|
|
109
167
|
|
|
110
|
-
|
|
111
|
-
|
|
168
|
+
const { temperature, topK } = options;
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
typeof temperature !== 'number' ||
|
|
172
|
+
Number.isNaN(temperature) ||
|
|
173
|
+
typeof topK !== 'number' ||
|
|
174
|
+
Number.isNaN(topK)
|
|
175
|
+
) {
|
|
176
|
+
throw new DOMException(
|
|
177
|
+
'The provided temperature and topK must be numbers.',
|
|
178
|
+
'NotSupportedError'
|
|
179
|
+
);
|
|
112
180
|
}
|
|
113
181
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.addEventListener('quotaoverflow', handler);
|
|
182
|
+
if (temperature < 0 || temperature > maxTemperature || topK > maxTopK) {
|
|
183
|
+
throw new DOMException(
|
|
184
|
+
'The provided temperature or topK is outside the supported range.',
|
|
185
|
+
'NotSupportedError'
|
|
186
|
+
);
|
|
120
187
|
}
|
|
188
|
+
}
|
|
121
189
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
190
|
+
static async params() {
|
|
191
|
+
return {
|
|
192
|
+
// Values from https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-5-flash-lite#:~:text=%2C%20audio/webm-,Parameter%20defaults,-tune.
|
|
193
|
+
defaultTemperature: 1.0,
|
|
194
|
+
defaultTopK: 64,
|
|
195
|
+
maxTemperature: 2.0,
|
|
196
|
+
maxTopK: 64, // Fixed
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static async create(options = {}) {
|
|
201
|
+
const availability = await LanguageModel.availability(options);
|
|
202
|
+
if (availability === 'downloadable' || availability === 'downloading') {
|
|
203
|
+
throw new DOMException(
|
|
204
|
+
'Requires a user gesture when availability is "downloading" or "downloadable".',
|
|
205
|
+
'NotAllowedError'
|
|
206
|
+
);
|
|
125
207
|
}
|
|
126
208
|
|
|
127
|
-
|
|
128
|
-
|
|
209
|
+
// --- Backend Selection Logic ---
|
|
210
|
+
const info = LanguageModel.#getBackendInfo();
|
|
211
|
+
|
|
212
|
+
const BackendClass = await LanguageModel.#getBackendClass();
|
|
213
|
+
const backend = new BackendClass(info.configValue);
|
|
214
|
+
|
|
215
|
+
const defaults = {
|
|
216
|
+
temperature: 1.0,
|
|
217
|
+
topK: 3,
|
|
218
|
+
};
|
|
129
219
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
220
|
+
const resolvedOptions = { ...defaults, ...options };
|
|
221
|
+
|
|
222
|
+
const inCloudParams = {
|
|
223
|
+
model: backend.modelName,
|
|
224
|
+
generationConfig: {
|
|
225
|
+
temperature: resolvedOptions.temperature,
|
|
226
|
+
topK: resolvedOptions.topK,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
let initialHistory = [];
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
resolvedOptions.initialPrompts &&
|
|
234
|
+
Array.isArray(resolvedOptions.initialPrompts)
|
|
235
|
+
) {
|
|
236
|
+
const systemPrompts = resolvedOptions.initialPrompts.filter(
|
|
237
|
+
(p) => p.role === 'system'
|
|
238
|
+
);
|
|
239
|
+
const conversationPrompts = resolvedOptions.initialPrompts.filter(
|
|
240
|
+
(p) => p.role !== 'system'
|
|
133
241
|
);
|
|
134
|
-
const hasTopK = Object.prototype.hasOwnProperty.call(options, 'topK');
|
|
135
242
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
'
|
|
140
|
-
);
|
|
243
|
+
if (systemPrompts.length > 0) {
|
|
244
|
+
inCloudParams.systemInstruction = systemPrompts
|
|
245
|
+
.map((p) => p.content)
|
|
246
|
+
.join('\n');
|
|
141
247
|
}
|
|
248
|
+
// Await the conversion of history items (in case of images in history)
|
|
249
|
+
initialHistory = await convertToHistory(conversationPrompts);
|
|
250
|
+
}
|
|
142
251
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
252
|
+
const model = backend.createSession(resolvedOptions, inCloudParams);
|
|
253
|
+
|
|
254
|
+
// If a monitor callback is provided, simulate simple downloadprogress events
|
|
255
|
+
if (typeof resolvedOptions.monitor === 'function') {
|
|
256
|
+
const monitorTarget = new EventTarget();
|
|
147
257
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
Number.isNaN(temperature) ||
|
|
153
|
-
typeof topK !== 'number' ||
|
|
154
|
-
Number.isNaN(topK)
|
|
155
|
-
) {
|
|
156
|
-
throw new DOMException(
|
|
157
|
-
'The provided temperature and topK must be numbers.',
|
|
158
|
-
'NotSupportedError'
|
|
159
|
-
);
|
|
258
|
+
try {
|
|
259
|
+
resolvedOptions.monitor(monitorTarget);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
console.error('Error in monitor callback:', e);
|
|
160
262
|
}
|
|
161
263
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
264
|
+
try {
|
|
265
|
+
const startEvent = new ProgressEvent('downloadprogress', {
|
|
266
|
+
loaded: 0,
|
|
267
|
+
total: 1,
|
|
268
|
+
});
|
|
269
|
+
const endEvent = new ProgressEvent('downloadprogress', {
|
|
270
|
+
loaded: 1,
|
|
271
|
+
total: 1,
|
|
272
|
+
});
|
|
273
|
+
monitorTarget.dispatchEvent(startEvent);
|
|
274
|
+
monitorTarget.dispatchEvent(endEvent);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error('Error dispatching downloadprogress events:', e);
|
|
167
277
|
}
|
|
168
278
|
}
|
|
169
279
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
280
|
+
return new LanguageModel(
|
|
281
|
+
backend,
|
|
282
|
+
model,
|
|
283
|
+
initialHistory,
|
|
284
|
+
resolvedOptions,
|
|
285
|
+
inCloudParams
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Instance Methods
|
|
290
|
+
|
|
291
|
+
async clone(options = {}) {
|
|
292
|
+
if (this.#destroyed) {
|
|
293
|
+
throw new DOMException('Session is destroyed', 'InvalidStateError');
|
|
178
294
|
}
|
|
179
295
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// model that needs downloading and simulates the Prompt API's behavior.
|
|
184
|
-
if (availability === 'downloadable' || availability === 'downloading') {
|
|
185
|
-
throw new DOMException(
|
|
186
|
-
'Requires a user gesture when availability is "downloading" or "downloadable".',
|
|
187
|
-
'NotAllowedError'
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
const defaults = {
|
|
191
|
-
temperature: 1.0,
|
|
192
|
-
topK: 3,
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const resolvedOptions = { ...defaults, ...options };
|
|
196
|
-
|
|
197
|
-
const inCloudParams = {
|
|
198
|
-
model: MODEL_NAME,
|
|
199
|
-
generationConfig: {
|
|
200
|
-
temperature: resolvedOptions.temperature,
|
|
201
|
-
topK: resolvedOptions.topK,
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
let initialHistory = [];
|
|
206
|
-
let systemInstruction = undefined;
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
resolvedOptions.initialPrompts &&
|
|
210
|
-
Array.isArray(resolvedOptions.initialPrompts)
|
|
211
|
-
) {
|
|
212
|
-
const systemPrompts = resolvedOptions.initialPrompts.filter(
|
|
213
|
-
(p) => p.role === 'system'
|
|
214
|
-
);
|
|
215
|
-
const conversationPrompts = resolvedOptions.initialPrompts.filter(
|
|
216
|
-
(p) => p.role !== 'system'
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
if (systemPrompts.length > 0) {
|
|
220
|
-
inCloudParams.systemInstruction = systemPrompts
|
|
221
|
-
.map((p) => p.content)
|
|
222
|
-
.join('\n');
|
|
223
|
-
}
|
|
224
|
-
// Await the conversion of history items (in case of images in history)
|
|
225
|
-
initialHistory = await convertToFirebaseHistory(conversationPrompts);
|
|
226
|
-
}
|
|
296
|
+
const historyCopy = JSON.parse(JSON.stringify(this.#history));
|
|
297
|
+
const mergedOptions = { ...this.#options, ...options };
|
|
298
|
+
const mergedInCloudParams = { ...this.#inCloudParams };
|
|
227
299
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
300
|
+
if (options.temperature !== undefined) {
|
|
301
|
+
mergedInCloudParams.generationConfig.temperature = options.temperature;
|
|
302
|
+
}
|
|
303
|
+
if (options.topK !== undefined) {
|
|
304
|
+
mergedInCloudParams.generationConfig.topK = options.topK;
|
|
305
|
+
}
|
|
232
306
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
307
|
+
// Re-create the backend for the clone since it now holds state (#model)
|
|
308
|
+
const BackendClass = await LanguageModel.#getBackendClass();
|
|
309
|
+
const info = LanguageModel.#getBackendInfo();
|
|
310
|
+
const newBackend = new BackendClass(info.configValue);
|
|
311
|
+
const newModel = newBackend.createSession(
|
|
312
|
+
mergedOptions,
|
|
313
|
+
mergedInCloudParams
|
|
314
|
+
);
|
|
236
315
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
316
|
+
return new LanguageModel(
|
|
317
|
+
newBackend,
|
|
318
|
+
newModel,
|
|
319
|
+
historyCopy,
|
|
320
|
+
mergedOptions,
|
|
321
|
+
mergedInCloudParams
|
|
322
|
+
);
|
|
323
|
+
}
|
|
243
324
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
total: 1,
|
|
249
|
-
});
|
|
250
|
-
const endEvent = new ProgressEvent('downloadprogress', {
|
|
251
|
-
loaded: 1,
|
|
252
|
-
total: 1,
|
|
253
|
-
});
|
|
254
|
-
// The `ProgressEvent`'s `currentTarget`, `srcElement` and `target`
|
|
255
|
-
// properties are `EventTarget`, not `CreateMonitor`, when using the
|
|
256
|
-
// polyfill. Hopefully developers won't rely on these properties.
|
|
257
|
-
monitorTarget.dispatchEvent(startEvent);
|
|
258
|
-
monitorTarget.dispatchEvent(endEvent);
|
|
259
|
-
} catch (e) {
|
|
260
|
-
console.error('Error dispatching downloadprogress events:', e);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
325
|
+
destroy() {
|
|
326
|
+
this.#destroyed = true;
|
|
327
|
+
this.#history = null;
|
|
328
|
+
}
|
|
263
329
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
);
|
|
330
|
+
async prompt(input, options = {}) {
|
|
331
|
+
if (this.#destroyed) {
|
|
332
|
+
throw new DOMException('Session is destroyed', 'InvalidStateError');
|
|
333
|
+
}
|
|
334
|
+
if (options.signal?.aborted) {
|
|
335
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
270
336
|
}
|
|
271
337
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
throw new DOMException('Session is destroyed', 'InvalidStateError');
|
|
277
|
-
// Clone private history
|
|
278
|
-
const historyCopy = JSON.parse(JSON.stringify(this.#history));
|
|
279
|
-
return new LanguageModel(
|
|
280
|
-
this.#model,
|
|
281
|
-
historyCopy,
|
|
282
|
-
{
|
|
283
|
-
...this.#options,
|
|
284
|
-
...options,
|
|
285
|
-
},
|
|
286
|
-
this.#inCloudParams
|
|
338
|
+
if (options.responseConstraint) {
|
|
339
|
+
// Update Schema
|
|
340
|
+
const schema = convertJsonSchemaToVertexSchema(
|
|
341
|
+
options.responseConstraint
|
|
287
342
|
);
|
|
288
|
-
|
|
343
|
+
this.#inCloudParams.generationConfig.responseMimeType =
|
|
344
|
+
'application/json';
|
|
345
|
+
this.#inCloudParams.generationConfig.responseSchema = schema;
|
|
289
346
|
|
|
290
|
-
|
|
291
|
-
this.#
|
|
292
|
-
|
|
347
|
+
// Re-create model with new config/schema (stored in backend)
|
|
348
|
+
this.#model = this.#backend.createSession(
|
|
349
|
+
this.#options,
|
|
350
|
+
this.#inCloudParams
|
|
351
|
+
);
|
|
293
352
|
}
|
|
294
353
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (options.signal?.aborted)
|
|
299
|
-
throw new DOMException('Aborted', 'AbortError');
|
|
300
|
-
|
|
301
|
-
if (options.responseConstraint) {
|
|
302
|
-
const vertexSchema = convertJsonSchemaToVertexSchema(
|
|
303
|
-
options.responseConstraint
|
|
304
|
-
);
|
|
305
|
-
this.#inCloudParams.generationConfig.responseMimeType =
|
|
306
|
-
'application/json';
|
|
307
|
-
this.#inCloudParams.generationConfig.responseSchema = vertexSchema;
|
|
308
|
-
this.#model = getGenerativeModel(ai, {
|
|
309
|
-
mode: InferenceMode.ONLY_IN_CLOUD,
|
|
310
|
-
inCloudParams: this.#inCloudParams,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
354
|
+
// Process Input (Async conversion of Blob/Canvas/AudioBuffer)
|
|
355
|
+
const parts = await this.#processInput(input);
|
|
356
|
+
const userContent = { role: 'user', parts: parts };
|
|
313
357
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
358
|
+
try {
|
|
359
|
+
// Estimate usage
|
|
360
|
+
const totalTokens = await this.#backend.countTokens([
|
|
361
|
+
{ role: 'user', parts },
|
|
362
|
+
]);
|
|
317
363
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
contents: [{ role: 'user', parts }],
|
|
322
|
-
});
|
|
323
|
-
if (this.#inputUsage + totalTokens > this.inputQuota)
|
|
324
|
-
this.dispatchEvent(new Event('quotaoverflow'));
|
|
364
|
+
if (this.#inputUsage + totalTokens > this.inputQuota) {
|
|
365
|
+
this.dispatchEvent(new Event('quotaoverflow'));
|
|
366
|
+
}
|
|
325
367
|
|
|
326
|
-
|
|
368
|
+
const requestContents = [...this.#history, userContent];
|
|
327
369
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
});
|
|
370
|
+
const { text, usage } =
|
|
371
|
+
await this.#backend.generateContent(requestContents);
|
|
331
372
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
373
|
+
if (usage) {
|
|
374
|
+
this.#inputUsage = usage;
|
|
375
|
+
}
|
|
336
376
|
|
|
337
|
-
|
|
377
|
+
this.#history.push(userContent);
|
|
378
|
+
this.#history.push({ role: 'model', parts: [{ text }] });
|
|
338
379
|
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
return text;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('Prompt API Polyfill Error:', error);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
341
386
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
387
|
+
promptStreaming(input, options = {}) {
|
|
388
|
+
if (this.#destroyed) {
|
|
389
|
+
throw new DOMException('Session is destroyed', 'InvalidStateError');
|
|
390
|
+
}
|
|
391
|
+
if (options.signal?.aborted) {
|
|
392
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
347
393
|
}
|
|
348
394
|
|
|
349
|
-
|
|
350
|
-
if (this.#destroyed)
|
|
351
|
-
throw new DOMException('Session is destroyed', 'InvalidStateError');
|
|
352
|
-
if (options.signal?.aborted)
|
|
353
|
-
throw new DOMException('Aborted', 'AbortError');
|
|
354
|
-
|
|
355
|
-
const _this = this; // Capture 'this' to access private fields in callback
|
|
356
|
-
|
|
357
|
-
if (options.responseConstraint) {
|
|
358
|
-
const vertexSchema = convertJsonSchemaToVertexSchema(
|
|
359
|
-
options.responseConstraint
|
|
360
|
-
);
|
|
361
|
-
this.#inCloudParams.generationConfig.responseMimeType =
|
|
362
|
-
'application/json';
|
|
363
|
-
this.#inCloudParams.generationConfig.responseSchema = vertexSchema;
|
|
364
|
-
this.#model = getGenerativeModel(ai, {
|
|
365
|
-
mode: InferenceMode.ONLY_IN_CLOUD,
|
|
366
|
-
inCloudParams: this.#inCloudParams,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
395
|
+
const _this = this; // Capture 'this' to access private fields in callback
|
|
369
396
|
|
|
370
|
-
|
|
397
|
+
const signal = options.signal;
|
|
371
398
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
399
|
+
return new ReadableStream({
|
|
400
|
+
async start(controller) {
|
|
401
|
+
const abortError = new DOMException('Aborted', 'AbortError');
|
|
375
402
|
|
|
376
|
-
|
|
377
|
-
|
|
403
|
+
if (signal?.aborted) {
|
|
404
|
+
controller.error(abortError);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let aborted = false;
|
|
409
|
+
const onAbort = () => {
|
|
410
|
+
aborted = true;
|
|
411
|
+
try {
|
|
378
412
|
controller.error(abortError);
|
|
379
|
-
|
|
413
|
+
} catch {
|
|
414
|
+
// Ignore
|
|
380
415
|
}
|
|
416
|
+
};
|
|
381
417
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
controller.error(abortError);
|
|
387
|
-
} catch {
|
|
388
|
-
// Controller might already be closed/errored; ignore.
|
|
389
|
-
}
|
|
390
|
-
};
|
|
418
|
+
if (signal) {
|
|
419
|
+
signal.addEventListener('abort', onAbort);
|
|
420
|
+
}
|
|
391
421
|
|
|
392
|
-
|
|
393
|
-
|
|
422
|
+
try {
|
|
423
|
+
if (options.responseConstraint) {
|
|
424
|
+
const schema = convertJsonSchemaToVertexSchema(
|
|
425
|
+
options.responseConstraint
|
|
426
|
+
);
|
|
427
|
+
_this.#inCloudParams.generationConfig.responseMimeType =
|
|
428
|
+
'application/json';
|
|
429
|
+
_this.#inCloudParams.generationConfig.responseSchema = schema;
|
|
430
|
+
_this.#model = _this.#backend.createSession(
|
|
431
|
+
_this.#options,
|
|
432
|
+
_this.#inCloudParams
|
|
433
|
+
);
|
|
394
434
|
}
|
|
395
435
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const parts = await _this.#processInput(input);
|
|
399
|
-
const userContent = { role: 'user', parts: parts };
|
|
436
|
+
const parts = await _this.#processInput(input);
|
|
437
|
+
const userContent = { role: 'user', parts: parts };
|
|
400
438
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
});
|
|
405
|
-
if (_this.#inputUsage + totalTokens > this.inputQuota)
|
|
406
|
-
this.dispatchEvent(new Event('quotaoverflow'));
|
|
439
|
+
const totalTokens = await _this.#backend.countTokens([
|
|
440
|
+
{ role: 'user', parts },
|
|
441
|
+
]);
|
|
407
442
|
|
|
408
|
-
|
|
443
|
+
if (_this.#inputUsage + totalTokens > _this.inputQuota) {
|
|
444
|
+
_this.dispatchEvent(new Event('quotaoverflow'));
|
|
445
|
+
}
|
|
409
446
|
|
|
410
|
-
|
|
411
|
-
contents: requestContents,
|
|
412
|
-
});
|
|
447
|
+
const requestContents = [..._this.#history, userContent];
|
|
413
448
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
if (chunk.usageMetadata?.totalTokenCount) {
|
|
429
|
-
_this.#inputUsage += chunk.usageMetadata.totalTokenCount;
|
|
449
|
+
const stream =
|
|
450
|
+
await _this.#backend.generateContentStream(requestContents);
|
|
451
|
+
|
|
452
|
+
let fullResponseText = '';
|
|
453
|
+
|
|
454
|
+
for await (const chunk of stream) {
|
|
455
|
+
if (aborted) {
|
|
456
|
+
// Try to cancel if supported
|
|
457
|
+
if (typeof stream.return === 'function') {
|
|
458
|
+
await stream.return();
|
|
430
459
|
}
|
|
431
|
-
|
|
432
|
-
fullResponseText += chunkText;
|
|
433
|
-
controller.enqueue(chunkText);
|
|
460
|
+
return;
|
|
434
461
|
}
|
|
435
462
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
_this.#history.push({
|
|
439
|
-
role: 'model',
|
|
440
|
-
parts: [{ text: fullResponseText }],
|
|
441
|
-
});
|
|
463
|
+
const chunkText = chunk.text();
|
|
464
|
+
fullResponseText += chunkText;
|
|
442
465
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
} catch (error) {
|
|
446
|
-
// If we aborted, we've already signaled an AbortError; otherwise surface the error.
|
|
447
|
-
if (!aborted) {
|
|
448
|
-
controller.error(error);
|
|
449
|
-
}
|
|
450
|
-
} finally {
|
|
451
|
-
if (signal) {
|
|
452
|
-
signal.removeEventListener('abort', onAbort);
|
|
466
|
+
if (chunk.usageMetadata?.totalTokenCount) {
|
|
467
|
+
_this.#inputUsage = chunk.usageMetadata.totalTokenCount;
|
|
453
468
|
}
|
|
469
|
+
|
|
470
|
+
controller.enqueue(chunkText);
|
|
454
471
|
}
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
472
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
473
|
+
if (!aborted) {
|
|
474
|
+
_this.#history.push(userContent);
|
|
475
|
+
_this.#history.push({
|
|
476
|
+
role: 'model',
|
|
477
|
+
parts: [{ text: fullResponseText }],
|
|
478
|
+
});
|
|
464
479
|
|
|
465
|
-
|
|
466
|
-
|
|
480
|
+
controller.close();
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (!aborted) {
|
|
484
|
+
controller.error(error);
|
|
485
|
+
}
|
|
486
|
+
} finally {
|
|
487
|
+
if (signal) {
|
|
488
|
+
signal.removeEventListener('abort', onAbort);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
467
494
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
495
|
+
async append(input, options = {}) {
|
|
496
|
+
if (this.#destroyed) {
|
|
497
|
+
throw new DOMException('Session is destroyed', 'InvalidStateError');
|
|
498
|
+
}
|
|
499
|
+
if (options.signal?.aborted) {
|
|
500
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const parts = await this.#processInput(input);
|
|
504
|
+
const content = { role: 'user', parts: parts };
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const totalTokens = await this.#backend.countTokens([
|
|
508
|
+
...this.#history,
|
|
509
|
+
content,
|
|
510
|
+
]);
|
|
511
|
+
this.#inputUsage = totalTokens;
|
|
512
|
+
} catch {
|
|
513
|
+
// Do nothing.
|
|
514
|
+
}
|
|
477
515
|
|
|
478
|
-
|
|
516
|
+
this.#history.push(content);
|
|
479
517
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
518
|
+
if (this.#inputUsage > this.inputQuota) {
|
|
519
|
+
this.dispatchEvent(new Event('quotaoverflow'));
|
|
483
520
|
}
|
|
521
|
+
}
|
|
484
522
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
523
|
+
async measureInputUsage(input) {
|
|
524
|
+
if (this.#destroyed) {
|
|
525
|
+
throw new DOMException('Session is destroyed', 'InvalidStateError');
|
|
526
|
+
}
|
|
488
527
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return 0;
|
|
501
|
-
}
|
|
528
|
+
try {
|
|
529
|
+
const parts = await this.#processInput(input);
|
|
530
|
+
const totalTokens = await this.#backend.countTokens([
|
|
531
|
+
{ role: 'user', parts },
|
|
532
|
+
]);
|
|
533
|
+
return totalTokens || 0;
|
|
534
|
+
} catch (e) {
|
|
535
|
+
console.warn(
|
|
536
|
+
'The underlying API call failed, quota usage (0) is not reported accurately.'
|
|
537
|
+
);
|
|
538
|
+
return 0;
|
|
502
539
|
}
|
|
540
|
+
}
|
|
503
541
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
542
|
+
// Private Helper to process diverse input types
|
|
543
|
+
async #processInput(input) {
|
|
544
|
+
if (typeof input === 'string') {
|
|
545
|
+
return [{ text: input }];
|
|
546
|
+
}
|
|
509
547
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
);
|
|
529
|
-
combinedParts.push(part);
|
|
530
|
-
}
|
|
548
|
+
if (Array.isArray(input)) {
|
|
549
|
+
if (input.length > 0 && input[0].role) {
|
|
550
|
+
let combinedParts = [];
|
|
551
|
+
for (const msg of input) {
|
|
552
|
+
if (typeof msg.content === 'string') {
|
|
553
|
+
combinedParts.push({ text: msg.content });
|
|
554
|
+
if (msg.prefix) {
|
|
555
|
+
console.warn(
|
|
556
|
+
"The `prefix` flag isn't supported and was ignored."
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
} else if (Array.isArray(msg.content)) {
|
|
560
|
+
for (const c of msg.content) {
|
|
561
|
+
if (c.type === 'text') {
|
|
562
|
+
combinedParts.push({ text: c.value });
|
|
563
|
+
} else {
|
|
564
|
+
const part = await MultimodalConverter.convert(c.type, c.value);
|
|
565
|
+
combinedParts.push(part);
|
|
531
566
|
}
|
|
532
567
|
}
|
|
533
568
|
}
|
|
534
|
-
return combinedParts;
|
|
535
569
|
}
|
|
536
|
-
return
|
|
570
|
+
return combinedParts;
|
|
537
571
|
}
|
|
538
|
-
|
|
539
|
-
return [{ text: JSON.stringify(input) }];
|
|
572
|
+
return input.map((s) => ({ text: String(s) }));
|
|
540
573
|
}
|
|
574
|
+
|
|
575
|
+
return [{ text: JSON.stringify(input) }];
|
|
541
576
|
}
|
|
577
|
+
}
|
|
542
578
|
|
|
579
|
+
if (!('LanguageModel' in window) || window.__FORCE_PROMPT_API_POLYFILL__) {
|
|
543
580
|
// Attach to window
|
|
544
581
|
window.LanguageModel = LanguageModel;
|
|
582
|
+
LanguageModel.__isPolyfill = true;
|
|
545
583
|
console.log(
|
|
546
|
-
'Polyfill: window.LanguageModel is now backed by
|
|
584
|
+
'Polyfill: window.LanguageModel is now backed by the Prompt API polyfill.'
|
|
547
585
|
);
|
|
548
|
-
}
|
|
586
|
+
}
|