Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e150802
[Firebase AI] Add `Generable` scaffolding
andrewheard Nov 24, 2025
4f7b30a
Add `asOpenAPISchema()` to `GenerationSchema`
andrewheard Nov 24, 2025
97cee24
Rename `GenerationSchema` to `JSONSchema`
andrewheard Nov 24, 2025
d8d983f
Rename `GeneratedContent` to `ModelOutput`
andrewheard Nov 24, 2025
d1b07f1
Merge remote-tracking branch 'origin/main' into ah/ai-generable-macro
andrewheard Nov 24, 2025
7184df4
Add `available(iOS 15.0, macOS 12.0, ..., *)` annotations
andrewheard Nov 24, 2025
d555f0c
Add example output for the `Generable` macro
andrewheard Nov 24, 2025
c435c92
Implement `ModelOutput.value(_:forProperty:)` and add tests
andrewheard Nov 24, 2025
6144992
Add `GenerativeModel.GenerationError` and throw `decodingFailure`
andrewheard Nov 25, 2025
5541a00
Fix Xcode 16 build errors
andrewheard Nov 25, 2025
bb0e9e5
Merge remote-tracking branch 'origin/main' into ah/ai-generable-macro
andrewheard Nov 25, 2025
4cec2b3
More build fixes
andrewheard Nov 25, 2025
a1aa0b0
Add `SendableMetatype` typealias for older Xcode versions
andrewheard Nov 25, 2025
c01d338
Merge remote-tracking branch 'origin/main' into ah/ai-generable-macro
andrewheard Nov 25, 2025
f687ab1
Another Xcode 16 workaround
andrewheard Nov 25, 2025
a6e8e4e
Merge remote-tracking branch 'origin/main' into ah/ai-generable-macro
andrewheard Nov 25, 2025
a219b40
Coalesce type conversion failures
andrewheard Nov 25, 2025
5cafb19
Implement TODOs and add tests (#15535)
paulb777 Nov 26, 2025
e0febca
Decode `Float` and `Double` more leniently
andrewheard Nov 26, 2025
52b4e17
Update error messages in `ModelOutput.value(...)` methods
andrewheard Nov 26, 2025
aebc24e
Merge remote-tracking branch 'origin/main' into ah/ai-generable-macro
andrewheard Nov 26, 2025
4c113d0
Replace `ModelOutput.DecodingError` with `GenerativeModel.GenerationE…
andrewheard Nov 26, 2025
65d7860
Fix `GenerableTests` on Xcode 16.2
andrewheard Nov 27, 2025
33c2a1e
Add `available(iOS 15.0, macOS 12.0, ..., *)` to `GenerableTests`
andrewheard Nov 27, 2025
ad396d5
Try forcing Swift 6 mode on macOS 14 / Xcode 16.2
andrewheard Nov 27, 2025
46ab2c7
Merge remote-tracking branch 'origin/main' into ah/ai-generable-macro
andrewheard Nov 27, 2025
9922957
Rewrite `GenerableTests` from Swift Testing to XCTest
andrewheard Nov 27, 2025
bfb5bf4
Add `available(iOS 15.0, macOS 12.0, ..., *)` annotation
andrewheard Nov 27, 2025
7f8bd29
Add converters between FirebaseAI and AFM types
andrewheard Dec 10, 2025
f50ccbc
Merge branch 'main' into ah/ai-generable
andrewheard Dec 10, 2025
839bbca
Add `FoundationModels` import in `GenerativeModel`
andrewheard Dec 10, 2025
06e19aa
Fix build on Xcode 15
andrewheard Dec 10, 2025
76c8ee2
Merge branch 'main' into ah/ai-generable
andrewheard Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/firebaseai.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ jobs:
with:
product: ${{ matrix.product }}
supports_swift6: true
setup_command: scripts/update_vertexai_responses.sh
setup_command: |
scripts/update_vertexai_responses.sh; \
sed -i "" "s/s.swift_version[[:space:]]*=[[:space:]]*'5.9'/s.swift_version = '6.0'/" ${{ matrix.product }}.podspec

quickstart:
if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request'
Expand Down
10 changes: 5 additions & 5 deletions FirebaseAI/Sources/GenerationConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct GenerationConfig: Sendable {
let topK: Int?

/// The number of response variations to return.
let candidateCount: Int?
var candidateCount: Int?

/// Maximum number of tokens that can be generated in the response.
let maxOutputTokens: Int?
Expand All @@ -43,18 +43,18 @@ public struct GenerationConfig: Sendable {
let stopSequences: [String]?

/// Output response MIME type of the generated candidate text.
let responseMIMEType: String?
var responseMIMEType: String?

/// Output schema of the generated candidate text.
let responseSchema: Schema?
var responseSchema: Schema?

/// Output schema of the generated response in [JSON Schema](https://json-schema.org/) format.
///
/// If set, `responseSchema` must be omitted and `responseMIMEType` is required.
let responseJSONSchema: JSONObject?
var responseJSONSchema: JSONObject?

/// Supported modalities of the response.
let responseModalities: [ResponseModality]?
var responseModalities: [ResponseModality]?

/// Configuration for controlling the "thinking" behavior of compatible Gemini models.
let thinkingConfig: ThinkingConfig?
Expand Down
67 changes: 67 additions & 0 deletions FirebaseAI/Sources/GenerativeModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import FirebaseAppCheckInterop
import FirebaseAuthInterop
import Foundation
#if canImport(FoundationModels)
import FoundationModels
#endif // canImport(FoundationModels)

/// A type that represents a remote multimodal model (like Gemini), with the ability to generate
/// content based on various input types.
Expand Down Expand Up @@ -144,6 +147,12 @@ public final class GenerativeModel: Sendable {
/// - Throws: A ``GenerateContentError`` if the request failed.
public func generateContent(_ content: [ModelContent]) async throws
-> GenerateContentResponse {
return try await generateContent(content, generationConfig: generationConfig)
}

public func generateContent(_ content: [ModelContent],
generationConfig: GenerationConfig?) async throws
-> GenerateContentResponse {
try content.throwIfError()
let response: GenerateContentResponse
let generateContentRequest = GenerateContentRequest(
Expand Down Expand Up @@ -357,6 +366,52 @@ public final class GenerativeModel: Sendable {
return try await generativeAIService.loadRequest(request: countTokensRequest)
}

#if canImport(FoundationModels)
/// Produces a generable object as a response to a prompt.
///
/// - Parameters:
/// - prompt: A prompt for the model to respond to.
/// - type: A type to produce as the response.
/// - Returns: ``GeneratedContent`` containing the fields and values defined in the schema.
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public final func generateObject<Content>(_ type: Content.Type = Content.self,
parts: any PartsRepresentable...) async throws
-> Response<Content>
where Content: FoundationModels.Generable {
let jsonSchema = try type.generationSchema.asGeminiJSONSchema()

let generationConfig = {
var generationConfig = self.generationConfig ?? GenerationConfig()
if generationConfig.candidateCount != nil {
generationConfig.candidateCount = nil
}
generationConfig.responseMIMEType = "application/json"
if generationConfig.responseSchema != nil {
generationConfig.responseSchema = nil
}
generationConfig.responseJSONSchema = jsonSchema
if generationConfig.responseModalities != nil {
generationConfig.responseModalities = nil
}

return generationConfig
}()

let response = try await generateContent(
[ModelContent(parts: parts)],
generationConfig: generationConfig
)

let generatedContent = try GeneratedContent(json: response.text ?? "")
let content = try Content(generatedContent)
let rawContent = try ModelOutput(generatedContent)

return Response(content: content, rawContent: rawContent)
}
#endif // canImport(FoundationModels)

/// Returns a `GenerateContentError` (for public consumption) from an internal error.
///
/// If `error` is already a `GenerateContentError` the error is returned unchanged.
Expand All @@ -366,4 +421,16 @@ public final class GenerativeModel: Sendable {
}
return GenerateContentError.internalError(underlying: error)
}

/// A structure that stores the output of a response call.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct Response<Content> {
/// The response content.
public let content: Content

/// The raw response content.
///
/// When `Content` is `ModelOutput`, this is the same as `content`.
public let rawContent: ModelOutput
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#if canImport(FoundationModels)
import Foundation
import FoundationModels

@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension GenerationSchema {
func asGeminiJSONSchema() throws -> JSONObject {
let jsonData = try JSONEncoder().encode(self)
var jsonSchema = try JSONDecoder().decode(JSONObject.self, from: jsonData)
updatePropertyOrdering(&jsonSchema)

return jsonSchema
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
fileprivate func updatePropertyOrdering(_ schema: inout JSONObject) {
guard let propertyOrdering = schema.removeValue(forKey: "x-order") else {
return
}
guard case let .array(values) = propertyOrdering else {
return
}
guard values.allSatisfy({
guard case .string = $0 else { return false }
return true
}) else {
return
}

schema["propertyOrdering"] = propertyOrdering
}
#endif // canImport(FoundationModels)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
#if canImport(FoundationModels)
import FoundationModels
#endif // canImport(FoundationModels)

/// A type that can be initialized from model output.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ConvertibleFromModelOutput: SendableMetatype {
/// Creates an instance from content generated by a model.
///
/// Conformance to this protocol is provided by the `@Generable` macro. A manual implementation
/// may be used to map values onto properties using different names. To manually initialize your
/// type from model output, decode the values as shown below:
///
/// ```swift
/// struct Person: ConvertibleFromModelOutput {
/// var name: String
/// var age: Int
///
/// init(_ content: ModelOutput) {
/// self.name = try content.value(forProperty: "firstName")
/// self.age = try content.value(forProperty: "ageInYears")
/// }
/// }
/// ```
///
/// - Important: If your type also conforms to ``ConvertibleToModelOutput``, it is critical
/// that this implementation be symmetrical with
/// ``ConvertibleToModelOutput/modelOutput``.
///
/// - SeeAlso: `@Generable` macro ``Generable(description:)``
init(_ content: ModelOutput) throws
}

#if canImport(FoundationModels)
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension FoundationModels.GeneratedContent: ConvertibleFromModelOutput {}
#endif // canImport(FoundationModels)

#if canImport(FoundationModels)
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public extension FoundationModels.ConvertibleFromGeneratedContent
where Self: ConvertibleFromModelOutput {
init(_ content: ModelOutput) throws {
try self.init(content.generatedContent)
}
}
#endif // canImport(FoundationModels)
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#if canImport(FoundationModels)
import FoundationModels
#endif // canImport(FoundationModels)

/// A type that can be converted to model output.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ConvertibleToModelOutput {
/// This instance represented as model output.
///
/// Conformance to this protocol is provided by the `@Generable` macro. A manual implementation
/// may be used to map values onto properties using different names. Use the `modelOutput`
/// property as shown below, to manually return a new ``ModelOutput`` with the properties
/// you specify.
///
/// ```swift
/// struct Person: ConvertibleToModelOutput {
/// var name: String
/// var age: Int
///
/// var modelOutput: ModelOutput {
/// ModelOutput(properties: [
/// "firstName": name,
/// "ageInYears": age
/// ])
/// }
/// }
/// ```
///
/// - Important: If your type also conforms to ``ConvertibleFromModelOutput``, it is
/// critical that this implementation be symmetrical with
/// ``ConvertibleFromModelOutput/init(_:)``.
var modelOutput: ModelOutput { get }
}

#if canImport(FoundationModels)
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension FoundationModels.GeneratedContent: ConvertibleToModelOutput {
public var modelOutput: ModelOutput {
switch kind {
case .null:
return ModelOutput(kind: .null)
case let .bool(value):
return ModelOutput(kind: .bool(value))
case let .number(value):
return ModelOutput(kind: .number(value))
case let .string(value):
return ModelOutput(kind: .string(value))
case let .array(values):
return ModelOutput(kind: .array(values.map { $0.modelOutput }))
case let .structure(properties: properties, orderedKeys: orderedKeys):
return ModelOutput(kind: .structure(
properties: properties.mapValues { $0.modelOutput }, orderedKeys: orderedKeys
))
@unknown default:
fatalError("Unsupported GeneratedContent kind: \(kind)")
}
}
}
#endif // canImport(FoundationModels)

#if canImport(FoundationModels)
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public extension FoundationModels.ConvertibleToGeneratedContent
where Self: ConvertibleToModelOutput {
var generatedContent: GeneratedContent {
modelOutput.generatedContent
}
}
#endif // canImport(FoundationModels)

#if canImport(FoundationModels)
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public extension ConvertibleToModelOutput
where Self: FoundationModels.ConvertibleToGeneratedContent {
var modelOutput: ModelOutput {
generatedContent.modelOutput
}
}
#endif // canImport(FoundationModels)
Loading
Loading