Skip to content

Commit 04052dc

Browse files
authored
Merge branch 'firebase:main' into JesusRojass/#15802
2 parents 21f96ce + 3f3a288 commit 04052dc

33 files changed

+2316
-132
lines changed

Crashlytics/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Unreleased
1+
# 12.11.0
22
- [fixed] Fixed an issue where Crashlytics API calls were silently dropped if invoked immediately after Firebase initialization.
33

44
# 12.10.0

FirebaseAI/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 12.11.0
2+
- [feature] **Public Preview**: Introduces `GenerativeModelSession` providing
3+
APIs for generating structured data from Gemini via the same `@Generable` and
4+
`@Guide` macros that are used with Foundation Models.
5+
16
# 12.9.0
27
- [changed] The URL context tool APIs are now GA.
38
- [feature] Added support for implicit caching (context caching) metadata in `GenerateContentResponse`.

FirebaseAI/Sources/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This directory contains the source code for the FirebaseAI library.
2424
- **`GenerativeAIRequest.swift`**: Defines the `GenerativeAIRequest` protocol for requests sent to the generative AI backend. It also defines `RequestOptions`.
2525
- **`GenerativeAIService.swift`**: Defines the `GenerativeAIService` struct, which is responsible for making requests to the generative AI backend. It handles things like authentication, URL construction, and response parsing.
2626
- **`GenerativeModel.swift`**: Defines the `GenerativeModel` class, which represents a remote multimodal model. It provides methods for generating content, counting tokens, and starting a chat via `startChat(history:)`, which returns a `Chat` instance.
27+
- **`GenerativeModelSession.swift`**: Defines the `GenerativeModelSession` class, which provides a simplified interface for single-turn interactions with a generative model. It's particularly useful for generating typed objects from a model's response using the `@Generable` macro, without the conversational turn-based structure of a `Chat`.
2728
- **`History.swift`**: Defines the `History` class, a thread-safe class for managing the chat history, used by the `Chat` class.
2829
- **`JSONValue.swift`**: Defines the `JSONValue` enum and `JSONObject` typealias for representing JSON values.
2930
- **`ModalityTokenCount.swift`**: Defines the `ModalityTokenCount` and `ContentModality` structs for representing token counting information for a single modality.

FirebaseAI/Sources/Chat.swift

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public final class Chat: Sendable {
3737
}
3838
}
3939

40+
var generationConfig: GenerationConfig? { model.generationConfig }
41+
4042
/// Sends a message using the existing history of this chat as context. If successful, the message
4143
/// and response will be added to the history. If unsuccessful, history will remain unchanged.
4244
/// - Parameter parts: The new content to send as a single chat message.
@@ -52,14 +54,40 @@ public final class Chat: Sendable {
5254
/// - Parameter content: The new content to send as a single chat message.
5355
/// - Returns: The model's response if no error occurred.
5456
/// - Throws: A ``GenerateContentError`` if an error occurred.
55-
public func sendMessage(_ content: [ModelContent]) async throws
56-
-> GenerateContentResponse {
57+
public func sendMessage(_ content: [ModelContent]) async throws -> GenerateContentResponse {
58+
return try await sendMessage(content, generationConfig: generationConfig)
59+
}
60+
61+
/// Sends a message using the existing history of this chat as context. If successful, the message
62+
/// and response will be added to the history. If unsuccessful, history will remain unchanged.
63+
/// - Parameter parts: The new content to send as a single chat message.
64+
/// - Returns: A stream containing the model's response or an error if an error occurred.
65+
@available(macOS 12.0, *)
66+
public func sendMessageStream(_ parts: any PartsRepresentable...) throws
67+
-> AsyncThrowingStream<GenerateContentResponse, Error> {
68+
return try sendMessageStream([ModelContent(parts: parts)])
69+
}
70+
71+
/// Sends a message using the existing history of this chat as context. If successful, the message
72+
/// and response will be added to the history. If unsuccessful, history will remain unchanged.
73+
/// - Parameter content: The new content to send as a single chat message.
74+
/// - Returns: A stream containing the model's response or an error if an error occurred.
75+
@available(macOS 12.0, *)
76+
public func sendMessageStream(_ content: [ModelContent]) throws
77+
-> AsyncThrowingStream<GenerateContentResponse, Error> {
78+
return try sendMessageStream(content, generationConfig: generationConfig)
79+
}
80+
81+
// MARK: - Internal
82+
83+
func sendMessage(_ content: [ModelContent],
84+
generationConfig: GenerationConfig?) async throws -> GenerateContentResponse {
5785
// Ensure that the new content has the role set.
5886
let newContent = content.map(populateContentRole(_:))
5987

6088
// Send the history alongside the new message as context.
6189
let request = history + newContent
62-
let result = try await model.generateContent(request)
90+
let result = try await model.generateContent(request, generationConfig: generationConfig)
6391
guard let reply = result.candidates.first?.content else {
6492
let error = NSError(domain: "com.google.generative-ai",
6593
code: -1,
@@ -78,29 +106,14 @@ public final class Chat: Sendable {
78106
return result
79107
}
80108

81-
/// Sends a message using the existing history of this chat as context. If successful, the message
82-
/// and response will be added to the history. If unsuccessful, history will remain unchanged.
83-
/// - Parameter parts: The new content to send as a single chat message.
84-
/// - Returns: A stream containing the model's response or an error if an error occurred.
85-
@available(macOS 12.0, *)
86-
public func sendMessageStream(_ parts: any PartsRepresentable...) throws
87-
-> AsyncThrowingStream<GenerateContentResponse, Error> {
88-
return try sendMessageStream([ModelContent(parts: parts)])
89-
}
90-
91-
/// Sends a message using the existing history of this chat as context. If successful, the message
92-
/// and response will be added to the history. If unsuccessful, history will remain unchanged.
93-
/// - Parameter content: The new content to send as a single chat message.
94-
/// - Returns: A stream containing the model's response or an error if an error occurred.
95-
@available(macOS 12.0, *)
96-
public func sendMessageStream(_ content: [ModelContent]) throws
109+
func sendMessageStream(_ content: [ModelContent], generationConfig: GenerationConfig?) throws
97110
-> AsyncThrowingStream<GenerateContentResponse, Error> {
98111
// Ensure that the new content has the role set.
99112
let newContent: [ModelContent] = content.map(populateContentRole(_:))
100113

101114
// Send the history alongside the new message as context.
102115
let request = history + newContent
103-
let stream = try model.generateContentStream(request)
116+
let stream = try model.generateContentStream(request, generationConfig: generationConfig)
104117
return AsyncThrowingStream { continuation in
105118
Task {
106119
var aggregatedContent: [ModelContent] = []
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Internal Extensions
2+
3+
This directory contains internal extensions to data models and other types. These extensions provide functionality that is specific to the internal workings of the Firebase AI SDK and are not part of the public API.
4+
5+
## Files
6+
7+
- **`GenerationSchema+Gemini.swift`**: This file extends `GenerationSchema` to provide a `toGeminiJSONSchema()` method. This method transforms the schema into a format that is compatible with the Gemini backend, including renaming properties like `x-order` to `propertyOrdering`. This file is conditionally compiled and is only available when `FoundationModels` can be imported.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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) && canImport(FoundationModels)
16+
import FoundationModels
17+
18+
@available(iOS 26.0, macOS 26.0, *)
19+
@available(tvOS, unavailable)
20+
@available(watchOS, unavailable)
21+
public extension FoundationModels.ConvertibleFromGeneratedContent {
22+
/// Initializes an instance from `FirebaseAI.GeneratedContent`.
23+
///
24+
/// **Public Preview**: This API is a public preview and may be subject to change.
25+
///
26+
/// - Parameters:
27+
/// - content: The `FirebaseAI.GeneratedContent` from which to initialize.
28+
/// - Throws: An error if initialization fails.
29+
init(_ content: FirebaseAI.GeneratedContent) throws {
30+
try self.init(content.generatedContent)
31+
}
32+
}
33+
#endif // compiler(>=6.2) && canImport(FoundationModels)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
import Foundation
16+
17+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
18+
extension FirebaseAI.GenerationSchema {
19+
/// Returns a Gemini-compatible JSON Schema of this `GenerationSchema`.
20+
func toGeminiJSONSchema() throws -> JSONObject {
21+
let encoder = JSONEncoder()
22+
encoder.keyEncodingStrategy = .custom { keys in
23+
guard let lastKey = keys.last else {
24+
assertionFailure("Unexpected empty coding path.")
25+
return SchemaCodingKey(stringValue: "")
26+
}
27+
if lastKey.stringValue == "x-order" {
28+
return SchemaCodingKey(stringValue: "propertyOrdering")
29+
}
30+
return lastKey
31+
}
32+
33+
#if canImport(FoundationModels)
34+
if #available(iOS 26.0, macOS 26.0, visionOS 26.0, *) {
35+
let generationSchemaData = try encoder.encode(self)
36+
let jsonSchema = try JSONDecoder().decode(JSONObject.self, from: generationSchemaData)
37+
38+
return jsonSchema
39+
}
40+
#endif // canImport(FoundationModels)
41+
42+
// TODO: Implement FirebaseAI.GenerationSchema encoding for iOS < 26.
43+
throw EncodingError.invalidValue(self, .init(codingPath: [], debugDescription: """
44+
\(Self.self).#\(#function): `GenerationSchema` encoding is not yet implemented for iOS < 26.
45+
"""))
46+
}
47+
48+
private struct SchemaCodingKey: CodingKey {
49+
let stringValue: String
50+
let intValue: Int? = nil
51+
52+
init(stringValue: String) {
53+
self.stringValue = stringValue
54+
}
55+
56+
init?(intValue: Int) {
57+
assertionFailure("Unexpected \(Self.self) with integer value: \(intValue)")
58+
return nil
59+
}
60+
}
61+
}

FirebaseAI/Sources/FirebaseAI.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,28 @@ public final class FirebaseAI: Sendable {
104104
)
105105
}
106106

107+
// TODO: Remove the `#if compiler(>=6.2)` when Xcode 26 is the minimum supported version.
108+
#if compiler(>=6.2)
109+
/// Creates a new `GenerativeModelSession` with the given model.
110+
///
111+
/// - Important: **Public Preview** - This API is a public preview and may be subject to change.
112+
///
113+
/// - Parameters:
114+
/// - model: The name of the model to use; see [available model names
115+
/// ](https://firebase.google.com/docs/vertex-ai/gemini-models#available-model-names)
116+
/// for a list of supported model names.
117+
/// - instructions: System instructions that direct the model's behavior.
118+
public func generativeModelSession(model: String,
119+
instructions: String? = nil) -> GenerativeModelSession {
120+
let model = generativeModel(
121+
modelName: model,
122+
systemInstruction: instructions.map { ModelContent(role: "system", parts: $0) }
123+
)
124+
125+
return GenerativeModelSession(model: model)
126+
}
127+
#endif // compiler(>=6.2)
128+
107129
/// Initializes an ``ImagenModel`` with the given parameters.
108130
///
109131
/// - Note: Refer to [Imagen models](https://firebase.google.com/docs/vertex-ai/models) for

FirebaseAI/Sources/GenerateContentResponse.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public struct GenerateContentResponse: Sendable {
7171
/// Token usage metadata for processing the generate content request.
7272
public let usageMetadata: UsageMetadata?
7373

74+
let responseID: String?
75+
7476
/// The response's content as text, if it exists.
7577
///
7678
/// - Note: This does not include thought summaries; see ``thoughtSummary`` for more details.
@@ -123,6 +125,7 @@ public struct GenerateContentResponse: Sendable {
123125
self.candidates = candidates
124126
self.promptFeedback = promptFeedback
125127
self.usageMetadata = usageMetadata
128+
responseID = nil
126129
}
127130

128131
func text(isThought: Bool) -> String? {
@@ -453,10 +456,11 @@ public struct Segment: Sendable, Equatable, Hashable {
453456

454457
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
455458
extension GenerateContentResponse: Decodable {
456-
enum CodingKeys: CodingKey {
459+
enum CodingKeys: String, CodingKey {
457460
case candidates
458461
case promptFeedback
459462
case usageMetadata
463+
case responseID = "responseId"
460464
}
461465

462466
public init(from decoder: Decoder) throws {
@@ -482,6 +486,7 @@ extension GenerateContentResponse: Decodable {
482486
}
483487
promptFeedback = try container.decodeIfPresent(PromptFeedback.self, forKey: .promptFeedback)
484488
usageMetadata = try container.decodeIfPresent(UsageMetadata.self, forKey: .usageMetadata)
489+
responseID = try container.decodeIfPresent(String.self, forKey: .responseID)
485490
}
486491
}
487492

FirebaseAI/Sources/GenerationConfig.swift

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,45 @@ import Foundation
1919
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
2020
public struct GenerationConfig: Sendable {
2121
/// Controls the degree of randomness in token selection.
22-
let temperature: Float?
22+
var temperature: Float?
2323

2424
/// Controls diversity of generated text.
25-
let topP: Float?
25+
var topP: Float?
2626

2727
/// Limits the number of highest probability words considered.
28-
let topK: Int?
28+
var topK: Int?
2929

3030
/// The number of response variations to return.
31-
let candidateCount: Int?
31+
var candidateCount: Int?
3232

3333
/// Maximum number of tokens that can be generated in the response.
34-
let maxOutputTokens: Int?
34+
var maxOutputTokens: Int?
3535

3636
/// Controls the likelihood of repeating the same words or phrases already generated in the text.
37-
let presencePenalty: Float?
37+
var presencePenalty: Float?
3838

3939
/// Controls the likelihood of repeating words, with the penalty increasing for each repetition.
40-
let frequencyPenalty: Float?
40+
var frequencyPenalty: Float?
4141

4242
/// A set of up to 5 `String`s that will stop output generation.
43-
let stopSequences: [String]?
43+
var stopSequences: [String]?
4444

4545
/// Output response MIME type of the generated candidate text.
46-
let responseMIMEType: String?
46+
var responseMIMEType: String?
4747

4848
/// Output schema of the generated candidate text.
49-
let responseSchema: Schema?
49+
var responseSchema: Schema?
5050

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

5656
/// Supported modalities of the response.
57-
let responseModalities: [ResponseModality]?
57+
var responseModalities: [ResponseModality]?
5858

5959
/// Configuration for controlling the "thinking" behavior of compatible Gemini models.
60-
let thinkingConfig: ThinkingConfig?
60+
var thinkingConfig: ThinkingConfig?
6161

6262
/// Creates a new `GenerationConfig` value.
6363
///
@@ -203,6 +203,54 @@ public struct GenerationConfig: Sendable {
203203
self.responseModalities = responseModalities
204204
self.thinkingConfig = thinkingConfig
205205
}
206+
207+
/// Merges two configurations, giving precedence to values found in the `overrides` parameter.
208+
///
209+
/// - Parameters:
210+
/// - base: The foundational configuration (e.g., model-level defaults).
211+
/// - overrides: The configuration containing values that should supersede the base (e.g.,
212+
/// request-level specific settings).
213+
/// - Returns: A merged `GenerationConfig` prioritizing `overrides`, or `nil` if both inputs are
214+
/// `nil`.
215+
static func merge(_ base: GenerationConfig?,
216+
with overrides: GenerationConfig?) -> GenerationConfig? {
217+
// 1. If the base config is missing, return the overrides (which might be nil).
218+
guard let baseConfig = base else {
219+
return overrides
220+
}
221+
222+
// 2. If overrides are missing, strictly return the base.
223+
guard let overrideConfig = overrides else {
224+
return baseConfig
225+
}
226+
227+
// 3. Start with a copy of the base config.
228+
var config = baseConfig
229+
230+
// 4. Overwrite with any non-nil values found in the overrides.
231+
config.temperature = overrideConfig.temperature ?? config.temperature
232+
config.topP = overrideConfig.topP ?? config.topP
233+
config.topK = overrideConfig.topK ?? config.topK
234+
config.candidateCount = overrideConfig.candidateCount ?? config.candidateCount
235+
config.maxOutputTokens = overrideConfig.maxOutputTokens ?? config.maxOutputTokens
236+
config.presencePenalty = overrideConfig.presencePenalty ?? config.presencePenalty
237+
config.frequencyPenalty = overrideConfig.frequencyPenalty ?? config.frequencyPenalty
238+
config.stopSequences = overrideConfig.stopSequences ?? config.stopSequences
239+
config.responseMIMEType = overrideConfig.responseMIMEType ?? config.responseMIMEType
240+
config.responseModalities = overrideConfig.responseModalities ?? config.responseModalities
241+
config.thinkingConfig = overrideConfig.thinkingConfig ?? config.thinkingConfig
242+
243+
// 5. Handle Schema mutual exclusivity with precedence for `responseJSONSchema`.
244+
if let responseJSONSchema = overrideConfig.responseJSONSchema {
245+
config.responseJSONSchema = responseJSONSchema
246+
config.responseSchema = nil
247+
} else if let responseSchema = overrideConfig.responseSchema {
248+
config.responseSchema = responseSchema
249+
config.responseJSONSchema = nil
250+
}
251+
252+
return config
253+
}
206254
}
207255

208256
// MARK: - Codable Conformances

0 commit comments

Comments
 (0)