Skip to content

Commit 8751e39

Browse files
authored
Merge 5dfd435 into 08d341f
2 parents 08d341f + 5dfd435 commit 8751e39

File tree

6 files changed

+464
-30
lines changed

6 files changed

+464
-30
lines changed

e2e/sample-apps/modular.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ import {
5858
onValue,
5959
off
6060
} from 'firebase/database';
61-
import { getGenerativeModel, getVertexAI, VertexAI } from 'firebase/vertexai';
61+
import {
62+
getGenerativeModel,
63+
getVertexAI,
64+
InferenceMode,
65+
VertexAI
66+
} from 'firebase/vertexai';
6267
import { getDataConnect, DataConnect } from 'firebase/data-connect';
6368

6469
/**
@@ -332,6 +337,17 @@ function callDataConnect(app) {
332337
console.log('[DATACONNECT] initialized');
333338
}
334339

340+
async function callVertex(app) {
341+
console.log('[VERTEX] start');
342+
const vertex = getVertexAI(app);
343+
const model = getGenerativeModel(vertex, {
344+
mode: InferenceMode.PREFER_ON_DEVICE
345+
});
346+
const result = await model.generateContent("What is Roko's Basalisk?");
347+
console.log(result.response.text());
348+
console.log('[VERTEX] initialized');
349+
}
350+
335351
/**
336352
* Run smoke tests for all products.
337353
* Comment out any products you want to ignore.
@@ -353,6 +369,7 @@ async function main() {
353369
await callVertexAI(app);
354370
callDataConnect(app);
355371
await authLogout(app);
372+
await callVertex(app);
356373
console.log('DONE');
357374
}
358375

packages/util/src/environment.ts

+6
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ export function isSafari(): boolean {
173173
);
174174
}
175175

176+
export function isChrome(): boolean {
177+
return (
178+
!isNode() && !!navigator.userAgent && navigator.userAgent.includes('Chrome')
179+
);
180+
}
181+
176182
/**
177183
* This method checks if indexedDB is supported by current browser/service worker context
178184
* @return true if indexedDB is supported by current browser/service worker context
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import sinonChai from 'sinon-chai';
20+
import chaiAsPromised from 'chai-as-promised';
21+
import { ChromeAdapter } from './chrome-adapter';
22+
import { stub } from 'sinon';
23+
import * as util from '@firebase/util';
24+
25+
use(sinonChai);
26+
use(chaiAsPromised);
27+
28+
describe('ChromeAdapter', () => {
29+
describe('isAvailable', () => {
30+
it('returns false if mode is only cloud', async () => {
31+
const adapter = new ChromeAdapter(undefined, 'only_in_cloud');
32+
expect(
33+
await adapter.isAvailable({
34+
contents: []
35+
})
36+
).to.be.false;
37+
});
38+
it('returns false if browser is not Chrome', async () => {
39+
const chromeStub = stub(util, 'isChrome').returns(false);
40+
const adapter = new ChromeAdapter(undefined, 'prefer_on_device');
41+
expect(
42+
await adapter.isAvailable({
43+
contents: []
44+
})
45+
).to.be.false;
46+
chromeStub.restore();
47+
});
48+
it('returns false if AI API is undefined', async () => {
49+
const adapter = new ChromeAdapter(undefined, 'prefer_on_device');
50+
expect(
51+
await adapter.isAvailable({
52+
contents: []
53+
})
54+
).to.be.false;
55+
});
56+
it('returns false if LanguageModel API is undefined', async () => {
57+
const adapter = new ChromeAdapter({} as AI, 'prefer_on_device');
58+
expect(
59+
await adapter.isAvailable({
60+
contents: []
61+
})
62+
).to.be.false;
63+
});
64+
it('returns false if request contents empty', async () => {
65+
const adapter = new ChromeAdapter({} as AI, 'prefer_on_device');
66+
expect(
67+
await adapter.isAvailable({
68+
contents: []
69+
})
70+
).to.be.false;
71+
});
72+
it('returns false if request content has function role', async () => {
73+
const adapter = new ChromeAdapter({} as AI, 'prefer_on_device');
74+
expect(
75+
await adapter.isAvailable({
76+
contents: [
77+
{
78+
role: 'function',
79+
parts: []
80+
}
81+
]
82+
})
83+
).to.be.false;
84+
});
85+
it('returns false if request content has multiple parts', async () => {
86+
const adapter = new ChromeAdapter({} as AI, 'prefer_on_device');
87+
expect(
88+
await adapter.isAvailable({
89+
contents: [
90+
{
91+
role: 'user',
92+
parts: [{ text: 'a' }, { text: 'b' }]
93+
}
94+
]
95+
})
96+
).to.be.false;
97+
});
98+
it('returns false if request content has non-text part', async () => {
99+
const adapter = new ChromeAdapter({} as AI, 'prefer_on_device');
100+
expect(
101+
await adapter.isAvailable({
102+
contents: [
103+
{
104+
role: 'user',
105+
parts: [{ inlineData: { mimeType: 'a', data: 'b' } }]
106+
}
107+
]
108+
})
109+
).to.be.false;
110+
});
111+
it('returns true if model is readily available', async () => {
112+
const aiProvider = {
113+
languageModel: {
114+
capabilities: () =>
115+
Promise.resolve({
116+
available: 'readily'
117+
})
118+
}
119+
} as AI;
120+
const adapter = new ChromeAdapter(aiProvider, 'prefer_on_device');
121+
expect(
122+
await adapter.isAvailable({
123+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }]
124+
})
125+
).to.be.true;
126+
});
127+
it('returns false and triggers download when model is available after download', async () => {
128+
const aiProvider = {
129+
languageModel: {
130+
capabilities: () =>
131+
Promise.resolve({
132+
available: 'after-download'
133+
}),
134+
create: () => Promise.resolve({})
135+
}
136+
} as AI;
137+
const createStub = stub(aiProvider.languageModel, 'create').resolves(
138+
{} as AILanguageModel
139+
);
140+
const onDeviceParams = {} as AILanguageModelCreateOptionsWithSystemPrompt;
141+
const adapter = new ChromeAdapter(
142+
aiProvider,
143+
'prefer_on_device',
144+
onDeviceParams
145+
);
146+
expect(
147+
await adapter.isAvailable({
148+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }]
149+
})
150+
).to.be.false;
151+
expect(createStub).to.have.been.calledOnceWith(onDeviceParams);
152+
});
153+
it('avoids redundant downloads', async () => {
154+
const aiProvider = {
155+
languageModel: {
156+
capabilities: () =>
157+
Promise.resolve({
158+
available: 'after-download'
159+
}),
160+
create: () => {}
161+
}
162+
} as AI;
163+
const downloadPromise = new Promise<AILanguageModel>(() => {
164+
/* never resolves */
165+
});
166+
const createStub = stub(aiProvider.languageModel, 'create').returns(
167+
downloadPromise
168+
);
169+
const adapter = new ChromeAdapter(aiProvider);
170+
await adapter.isAvailable({
171+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }]
172+
});
173+
await adapter.isAvailable({
174+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }]
175+
});
176+
expect(createStub).to.have.been.calledOnce;
177+
});
178+
it('clears state when download completes', async () => {
179+
const aiProvider = {
180+
languageModel: {
181+
capabilities: () =>
182+
Promise.resolve({
183+
available: 'after-download'
184+
}),
185+
create: () => {}
186+
}
187+
} as AI;
188+
let resolveDownload;
189+
const downloadPromise = new Promise<AILanguageModel>(resolveCallback => {
190+
resolveDownload = resolveCallback;
191+
});
192+
const createStub = stub(aiProvider.languageModel, 'create').returns(
193+
downloadPromise
194+
);
195+
const adapter = new ChromeAdapter(aiProvider);
196+
await adapter.isAvailable({
197+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }]
198+
});
199+
resolveDownload!();
200+
await adapter.isAvailable({
201+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }]
202+
});
203+
expect(createStub).to.have.been.calledTwice;
204+
});
205+
it('returns false when model is never available', async () => {
206+
const aiProvider = {
207+
languageModel: {
208+
capabilities: () =>
209+
Promise.resolve({
210+
available: 'no'
211+
})
212+
}
213+
} as AI;
214+
const adapter = new ChromeAdapter(aiProvider, 'prefer_on_device');
215+
expect(
216+
await adapter.isAvailable({
217+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }]
218+
})
219+
).to.be.false;
220+
});
221+
});
222+
describe('generateContentOnDevice', () => {
223+
it('Extracts and concats initial prompts', async () => {
224+
const aiProvider = {
225+
languageModel: {
226+
create: () => Promise.resolve({})
227+
}
228+
} as AI;
229+
const factoryStub = stub(aiProvider.languageModel, 'create').resolves({
230+
prompt: s => Promise.resolve(s)
231+
} as AILanguageModel);
232+
const text = ['first', 'second', 'third'];
233+
const onDeviceParams = {
234+
initialPrompts: [{ role: 'user', content: text[0] }]
235+
} as AILanguageModelCreateOptionsWithSystemPrompt;
236+
const adapter = new ChromeAdapter(
237+
aiProvider,
238+
'prefer_on_device',
239+
onDeviceParams
240+
);
241+
const response = await adapter.generateContentOnDevice({
242+
contents: [
243+
{ role: 'model', parts: [{ text: text[1] }] },
244+
{ role: 'user', parts: [{ text: text[2] }] }
245+
]
246+
});
247+
expect(factoryStub).to.have.been.calledOnceWith({
248+
initialPrompts: [
249+
{ role: 'user', content: text[0] },
250+
// Asserts tail is passed as initial prompts, and
251+
// role is normalized from model to assistant.
252+
{ role: 'assistant', content: text[1] }
253+
]
254+
});
255+
expect(await response.json()).to.deep.equal({
256+
candidates: [
257+
{
258+
content: {
259+
parts: [{ text: text[2] }]
260+
}
261+
}
262+
]
263+
});
264+
});
265+
it('Extracts system prompt', async () => {
266+
const aiProvider = {
267+
languageModel: {
268+
create: () => Promise.resolve({})
269+
}
270+
} as AI;
271+
const factoryStub = stub(aiProvider.languageModel, 'create').resolves({
272+
prompt: s => Promise.resolve(s)
273+
} as AILanguageModel);
274+
const onDeviceParams = {
275+
systemPrompt: 'be yourself'
276+
} as AILanguageModelCreateOptionsWithSystemPrompt;
277+
const adapter = new ChromeAdapter(
278+
aiProvider,
279+
'prefer_on_device',
280+
onDeviceParams
281+
);
282+
const text = 'hi';
283+
const response = await adapter.generateContentOnDevice({
284+
contents: [{ role: 'user', parts: [{ text }] }]
285+
});
286+
expect(factoryStub).to.have.been.calledOnceWith({
287+
initialPrompts: [],
288+
systemPrompt: onDeviceParams.systemPrompt
289+
});
290+
expect(await response.json()).to.deep.equal({
291+
candidates: [
292+
{
293+
content: {
294+
parts: [{ text }]
295+
}
296+
}
297+
]
298+
});
299+
});
300+
});
301+
});

0 commit comments

Comments
 (0)