Skip to content

Commit b1c9e27

Browse files
authored
[AI] Add hybrid support with Foundation Models (#16111)
1 parent f0ec14c commit b1c9e27

15 files changed

Lines changed: 487 additions & 4 deletions
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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.3) && canImport(FoundationModels)
16+
import Foundation
17+
import FoundationModels
18+
19+
@available(iOS 26.0, macOS 26.0, visionOS 26.0, *)
20+
@available(tvOS, unavailable)
21+
@available(watchOS, unavailable)
22+
extension FoundationModels.LanguageModelSession: _ModelSession {
23+
/// Returns `true` if the session has history (i.e., it has already had one or more chat turns).
24+
///
25+
/// > Important: This property is for **internal use only** and may change at any time.
26+
public var _hasHistory: Bool {
27+
return transcript.contains { entry in
28+
if case .instructions = entry {
29+
return false
30+
}
31+
32+
return true
33+
}
34+
}
35+
36+
/// Sends a prompt to the model and returns a ``_ModelSessionResponse``.
37+
///
38+
/// > Important: This method is for **internal use only** and may change at any time.
39+
///
40+
/// - Parameters:
41+
/// - prompt: The content to send to the model.
42+
/// - schema: An optional schema for structured outputs.
43+
/// - includeSchemaInPrompt: Whether to include the `schema` in the request to the model; if
44+
/// `false`, structured output (JSON) is requested but the schema is not strictly enforced.
45+
/// - options: A set of options, represented as a ``GenerationOptionsRepresentable`` type.
46+
public func _respond(to prompt: [any Part], schema: FirebaseAI.GenerationSchema?,
47+
includeSchemaInPrompt: Bool,
48+
options: any GenerationOptionsRepresentable) async throws
49+
-> _ModelSessionResponse {
50+
let prompt = try prompt.toFoundationModelsPrompt()
51+
52+
let response: FoundationModels.LanguageModelSession
53+
.Response<FoundationModels.GeneratedContent>
54+
if let schema {
55+
response = try await respond(
56+
to: prompt,
57+
schema: schema.generationSchema,
58+
includeSchemaInPrompt: includeSchemaInPrompt,
59+
options: options.generationOptions
60+
)
61+
} else {
62+
response = try await respond(
63+
to: prompt,
64+
schema: String.generationSchema,
65+
options: options.generationOptions
66+
)
67+
}
68+
69+
return makeResponse(from: response.rawContent, schema: schema)
70+
}
71+
72+
/// Sends a prompt to the model and streams the model's response.
73+
///
74+
/// - Parameters:
75+
/// - prompt: The content to send to the model.
76+
/// - schema: An optional schema for structured outputs.
77+
/// - includeSchemaInPrompt: Whether to include the `schema` in the request to the model; if
78+
/// `false`, structured output (JSON) is requested but the schema is not strictly enforced.
79+
/// - options: A set of options, represented as a ``GenerationOptionsRepresentable`` type.
80+
public func _streamResponse(to prompt: [any Part], schema: FirebaseAI.GenerationSchema?,
81+
includeSchemaInPrompt: Bool,
82+
options: any GenerationOptionsRepresentable)
83+
-> sending AsyncThrowingStream<_ModelSessionResponse, any Error> {
84+
return AsyncThrowingStream { continuation in
85+
let foundationModelsPrompt: Prompt
86+
do {
87+
foundationModelsPrompt = try prompt.toFoundationModelsPrompt()
88+
} catch {
89+
continuation.finish(throwing: error)
90+
return
91+
}
92+
93+
let stream: FoundationModels.LanguageModelSession
94+
.ResponseStream<FoundationModels.GeneratedContent>
95+
if let schema {
96+
stream = streamResponse(
97+
to: foundationModelsPrompt,
98+
schema: schema.generationSchema,
99+
includeSchemaInPrompt: includeSchemaInPrompt,
100+
options: options.generationOptions
101+
)
102+
} else {
103+
stream = streamResponse(
104+
to: foundationModelsPrompt,
105+
schema: String.generationSchema,
106+
options: options.generationOptions
107+
)
108+
}
109+
110+
let task = Task {
111+
do {
112+
for try await snapshot in stream {
113+
let response = makeResponse(from: snapshot.rawContent, schema: schema)
114+
115+
continuation.yield(response)
116+
}
117+
continuation.finish()
118+
} catch {
119+
continuation.finish(throwing: error)
120+
return
121+
}
122+
}
123+
continuation.onTermination = { _ in task.cancel() }
124+
}
125+
}
126+
127+
private func makeResponse(from content: FoundationModels.GeneratedContent,
128+
schema: FirebaseAI.GenerationSchema?) -> _ModelSessionResponse {
129+
let responseText: String
130+
if schema == nil, case let .string(text) = content.kind {
131+
responseText = text
132+
} else {
133+
responseText = content.jsonString
134+
}
135+
136+
let generatedContent = content.firebaseGeneratedContent
137+
let modelContent = ModelContent(
138+
role: "model",
139+
parts: [TextPart(responseText, isThought: false, thoughtSignature: nil)]
140+
)
141+
let candidate = Candidate(
142+
content: modelContent,
143+
safetyRatings: [],
144+
finishReason: nil,
145+
citationMetadata: nil
146+
)
147+
let rawResponse = GenerateContentResponse(
148+
candidates: [candidate],
149+
modelVersion: FirebaseAI.SystemLanguageModel.modelName
150+
)
151+
152+
return _ModelSessionResponse(rawContent: generatedContent, rawResponse: rawResponse)
153+
}
154+
}
155+
156+
@available(iOS 26.0, macOS 26.0, visionOS 26.0, *)
157+
@available(tvOS, unavailable)
158+
@available(watchOS, unavailable)
159+
private extension GenerationOptionsRepresentable {
160+
var generationOptions: FoundationModels.GenerationOptions {
161+
guard let options = responseGenerationOptions.foundationModelsGenerationOptions else {
162+
return FoundationModels.GenerationOptions()
163+
}
164+
165+
return options.toFoundationModels()
166+
}
167+
}
168+
#endif // compiler(>=6.2.3) && canImport(FoundationModels)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.3)
16+
import Foundation
17+
#if canImport(FoundationModels)
18+
import FoundationModels
19+
#endif // canImport(FoundationModels)
20+
21+
extension FirebaseAI.SystemLanguageModel: LanguageModel {
22+
static let modelName = "apple-foundation-models-system-language-model"
23+
24+
/// Returns the name of the model.
25+
///
26+
/// > Important: This property is for **internal use only** and may change at any time.
27+
public var _modelName: String {
28+
return FirebaseAI.SystemLanguageModel.modelName
29+
}
30+
31+
/// Returns a new session for this model.
32+
///
33+
/// > Important: This method is for **internal use only** and may change at any time.
34+
public func _startSession(tools: [any ToolRepresentable]?,
35+
instructions: String?) throws -> any _ModelSession {
36+
switch availability {
37+
case .available:
38+
break
39+
case let .unavailable(reason):
40+
throw GenerativeModelSession.GenerationError.assetsUnavailable(
41+
GenerativeModelSession.GenerationError.Context(debugDescription: """
42+
The Foundation Models `SystemLanguageModel` is unavailable: \(reason)
43+
""")
44+
)
45+
}
46+
47+
#if canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM
48+
if #available(iOS 26.0, macOS 26.0, visionOS 26.0, *) {
49+
var afmTools = [any FoundationModels.Tool]()
50+
for tool in tools ?? [] {
51+
// Only function calling tools are supported by Foundation Models.
52+
if !tool.toolRepresentation.isFoundationModeCompatible {
53+
assertionFailure("""
54+
The tool "\(tool.toolRepresentation)" is not supported when using the on-device model.
55+
""")
56+
}
57+
58+
let functionDeclarations = tool.toolRepresentation.functionDeclarations ?? []
59+
for functionDeclaration in functionDeclarations {
60+
switch functionDeclaration.kind {
61+
case .manual:
62+
assertionFailure("""
63+
The manual function declaration "\(functionDeclaration)" is not supported by the
64+
Foundation Models framework; function declarations must be `FoundationModels.Tool`
65+
types for use with the on-device model.
66+
""")
67+
continue
68+
case let .foundationModels(afmTool):
69+
guard let afmTool = afmTool as? (any FoundationModels.Tool) else {
70+
assertionFailure("""
71+
The function declaration "\(afmTool)" in the tool "\(tool)" is not a
72+
`FoundationModels.Tool` type.
73+
""")
74+
continue
75+
}
76+
afmTools.append(afmTool)
77+
}
78+
}
79+
}
80+
81+
return LanguageModelSession(
82+
model: self.systemLanguageModel,
83+
tools: afmTools,
84+
instructions: instructions
85+
)
86+
}
87+
#endif // canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM
88+
89+
throw GenerativeModelSession.GenerationError.assetsUnavailable(
90+
GenerativeModelSession.GenerationError.Context(debugDescription: """
91+
Failed to start a `LanguageModelSession`. The Foundation Models `SystemLanguageModel` is not
92+
available on the current platform.
93+
""")
94+
)
95+
}
96+
}
97+
#endif // compiler(>=6.2.3)

FirebaseAI/Sources/GenerativeModelSession.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,9 @@
727727

728728
case concurrentRequests(GenerativeModelSession.GenerationError.Context)
729729

730+
/// The content provided as a prompt is not supported by the model.
731+
case unsupportedPromptContent(GenerativeModelSession.GenerationError.Context)
732+
730733
case internalError(GenerativeModelSession.GenerationError.Context, underlyingError: any Error)
731734
}
732735

FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@
3333
@available(iOS 26.0, macOS 26.0, visionOS 26.0, *)
3434
@available(tvOS, unavailable)
3535
@available(watchOS, unavailable)
36-
extension FirebaseAI.ConvertibleToGeneratedContent
37-
where Self: FoundationModels.ConvertibleToGeneratedContent {
36+
extension FoundationModels.ConvertibleToGeneratedContent {
3837
var firebaseGeneratedContent: FirebaseAI.GeneratedContent {
3938
return FirebaseAI.GeneratedContent(
4039
kind: generatedContent.kind,

FirebaseAI/Sources/Protocols/Public/LanguageModelProvider.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@
4747
}
4848
}
4949

50+
public extension LanguageModelProvider where Self == FirebaseAI.SystemLanguageModel {
51+
/// **[Public Preview]** Creates a model provider for the on-device `SystemLanguageModel`
52+
/// provided by Apple's Foundation Models framework.
53+
///
54+
/// > Warning: This API is a public preview and may be subject to change.
55+
///
56+
/// For more details about the configuration options, see the Apple
57+
/// [documentation](https://developer.apple.com/documentation/foundationmodels/systemlanguagemodel/init(usecase:guardrails:)).
58+
///
59+
/// - Parameters:
60+
/// - useCase: The ``UseCase`` that the model is tuned for; defaults to ``UseCase/general``.
61+
/// - guardrails: The ``Guardrails`` that configure how the model handles potentially harmful
62+
/// content; defaults to ``Guardrails/default``.
63+
static func systemModel(useCase: FirebaseAI.SystemLanguageModel.UseCase = .general,
64+
guardrails: FirebaseAI.SystemLanguageModel.Guardrails = .default)
65+
-> FirebaseAI.SystemLanguageModel {
66+
return FirebaseAI.SystemLanguageModel(useCase: useCase, guardrails: guardrails)
67+
}
68+
}
69+
5070
public extension LanguageModelProvider where Self == HybridModelProvider {
5171
/// **[Public Preview]** Creates a ``HybridModelProvider`` for the specified models.
5272
///

FirebaseAI/Sources/Tool.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ public struct Tool: Sendable {
132132
self.urlContext = urlContext
133133
self.codeExecution = codeExecution
134134
}
135+
136+
/// Returns `true` if all tools contained in `Tool` are supported by Foundation Models.
137+
///
138+
/// Note: Currently only function declarations are supported.
139+
var isFoundationModeCompatible: Bool {
140+
return googleSearch == nil && googleMaps == nil && urlContext == nil && codeExecution == nil
141+
}
135142
}
136143

137144
/// Configuration for specifying function calling behavior.

FirebaseAI/Sources/Types/Internal/GeminiModelSession.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,23 @@ import Foundation
3131

3232
// MARK: ModelSession Conformance
3333

34+
/// Returns `true` if the session has history (i.e., it has already had one or more chat turns).
35+
///
36+
/// > Important: This property is for **internal use only** and may change at any time.
3437
var _hasHistory: Bool {
3538
return !chat.history.isEmpty
3639
}
3740

41+
/// Sends a prompt to the model and returns a ``_ModelSessionResponse``.
42+
///
43+
/// > Important: This method is for **internal use only** and may change at any time.
44+
///
45+
/// - Parameters:
46+
/// - prompt: The content to send to the model.
47+
/// - schema: An optional schema for structured outputs.
48+
/// - includeSchemaInPrompt: Whether to include the `schema` in the request to the model; if
49+
/// `false`, structured output (JSON) is requested but the schema is not strictly enforced.
50+
/// - options: A set of options, represented as a ``GenerationOptionsRepresentable`` type.
3851
nonisolated(nonsending)
3952
func _respond(to prompt: [any Part],
4053
schema: FirebaseAI.GenerationSchema?,
@@ -107,6 +120,14 @@ import Foundation
107120
return _ModelSessionResponse(rawContent: rawContent, rawResponse: response)
108121
}
109122

123+
/// Sends a prompt to the model and streams the model's response.
124+
///
125+
/// - Parameters:
126+
/// - prompt: The content to send to the model.
127+
/// - schema: An optional schema for structured outputs.
128+
/// - includeSchemaInPrompt: Whether to include the `schema` in the request to the model; if
129+
/// `false`, structured output (JSON) is requested but the schema is not strictly enforced.
130+
/// - options: A set of options, represented as a ``GenerationOptionsRepresentable`` type.
110131
@available(macOS 12.0, watchOS 8.0, *)
111132
func _streamResponse(to prompt: [any Part],
112133
schema: FirebaseAI.GenerationSchema?,

0 commit comments

Comments
 (0)