Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
b81c154
Add `FirebaseAILogicMacros` SPM target with placeholder macro
andrewheard Nov 27, 2025
551cfd7
Workaround transient SPM build issues due to prebuilt swift-syntax
andrewheard Nov 27, 2025
94f989f
Revert "Try forcing Swift 6 mode on macOS 14 / Xcode 16.2"
andrewheard Nov 27, 2025
21816b0
Try removing `spm-package-resolved` job
andrewheard Nov 28, 2025
fb5f2e2
Revert "Try removing `spm-package-resolved` job"
andrewheard Nov 28, 2025
fdb4dbf
Set `IDEPackageEnablePrebuilts NO`
andrewheard Nov 28, 2025
2395e13
Revert "Set `IDEPackageEnablePrebuilts NO`"
andrewheard Nov 28, 2025
33bda28
Refactor to more closely match starter project from Xcode 16.2
andrewheard Nov 28, 2025
45c4300
Switch `swift-syntax` dependency version based on Swift version
andrewheard Nov 28, 2025
e817d78
Try disabling Swift prebuilts again
andrewheard Nov 28, 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
6 changes: 6 additions & 0 deletions .github/workflows/common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ jobs:
with:
path: .build
key: ${{needs.spm-package-resolved.outputs.cache_key}}
- name: Disable SwiftPM prebuilts in Xcode
run: |
defaults write com.apple.dt.Xcode IDEPackageEnablePrebuilts -bool NO
defaults read com.apple.dt.Xcode IDEPackageEnablePrebuilts
- name: Clear SwiftPM caches
run: rm -rf ~/.swiftpm/artifacts ~/.swiftpm/cache
- name: Xcode
run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer
- name: Run setup command, if needed.
Expand Down
23 changes: 23 additions & 0 deletions FirebaseAI/Sources/Macros/FirebaseAILogicPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 SwiftCompilerPlugin

Check failure on line 15 in FirebaseAI/Sources/Macros/FirebaseAILogicPlugin.swift

View workflow job for this annotation

GitHub Actions / spm (FirebaseAIUnit) / spm (macos-15, Xcode_16.4, macOS)

could not find module 'SwiftCompilerPlugin' for target 'arm64-apple-macos'; found: x86_64-apple-macos, at: /Users/runner/Library/Developer/Xcode/DerivedData/firebase-ios-sdk-dkhlbuenaibuowhffbszhgvmwpqa/Build/Products/Debug/SwiftCompilerPlugin.swiftmodule

Check failure on line 15 in FirebaseAI/Sources/Macros/FirebaseAILogicPlugin.swift

View workflow job for this annotation

GitHub Actions / spm (FirebaseAIUnit) / spm (macos-15, Xcode_16.4, macOS)

could not find module 'SwiftCompilerPlugin' for target 'arm64-apple-macos'; found: x86_64-apple-macos, at: /Users/runner/Library/Developer/Xcode/DerivedData/firebase-ios-sdk-dkhlbuenaibuowhffbszhgvmwpqa/Build/Products/Debug/SwiftCompilerPlugin.swiftmodule

Check failure on line 15 in FirebaseAI/Sources/Macros/FirebaseAILogicPlugin.swift

View workflow job for this annotation

GitHub Actions / spm (FirebaseAIUnit) / spm (macos-15, Xcode_16.4, macOS)

could not find module 'SwiftCompilerPlugin' for target 'arm64-apple-macos'; found: x86_64-apple-macos, at: /Users/runner/Library/Developer/Xcode/DerivedData/firebase-ios-sdk-dkhlbuenaibuowhffbszhgvmwpqa/Build/Products/Debug/SwiftCompilerPlugin.swiftmodule
import SwiftSyntaxMacros

@main
struct FirebaseAILogicPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
]
}
37 changes: 37 additions & 0 deletions FirebaseAI/Sources/Macros/StringifyMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

/// Implementation of the `stringify` macro, which takes an expression
/// of any type and produces a tuple containing the value of that expression
/// and the source code that produced the value. For example
///
/// #stringify(x + y)
///
/// will expand to
///
/// (x + y, "x + y")
public struct StringifyMacro: ExpressionMacro {
public static func expansion(of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext) -> ExprSyntax {
guard let argument = node.arguments.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}

return "(\(argument), \(literal: argument.description))"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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

/// 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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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.

/// 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 }
}
205 changes: 205 additions & 0 deletions FirebaseAI/Sources/Types/Public/Generable/Generable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// 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

/// A type that the model uses when responding to prompts.
///
/// Annotate your Swift structure or enumeration with the `@Generable` macro to allow the model to
/// respond to prompts by generating an instance of your type. Use the `@Guide` macro to provide
/// natural language descriptions of your properties, and programmatically control the values that
/// the model can generate.
///
/// ```swift
/// @Generable
/// struct SearchSuggestions {
/// @Guide(description: "A list of suggested search terms", .count(4))
/// var searchTerms: [SearchTerm]
///
/// @Generable
/// struct SearchTerm {
/// // Use a generation identifier for data structures the framework generates.
/// var id: GenerationID
///
/// @Guide(description: "A 2 or 3 word search term, like 'Beautiful sunsets'")
/// var searchTerm: String
/// }
/// }
/// ```
/// - SeeAlso: `@Generable` macro ``Generable(description:)`` and `@Guide` macro
/// ``Guide(description:)``.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol Generable: ConvertibleFromModelOutput, ConvertibleToModelOutput {
/// An instance of the JSON schema.
static var jsonSchema: JSONSchema { get }
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Optional where Wrapped: Generable {}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Optional: ConvertibleToModelOutput where Wrapped: ConvertibleToModelOutput {
public var modelOutput: ModelOutput {
guard let self else { return ModelOutput(kind: .null) }

return ModelOutput(self)
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Bool: Generable {
public static var jsonSchema: JSONSchema {
JSONSchema(kind: .boolean, source: "Bool")
}

public init(_ content: ModelOutput) throws {
guard case let .bool(value) = content.kind else {
throw Self.decodingFailure(content)
}
self = value
}

public var modelOutput: ModelOutput {
return ModelOutput(kind: .bool(self))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension String: Generable {
public static var jsonSchema: JSONSchema {
JSONSchema(kind: .string, source: "String")
}

public init(_ content: ModelOutput) throws {
guard case let .string(value) = content.kind else {
throw Self.decodingFailure(content)
}
self = value
}

public var modelOutput: ModelOutput {
return ModelOutput(kind: .string(self))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Int: Generable {
public static var jsonSchema: JSONSchema {
JSONSchema(kind: .integer, source: "Int")
}

public init(_ content: ModelOutput) throws {
guard case let .number(value) = content.kind, let integer = Int(exactly: value) else {
throw Self.decodingFailure(content)
}
self = integer
}

public var modelOutput: ModelOutput {
return ModelOutput(kind: .number(Double(self)))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Float: Generable {
public static var jsonSchema: JSONSchema {
JSONSchema(kind: .double, source: "Number")
}

public init(_ content: ModelOutput) throws {
guard case let .number(value) = content.kind else {
throw Self.decodingFailure(content)
}
self = Float(value)
}

public var modelOutput: ModelOutput {
return ModelOutput(kind: .number(Double(self)))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Double: Generable {
public static var jsonSchema: JSONSchema {
JSONSchema(kind: .double, source: "Number")
}

public init(_ content: ModelOutput) throws {
guard case let .number(value) = content.kind else {
throw Self.decodingFailure(content)
}
self = value
}

public var modelOutput: ModelOutput {
return ModelOutput(kind: .number(self))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Decimal: Generable {
public static var jsonSchema: JSONSchema {
JSONSchema(kind: .double, source: "Number")
}

public init(_ content: ModelOutput) throws {
guard case let .number(value) = content.kind else {
throw Self.decodingFailure(content)
}
self = Decimal(value)
}

public var modelOutput: ModelOutput {
let doubleValue = (self as NSDecimalNumber).doubleValue
return ModelOutput(kind: .number(doubleValue))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Array: Generable where Element: Generable {
public static var jsonSchema: JSONSchema {
JSONSchema(kind: .array(item: Element.self), source: String(describing: self))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Array: ConvertibleToModelOutput where Element: ConvertibleToModelOutput {
public var modelOutput: ModelOutput {
let values = map { $0.modelOutput }
return ModelOutput(kind: .array(values))
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension Array: ConvertibleFromModelOutput where Element: ConvertibleFromModelOutput {
public init(_ content: ModelOutput) throws {
guard case let .array(values) = content.kind else {
throw Self.decodingFailure(content)
}
self = try values.map { try Element($0) }
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
private extension ConvertibleFromModelOutput {
/// Helper method to create ``GenerativeModel/GenerationError/decodingFailure(_:)`` instances.
static func decodingFailure(_ content: ModelOutput) -> GenerativeModel.GenerationError {
return GenerativeModel.GenerationError.decodingFailure(
GenerativeModel.GenerationError.Context(debugDescription: """
\(content.self) does not contain \(Self.self).
Content: \(content)
""")
)
}
}
17 changes: 17 additions & 0 deletions FirebaseAI/Sources/Types/Public/Generable/GenerationGuide.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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.

/// Guides that control how values are generated.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public struct GenerationGuide<Value> {}
Loading
Loading