prompt-api-polyfill 0.1.0 → 0.2.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 CHANGED
@@ -1,8 +1,12 @@
1
- # Prompt API Polyfill (Firebase AI Logic backend)
1
+ # Prompt API Polyfill
2
2
 
3
3
  This package provides a browser polyfill for the
4
- [Prompt API `LanguageModel`](https://github.com/webmachinelearning/prompt-api)
5
- backed by **Firebase AI Logic**.
4
+ [Prompt API `LanguageModel`](https://github.com/webmachinelearning/prompt-api),
5
+ supporting dynamic backends:
6
+
7
+ - **Firebase AI Logic**
8
+ - **Google Gemini API**
9
+ - **OpenAI API**
6
10
 
7
11
  When loaded in the browser, it defines a global:
8
12
 
@@ -13,8 +17,28 @@ window.LanguageModel;
13
17
  so you can use the Prompt API shape even in environments where it is not yet
14
18
  natively available.
15
19
 
16
- - Back end: Firebase AI Logic
17
- - Default model: `gemini-2.5-flash-lite` (configurable via `modelName`)
20
+ ## Supported Backends
21
+
22
+ ### Firebase AI Logic
23
+
24
+ - **Uses**: `firebase/ai` SDK.
25
+ - **Select by setting**: `window.FIREBASE_CONFIG`.
26
+ - **Model**: Uses default if not specified (see
27
+ [`backends/defaults.js`](backends/defaults.js)).
28
+
29
+ ### Google Gemini API
30
+
31
+ - **Uses**: `@google/generative-ai` SDK.
32
+ - **Select by setting**: `window.GEMINI_CONFIG`.
33
+ - **Model**: Uses default if not specified (see
34
+ [`backends/defaults.js`](backends/defaults.js)).
35
+
36
+ ### OpenAI API
37
+
38
+ - **Uses**: `openai` SDK.
39
+ - **Select by setting**: `window.OPENAI_CONFIG`.
40
+ - **Model**: Uses default if not specified (see
41
+ [`backends/defaults.js`](backends/defaults.js)).
18
42
 
19
43
  ---
20
44
 
@@ -28,37 +52,78 @@ npm install prompt-api-polyfill
28
52
 
29
53
  ## Quick start
30
54
 
31
- 1. **Create a Firebase project with Generative AI enabled** (see Configuration
32
- below).
33
- 2. **Provide your Firebase config** on `window.FIREBASE_CONFIG`.
34
- 3. **Import the polyfill** so it can attach `window.LanguageModel`.
55
+ ### Backed by Firebase
35
56
 
36
- ### Example (using a JSON config file)
37
-
38
- Create a `.env.json` file (see
39
- [Configuring `dot_env.json` / `.env.json`](#configuring-dot_envjson--envjson))
40
- and then use it from a browser entry point:
57
+ 1. **Create a Firebase project with Generative AI enabled**.
58
+ 2. **Provide your Firebase config** on `window.FIREBASE_CONFIG`.
59
+ 3. **Import the polyfill**.
41
60
 
42
61
  ```html
43
62
  <script type="module">
44
63
  import firebaseConfig from './.env.json' with { type: 'json' };
45
64
 
46
- // Make the config available to the polyfill
65
+ // Set FIREBASE_CONFIG to select the Firebase backend
47
66
  window.FIREBASE_CONFIG = firebaseConfig;
48
67
 
49
- // Only load the polyfill if LanguageModel is not available natively
50
68
  if (!('LanguageModel' in window)) {
51
69
  await import('prompt-api-polyfill');
52
70
  }
53
71
 
54
72
  const session = await LanguageModel.create();
55
- const text = await session.prompt('Say hello from the polyfill!');
56
- console.log(text);
57
73
  </script>
58
74
  ```
59
75
 
60
- > **Note**: The polyfill attaches `LanguageModel` to `window` as a side effect.
61
- > There are no named exports.
76
+ ### Backed by Gemini API
77
+
78
+ 1. **Get a Gemini API Key** from
79
+ [Google AI Studio](https://aistudio.google.com/).
80
+ 2. **Provide your API Key** on `window.GEMINI_CONFIG`.
81
+ 3. **Import the polyfill**.
82
+
83
+ ```html
84
+ <script type="module">
85
+ // NOTE: Do not expose real keys in production source code!
86
+ // Set GEMINI_CONFIG to select the Gemini backend
87
+ window.GEMINI_CONFIG = { apiKey: 'YOUR_GEMINI_API_KEY' };
88
+
89
+ if (!('LanguageModel' in window)) {
90
+ await import('prompt-api-polyfill');
91
+ }
92
+
93
+ const session = await LanguageModel.create();
94
+ </script>
95
+ ```
96
+
97
+ ### Backed by OpenAI API
98
+
99
+ 1. **Get an OpenAI API Key** from the
100
+ [OpenAI Platform](https://platform.openai.com/).
101
+ 2. **Provide your API Key** on `window.OPENAI_CONFIG`.
102
+ 3. **Import the polyfill**.
103
+
104
+ ```html
105
+ <script type="module">
106
+ // NOTE: Do not expose real keys in production source code!
107
+ // Set OPENAI_CONFIG to select the OpenAI backend
108
+ window.OPENAI_CONFIG = { apiKey: 'YOUR_OPENAI_API_KEY' };
109
+
110
+ if (!('LanguageModel' in window)) {
111
+ await import('prompt-api-polyfill');
112
+ }
113
+
114
+ const session = await LanguageModel.create();
115
+ </script>
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Configuration
121
+
122
+ ### Example (using a JSON config file)
123
+
124
+ Create a `.env.json` file (see
125
+ [Configuring `dot_env.json` / `.env.json`](#configuring-dot_envjson--envjson))
126
+ and then use it from a browser entry point.
62
127
 
63
128
  ### Example based on `index.html` in this repo
64
129
 
@@ -76,8 +141,8 @@ A simplified version of how it is wired up:
76
141
 
77
142
  ```html
78
143
  <script type="module">
79
- import firebaseConfig from './.env.json' with { type: 'json' };
80
- window.FIREBASE_CONFIG = firebaseConfig;
144
+ // Set GEMINI_CONFIG to select the Gemini backend
145
+ window.GEMINI_CONFIG = { apiKey: 'YOUR_GEMINI_API_KEY' };
81
146
 
82
147
  // Load the polyfill only when necessary
83
148
  if (!('LanguageModel' in window)) {
@@ -110,17 +175,20 @@ This repo ships with a template file:
110
175
  ```jsonc
111
176
  // dot_env.json
112
177
  {
113
- "apiKey": "",
178
+ // For Firebase:
114
179
  "projectId": "",
115
180
  "appId": "",
116
181
  "modelName": "",
182
+
183
+ // For Firebase OR Gemini OR OpenAI:
184
+ "apiKey": "",
117
185
  }
118
186
  ```
119
187
 
120
188
  You should treat `dot_env.json` as a **template** and create a real `.env.json`
121
189
  that is **not committed** with your secrets.
122
190
 
123
- ### 1. Create `.env.json`
191
+ ### Create `.env.json`
124
192
 
125
193
  Copy the template:
126
194
 
@@ -128,63 +196,56 @@ Copy the template:
128
196
  cp dot_env.json .env.json
129
197
  ```
130
198
 
131
- Then open `.env.json` and fill in the values from your Firebase project:
199
+ Then open `.env.json` and fill in the values.
200
+
201
+ **For Firebase:**
132
202
 
133
203
  ```json
134
204
  {
135
205
  "apiKey": "YOUR_FIREBASE_WEB_API_KEY",
136
206
  "projectId": "your-gcp-project-id",
137
207
  "appId": "YOUR_FIREBASE_APP_ID",
138
- "modelName": "gemini-2.5-flash-lite"
208
+ "modelName": "choose-model-for-firebase"
139
209
  }
140
210
  ```
141
211
 
142
- ### 2. Field-by-field explanation
143
-
144
- - `apiKey` Your **Firebase Web API key**. You can find this in the Firebase
145
- Console under: _Project settings → General → Your apps → Web app_.
212
+ **For Gemini:**
146
213
 
147
- - `projectId` The **GCP / Firebase project ID**, e.g. `my-ai-project`.
148
-
149
- - `appId` The **Firebase Web app ID**, e.g. `1:1234567890:web:abcdef123456`.
150
-
151
- - `modelName` (optional) The Gemini model ID to use. If omitted, the polyfill
152
- defaults to:
214
+ ```json
215
+ {
216
+ "apiKey": "YOUR_GEMINI_CONFIG",
217
+ "modelName": "choose-model-for-gemini"
218
+ }
219
+ ```
153
220
 
154
- ```json
155
- "modelName": "gemini-2.5-flash-lite"
156
- ```
221
+ **For OpenAI:**
157
222
 
158
- You can substitute another supported Gemini model here if desired.
223
+ ```json
224
+ {
225
+ "apiKey": "YOUR_OPENAI_API_KEY",
226
+ "modelName": "choose-model-for-openai"
227
+ }
228
+ ```
159
229
 
160
- These fields are passed directly to:
230
+ ### Field-by-field explanation
161
231
 
162
- - `initializeApp(firebaseConfig)` from Firebase
163
- - `getAI(app, { backend: new GoogleAIBackend() })` from the Firebase AI SDK
232
+ - `apiKey`:
233
+ - **Firebase**: Your Firebase Web API key.
234
+ - **Gemini**: Your Gemini API Key.
235
+ - **OpenAI**: Your OpenAI API Key.
236
+ - `projectId` / `appId`: **Firebase only**.
164
237
 
165
- and `modelName` is used to select which Gemini model to call.
238
+ - `modelName` (optional): The model ID to use. If not provided, the polyfill
239
+ uses the defaults defined in [`backends/defaults.js`](backends/defaults.js).
166
240
 
167
241
  > **Important:** Do **not** commit a real `.env.json` with production
168
242
  > credentials to source control. Use `dot_env.json` as the committed template
169
243
  > and keep `.env.json` local.
170
244
 
171
- ### 3. Wiring the config into the polyfill
245
+ ### Wiring the config into the polyfill
172
246
 
173
- Once `.env.json` is filled out, you can import it and expose it to the polyfill
174
- exactly like in `index.html`:
175
-
176
- ```js
177
- import firebaseConfig from './.env.json' with { type: 'json' };
178
-
179
- window.FIREBASE_CONFIG = firebaseConfig;
180
-
181
- if (!('LanguageModel' in window)) {
182
- await import('prompt-api-polyfill');
183
- }
184
- ```
185
-
186
- From this point on, `LanguageModel.create()` will use your Firebase
187
- configuration.
247
+ Once `.env.json` is filled out, you can import it and expose it to the polyfill.
248
+ See the [Quick start](#quick-start) examples above.
188
249
 
189
250
  ---
190
251
 
@@ -200,8 +261,7 @@ For a complete, end-to-end example, see the `index.html` file in this directory.
200
261
 
201
262
  ## Running the demo locally
202
263
 
203
- 1. Install dependencies and this package (if using the npm-installed version in
204
- another project):
264
+ 1. Install dependencies:
205
265
 
206
266
  ```bash
207
267
  npm install
@@ -211,17 +271,34 @@ For a complete, end-to-end example, see the `index.html` file in this directory.
211
271
 
212
272
  ```bash
213
273
  cp dot_env.json .env.json
214
- # then edit .env.json with your Firebase and model settings
215
274
  ```
216
275
 
217
276
  3. Serve `index.html`:
218
-
219
277
  ```bash
220
278
  npm start
221
279
  ```
222
280
 
223
- You should see network requests to the Vertex AI / Firebase AI backend and
224
- streaming responses logged in the console.
281
+ You should see network requests to the backends logs.
282
+
283
+ ---
284
+
285
+ ## Testing
286
+
287
+ The project includes a comprehensive test suite that runs in a headless browser.
288
+
289
+ ### Running Browser Tests
290
+
291
+ Uses `playwright` to run tests in a real Chromium instance. This is the
292
+ recommended way to verify environmental fidelity and multimodal support.
293
+
294
+ ```bash
295
+ npm run test:browser
296
+ ```
297
+
298
+ To see the browser and DevTools while testing, you can modify
299
+ `vitest.browser.config.js` to set `headless: false`.
300
+
301
+ ---
225
302
 
226
303
  ## License
227
304
 
@@ -6,7 +6,9 @@ import { Schema } from 'https://esm.run/firebase/ai';
6
6
  * @returns {Schema} - The Firebase Vertex AI Schema instance.
7
7
  */
8
8
  export function convertJsonSchemaToVertexSchema(jsonSchema) {
9
- if (!jsonSchema) return undefined;
9
+ if (!jsonSchema) {
10
+ return undefined;
11
+ }
10
12
 
11
13
  // Extract common base parameters supported by all Schema types
12
14
  const baseParams = {
@@ -1,7 +1,11 @@
1
1
  export default class MultimodalConverter {
2
2
  static async convert(type, value) {
3
- if (type === 'image') return this.processImage(value);
4
- if (type === 'audio') return this.processAudio(value);
3
+ if (type === 'image') {
4
+ return this.processImage(value);
5
+ }
6
+ if (type === 'audio') {
7
+ return this.processAudio(value);
8
+ }
5
9
  throw new DOMException(
6
10
  `Unsupported media type: ${type}`,
7
11
  'NotSupportedError'
@@ -16,13 +20,16 @@ export default class MultimodalConverter {
16
20
 
17
21
  // BufferSource (ArrayBuffer/View) -> Sniff or Default
18
22
  if (ArrayBuffer.isView(source) || source instanceof ArrayBuffer) {
19
- const buffer = source instanceof ArrayBuffer ? source : source.buffer;
23
+ const u8 =
24
+ source instanceof ArrayBuffer
25
+ ? new Uint8Array(source)
26
+ : new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
27
+ const buffer = u8.buffer.slice(
28
+ u8.byteOffset,
29
+ u8.byteOffset + u8.byteLength
30
+ );
20
31
  const base64 = this.arrayBufferToBase64(buffer);
21
- // Basic sniffing for PNG/JPEG magic bytes
22
- const u8 = new Uint8Array(buffer);
23
- let mimeType = 'image/png'; // Default
24
- if (u8[0] === 0xff && u8[1] === 0xd8) mimeType = 'image/jpeg';
25
- else if (u8[0] === 0x89 && u8[1] === 0x50) mimeType = 'image/png';
32
+ const mimeType = this.#sniffImageMimeType(u8) || 'image/png';
26
33
 
27
34
  return { inlineData: { data: base64, mimeType } };
28
35
  }
@@ -32,6 +39,111 @@ export default class MultimodalConverter {
32
39
  return this.canvasSourceToInlineData(source);
33
40
  }
34
41
 
42
+ static #sniffImageMimeType(u8) {
43
+ const len = u8.length;
44
+ if (len < 4) {
45
+ return null;
46
+ }
47
+
48
+ // JPEG: FF D8 FF
49
+ if (u8[0] === 0xff && u8[1] === 0xd8 && u8[2] === 0xff) {
50
+ return 'image/jpeg';
51
+ }
52
+
53
+ // PNG: 89 50 4E 47 0D 0A 1A 0A
54
+ if (
55
+ u8[0] === 0x89 &&
56
+ u8[1] === 0x50 &&
57
+ u8[2] === 0x4e &&
58
+ u8[3] === 0x47 &&
59
+ u8[4] === 0x0d &&
60
+ u8[5] === 0x0a &&
61
+ u8[6] === 0x1a &&
62
+ u8[7] === 0x0a
63
+ ) {
64
+ return 'image/png';
65
+ }
66
+
67
+ // GIF: GIF87a / GIF89a
68
+ if (u8[0] === 0x47 && u8[1] === 0x49 && u8[2] === 0x46 && u8[3] === 0x38) {
69
+ return 'image/gif';
70
+ }
71
+
72
+ // WebP: RIFF (offset 0) + WEBP (offset 8)
73
+ if (
74
+ u8[0] === 0x52 &&
75
+ u8[1] === 0x49 &&
76
+ u8[2] === 0x46 &&
77
+ u8[3] === 0x46 &&
78
+ u8[8] === 0x57 &&
79
+ u8[9] === 0x45 &&
80
+ u8[10] === 0x42 &&
81
+ u8[11] === 0x50
82
+ ) {
83
+ return 'image/webp';
84
+ }
85
+
86
+ // BMP: BM
87
+ if (u8[0] === 0x42 && u8[1] === 0x4d) {
88
+ return 'image/bmp';
89
+ }
90
+
91
+ // ICO: 00 00 01 00
92
+ if (u8[0] === 0x00 && u8[1] === 0x00 && u8[2] === 0x01 && u8[3] === 0x00) {
93
+ return 'image/x-icon';
94
+ }
95
+
96
+ // TIFF: II* (LE) / MM* (BE)
97
+ if (
98
+ (u8[0] === 0x49 && u8[1] === 0x49 && u8[2] === 0x2a) ||
99
+ (u8[0] === 0x4d && u8[1] === 0x4d && u8[2] === 0x2a)
100
+ ) {
101
+ return 'image/tiff';
102
+ }
103
+
104
+ // ISOBMFF (AVIF / HEIC / HEIF)
105
+ // "ftyp" at offset 4
106
+ if (u8[4] === 0x66 && u8[5] === 0x74 && u8[6] === 0x79 && u8[7] === 0x70) {
107
+ const type = String.fromCharCode(u8[8], u8[9], u8[10], u8[11]);
108
+ if (type === 'avif' || type === 'avis') {
109
+ return 'image/avif';
110
+ }
111
+ if (
112
+ type === 'heic' ||
113
+ type === 'heix' ||
114
+ type === 'hevc' ||
115
+ type === 'hevx'
116
+ ) {
117
+ return 'image/heic';
118
+ }
119
+ if (type === 'mif1' || type === 'msf1') {
120
+ return 'image/heif';
121
+ }
122
+ }
123
+
124
+ // JPEG XL: FF 0A or container bits
125
+ if (u8[0] === 0xff && u8[1] === 0x0a) {
126
+ return 'image/jxl';
127
+ }
128
+ // Container: 00 00 00 0c 4a 58 4c 20 0d 0a 87 0a (JXL )
129
+ if (u8[0] === 0x00 && u8[4] === 0x4a && u8[5] === 0x58 && u8[6] === 0x4c) {
130
+ return 'image/jxl';
131
+ }
132
+
133
+ // JPEG 2000
134
+ if (u8[0] === 0x00 && u8[4] === 0x6a && u8[5] === 0x50 && u8[6] === 0x20) {
135
+ return 'image/jp2';
136
+ }
137
+
138
+ // SVG: Check for <svg or <?xml (heuristics)
139
+ const preview = String.fromCharCode(...u8.slice(0, 100)).toLowerCase();
140
+ if (preview.includes('<svg') || preview.includes('<?xml')) {
141
+ return 'image/svg+xml';
142
+ }
143
+
144
+ return null;
145
+ }
146
+
35
147
  static async processAudio(source) {
36
148
  // Blob
37
149
  if (source instanceof Blob) {
@@ -46,8 +158,20 @@ export default class MultimodalConverter {
46
158
  }
47
159
 
48
160
  // BufferSource -> Assume it's already an audio file (mp3/wav)
49
- if (ArrayBuffer.isView(source) || source instanceof ArrayBuffer) {
50
- const buffer = source instanceof ArrayBuffer ? source : source.buffer;
161
+ const isArrayBuffer =
162
+ source instanceof ArrayBuffer ||
163
+ (source &&
164
+ source.constructor &&
165
+ source.constructor.name === 'ArrayBuffer');
166
+ const isView =
167
+ ArrayBuffer.isView(source) ||
168
+ (source &&
169
+ source.buffer &&
170
+ (source.buffer instanceof ArrayBuffer ||
171
+ source.buffer.constructor.name === 'ArrayBuffer'));
172
+
173
+ if (isArrayBuffer || isView) {
174
+ const buffer = isArrayBuffer ? source : source.buffer;
51
175
  return {
52
176
  inlineData: {
53
177
  data: this.arrayBufferToBase64(buffer),
@@ -65,14 +189,16 @@ export default class MultimodalConverter {
65
189
  return new Promise((resolve, reject) => {
66
190
  const reader = new FileReader();
67
191
  reader.onloadend = () => {
68
- if (reader.error) reject(reader.error);
69
- else
192
+ if (reader.error) {
193
+ reject(reader.error);
194
+ } else {
70
195
  resolve({
71
196
  inlineData: {
72
197
  data: reader.result.split(',')[1],
73
198
  mimeType: blob.type,
74
199
  },
75
200
  });
201
+ }
76
202
  };
77
203
  reader.readAsDataURL(blob);
78
204
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "prompt-api-polyfill",
3
- "version": "0.1.0",
4
- "description": "Polyfill for the Prompt API (`LanguageModel`) backed by Firebase AI Logic.",
3
+ "version": "0.2.0",
4
+ "description": "Polyfill for the Prompt API (`LanguageModel`) backed by Firebase AI Logic, Gemini API, or OpenAI API.",
5
5
  "type": "module",
6
6
  "main": "./prompt-api-polyfill.js",
7
7
  "module": "./prompt-api-polyfill.js",
@@ -22,6 +22,8 @@
22
22
  "language-model",
23
23
  "polyfill",
24
24
  "firebase",
25
+ "gemini",
26
+ "openai",
25
27
  "web-ai"
26
28
  ],
27
29
  "repository": {
@@ -35,9 +37,22 @@
35
37
  "homepage": "https://github.com/GoogleChromeLabs/web-ai-demos/tree/main/prompt-api-polyfill/README.md",
36
38
  "license": "Apache-2.0",
37
39
  "scripts": {
38
- "start": "npx http-server"
40
+ "start": "npx http-server",
41
+ "test:browser": "node scripts/list-backends.js && vitest run -c vitest.browser.config.js .browser.test.js",
42
+ "fix": "npx prettier --write ."
39
43
  },
40
44
  "devDependencies": {
41
- "http-server": "^14.1.1"
45
+ "@firebase/ai": "^2.6.1",
46
+ "@google/generative-ai": "^0.24.1",
47
+ "@vitest/browser": "^4.0.17",
48
+ "@vitest/browser-playwright": "^4.0.17",
49
+ "ajv": "^8.17.1",
50
+ "firebase": "^12.7.0",
51
+ "http-server": "^14.1.1",
52
+ "jsdom": "^27.4.0",
53
+ "openai": "^6.16.0",
54
+ "playwright": "^1.57.0",
55
+ "prettier-plugin-curly": "^0.4.1",
56
+ "vitest": "^4.0.17"
42
57
  }
43
58
  }