Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix(ai): skip unsupported parts in ModelContent instead of crashing
Addresses review feedback: replace the fatalError() in ModelContent.init(role:parts:) for unrecognized Part types with an AILog error and skip the part. Part is a public protocol that apps can conform to, and the internal ErrorPart (returned for failed image conversions) also reached this branch, so both are runtime-reachable paths. ErrorPart is handled explicitly with its own message code.
  • Loading branch information
paulb777 committed Oct 1, 2026
commit 94168bf577a0b0a659cc883eef97f527d569b583
6 changes: 4 additions & 2 deletions FirebaseAI/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Unreleased
- [fixed] Fixed crashes when a chat session's history contained parts with
unrecognized data or code execution parts received from the server, and when
decoding a malformed duration from the server. (#16728)
unrecognized data or code execution parts received from the server, when
creating a `ModelContent` with an unsupported `Part` type or an image that
failed to convert, and when decoding a malformed duration from the server.
(#16728)

# 13.0.0
- [changed] **Breaking Change**: Renamed `Tool` to `GenerativeModel.Tool` to
Expand Down
2 changes: 2 additions & 0 deletions FirebaseAI/Sources/AILog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ enum AILog {
case invalidWebsocketURL = 4004
case duplicateLiveSessionSetupComplete = 4005
case malformedURL = 4006
case modelContentUnsupportedPartType = 4007
case modelContentPartConversionFailed = 4008

// SDK Debugging
case loadRequestStreamResponseLine = 5000
Expand Down
10 changes: 9 additions & 1 deletion FirebaseAI/Sources/ModelContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,16 @@ public struct ModelContent: Equatable, Sendable {
isThought: codeExecutionResultPart._isThought,
thoughtSignature: codeExecutionResultPart.thoughtSignature
))
case let errorPart as ErrorPart:
AILog.error(
code: .modelContentPartConversionFailed,
"Skipping a part that failed to convert to model content: \(errorPart.error)"
)
Comment thread
andrewheard marked this conversation as resolved.
Outdated
default:
fatalError()
AILog.error(
code: .modelContentUnsupportedPartType,
"Skipping a part with unsupported type: \(type(of: part))"
)
}
}
internalParts = convertedParts
Expand Down
38 changes: 38 additions & 0 deletions FirebaseAI/Tests/Unit/PartTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,46 @@ final class PartTests: XCTestCase {
""")
}

// MARK: - ModelContent Conversion

func testModelContent_unsupportedPartType_isSkippedAndLogged() throws {
var loggedCodes: [AILog.MessageCode] = []
AILog.logInterceptor = { _, code, _ in
loggedCodes.append(code)
}
defer { AILog.logInterceptor = nil }

let content = ModelContent(parts: [TextPart("Hello"), CustomPart(), TextPart("world")])

XCTAssertEqual(content.parts.count, 2)
XCTAssertEqual(content.parts.compactMap { ($0 as? TextPart)?.text }, ["Hello", "world"])
XCTAssertEqual(loggedCodes, [.modelContentUnsupportedPartType])
}

func testModelContent_errorPart_isSkippedAndLogged() throws {
var loggedCodes: [AILog.MessageCode] = []
AILog.logInterceptor = { _, code, _ in
loggedCodes.append(code)
}
defer { AILog.logInterceptor = nil }

let content = ModelContent(parts: [
TextPart("Hello"),
ErrorPart(ImageConversionError.couldNotConvertToJPEG),
])

XCTAssertEqual(content.parts.count, 1)
XCTAssertEqual((content.parts.first as? TextPart)?.text, "Hello")
XCTAssertEqual(loggedCodes, [.modelContentPartConversionFailed])
}

// MARK: - Helpers

/// A `Part` conformance defined outside of the SDK.
private struct CustomPart: Part {
var isThought: Bool { false }
}

private static func bundle() -> Bundle {
#if SWIFT_PACKAGE
return Bundle.module
Expand Down
Loading