From 50132b3fc32db92d284b536d0b1cb1af7e052c16 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Sun, 27 Apr 2025 13:49:17 -0400 Subject: [PATCH 1/4] [Firebase AI] Return default proto values with `$outputDefaults` --- FirebaseAI/Sources/GenerativeAIService.swift | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/FirebaseAI/Sources/GenerativeAIService.swift b/FirebaseAI/Sources/GenerativeAIService.swift index e1538af997f..fe284fbcd06 100644 --- a/FirebaseAI/Sources/GenerativeAIService.swift +++ b/FirebaseAI/Sources/GenerativeAIService.swift @@ -167,7 +167,7 @@ struct GenerativeAIService { // MARK: - Private Helpers private func urlRequest(request: T) async throws -> URLRequest { - var urlRequest = URLRequest(url: request.url) + var urlRequest = try URLRequest(url: requestURL(request: request)) urlRequest.httpMethod = "POST" urlRequest.setValue(firebaseInfo.apiKey, forHTTPHeaderField: "x-goog-api-key") urlRequest.setValue( @@ -207,6 +207,29 @@ struct GenerativeAIService { return urlRequest } + private func requestURL(request: T) throws -> URL { + guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL, userInfo: [ + NSLocalizedDescriptionKey: "Invalid Request URL: \(request.url)", + ]) + } + var urlQueryItems = urlComponents.queryItems ?? [] + + // The query parameter `$outputDefaults` forces the backend to output proto default values for + // JSON responses instead of omitting them. See + // https://cloud.google.com/apis/docs/system-parameters#definitions for more details. + urlQueryItems.append(URLQueryItem(name: "$outputDefaults", value: "true")) + + urlComponents.queryItems = urlQueryItems + guard let url = urlComponents.url else { + throw URLError(.badURL, userInfo: [ + NSLocalizedDescriptionKey: "Invalid Request URL with components: \(urlComponents)", + ]) + } + + return url + } + private func httpResponse(urlResponse: URLResponse) throws -> HTTPURLResponse { // The following condition should always be true: "Whenever you make HTTP URL load requests, any // response objects you get back from the URLSession, NSURLConnection, or NSURLDownload class From 3a207b86e99597235403b0a2560934086045cb06 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Sun, 27 Apr 2025 13:51:54 -0400 Subject: [PATCH 2/4] Update token count to fix `testCountTokens_functionCalling` --- .../Tests/TestApp/Tests/Integration/IntegrationTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift b/FirebaseAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift index 9b62326dfd6..72b23a44e33 100644 --- a/FirebaseAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift +++ b/FirebaseAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift @@ -190,12 +190,12 @@ final class IntegrationTests: XCTestCase { ModelContent(role: "function", parts: sumResponse), ]) - XCTAssertEqual(response.totalTokens, 24) + XCTAssertEqual(response.totalTokens, 30) XCTAssertEqual(response.totalBillableCharacters, 71) XCTAssertEqual(response.promptTokensDetails.count, 1) let promptTokensDetails = try XCTUnwrap(response.promptTokensDetails.first) XCTAssertEqual(promptTokensDetails.modality, .text) - XCTAssertEqual(promptTokensDetails.tokenCount, 24) + XCTAssertEqual(promptTokensDetails.tokenCount, response.totalTokens) } func testCountTokens_appCheckNotConfigured_shouldFail() async throws { From dcf145fbf95feadd88b3f12c06dcfa93fd96d8a1 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Sun, 27 Apr 2025 14:41:03 -0400 Subject: [PATCH 3/4] Add internal `unspecified` enums to prevent unknown enum value logs --- FirebaseAI/Sources/GenerateContentResponse.swift | 16 +++++++++------- FirebaseAI/Sources/GenerativeModel.swift | 3 ++- FirebaseAI/Sources/Safety.swift | 3 +++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/FirebaseAI/Sources/GenerateContentResponse.swift b/FirebaseAI/Sources/GenerateContentResponse.swift index 8f8026ef376..a806bf67254 100644 --- a/FirebaseAI/Sources/GenerateContentResponse.swift +++ b/FirebaseAI/Sources/GenerateContentResponse.swift @@ -171,6 +171,7 @@ public struct Citation: Sendable { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct FinishReason: DecodableProtoEnum, Hashable, Sendable { enum Kind: String { + case unspecified = "FINISH_REASON_UNSPECIFIED" case stop = "STOP" case maxTokens = "MAX_TOKENS" case safety = "SAFETY" @@ -366,16 +367,17 @@ extension Candidate: Decodable { } } - if let safetyRatings = try container.decodeIfPresent( - [SafetyRating].self, - forKey: .safetyRatings - ) { - self.safetyRatings = safetyRatings - } else { - safetyRatings = [] + let safetyRatings = try container + .decodeIfPresent([SafetyRating].self, forKey: .safetyRatings) ?? [] + // Filter out safety ratings where the category and probability are both unspecified by the + // backend. + self.safetyRatings = safetyRatings.filter { + $0.category.rawValue != HarmCategory.Kind.unspecified.rawValue + && $0.probability.rawValue != SafetyRating.HarmProbability.Kind.unspecified.rawValue } finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason) + .flatMap { $0.rawValue == FinishReason.Kind.unspecified.rawValue ? nil : $0 } citationMetadata = try container.decodeIfPresent( CitationMetadata.self, diff --git a/FirebaseAI/Sources/GenerativeModel.swift b/FirebaseAI/Sources/GenerativeModel.swift index a9ebef87b8b..7bda1ea3e18 100644 --- a/FirebaseAI/Sources/GenerativeModel.swift +++ b/FirebaseAI/Sources/GenerativeModel.swift @@ -167,7 +167,8 @@ public final class GenerativeModel: Sendable { } // Check to see if an error should be thrown for stop reason. - if let reason = response.candidates.first?.finishReason, reason != .stop { + if let reason = response.candidates.first?.finishReason, reason != .stop, + reason.rawValue != FinishReason.Kind.unspecified.rawValue { throw GenerateContentError.responseStoppedEarly(reason: reason, response: response) } diff --git a/FirebaseAI/Sources/Safety.swift b/FirebaseAI/Sources/Safety.swift index a4b28402cd4..ae19e6ad773 100644 --- a/FirebaseAI/Sources/Safety.swift +++ b/FirebaseAI/Sources/Safety.swift @@ -78,6 +78,7 @@ public struct SafetyRating: Equatable, Hashable, Sendable { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct HarmProbability: DecodableProtoEnum, Hashable, Sendable { enum Kind: String { + case unspecified = "HARM_CATEGORY_UNSPECIFIED" case negligible = "NEGLIGIBLE" case low = "LOW" case medium = "MEDIUM" @@ -114,6 +115,7 @@ public struct SafetyRating: Equatable, Hashable, Sendable { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct HarmSeverity: DecodableProtoEnum, Hashable, Sendable { enum Kind: String { + case unspecified = "HARM_SEVERITY_UNSPECIFIED" case negligible = "HARM_SEVERITY_NEGLIGIBLE" case low = "HARM_SEVERITY_LOW" case medium = "HARM_SEVERITY_MEDIUM" @@ -234,6 +236,7 @@ public struct SafetySetting: Sendable { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct HarmCategory: CodableProtoEnum, Hashable, Sendable { enum Kind: String { + case unspecified = "HARM_CATEGORY_UNSPECIFIED" case harassment = "HARM_CATEGORY_HARASSMENT" case hateSpeech = "HARM_CATEGORY_HATE_SPEECH" case sexuallyExplicit = "HARM_CATEGORY_SEXUALLY_EXPLICIT" From c7a9ee03f9601e4c6b2b96d906e37a349da9dc78 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Sun, 27 Apr 2025 14:42:50 -0400 Subject: [PATCH 4/4] Re-enable Firebase proxy testing for prod and staging --- .../Integration/GenerateContentIntegrationTests.swift | 5 ++--- .../Tests/TestApp/Tests/Utilities/InstanceConfig.swift | 10 ++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift b/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift index b40112dd9c3..d9813b30550 100644 --- a/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift +++ b/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift @@ -116,9 +116,8 @@ struct GenerateContentIntegrationTests { @Test(arguments: [ InstanceConfig.vertexV1Beta, - // TODO(andrewheard): Configs temporarily disabled due to backend issue. - // InstanceConfig.developerV1Beta, - // InstanceConfig.developerV1BetaStaging + InstanceConfig.developerV1Beta, + InstanceConfig.developerV1BetaStaging, InstanceConfig.developerV1BetaSpark, ]) func generateImage(_ config: InstanceConfig) async throws { diff --git a/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift b/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift index a0f2279dbcb..86d024c7a87 100644 --- a/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift +++ b/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift @@ -51,9 +51,8 @@ struct InstanceConfig { vertexV1Staging, vertexV1Beta, vertexV1BetaStaging, - // TODO(andrewheard): Configs temporarily disabled due to backend issue: - // developerV1Beta, - // developerV1BetaStaging, + developerV1Beta, + developerV1BetaStaging, developerV1Spark, developerV1BetaSpark, ] @@ -63,9 +62,8 @@ struct InstanceConfig { vertexV1Staging, vertexV1Beta, vertexV1BetaStaging, - // TODO(andrewheard): Configs temporarily disabled due to backend issue: - // developerV1Beta, - // developerV1BetaStaging, + developerV1Beta, + developerV1BetaStaging, developerV1BetaSpark, ]