Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 17 additions & 15 deletions FirebaseAI/Sources/GenerationConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public struct GenerationConfig: Sendable {
/// Output schema of the generated response in [JSON Schema](https://json-schema.org/) format.
///
/// If set, `responseSchema` must be omitted and `responseMIMEType` is required.
var responseJSONSchema: JSONSchema?
var responseFirebaseGenerationSchema: FirebaseGenerationSchema?

/// Supported modalities of the response.
var responseModalities: [ResponseModality]?
Expand Down Expand Up @@ -180,14 +180,15 @@ public struct GenerationConfig: Sendable {
self.stopSequences = stopSequences
self.responseMIMEType = responseMIMEType
self.responseSchema = responseSchema
responseJSONSchema = nil
responseFirebaseGenerationSchema = nil
self.responseModalities = responseModalities
self.thinkingConfig = thinkingConfig
}

init(temperature: Float? = nil, topP: Float? = nil, topK: Int? = nil, candidateCount: Int? = nil,
maxOutputTokens: Int? = nil, presencePenalty: Float? = nil, frequencyPenalty: Float? = nil,
stopSequences: [String]? = nil, responseMIMEType: String, responseJSONSchema: JSONSchema,
stopSequences: [String]? = nil, responseMIMEType: String,
responseFirebaseGenerationSchema: FirebaseGenerationSchema,
responseModalities: [ResponseModality]? = nil, thinkingConfig: ThinkingConfig? = nil) {
self.temperature = temperature
self.topP = topP
Expand All @@ -199,7 +200,7 @@ public struct GenerationConfig: Sendable {
self.stopSequences = stopSequences
self.responseMIMEType = responseMIMEType
responseSchema = nil
self.responseJSONSchema = responseJSONSchema
self.responseFirebaseGenerationSchema = responseFirebaseGenerationSchema
self.responseModalities = responseModalities
self.thinkingConfig = thinkingConfig
}
Expand Down Expand Up @@ -240,13 +241,13 @@ public struct GenerationConfig: Sendable {
config.responseModalities = overrideConfig.responseModalities ?? config.responseModalities
config.thinkingConfig = overrideConfig.thinkingConfig ?? config.thinkingConfig

// 5. Handle Schema mutual exclusivity with precedence for `responseJSONSchema`.
if let responseJSONSchema = overrideConfig.responseJSONSchema {
config.responseJSONSchema = responseJSONSchema
// 5. Handle Schema mutual exclusivity with precedence for `responseFirebaseGenerationSchema`.
if let responseFirebaseGenerationSchema = overrideConfig.responseFirebaseGenerationSchema {
config.responseFirebaseGenerationSchema = responseFirebaseGenerationSchema
config.responseSchema = nil
} else if let responseSchema = overrideConfig.responseSchema {
config.responseSchema = responseSchema
config.responseJSONSchema = nil
config.responseFirebaseGenerationSchema = nil
}

return config
Expand All @@ -257,17 +258,18 @@ public struct GenerationConfig: Sendable {
/// - Parameters:
/// - base: The foundational configuration (e.g., model defaults).
/// - overrides: The configuration containing overrides (e.g., request specific).
/// - jsonSchema: The JSON schema to enforce on the output.
/// - firebaseGenerationSchema: The JSON schema to enforce on the output.
/// - Returns: A non-nil `GenerationConfig` with the merged values and JSON constraints applied.
static func merge(_ base: GenerationConfig?,
with overrides: GenerationConfig?,
enforcingJSONSchema jsonSchema: JSONSchema) -> GenerationConfig {
enforcingFirebaseGenerationSchema firebaseGenerationSchema: FirebaseGenerationSchema)
-> GenerationConfig {
// 1. Merge base and overrides, defaulting to a fresh config if both are nil.
var config = GenerationConfig.merge(base, with: overrides) ?? GenerationConfig()

// 2. Enforce the specific constraints for JSON Schema generation.
config.responseMIMEType = "application/json"
config.responseJSONSchema = jsonSchema
config.responseFirebaseGenerationSchema = firebaseGenerationSchema
config.responseSchema = nil // Clear conflicting legacy schema

// 3. Clear incompatible or conflicting options.
Expand All @@ -293,7 +295,7 @@ extension GenerationConfig: Encodable {
case stopSequences
case responseMIMEType = "responseMimeType"
case responseSchema
case responseJSONSchema = "responseJsonSchema"
case responseFirebaseGenerationSchema = "responseJsonSchema"
case responseModalities
case thinkingConfig
}
Expand All @@ -310,10 +312,10 @@ extension GenerationConfig: Encodable {
try container.encodeIfPresent(stopSequences, forKey: .stopSequences)
try container.encodeIfPresent(responseMIMEType, forKey: .responseMIMEType)
try container.encodeIfPresent(responseSchema, forKey: .responseSchema)
if let responseJSONSchema = responseJSONSchema {
if let responseFirebaseGenerationSchema {
let schemaEncoder = SchemaEncoder(target: .gemini)
let jsonSchema = try schemaEncoder.encode(responseJSONSchema)
try container.encode(jsonSchema, forKey: .responseJSONSchema)
let firebaseGenerationSchema = try schemaEncoder.encode(responseFirebaseGenerationSchema)
try container.encode(firebaseGenerationSchema, forKey: .responseFirebaseGenerationSchema)
}
try container.encodeIfPresent(responseModalities, forKey: .responseModalities)
try container.encodeIfPresent(thinkingConfig, forKey: .thinkingConfig)
Expand Down
54 changes: 34 additions & 20 deletions FirebaseAI/Sources/GenerativeModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,12 @@ public final class GenerativeModel: Sendable {

@discardableResult
public final nonisolated(nonsending)
func respond(to prompt: any PartsRepresentable, schema: JSONSchema,
func respond(to prompt: any PartsRepresentable, schema: FirebaseGenerationSchema,
includeSchemaInPrompt: Bool = true, options: GenerationConfig? = nil)
async throws -> GenerativeModel.Response<ModelOutput> {
async throws -> GenerativeModel.Response<FirebaseGeneratedContent> {
return try await respond(
to: prompt,
generating: ModelOutput.self,
generating: FirebaseGeneratedContent.self,
schema: schema,
includeSchemaInPrompt: includeSchemaInPrompt,
options: options
Expand All @@ -217,7 +217,7 @@ public final class GenerativeModel: Sendable {
return try await respond(
to: prompt,
generating: type,
schema: type.jsonSchema,
schema: type.firebaseGenerationSchema,
includeSchemaInPrompt: includeSchemaInPrompt,
options: options
)
Expand All @@ -230,11 +230,12 @@ public final class GenerativeModel: Sendable {
includeSchemaInPrompt: false, options: options)
}

public final func streamResponse(to prompt: any PartsRepresentable, schema: JSONSchema,
public final func streamResponse(to prompt: any PartsRepresentable,
schema: FirebaseGenerationSchema,
includeSchemaInPrompt: Bool = true,
options: GenerationConfig? = nil)
-> sending GenerativeModel.ResponseStream<ModelOutput> {
return streamResponse(to: prompt, generating: ModelOutput.self, schema: schema,
-> sending GenerativeModel.ResponseStream<FirebaseGeneratedContent> {
return streamResponse(to: prompt, generating: FirebaseGeneratedContent.self, schema: schema,
includeSchemaInPrompt: includeSchemaInPrompt, options: options)
}

Expand All @@ -243,7 +244,7 @@ public final class GenerativeModel: Sendable {
includeSchemaInPrompt: Bool = true,
options: GenerationConfig? = nil)
-> sending GenerativeModel.ResponseStream<Content> where Content: FirebaseGenerable {
return streamResponse(to: prompt, generating: type, schema: type.jsonSchema,
return streamResponse(to: prompt, generating: type, schema: type.firebaseGenerationSchema,
includeSchemaInPrompt: includeSchemaInPrompt, options: options)
}
#endif // compiler(>=6.2)
Expand Down Expand Up @@ -455,15 +456,15 @@ public final class GenerativeModel: Sendable {
#if compiler(>=6.2)
final nonisolated(nonsending)
func respond<Content>(to prompt: any PartsRepresentable, generating type: Content.Type,
schema: JSONSchema?, includeSchemaInPrompt: Bool,
schema: FirebaseGenerationSchema?, includeSchemaInPrompt: Bool,
options: GenerationConfig?)
async throws -> GenerativeModel.Response<Content> where Content: FirebaseGenerable {
let parts = [ModelContent(parts: prompt)]

let generationConfig: GenerationConfig?
if let schema {
generationConfig = GenerationConfig.merge(
self.generationConfig, with: options, enforcingJSONSchema: schema
self.generationConfig, with: options, enforcingFirebaseGenerationSchema: schema
)
} else {
generationConfig = GenerationConfig.merge(self.generationConfig, with: options)
Expand All @@ -475,15 +476,23 @@ public final class GenerativeModel: Sendable {
throw GenerationError.decodingFailure(.init(debugDescription: "No text in response."))
}
let responseID = response.responseID.map { ResponseID(responseID: $0) }
let modelOutput: ModelOutput
let firebaseGeneratedContent: FirebaseGeneratedContent
if schema == nil {
modelOutput = ModelOutput(kind: .string(text), id: responseID, isComplete: true)
firebaseGeneratedContent = FirebaseGeneratedContent(
kind: .string(text),
id: responseID,
isComplete: true
)
} else {
modelOutput = try ModelOutput(json: text, id: responseID, streaming: false)
firebaseGeneratedContent = try FirebaseGeneratedContent(
json: text,
id: responseID,
streaming: false
)
}
return try GenerativeModel.Response<Content>(
content: Content(modelOutput),
rawContent: modelOutput,
content: Content(firebaseGeneratedContent),
rawContent: firebaseGeneratedContent,
rawResponse: response
)
} catch let error as GenerationError {
Expand All @@ -499,15 +508,16 @@ public final class GenerativeModel: Sendable {
}

final func streamResponse<Content>(to prompt: any PartsRepresentable,
generating type: Content.Type, schema: JSONSchema?,
generating type: Content.Type,
schema: FirebaseGenerationSchema?,
includeSchemaInPrompt: Bool, options: GenerationConfig?)
-> sending GenerativeModel.ResponseStream<Content> where Content: FirebaseGenerable {
let parts = [ModelContent(parts: prompt)]

let generationConfig: GenerationConfig?
if let schema {
generationConfig = GenerationConfig.merge(
self.generationConfig, with: options, enforcingJSONSchema: schema
self.generationConfig, with: options, enforcingFirebaseGenerationSchema: schema
)
} else {
generationConfig = GenerationConfig.merge(self.generationConfig, with: options)
Expand All @@ -521,11 +531,15 @@ public final class GenerativeModel: Sendable {
if let text = response.text {
json += text
let responseID = response.responseID.map { ResponseID(responseID: $0) }
let modelOutput = try ModelOutput(json: json, id: responseID, streaming: true)
let firebaseGeneratedContent = try FirebaseGeneratedContent(
json: json,
id: responseID,
streaming: true
)
try await context.yield(
GenerativeModel.ResponseStream<Content>.Snapshot(
content: Content.Partial(modelOutput),
rawContent: modelOutput,
content: Content.Partial(firebaseGeneratedContent),
rawContent: firebaseGeneratedContent,
rawResponse: response
)
)
Expand Down
18 changes: 9 additions & 9 deletions FirebaseAI/Sources/JSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,24 +101,24 @@ extension JSONValue: Encodable {
extension JSONValue: Equatable {}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
extension JSONValue: ConvertibleToModelOutput {
public var modelOutput: ModelOutput {
extension JSONValue: ConvertibleToFirebaseGeneratedContent {
public var firebaseGeneratedContent: FirebaseGeneratedContent {
switch self {
case .null:
return ModelOutput(kind: .null)
return FirebaseGeneratedContent(kind: .null)
case let .number(value):
return ModelOutput(kind: .number(value))
return FirebaseGeneratedContent(kind: .number(value))
case let .string(value):
return ModelOutput(kind: .string(value))
return FirebaseGeneratedContent(kind: .string(value))
case let .bool(value):
return ModelOutput(kind: .bool(value))
return FirebaseGeneratedContent(kind: .bool(value))
case let .object(dictionary):
return ModelOutput(kind: .structure(
properties: dictionary.mapValues(ModelOutput.init),
return FirebaseGeneratedContent(kind: .structure(
properties: dictionary.mapValues(FirebaseGeneratedContent.init),
orderedKeys: dictionary.keys.map { $0 }
))
case let .array(values):
return ModelOutput(kind: .array(values.map(ModelOutput.init)))
return FirebaseGeneratedContent(kind: .array(values.map(FirebaseGeneratedContent.init)))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@
#endif // canImport(FoundationModels)

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ConvertibleFromModelOutput {
init(_ content: ModelOutput) throws
public protocol ConvertibleFromFirebaseGeneratedContent {
init(_ content: FirebaseGeneratedContent) throws
}

#if canImport(FoundationModels)
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public extension ConvertibleFromModelOutput where Self: ConvertibleFromGeneratedContent {
init(_ content: ModelOutput) throws {
public extension ConvertibleFromFirebaseGeneratedContent
where Self: ConvertibleFromGeneratedContent {
init(_ content: FirebaseGeneratedContent) throws {
try self.init(content.generatedContent)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@

#if compiler(>=6.2)
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ConvertibleToModelOutput: SendableMetatype {
var modelOutput: ModelOutput { get }
public protocol ConvertibleToFirebaseGeneratedContent: SendableMetatype {
var firebaseGeneratedContent: FirebaseGeneratedContent { get }
}
#else
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ConvertibleToModelOutput {
var modelOutput: ModelOutput { get }
public protocol ConvertibleToFirebaseGeneratedContent {
var firebaseGeneratedContent: FirebaseGeneratedContent { get }
}
#endif // compiler(>=6.2)

#if canImport(FoundationModels)
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public extension ConvertibleToModelOutput where Self: ConvertibleToGeneratedContent {
var modelOutput: ModelOutput {
generatedContent.modelOutput
public extension ConvertibleToFirebaseGeneratedContent where Self: ConvertibleToGeneratedContent {
var firebaseGeneratedContent: FirebaseGeneratedContent {
generatedContent.firebaseGeneratedContent
}
}
#endif // canImport(FoundationModels)
Loading
Loading