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 { isChrome } from '@firebase/util' ;
1
19
import {
20
+ Content ,
2
21
EnhancedGenerateContentResponse ,
3
22
GenerateContentRequest ,
4
- InferenceMode
23
+ InferenceMode ,
24
+ Part ,
25
+ Role ,
26
+ TextPart
5
27
} from '../types' ;
6
28
import { AI , AILanguageModelCreateOptionsWithSystemPrompt } from '../types/ai' ;
7
29
@@ -10,22 +32,175 @@ import { AI, AILanguageModelCreateOptionsWithSystemPrompt } from '../types/ai';
10
32
* and encapsulates logic for detecting when on-device is possible.
11
33
*/
12
34
export class ChromeAdapter {
35
+ downloadPromise : Promise < AILanguageModel > | undefined ;
36
+ oldSession : AILanguageModel | undefined ;
13
37
constructor (
14
38
private aiProvider ?: AI ,
15
39
private mode ?: InferenceMode ,
16
40
private onDeviceParams ?: AILanguageModelCreateOptionsWithSystemPrompt
17
41
) { }
18
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
42
+ /**
43
+ * Convenience method to check if a given request can be made on-device.
44
+ * Encapsulates a few concerns: 1) the mode, 2) API existence, 3) prompt formatting, and
45
+ * 4) model availability, including triggering download if necessary.
46
+ * Pros: caller needn't be concerned with details of on-device availability. Cons: this method
47
+ * spans a few concerns and splits request validation from usage. If instance variables weren't
48
+ * already part of the API, we could consider a better separation of concerns.
49
+ */
19
50
async isAvailable ( request : GenerateContentRequest ) : Promise < boolean > {
20
- return false ;
51
+ // Returns false if we should only use in-cloud inference.
52
+ if ( this . mode === InferenceMode . ONLY_ON_CLOUD ) {
53
+ return false ;
54
+ }
55
+ // Returns false if the on-device inference API is undefined.
56
+ const isLanguageModelAvailable =
57
+ isChrome ( ) && this . aiProvider && this . aiProvider . languageModel ;
58
+ if ( ! isLanguageModelAvailable ) {
59
+ return false ;
60
+ }
61
+ // Returns false if the request can't be run on-device.
62
+ if ( ! ChromeAdapter . _isOnDeviceRequest ( request ) ) {
63
+ return false ;
64
+ }
65
+ switch ( await this . availability ( ) ) {
66
+ case 'readily' :
67
+ // Returns true only if a model is immediately available.
68
+ return true ;
69
+ case 'after-download' :
70
+ // Triggers async model download.
71
+ this . download ( ) ;
72
+ case 'no' :
73
+ default :
74
+ return false ;
75
+ }
21
76
}
22
77
async generateContentOnDevice (
23
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
24
78
request : GenerateContentRequest
25
79
) : Promise < EnhancedGenerateContentResponse > {
80
+ const initialPrompts = ChromeAdapter . toInitialPrompts ( request . contents ) ;
81
+ // Assumes validation asserted there is at least one initial prompt.
82
+ const prompt = initialPrompts . pop ( ) ! ;
83
+ const systemPrompt = ChromeAdapter . toSystemPrompt (
84
+ request . systemInstruction
85
+ ) ;
86
+ const session = await this . session ( {
87
+ initialPrompts,
88
+ systemPrompt
89
+ } ) ;
90
+ const result = await session . prompt ( prompt . content ) ;
26
91
return {
27
- text : ( ) => '' ,
92
+ text : ( ) => result ,
28
93
functionCalls : ( ) => undefined
29
94
} ;
30
95
}
96
+ // Visible for testing
97
+ static _isOnDeviceRequest ( request : GenerateContentRequest ) : boolean {
98
+ if ( request . systemInstruction ) {
99
+ const systemContent = request . systemInstruction as Content ;
100
+ // Returns false if the role can't be represented on-device.
101
+ if ( systemContent . role && systemContent . role === 'function' ) {
102
+ return false ;
103
+ }
104
+
105
+ // Returns false if the system prompt is multi-part.
106
+ if ( systemContent . parts && systemContent . parts . length > 1 ) {
107
+ return false ;
108
+ }
109
+
110
+ // Returns false if the system prompt isn't text.
111
+ const systemText = request . systemInstruction as TextPart ;
112
+ if ( ! systemText . text ) {
113
+ return false ;
114
+ }
115
+ }
116
+
117
+ // Returns false if the prompt is empty.
118
+ if ( request . contents . length === 0 ) {
119
+ return false ;
120
+ }
121
+
122
+ // Applies the same checks as above, but for each content item.
123
+ for ( const content of request . contents ) {
124
+ if ( content . role === 'function' ) {
125
+ return false ;
126
+ }
127
+
128
+ if ( content . parts . length > 1 ) {
129
+ return false ;
130
+ }
131
+
132
+ if ( ! content . parts [ 0 ] . text ) {
133
+ return false ;
134
+ }
135
+ }
136
+
137
+ return true ;
138
+ }
139
+ private async availability ( ) : Promise < AICapabilityAvailability | undefined > {
140
+ return this . aiProvider ?. languageModel
141
+ . capabilities ( )
142
+ . then ( ( c : AILanguageModelCapabilities ) => c . available ) ;
143
+ }
144
+ private download ( ) : void {
145
+ if ( this . downloadPromise ) {
146
+ return ;
147
+ }
148
+ this . downloadPromise = this . aiProvider ?. languageModel
149
+ . create ( this . onDeviceParams )
150
+ . then ( ( model : AILanguageModel ) => {
151
+ delete this . downloadPromise ;
152
+ return model ;
153
+ } ) ;
154
+ return ;
155
+ }
156
+ private static toSystemPrompt (
157
+ prompt : string | Content | Part | undefined
158
+ ) : string | undefined {
159
+ if ( ! prompt ) {
160
+ return undefined ;
161
+ }
162
+
163
+ if ( typeof prompt === 'string' ) {
164
+ return prompt ;
165
+ }
166
+
167
+ const systemContent = prompt as Content ;
168
+ if (
169
+ systemContent . parts &&
170
+ systemContent . parts [ 0 ] &&
171
+ systemContent . parts [ 0 ] . text
172
+ ) {
173
+ return systemContent . parts [ 0 ] . text ;
174
+ }
175
+
176
+ const systemPart = prompt as Part ;
177
+ if ( systemPart . text ) {
178
+ return systemPart . text ;
179
+ }
180
+
181
+ return undefined ;
182
+ }
183
+ private static toOnDeviceRole ( role : Role ) : AILanguageModelPromptRole {
184
+ return role === 'model' ? 'assistant' : 'user' ;
185
+ }
186
+ private static toInitialPrompts (
187
+ contents : Content [ ]
188
+ ) : AILanguageModelPrompt [ ] {
189
+ return contents . map ( c => ( {
190
+ role : ChromeAdapter . toOnDeviceRole ( c . role ) ,
191
+ // Assumes contents have been verified to contain only a single TextPart.
192
+ content : c . parts [ 0 ] . text !
193
+ } ) ) ;
194
+ }
195
+ private async session (
196
+ opts : AILanguageModelCreateOptionsWithSystemPrompt
197
+ ) : Promise < AILanguageModel > {
198
+ const newSession = await this . aiProvider ! . languageModel . create ( opts ) ;
199
+ if ( this . oldSession ) {
200
+ this . oldSession . destroy ( ) ;
201
+ }
202
+ // Holds session reference, so model isn't unloaded from memory.
203
+ this . oldSession = newSession ;
204
+ return newSession ;
205
+ }
31
206
}
0 commit comments