From 8da3f49df51b737374262d0682200a6650e2d601 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 15 Jun 2026 15:48:43 -0400 Subject: [PATCH 1/4] refactor(ai): Use `GenerativeAIService` in `GeminiLanguageModel` --- FirebaseAI/Sources/FirebaseAI.swift | 1 - .../Types/Public/GeminiLanguageModel.swift | 35 +++++-------------- .../Public/GeminiLanguageModelExecutor.swift | 16 +++++---- .../Tests/Unit/ResponseStreamTests.swift | 4 +-- 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/FirebaseAI/Sources/FirebaseAI.swift b/FirebaseAI/Sources/FirebaseAI.swift index b05ab9e864b..423e507009b 100644 --- a/FirebaseAI/Sources/FirebaseAI.swift +++ b/FirebaseAI/Sources/FirebaseAI.swift @@ -113,7 +113,6 @@ public final class FirebaseAI: Sendable { requestOptions: RequestOptions = RequestOptions()) -> GeminiLanguageModel { return GeminiLanguageModel( - modelName: name, modelResourceName: modelResourceName(modelName: name), firebaseInfo: firebaseInfo, apiConfig: apiConfig, diff --git a/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift b/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift index 7898c5bd0aa..ea7651a75ac 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift @@ -19,39 +19,22 @@ import FoundationModels #if compiler(>=6.4) public struct GeminiLanguageModel { public struct ModelConfig: Sendable, Hashable { - let firebaseAppName: String let apiConfig: APIConfig - let useLimitedUseAppCheckTokens: Bool - - let modelName: String + let modelResourceName: String let safetySettings: [SafetySetting]? let serverTools: [InternalGeminiTool] let geminiOptions: GeminiGenerationOptions? let requestOptions: RequestOptions - - var firebaseAI: FirebaseAI { - let firebaseApp = FirebaseApp.app(name: firebaseAppName) - return FirebaseAI.createInstance( - app: firebaseApp, - apiConfig: apiConfig, - useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens - ) - } } let modelConfig: ModelConfig - let modelResourceName: String - let firebaseInfo: FirebaseInfo - let toolConfig: ToolConfig? - let urlSession: URLSession + let generativeAIService: GenerativeAIService - init(modelName: String, - modelResourceName: String, + init(modelResourceName: String, firebaseInfo: FirebaseInfo, apiConfig: APIConfig, safetySettings: [SafetySetting]? = nil, serverTools: [any GeminiTool]? = nil, - toolConfig: ToolConfig? = nil, geminiOptions: GeminiGenerationOptions? = nil, requestOptions: RequestOptions = RequestOptions(), urlSession: URLSession = GenAIURLSession.default) { @@ -75,19 +58,17 @@ import FoundationModels } modelConfig = ModelConfig( - firebaseAppName: firebaseInfo.app.name, apiConfig: apiConfig, - useLimitedUseAppCheckTokens: firebaseInfo.useLimitedUseAppCheckTokens, - modelName: modelName, + modelResourceName: modelResourceName, safetySettings: safetySettings, serverTools: serverTools, geminiOptions: geminiOptions, requestOptions: requestOptions ) - self.modelResourceName = modelResourceName - self.firebaseInfo = firebaseInfo - self.toolConfig = toolConfig - self.urlSession = urlSession + generativeAIService = GenerativeAIService( + firebaseInfo: firebaseInfo, + urlSession: urlSession + ) } } diff --git a/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift b/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift index b312b50f9fb..91498d5f404 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift @@ -38,13 +38,11 @@ import FoundationModels @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) public extension GeminiLanguageModel { struct Executor: LanguageModelExecutor { - let firebaseAI: FirebaseAI let configuration: GeminiLanguageModel.ModelConfig static let placeholderIDPrefix = "placeholder-id-" public init(configuration: GeminiLanguageModel.ModelConfig) throws { - firebaseAI = configuration.firebaseAI self.configuration = configuration } @@ -283,21 +281,25 @@ import FoundationModels } } - let generativeModel = firebaseAI.generativeModel( - modelName: configuration.modelName, + let generateContentRequest = GenerateContentRequest( + model: configuration.modelResourceName, + contents: parts, generationConfig: generationConfig, + safetySettings: configuration.safetySettings, tools: tools, toolConfig: toolConfig, systemInstruction: systemInstruction, - requestOptions: configuration.requestOptions + apiConfig: configuration.apiConfig, + apiMethod: .streamGenerateContent, + options: configuration.requestOptions ) // 2. Open the stream to your provider. The transport is your choice — // URLSession.bytes, a vendored SDK, gRPC, WebSocket, anything that // yields an async sequence of provider events. - let stream = try TaskLocals.$isFoundationModelsRequest.withValue(true) { - try generativeModel.generateContentStream(parts) + let stream = TaskLocals.$isFoundationModelsRequest.withValue(true) { + model.generativeAIService.loadRequestStream(request: generateContentRequest) } // 3. For each provider event, translate it into one or more channel diff --git a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift index 37822a14bca..b15905012b5 100644 --- a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift +++ b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift @@ -104,8 +104,8 @@ } } errorHandler: { error in // Assert that the error is one of the expected decoding failure types. - if let genError = error as? GenerativeModelSession.GenerationError, - case .decodingFailure = genError { + if #available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *), + error is FoundationModels.GeneratedContent.ParsingError { return // Expected error. } #if !os(watchOS) From 9d3189bbd7efc456ccd7eac169aed1e114287d37 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 16 Jun 2026 15:49:26 -0400 Subject: [PATCH 2/4] Update availability checks --- .../LanguageModelSession+ModelSession.swift | 10 ++++------ FirebaseAI/Sources/FirebaseAI.swift | 6 ++++-- .../Internal/ConvertibleToGeneratedContent.swift | 3 +-- .../Sources/Types/Public/GeminiLanguageModel.swift | 9 ++++++++- .../Types/Public/GeminiLanguageModelExecutor.swift | 14 ++++++++++---- FirebaseAI/Tests/Unit/ResponseStreamTests.swift | 14 ++++++++------ 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/FirebaseAI/Sources/Extensions/Internal/LanguageModelSession+ModelSession.swift b/FirebaseAI/Sources/Extensions/Internal/LanguageModelSession+ModelSession.swift index 9284db21ae7..7c3c61084c3 100644 --- a/FirebaseAI/Sources/Extensions/Internal/LanguageModelSession+ModelSession.swift +++ b/FirebaseAI/Sources/Extensions/Internal/LanguageModelSession+ModelSession.swift @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if compiler(>=6.2.3) && canImport(FoundationModels) +#if canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM import Foundation import FoundationModels - @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, watchOS 27.0, *) @available(tvOS, unavailable) - @available(watchOS, unavailable) extension FoundationModels.LanguageModelSession: _ModelSession { /// Returns `true` if the session has history (i.e., it has already had one or more chat turns). /// @@ -179,9 +178,8 @@ } } - @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, watchOS 27.0, *) @available(tvOS, unavailable) - @available(watchOS, unavailable) private extension GenerationOptionsRepresentable { var generationOptions: FoundationModels.GenerationOptions { guard let options = responseGenerationOptions.foundationModelsGenerationOptions else { @@ -191,4 +189,4 @@ return options.toFoundationModels() } } -#endif // compiler(>=6.2.3) && canImport(FoundationModels) +#endif // canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM diff --git a/FirebaseAI/Sources/FirebaseAI.swift b/FirebaseAI/Sources/FirebaseAI.swift index 423e507009b..12353a1e9a2 100644 --- a/FirebaseAI/Sources/FirebaseAI.swift +++ b/FirebaseAI/Sources/FirebaseAI.swift @@ -106,7 +106,9 @@ public final class FirebaseAI: Sendable { ) } - #if compiler(>=6.4) + #if compiler(>=6.4) && canImport(FoundationModels) + @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) public func geminiLanguageModel(name: String, safetySettings: [SafetySetting]? = nil, options: GeminiGenerationOptions? = nil, serverTools: [any GeminiTool]? = nil, @@ -122,7 +124,7 @@ public final class FirebaseAI: Sendable { requestOptions: requestOptions, ) } - #endif // compiler(>=6.4) + #endif // compiler(>=6.4) && canImport(FoundationModels) // TODO: Remove the `#if compiler(>=6.2.3)` when Xcode 26.2 is the minimum supported version. #if compiler(>=6.2.3) diff --git a/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift b/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift index 9059b6b9fb1..97280e26b8f 100644 --- a/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift +++ b/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift @@ -30,9 +30,8 @@ } #if canImport(FoundationModels) - @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, watchOS 27.0, *) @available(tvOS, unavailable) - @available(watchOS, unavailable) extension FoundationModels.ConvertibleToGeneratedContent { var firebaseGeneratedContent: FirebaseAI.GeneratedContent { return FirebaseAI.GeneratedContent( diff --git a/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift b/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift index ea7651a75ac..9d73a322eb2 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift @@ -14,9 +14,13 @@ import FirebaseCore import Foundation -import FoundationModels +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) #if compiler(>=6.4) + @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) public struct GeminiLanguageModel { public struct ModelConfig: Sendable, Hashable { let apiConfig: APIConfig @@ -73,6 +77,7 @@ import FoundationModels } @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) extension GeminiLanguageModel: FoundationModels.LanguageModel { public var capabilities: LanguageModelCapabilities { return LanguageModelCapabilities(capabilities: [ @@ -88,6 +93,8 @@ import FoundationModels } } + @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) public struct GeminiGenerationOptions: Sendable, Equatable, Hashable { /// Supported modalities of the response. public var responseModalities: [ResponseModality]? diff --git a/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift b/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift index 91498d5f404..844694136e1 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift @@ -14,10 +14,12 @@ import CoreGraphics import Foundation -import FoundationModels +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) +import ImageIO -#if canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM && compiler(>=6.4) - import ImageIO +#if compiler(>=6.4) @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) extension Transcript { @@ -36,6 +38,7 @@ import FoundationModels } @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) public extension GeminiLanguageModel { struct Executor: LanguageModelExecutor { let configuration: GeminiLanguageModel.ModelConfig @@ -537,6 +540,7 @@ import FoundationModels } @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) extension Transcript.Instructions { func toGeminiSystemInstruction() throws -> ModelContent? { var instructions = "" @@ -584,6 +588,7 @@ import FoundationModels } @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) extension Transcript.Prompt { func toGeminiPrompt() throws -> ModelContent { var parts = [any Part]() @@ -637,6 +642,7 @@ import FoundationModels } @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) + @available(tvOS, unavailable) extension Transcript.Response { func toGeminiResponse() throws -> ModelContent { var parts = [any Part]() @@ -681,4 +687,4 @@ import FoundationModels return ModelContent(role: "model", parts: parts) } } -#endif // canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM && compiler(>=6.4) +#endif // compiler(>=6.4) diff --git a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift index b15905012b5..14723e80b10 100644 --- a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift +++ b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift @@ -104,11 +104,13 @@ } } errorHandler: { error in // Assert that the error is one of the expected decoding failure types. - if #available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *), - error is FoundationModels.GeneratedContent.ParsingError { - return // Expected error. - } - #if !os(watchOS) + #if compiler(>=6.4) + if #available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *), + error is FoundationModels.GeneratedContent.ParsingError { + return // Expected error. + } + #endif // compiler(>=6.4) + #if IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM if #available(iOS 26.0, macOS 26.0, visionOS 26.0, *), let foundationError = error as? FoundationModels.LanguageModelSession .GenerationError, @@ -118,7 +120,7 @@ return // Expected error. } - #endif // !os(watchOS) + #endif // IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM XCTFail("Expected a decoding failure error, but got \(error) instead.") } From 719bf458776be3102d906f2d437b02047609fac9 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 16 Jun 2026 20:30:29 -0400 Subject: [PATCH 3/4] Attempt to fix macOS 15 test --- FirebaseAI/Tests/Unit/ResponseStreamTests.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift index 14723e80b10..71484df505b 100644 --- a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift +++ b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift @@ -109,18 +109,17 @@ error is FoundationModels.GeneratedContent.ParsingError { return // Expected error. } - #endif // compiler(>=6.4) - #if IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM + #else if #available(iOS 26.0, macOS 26.0, visionOS 26.0, *), let foundationError = error as? FoundationModels.LanguageModelSession .GenerationError, case .decodingFailure = foundationError { - // TODO: Remove this else-if after wrapping `FoundationModels.GenerationError` errors - // into equivalent `GenerativeModelSession.GenerationError` values. - + return // Expected error. + } else if let generationError = error as? GenerativeModelSession.GenerationError, + case .decodingFailure = generationError { return // Expected error. } - #endif // IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM + #endif // compiler(>=6.4) XCTFail("Expected a decoding failure error, but got \(error) instead.") } From bc2ccd8d51c76f7edad80780ef519f7ec1a7aefd Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 16 Jun 2026 21:18:14 -0400 Subject: [PATCH 4/4] Fix more `#if`s --- .../Sources/Types/Public/GeminiLanguageModel.swift | 10 ++++------ .../Types/Public/GeminiLanguageModelExecutor.swift | 13 +++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift b/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift index 9d73a322eb2..2302207b46d 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift @@ -12,13 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseCore -import Foundation -#if canImport(FoundationModels) +#if compiler(>=6.4) && canImport(FoundationModels) + import FirebaseCore + import Foundation import FoundationModels -#endif // canImport(FoundationModels) -#if compiler(>=6.4) @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) @available(tvOS, unavailable) public struct GeminiLanguageModel { @@ -121,4 +119,4 @@ import Foundation self.includeThoughts = includeThoughts } } -#endif // compiler(>=6.4) +#endif // compiler(>=6.4) && canImport(FoundationModels) diff --git a/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift b/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift index 844694136e1..2f60df3ff29 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift @@ -12,14 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import CoreGraphics -import Foundation -#if canImport(FoundationModels) +#if compiler(>=6.4) && canImport(FoundationModels) + import CoreGraphics + import Foundation import FoundationModels -#endif // canImport(FoundationModels) -import ImageIO - -#if compiler(>=6.4) + import ImageIO @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) extension Transcript { @@ -687,4 +684,4 @@ import ImageIO return ModelContent(role: "model", parts: parts) } } -#endif // compiler(>=6.4) +#endif // compiler(>=6.4) && canImport(FoundationModels)