Skip to content

Commit a67c757

Browse files
authored
[AI] Add GenerationOptionsRepresentable for hybrid generation opts (#16105)
1 parent ae3c030 commit a67c757

8 files changed

Lines changed: 400 additions & 45 deletions

File tree

FirebaseAI/Sources/GenerativeModelSession.swift

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@
8282
/// - Throws: A `GenerationError` if the model fails to generate a response.
8383
@discardableResult
8484
public nonisolated(nonsending)
85-
func respond(to prompt: PartsRepresentable..., options: GenerationConfig? = nil) async throws
85+
func respond(to prompt: PartsRepresentable...,
86+
options: any GenerationOptionsRepresentable
87+
= ResponseGenerationOptions.default) async throws
8688
-> GenerativeModelSession.Response<String> {
8789
return try await respond(
8890
to: prompt,
@@ -112,9 +114,11 @@
112114
@available(watchOS, unavailable)
113115
@discardableResult
114116
nonisolated(nonsending)
115-
func respond(to prompt: PartsRepresentable..., schema: FoundationModels.GenerationSchema,
117+
func respond(to prompt: PartsRepresentable...,
118+
schema: FoundationModels.GenerationSchema,
116119
includeSchemaInPrompt: Bool = true,
117-
options: GenerationConfig? = nil) async throws
120+
options: any GenerationOptionsRepresentable =
121+
ResponseGenerationOptions.default) async throws
118122
-> GenerativeModelSession.Response<FirebaseAI.GeneratedContent> {
119123
// TODO: Replace `FoundationModels.GenerationSchema` and make this method public when
120124
// `FirebaseAI.GenerationSchema`'s public API is ready.
@@ -149,7 +153,8 @@
149153
func respond<Content>(to prompt: PartsRepresentable...,
150154
generating type: Content.Type = Content.self,
151155
includeSchemaInPrompt: Bool = true,
152-
options: GenerationConfig? = nil) async throws
156+
options: any GenerationOptionsRepresentable
157+
= ResponseGenerationOptions.default) async throws
153158
-> GenerativeModelSession.Response<Content> where Content: Generable {
154159
return try await respond(
155160
to: prompt,
@@ -175,7 +180,9 @@
175180
@available(watchOS, unavailable)
176181
func streamResponse(to prompt: PartsRepresentable...,
177182
schema: FoundationModels.GenerationSchema,
178-
includeSchemaInPrompt: Bool = true, options: GenerationConfig? = nil)
183+
includeSchemaInPrompt: Bool = true,
184+
options: any GenerationOptionsRepresentable
185+
= ResponseGenerationOptions.default)
179186
-> sending GenerativeModelSession.ResponseStream<
180187
FirebaseAI.GeneratedContent, FirebaseAI.GeneratedContent
181188
> {
@@ -206,9 +213,11 @@
206213
public func streamResponse<Content>(to prompt: PartsRepresentable...,
207214
generating type: Content.Type = Content.self,
208215
includeSchemaInPrompt: Bool = true,
209-
options: GenerationConfig? = nil)
210-
-> sending GenerativeModelSession.ResponseStream<Content, Content.PartiallyGenerated>
211-
where Content: Generable {
216+
options: any GenerationOptionsRepresentable =
217+
ResponseGenerationOptions.default)
218+
-> sending GenerativeModelSession.ResponseStream<
219+
Content, Content.PartiallyGenerated
220+
> where Content: Generable {
212221
return streamResponse(
213222
to: prompt,
214223
schema: FirebaseAI.GenerationSchema(type.generationSchema),
@@ -227,7 +236,9 @@
227236
/// generation configuration.
228237
/// - Returns: A `ResponseStream` that yields snapshots of the generated content.
229238
@available(macOS 12.0, watchOS 8.0, *)
230-
public func streamResponse(to prompt: PartsRepresentable..., options: GenerationConfig? = nil)
239+
public func streamResponse(to prompt: PartsRepresentable...,
240+
options: any GenerationOptionsRepresentable
241+
= ResponseGenerationOptions.default)
231242
-> sending GenerativeModelSession.ResponseStream<String, String> {
232243
return streamResponse(
233244
to: prompt,
@@ -243,11 +254,11 @@
243254
private nonisolated(nonsending)
244255
func respond<Content>(to prompt: [PartsRepresentable], schema: FirebaseAI.GenerationSchema?,
245256
generating type: Content.Type, includeSchemaInPrompt: Bool,
246-
options: GenerationConfig?) async throws
257+
options: any GenerationOptionsRepresentable) async throws
247258
-> GenerativeModelSession.Response<Content> {
248259
let parts = [ModelContent(parts: prompt)]
249260
let config = try buildConfig(
250-
options: options,
261+
options: options.responseGenerationOptions.geminiGenerationConfig,
251262
schema: schema,
252263
includeSchemaInPrompt: includeSchemaInPrompt
253264
)
@@ -317,13 +328,16 @@
317328
schema: FirebaseAI.GenerationSchema?,
318329
generating type: Content.Type,
319330
includeSchemaInPrompt: Bool,
320-
options: GenerationConfig?)
321-
-> sending GenerativeModelSession.ResponseStream<Content, PartialContent> {
331+
options: any GenerationOptionsRepresentable)
332+
-> sending GenerativeModelSession.ResponseStream<
333+
Content,
334+
PartialContent
335+
> {
322336
let initialParts = [ModelContent(parts: prompt)]
323337
return GenerativeModelSession.ResponseStream { context in
324338
do {
325339
let config = try self.buildConfig(
326-
options: options,
340+
options: options.responseGenerationOptions.geminiGenerationConfig,
327341
schema: schema,
328342
includeSchemaInPrompt: includeSchemaInPrompt
329343
)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if compiler(>=6.2.3)
16+
#if canImport(FoundationModels)
17+
import FoundationModels
18+
#endif // canImport(FoundationModels)
19+
20+
/// A type that represents options for controlling model response generation.
21+
///
22+
/// For Gemini models, this may be a ``GenerationConfig`` value. For the `SystemLanguageModel`
23+
/// provided by the Apple Foundation Models framework, this may be a
24+
/// ``FirebaseAI/GenerationOptions`` or a `Foundation Models`
25+
/// [`GenerationOptions`](https://developer.apple.com/documentation/foundationmodels/generationoptions)
26+
/// value. For hybrid (on-device and cloud) configurations, use
27+
/// ``hybrid(gemini:foundationModels:)`` to specify options for each model.
28+
public protocol GenerationOptionsRepresentable: Sendable {
29+
/// Options for controlling model response generation.
30+
var responseGenerationOptions: ResponseGenerationOptions { get }
31+
}
32+
33+
extension GenerationConfig: GenerationOptionsRepresentable {
34+
public var responseGenerationOptions: ResponseGenerationOptions {
35+
return ResponseGenerationOptions(geminiGenerationConfig: self)
36+
}
37+
}
38+
39+
extension FirebaseAI.GenerationOptions: GenerationOptionsRepresentable {
40+
public var responseGenerationOptions: ResponseGenerationOptions {
41+
return ResponseGenerationOptions(foundationModelsGenerationOptions: self)
42+
}
43+
}
44+
45+
#if canImport(FoundationModels)
46+
@available(iOS 26.0, macOS 26.0, *)
47+
@available(tvOS, unavailable)
48+
@available(watchOS, unavailable)
49+
extension FoundationModels.GenerationOptions: GenerationOptionsRepresentable {
50+
public var responseGenerationOptions: ResponseGenerationOptions {
51+
return ResponseGenerationOptions(
52+
foundationModelsGenerationOptions: FirebaseAI.GenerationOptions(self)
53+
)
54+
}
55+
}
56+
#endif // canImport(FoundationModels)
57+
58+
public extension GenerationOptionsRepresentable where Self == ResponseGenerationOptions {
59+
/// The default response generation options for a model.
60+
static var `default`: ResponseGenerationOptions { return ResponseGenerationOptions() }
61+
62+
/// Returns response generation options for Gemini requests.
63+
///
64+
/// - Parameter generationConfig: Generation options for Gemini models.
65+
static func gemini(_ generationConfig: GenerationConfig) -> ResponseGenerationOptions {
66+
return generationConfig.responseGenerationOptions
67+
}
68+
69+
/// Returns response generation options for on-device requests.
70+
///
71+
/// - Parameter generationOptions: Generation options for the on-device `SystemLanguageModel`
72+
/// provided by the Foundation Models framework.
73+
static func foundationModels(_ generationOptions: FirebaseAI.GenerationOptions)
74+
-> ResponseGenerationOptions {
75+
return generationOptions.responseGenerationOptions
76+
}
77+
78+
#if canImport(FoundationModels)
79+
/// Returns response generation options for on-device requests.
80+
///
81+
/// - Parameter generationOptions: Generation options for the on-device `SystemLanguageModel`
82+
/// provided by the Foundation Models framework.
83+
@available(iOS 26.0, macOS 26.0, *)
84+
@available(tvOS, unavailable)
85+
@available(watchOS, unavailable)
86+
static func foundationModels(_ generationOptions: FoundationModels.GenerationOptions)
87+
-> ResponseGenerationOptions {
88+
return generationOptions.responseGenerationOptions
89+
}
90+
91+
/// Returns response generation options for hybrid (on-device and cloud) requests.
92+
///
93+
/// - Parameters:
94+
/// - gemini: Generation options for Gemini models.
95+
/// - foundationModels: Generation options for the on-device `SystemLanguageModel` provided
96+
/// by the Foundation Models framework.
97+
@available(iOS 26.0, macOS 26.0, *)
98+
@available(tvOS, unavailable)
99+
@available(watchOS, unavailable)
100+
static func hybrid(gemini: GenerationConfig,
101+
foundationModels: FoundationModels.GenerationOptions)
102+
-> ResponseGenerationOptions {
103+
return ResponseGenerationOptions(
104+
geminiGenerationConfig: gemini,
105+
foundationModelsGenerationOptions: FirebaseAI.GenerationOptions(foundationModels)
106+
)
107+
}
108+
#endif // canImport(FoundationModels)
109+
110+
/// Returns response generation options for hybrid (on-device and cloud) requests.
111+
///
112+
/// - Parameters:
113+
/// - gemini: Generation options for Gemini models.
114+
/// - foundationModels: Generation options for the on-device `SystemLanguageModel` provided by
115+
/// the Foundation Models framework.
116+
static func hybrid(gemini: GenerationConfig,
117+
foundationModels: FirebaseAI.GenerationOptions)
118+
-> ResponseGenerationOptions {
119+
return ResponseGenerationOptions(
120+
geminiGenerationConfig: gemini,
121+
foundationModelsGenerationOptions: foundationModels
122+
)
123+
}
124+
}
125+
126+
#endif // compiler(>=6.2.3)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if compiler(>=6.2.3)
16+
/// Options for controlling model response generation.
17+
///
18+
/// See ``GenerationOptionsRepresentable`` for details on how to configure generation options.
19+
public struct ResponseGenerationOptions: Sendable, Equatable {
20+
let geminiGenerationConfig: GenerationConfig?
21+
let foundationModelsGenerationOptions: FirebaseAI.GenerationOptions?
22+
23+
init(geminiGenerationConfig: GenerationConfig? = nil,
24+
foundationModelsGenerationOptions: FirebaseAI.GenerationOptions? = nil) {
25+
self.geminiGenerationConfig = geminiGenerationConfig
26+
self.foundationModelsGenerationOptions = foundationModelsGenerationOptions
27+
}
28+
}
29+
30+
extension ResponseGenerationOptions: GenerationOptionsRepresentable {
31+
public var responseGenerationOptions: ResponseGenerationOptions {
32+
return self
33+
}
34+
}
35+
#endif // compiler(>=6.2.3)

FirebaseAI/Tests/TestApp/Tests/Integration/GenerativeModelSessionTests.swift

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424

2525
@Suite(.serialized)
2626
struct GenerativeModelSessionTests {
27+
let generationConfig = GenerationConfig(temperature: 0.0, topP: 0.0, topK: 1)
28+
2729
@Test(arguments: [InstanceConfig.vertexAI_v1beta_global, InstanceConfig.googleAI_v1beta])
2830
func respondText(_ config: InstanceConfig) async throws {
2931
let firebaseAI = FirebaseAI.componentInstance(config)
3032
let session = firebaseAI.generativeModelSession(model: ModelNames.gemini2_5_FlashLite)
3133
let prompt = "Why is the sky blue?"
3234

33-
let response = try await session.respond(to: prompt)
35+
let response = try await session.respond(to: prompt, options: .default)
3436

3537
let content = response.content
3638
#expect(!content.isEmpty)
@@ -69,7 +71,11 @@
6971
let session = firebaseAI.generativeModelSession(model: ModelNames.gemini2_5_FlashLite)
7072
let prompt = "Generate a cute rescue cat"
7173

72-
let response = try await session.respond(to: prompt, schema: CatProfile.generationSchema)
74+
let response = try await session.respond(
75+
to: prompt,
76+
schema: CatProfile.generationSchema,
77+
options: .gemini(generationConfig)
78+
)
7379

7480
let content = response.content
7581
#expect(content.isComplete)
@@ -180,7 +186,11 @@
180186
let session = firebaseAI.generativeModelSession(model: ModelNames.gemini2_5_FlashLite)
181187
let prompt = "Generate a recipe for a pasta dish with meat."
182188

183-
let response = try await session.respond(to: prompt, generating: Recipe.self)
189+
let response = try await session.respond(
190+
to: prompt,
191+
generating: Recipe.self,
192+
options: generationConfig
193+
)
184194

185195
let recipe = response.content
186196
#expect(!recipe.name.isEmpty)
@@ -205,7 +215,11 @@
205215
let prompt =
206216
"Generate three recipes for a full-course vegetarian meal (appetizer, main, dessert)."
207217

208-
let response = try await session.respond(to: prompt, generating: RecipeList.self)
218+
let response = try await session.respond(
219+
to: prompt,
220+
generating: RecipeList.self,
221+
options: .gemini(generationConfig)
222+
)
209223

210224
let recipeList = response.content
211225
#expect(recipeList.recipes.count == 3)
@@ -278,7 +292,7 @@
278292
)
279293
let prompt = "What is the current temperature in Waterloo, Ontario, Canada?"
280294

281-
let response = try await session.respond(to: prompt)
295+
let response = try await session.respond(to: prompt, options: generationConfig)
282296

283297
let content = response.content
284298
#expect(!content.isEmpty)
@@ -312,7 +326,8 @@
312326

313327
let response = try await session.respond(
314328
to: prompt,
315-
generating: GetTemperature.Temperature.self
329+
generating: GetTemperature.Temperature.self,
330+
options: .gemini(generationConfig)
316331
)
317332

318333
let content = response.content
@@ -332,7 +347,7 @@
332347
let url = "https://blog.google/innovation-and-ai/technology/developers-tools/functiongemma/"
333348
let prompt = "What was the name of the model announced in: \(url)"
334349

335-
let response = try await session.respond(to: prompt)
350+
let response = try await session.respond(to: prompt, options: generationConfig)
336351

337352
#expect(response.content.contains("FunctionGemma"))
338353
let candidate = try #require(response.rawResponse.candidates.first)
@@ -350,7 +365,7 @@
350365
let session = firebaseAI.generativeModelSession(model: ModelNames.gemini2_5_FlashLite)
351366
let prompt = "Why is the sky blue?"
352367

353-
let stream = session.streamResponse(to: prompt)
368+
let stream = session.streamResponse(to: prompt, options: .gemini(generationConfig))
354369

355370
var generationID: FirebaseAI.GenerationID?
356371
var isComplete = false
@@ -398,7 +413,8 @@
398413

399414
let stream = session.streamResponse(
400415
to: prompt,
401-
schema: CatProfile.generationSchema
416+
schema: CatProfile.generationSchema,
417+
options: generationConfig
402418
)
403419

404420
var generationID: FirebaseAI.GenerationID?
@@ -536,7 +552,7 @@
536552
)
537553
let prompt = "What is the current temperature in Waterloo, Ontario, Canada?"
538554

539-
let stream = session.streamResponse(to: prompt)
555+
let stream = session.streamResponse(to: prompt, options: .gemini(generationConfig))
540556

541557
var generationID: FirebaseAI.GenerationID?
542558
var isComplete = false

0 commit comments

Comments
 (0)