Skip to content

Commit a9eb3d8

Browse files
committed
Update
1 parent c6f3ba8 commit a9eb3d8

28 files changed

+1007
-166
lines changed

FirebaseAI/Sources/AILog.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,21 @@ enum AILog {
6666
case codeExecutionResultUnrecognizedOutcome = 3015
6767
case executableCodeUnrecognizedLanguage = 3016
6868
case fallbackValueUsed = 3017
69+
case liveSessionUnsupportedMessage = 3018
70+
case liveSessionFailedToEncodeClientMessage = 3019
71+
case liveSessionFailedToEncodeClientMessagePayload = 3020
72+
case liveSessionFailedToSendClientMessage = 3021
73+
case liveSessionUnexpectedResponse = 3022
74+
6975

7076
// SDK State Errors
7177
case generateContentResponseNoCandidates = 4000
7278
case generateContentResponseNoText = 4001
7379
case appCheckTokenFetchFailed = 4002
7480
case generateContentResponseEmptyCandidates = 4003
81+
case invalidWebsocketURL = 4004
82+
case duplicateLiveSessionSetupComplete = 4005
83+
7584

7685
// SDK Debugging
7786
case loadRequestStreamResponseLine = 5000

FirebaseAI/Sources/FirebaseAI.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,38 @@ public final class FirebaseAI: Sendable {
141141
)
142142
}
143143

144+
/// **[Public Preview]** Initializes a ``LiveGenerativeModel`` with the given parameters.
145+
///
146+
/// > Warning: For Firebase AI SDK, bidirectional streaming using Live models is in Public
147+
/// Preview, which means that the feature is not subject to any SLA or deprecation policy and
148+
/// could change in backwards-incompatible ways.
149+
///
150+
/// > Important: Only Live models (typically containing `live-*` in the name) are supported.
151+
///
152+
/// - Parameters:
153+
/// - modelName: The name of the Livemodel to use, for example
154+
/// `"gemini-live-2.5-flash-preview"`;
155+
/// see [model versions](https://firebase.google.com/docs/ai-logic/live-api?api=dev#models-that-support-capability)
156+
/// for a list of supported Live models.
157+
/// - generationConfig: The content generation parameters your model should use.
158+
/// - tools: A list of ``Tool`` objects that the model may use to generate the next response.
159+
/// - toolConfig: Tool configuration for any ``Tool`` specified in the request.
160+
/// - systemInstruction: Instructions that direct the model to behave a certain way; currently
161+
/// only text content is supported.
144162
public func liveModel(modelName: String,
145163
generationConfig: LiveGenerationConfig? = nil,
164+
tools: [Tool]? = nil,
165+
toolConfig: ToolConfig? = nil,
166+
systemInstruction: ModelContent? = nil,
146167
requestOptions: RequestOptions = RequestOptions()) -> LiveGenerativeModel {
147168
return LiveGenerativeModel(
148169
modelResourceName: modelResourceName(modelName: modelName),
149170
firebaseInfo: firebaseInfo,
150171
apiConfig: apiConfig,
151172
generationConfig: generationConfig,
152-
requestOptions: requestOptions
173+
tools: tools,
174+
toolConfig: toolConfig,
175+
systemInstruction: systemInstruction
153176
)
154177
}
155178

FirebaseAI/Sources/GenerativeAIService.swift

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ struct GenerativeAIService {
177177
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
178178

179179
if let appCheck = firebaseInfo.appCheck {
180-
let tokenResult = try await fetchAppCheckToken(appCheck: appCheck)
180+
let tokenResult = try await appCheck.fetchAppCheckToken(
181+
limitedUse: firebaseInfo.useLimitedUseAppCheckTokens,
182+
domain: "GenerativeAIService"
183+
)
181184
urlRequest.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
182185
if let error = tokenResult.error {
183186
AILog.error(
@@ -207,53 +210,6 @@ struct GenerativeAIService {
207210
return urlRequest
208211
}
209212

210-
private func fetchAppCheckToken(appCheck: AppCheckInterop) async throws
211-
-> FIRAppCheckTokenResultInterop {
212-
if firebaseInfo.useLimitedUseAppCheckTokens {
213-
if let token = await getLimitedUseAppCheckToken(appCheck: appCheck) {
214-
return token
215-
}
216-
217-
let errorMessage =
218-
"The provided App Check token provider doesn't implement getLimitedUseToken(), but requireLimitedUseTokens was enabled."
219-
220-
#if Debug
221-
fatalError(errorMessage)
222-
#else
223-
throw NSError(
224-
domain: "\(Constants.baseErrorDomain).\(Self.self)",
225-
code: AILog.MessageCode.appCheckTokenFetchFailed.rawValue,
226-
userInfo: [NSLocalizedDescriptionKey: errorMessage]
227-
)
228-
#endif
229-
}
230-
231-
return await appCheck.getToken(forcingRefresh: false)
232-
}
233-
234-
private func getLimitedUseAppCheckToken(appCheck: AppCheckInterop) async
235-
-> FIRAppCheckTokenResultInterop? {
236-
// At the moment, `await` doesn’t get along with Objective-C’s optional protocol methods.
237-
await withCheckedContinuation { (continuation: CheckedContinuation<
238-
FIRAppCheckTokenResultInterop?,
239-
Never
240-
>) in
241-
guard
242-
firebaseInfo.useLimitedUseAppCheckTokens,
243-
// `getLimitedUseToken(completion:)` is an optional protocol method. Optional binding
244-
// is performed to make sure `continuation` is called even if the method’s not implemented.
245-
let limitedUseTokenClosure = appCheck.getLimitedUseToken
246-
else {
247-
return continuation.resume(returning: nil)
248-
}
249-
250-
limitedUseTokenClosure { tokenResult in
251-
// The placeholder token should be used in the case of App Check error.
252-
continuation.resume(returning: tokenResult)
253-
}
254-
}
255-
}
256-
257213
private func httpResponse(urlResponse: URLResponse) throws -> HTTPURLResponse {
258214
// The following condition should always be true: "Whenever you make HTTP URL load requests, any
259215
// response objects you get back from the URLSession, NSURLConnection, or NSURLDownload class
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2025 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 FirebaseAppCheckInterop
16+
17+
// TODO: document
18+
internal extension AppCheckInterop {
19+
// TODO: Document
20+
func fetchAppCheckToken(limitedUse: Bool,
21+
domain: String) async throws -> FIRAppCheckTokenResultInterop {
22+
if limitedUse {
23+
if let token = await getLimitedUseTokenAsync() {
24+
return token
25+
}
26+
27+
let errorMessage =
28+
"The provided App Check token provider doesn't implement getLimitedUseToken(), but requireLimitedUseTokens was enabled."
29+
30+
#if Debug
31+
fatalError(errorMessage)
32+
#else
33+
throw NSError(
34+
domain: "\(Constants.baseErrorDomain).\(domain)",
35+
code: AILog.MessageCode.appCheckTokenFetchFailed.rawValue,
36+
userInfo: [NSLocalizedDescriptionKey: errorMessage]
37+
)
38+
#endif
39+
}
40+
41+
return await getToken(forcingRefresh: false)
42+
}
43+
44+
private func getLimitedUseTokenAsync() async
45+
-> FIRAppCheckTokenResultInterop? {
46+
// At the moment, `await` doesn’t get along with Objective-C’s optional protocol methods.
47+
await withCheckedContinuation { (continuation: CheckedContinuation<
48+
FIRAppCheckTokenResultInterop?,
49+
Never
50+
>) in
51+
guard
52+
// `getLimitedUseToken(completion:)` is an optional protocol method. Optional binding
53+
// is performed to make sure `continuation` is called even if the method’s not implemented.
54+
let limitedUseTokenClosure = getLimitedUseToken
55+
else {
56+
return continuation.resume(returning: nil)
57+
}
58+
59+
limitedUseTokenClosure { tokenResult in
60+
// The placeholder token should be used in the case of App Check error.
61+
continuation.resume(returning: tokenResult)
62+
}
63+
}
64+
}
65+
}

FirebaseAI/Sources/Types/Internal/InternalPart.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ struct FunctionCall: Equatable, Sendable {
5656
struct FunctionResponse: Codable, Equatable, Sendable {
5757
let name: String
5858
let response: JSONObject
59+
let id: String?
5960

60-
init(name: String, response: JSONObject) {
61+
init(name: String, response: JSONObject, id: String? = nil) {
6162
self.name = name
6263
self.response = response
64+
self.id = id
6365
}
6466
}
6567

FirebaseAI/Sources/Types/Internal/Live/AsyncWebSocket.swift

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ final class AsyncWebSocket: NSObject, @unchecked Sendable, URLSessionWebSocketDe
2121
private var continuationFinished = false
2222
private let continuationLock = NSLock()
2323

24-
private var _isConnected = false
25-
private let isConnectedLock = NSLock()
26-
private(set) var isConnected: Bool {
27-
get { isConnectedLock.withLock { _isConnected } }
28-
set { isConnectedLock.withLock { _isConnected = newValue } }
24+
private var _closeError: WebSocketClosedError? = nil
25+
private let closeErrorLock = NSLock()
26+
private(set) var closeError: WebSocketClosedError? {
27+
get { closeErrorLock.withLock { _closeError } }
28+
set { closeErrorLock.withLock { _closeError = newValue } }
2929
}
3030

3131
init(urlSession: URLSession = GenAIURLSession.default, urlRequest: URLRequest) {
@@ -40,44 +40,55 @@ final class AsyncWebSocket: NSObject, @unchecked Sendable, URLSessionWebSocketDe
4040

4141
func connect() -> AsyncThrowingStream<URLSessionWebSocketTask.Message, Error> {
4242
webSocketTask.resume()
43-
isConnected = true
43+
closeError = nil
4444
startReceiving()
4545
return stream
4646
}
4747

4848
func disconnect() {
49-
webSocketTask.cancel(with: .goingAway, reason: nil)
50-
isConnected = false
51-
continuationLock.withLock {
52-
self.continuation.finish()
53-
self.continuationFinished = true
54-
}
49+
if let closeError { return }
50+
51+
close(code: .goingAway, reason: nil)
5552
}
5653

5754
func send(_ message: URLSessionWebSocketTask.Message) async throws {
58-
// TODO: Throw error if socket already closed
55+
if let closeError {
56+
throw closeError
57+
}
5958
try await webSocketTask.send(message)
6059
}
6160

6261
private func startReceiving() {
6362
Task {
64-
while !Task.isCancelled && self.webSocketTask.isOpen && self.isConnected {
65-
let message = try await webSocketTask.receive()
66-
// TODO: Check continuationFinished before yielding. Use the same thread for NSLock.
67-
continuation.yield(message)
63+
while !Task.isCancelled && self.webSocketTask.isOpen && self.closeError == nil {
64+
do {
65+
let message = try await webSocketTask.receive()
66+
continuation.yield(message)
67+
} catch {
68+
close(code: webSocketTask.closeCode, reason: webSocketTask.closeReason)
69+
}
6870
}
6971
}
7072
}
7173

74+
private func close(code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
75+
let error = WebSocketClosedError(closeCode: code, closeReason: reason)
76+
closeError = error
77+
78+
webSocketTask.cancel(with: code, reason: reason)
79+
80+
continuationLock.withLock {
81+
guard !continuationFinished else { return }
82+
self.continuation.finish(throwing: error)
83+
self.continuationFinished = true
84+
}
85+
}
86+
7287
func urlSession(_ session: URLSession,
7388
webSocketTask: URLSessionWebSocketTask,
7489
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
7590
reason: Data?) {
76-
continuationLock.withLock {
77-
guard !continuationFinished else { return }
78-
continuation.finish()
79-
continuationFinished = true
80-
}
91+
close(code: closeCode, reason: reason)
8192
}
8293
}
8394

FirebaseAI/Sources/Types/Internal/Live/BidiGenerateContentRealtimeInput.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ struct BidiGenerateContentRealtimeInput: Encodable {
4343
let audioStreamEnd: Bool?
4444

4545
/// These form the realtime video input stream.
46-
let video: Data?
46+
let video: InlineData?
4747

4848
/// These form the realtime text input stream.
4949
let text: String?
@@ -61,4 +61,15 @@ struct BidiGenerateContentRealtimeInput: Encodable {
6161
/// Marks the end of user activity. This can only be sent if automatic (i.e.
6262
// server-side) activity detection is disabled.
6363
let activityEnd: ActivityEnd?
64+
65+
init(audio: InlineData? = nil, video: InlineData? = nil, text: String? = nil,
66+
activityStart: ActivityStart? = nil, activityEnd: ActivityEnd? = nil,
67+
audioStreamEnd: Bool? = nil) {
68+
self.audio = audio
69+
self.video = video
70+
self.text = text
71+
self.activityStart = activityStart
72+
self.activityEnd = activityEnd
73+
self.audioStreamEnd = audioStreamEnd
74+
}
6475
}

FirebaseAI/Sources/Types/Internal/Live/BidiGenerateContentServerContent.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Foundation
2020
/// Content is generated as quickly as possible, and not in realtime. Clients
2121
/// may choose to buffer and play it out in realtime.
2222
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
23-
struct BidiGenerateContentServerContent: Decodable {
23+
struct BidiGenerateContentServerContent: Decodable, Sendable {
2424
/// The content that the model has generated as part of the current
2525
/// conversation with the user.
2626
let modelTurn: ModelContent?
@@ -50,4 +50,6 @@ struct BidiGenerateContentServerContent: Decodable {
5050

5151
/// Metadata specifies sources used to ground generated content.
5252
let groundingMetadata: GroundingMetadata?
53+
54+
let outputTranscription: BidiGenerateContentTranscription?
5355
}

FirebaseAI/Sources/Types/Internal/Live/BidiGenerateContentServerMessage.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ import Foundation
1616

1717
/// Response message for BidiGenerateContent RPC call.
1818
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
19-
public struct BidiGenerateContentServerMessage: Sendable {
19+
struct BidiGenerateContentServerMessage: Sendable {
2020
// TODO: Make this type `internal`
2121

2222
/// The type of the message.
23-
enum MessageType {
23+
enum MessageType: Sendable {
2424
/// Sent in response to a `BidiGenerateContentSetup` message from the client.
2525
case setupComplete(BidiGenerateContentSetupComplete)
2626

FirebaseAI/Sources/Types/Internal/Live/BidiGenerateContentSetup.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,32 @@ struct BidiGenerateContentSetup: Encodable {
4343
/// knowledge and scope of the model.
4444
let tools: [Tool]?
4545

46+
let toolConfig: ToolConfig?
47+
4648
/// Configures the handling of realtime input.
4749
let realtimeInputConfig: RealtimeInputConfig?
4850

51+
let inputAudioTranscription: AudioTranscriptionConfig?
52+
53+
let outputAudioTranscription: AudioTranscriptionConfig?
54+
4955
init(model: String,
5056
generationConfig: LiveGenerationConfig? = nil,
5157
systemInstruction: ModelContent? = nil,
5258
tools: [Tool]? = nil,
53-
realtimeInputConfig: RealtimeInputConfig? = nil) {
59+
toolConfig: ToolConfig? = nil,
60+
realtimeInputConfig: RealtimeInputConfig? = nil,
61+
inputAudioTranscription: AudioTranscriptionConfig? = nil,
62+
outputAudioTranscription: AudioTranscriptionConfig? = nil) {
5463
self.model = model
5564
self.generationConfig = generationConfig
5665
self.systemInstruction = systemInstruction
5766
self.tools = tools
67+
self.toolConfig = toolConfig
5868
self.realtimeInputConfig = realtimeInputConfig
69+
self.inputAudioTranscription = inputAudioTranscription
70+
self.outputAudioTranscription = outputAudioTranscription
5971
}
6072
}
73+
74+
struct AudioTranscriptionConfig: Encodable {}

0 commit comments

Comments
 (0)