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 b05ab9e864b..12353a1e9a2 100644 --- a/FirebaseAI/Sources/FirebaseAI.swift +++ b/FirebaseAI/Sources/FirebaseAI.swift @@ -106,14 +106,15 @@ 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, requestOptions: RequestOptions = RequestOptions()) -> GeminiLanguageModel { return GeminiLanguageModel( - modelName: name, modelResourceName: modelResourceName(modelName: name), firebaseInfo: firebaseInfo, apiConfig: apiConfig, @@ -123,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 7898c5bd0aa..2302207b46d 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModel.swift @@ -12,46 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseCore -import Foundation -import FoundationModels +#if compiler(>=6.4) && canImport(FoundationModels) + import FirebaseCore + import Foundation + import 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 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,23 +60,22 @@ 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 + ) } } @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: [ @@ -107,6 +91,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]? @@ -133,4 +119,4 @@ import FoundationModels 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 b312b50f9fb..2f60df3ff29 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiLanguageModelExecutor.swift @@ -12,11 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import CoreGraphics -import Foundation -import FoundationModels - -#if canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM && compiler(>=6.4) +#if compiler(>=6.4) && canImport(FoundationModels) + import CoreGraphics + import Foundation + import FoundationModels import ImageIO @available(iOS 27.0, macOS 27.0, visionOS 27.0, watchOS 27.0, *) @@ -36,15 +35,14 @@ 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 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 @@ -535,6 +537,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 = "" @@ -582,6 +585,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]() @@ -635,6 +639,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]() @@ -679,4 +684,4 @@ import FoundationModels return ModelContent(role: "model", parts: parts) } } -#endif // canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM && compiler(>=6.4) +#endif // compiler(>=6.4) && canImport(FoundationModels) diff --git a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift index 37822a14bca..71484df505b 100644 --- a/FirebaseAI/Tests/Unit/ResponseStreamTests.swift +++ b/FirebaseAI/Tests/Unit/ResponseStreamTests.swift @@ -104,21 +104,22 @@ } } 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 { - 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. + } + #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 // !os(watchOS) + #endif // compiler(>=6.4) XCTFail("Expected a decoding failure error, but got \(error) instead.") }