Skip to content

Commit 10678f6

Browse files
authored
[Firebase AI] Add CodeExecution tool support (#15280)
1 parent a18f810 commit 10678f6

14 files changed

+799
-9
lines changed

FirebaseAI/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# 12.3.0
2+
- [feature] Added support for the Code Execution tool, which enables the model
3+
to generate and run code to perform complex tasks like solving mathematical
4+
equations or visualizing data. (#15280)
25
- [fixed] Fixed a decoding error when generating images with the
36
`gemini-2.5-flash-image-preview` model using `generateContentStream` or
47
`sendMessageStream` with the Gemini Developer API. (#15262)

FirebaseAI/Sources/AILog.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ enum AILog {
6363
case generateContentResponseUnrecognizedContentModality = 3012
6464
case decodedUnsupportedImagenPredictionType = 3013
6565
case decodedUnsupportedPartData = 3014
66+
case codeExecutionResultUnrecognizedOutcome = 3015
67+
case executableCodeUnrecognizedLanguage = 3016
68+
case fallbackValueUsed = 3017
6669

6770
// SDK State Errors
6871
case generateContentResponseNoCandidates = 4000
@@ -124,4 +127,32 @@ enum AILog {
124127
static func additionalLoggingEnabled() -> Bool {
125128
return ProcessInfo.processInfo.arguments.contains(enableArgumentKey)
126129
}
130+
131+
/// Returns the unwrapped optional value if non-nil or returns the fallback value and logs.
132+
///
133+
/// This convenience method is intended for use in place of `optionalValue ?? fallbackValue` with
134+
/// the addition of logging on use of the fallback value.
135+
///
136+
/// - Parameters:
137+
/// - optionalValue: The value to unwrap.
138+
/// - fallbackValue: The fallback (default) value to return when `optionalValue` is `nil`.
139+
/// - level: The logging level to use for fallback messages; defaults to
140+
/// `FirebaseLoggerLevel.warning`.
141+
/// - code: The message code to use for fallback messages; defaults to
142+
/// `MessageCode.fallbackValueUsed`.
143+
/// - caller: The name of the unwrapped value; defaults to the name of the computed property or
144+
/// function name from which the unwrapping occurred.
145+
static func safeUnwrap<T>(_ optionalValue: T?,
146+
fallback fallbackValue: T,
147+
level: FirebaseLoggerLevel = .warning,
148+
code: MessageCode = .fallbackValueUsed,
149+
caller: String = #function) -> T {
150+
guard let unwrappedValue = optionalValue else {
151+
AILog.log(level: level, code: code, """
152+
No value specified for '\(caller)' (\(T.self)); using fallback value '\(fallbackValue)'.
153+
""")
154+
return fallbackValue
155+
}
156+
return unwrappedValue
157+
}
127158
}

FirebaseAI/Sources/ModelContent.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ struct InternalPart: Equatable, Sendable {
3939
case fileData(FileData)
4040
case functionCall(FunctionCall)
4141
case functionResponse(FunctionResponse)
42+
case executableCode(ExecutableCode)
43+
case codeExecutionResult(CodeExecutionResult)
4244

4345
struct UnsupportedDataError: Error {
4446
let decodingError: DecodingError
@@ -93,6 +95,16 @@ public struct ModelContent: Equatable, Sendable {
9395
return FunctionResponsePart(
9496
functionResponse, isThought: part.isThought, thoughtSignature: part.thoughtSignature
9597
)
98+
case let .executableCode(executableCode):
99+
return ExecutableCodePart(
100+
executableCode, isThought: part.isThought, thoughtSignature: part.thoughtSignature
101+
)
102+
case let .codeExecutionResult(codeExecutionResult):
103+
return CodeExecutionResultPart(
104+
codeExecutionResult: codeExecutionResult,
105+
isThought: part.isThought,
106+
thoughtSignature: part.thoughtSignature
107+
)
96108
case .none:
97109
// Filter out parts that contain missing or unrecognized data
98110
return nil
@@ -212,6 +224,8 @@ extension InternalPart.OneOfData: Codable {
212224
case fileData
213225
case functionCall
214226
case functionResponse
227+
case executableCode
228+
case codeExecutionResult
215229
}
216230

217231
public func encode(to encoder: Encoder) throws {
@@ -227,6 +241,10 @@ extension InternalPart.OneOfData: Codable {
227241
try container.encode(functionCall, forKey: .functionCall)
228242
case let .functionResponse(functionResponse):
229243
try container.encode(functionResponse, forKey: .functionResponse)
244+
case let .executableCode(executableCode):
245+
try container.encode(executableCode, forKey: .executableCode)
246+
case let .codeExecutionResult(codeExecutionResult):
247+
try container.encode(codeExecutionResult, forKey: .codeExecutionResult)
230248
}
231249
}
232250

@@ -242,6 +260,12 @@ extension InternalPart.OneOfData: Codable {
242260
self = try .functionCall(values.decode(FunctionCall.self, forKey: .functionCall))
243261
} else if values.contains(.functionResponse) {
244262
self = try .functionResponse(values.decode(FunctionResponse.self, forKey: .functionResponse))
263+
} else if values.contains(.executableCode) {
264+
self = try .executableCode(values.decode(ExecutableCode.self, forKey: .executableCode))
265+
} else if values.contains(.codeExecutionResult) {
266+
self = try .codeExecutionResult(
267+
values.decode(CodeExecutionResult.self, forKey: .codeExecutionResult)
268+
)
245269
} else {
246270
let unexpectedKeys = values.allKeys.map { $0.stringValue }
247271
throw UnsupportedDataError(decodingError: DecodingError.dataCorrupted(

FirebaseAI/Sources/Tool.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,18 @@ public struct GoogleSearch: Sendable {
7171
public struct Tool: Sendable {
7272
/// A list of `FunctionDeclarations` available to the model.
7373
let functionDeclarations: [FunctionDeclaration]?
74+
7475
/// Specifies the Google Search configuration.
7576
let googleSearch: GoogleSearch?
7677

77-
init(functionDeclarations: [FunctionDeclaration]?) {
78-
self.functionDeclarations = functionDeclarations
79-
googleSearch = nil
80-
}
78+
let codeExecution: CodeExecution?
8179

82-
init(googleSearch: GoogleSearch) {
80+
init(functionDeclarations: [FunctionDeclaration]? = nil,
81+
googleSearch: GoogleSearch? = nil,
82+
codeExecution: CodeExecution? = nil) {
83+
self.functionDeclarations = functionDeclarations
8384
self.googleSearch = googleSearch
84-
functionDeclarations = nil
85+
self.codeExecution = codeExecution
8586
}
8687

8788
/// Creates a tool that allows the model to perform function calling.
@@ -126,6 +127,13 @@ public struct Tool: Sendable {
126127
public static func googleSearch(_ googleSearch: GoogleSearch = GoogleSearch()) -> Tool {
127128
return self.init(googleSearch: googleSearch)
128129
}
130+
131+
/// Creates a tool that allows the model to execute code.
132+
///
133+
/// For more details, see ``CodeExecution``.
134+
public static func codeExecution() -> Tool {
135+
return self.init(codeExecution: CodeExecution())
136+
}
129137
}
130138

131139
/// Configuration for specifying function calling behavior.

FirebaseAI/Sources/Types/Internal/InternalPart.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,54 @@ struct FunctionResponse: Codable, Equatable, Sendable {
6363
}
6464
}
6565

66+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
67+
struct ExecutableCode: Codable, Equatable, Sendable {
68+
struct Language: CodableProtoEnum, Sendable, Equatable {
69+
enum Kind: String {
70+
case unspecified = "LANGUAGE_UNSPECIFIED"
71+
case python = "PYTHON"
72+
}
73+
74+
let rawValue: String
75+
76+
static let unrecognizedValueMessageCode =
77+
AILog.MessageCode.executableCodeUnrecognizedLanguage
78+
}
79+
80+
let language: Language?
81+
let code: String?
82+
83+
init(language: Language, code: String) {
84+
self.language = language
85+
self.code = code
86+
}
87+
}
88+
89+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
90+
struct CodeExecutionResult: Codable, Equatable, Sendable {
91+
struct Outcome: CodableProtoEnum, Sendable, Equatable {
92+
enum Kind: String {
93+
case unspecified = "OUTCOME_UNSPECIFIED"
94+
case ok = "OUTCOME_OK"
95+
case failed = "OUTCOME_FAILED"
96+
case deadlineExceeded = "OUTCOME_DEADLINE_EXCEEDED"
97+
}
98+
99+
let rawValue: String
100+
101+
static let unrecognizedValueMessageCode =
102+
AILog.MessageCode.codeExecutionResultUnrecognizedOutcome
103+
}
104+
105+
let outcome: Outcome?
106+
let output: String?
107+
108+
init(outcome: Outcome, output: String) {
109+
self.outcome = outcome
110+
self.output = output
111+
}
112+
}
113+
66114
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
67115
struct ErrorPart: Part, Error {
68116
let error: Error

FirebaseAI/Sources/Types/Public/Part.swift

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,118 @@ public struct FunctionResponsePart: Part {
202202
self.thoughtSignature = thoughtSignature
203203
}
204204
}
205+
206+
/// A part containing code that was executed by the model.
207+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
208+
public struct ExecutableCodePart: Part {
209+
/// The language of the code in an ``ExecutableCodePart``.
210+
public struct Language: Sendable, Equatable, CustomStringConvertible {
211+
let internalLanguage: ExecutableCode.Language
212+
213+
/// The Python programming language.
214+
public static let python = ExecutableCodePart.Language(ExecutableCode.Language(kind: .python))
215+
216+
public var description: String { internalLanguage.rawValue }
217+
218+
init(_ language: ExecutableCode.Language) {
219+
internalLanguage = language
220+
}
221+
}
222+
223+
let executableCode: ExecutableCode
224+
let _isThought: Bool?
225+
let thoughtSignature: String?
226+
227+
/// The language of the code.
228+
public var language: ExecutableCodePart.Language {
229+
ExecutableCodePart.Language(
230+
// Fallback to "LANGUAGE_UNSPECIFIED" if the value is ever omitted by the backend; this should
231+
// never happen.
232+
AILog.safeUnwrap(
233+
executableCode.language, fallback: ExecutableCode.Language(kind: .unspecified)
234+
)
235+
)
236+
}
237+
238+
/// The code that was executed.
239+
public var code: String {
240+
// Fallback to empty string if `code` is ever omitted by the backend; this should never happen.
241+
AILog.safeUnwrap(executableCode.code, fallback: "")
242+
}
243+
244+
public var isThought: Bool { _isThought ?? false }
245+
246+
public init(language: ExecutableCodePart.Language, code: String) {
247+
self.init(
248+
ExecutableCode(language: language.internalLanguage, code: code),
249+
isThought: nil,
250+
thoughtSignature: nil
251+
)
252+
}
253+
254+
init(_ executableCode: ExecutableCode, isThought: Bool?, thoughtSignature: String?) {
255+
self.executableCode = executableCode
256+
_isThought = isThought
257+
self.thoughtSignature = thoughtSignature
258+
}
259+
}
260+
261+
/// The result of executing code.
262+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
263+
public struct CodeExecutionResultPart: Part {
264+
/// The outcome of a code execution.
265+
public struct Outcome: Sendable, Equatable, CustomStringConvertible {
266+
let internalOutcome: CodeExecutionResult.Outcome
267+
268+
/// The code executed without errors.
269+
public static let ok = CodeExecutionResultPart.Outcome(CodeExecutionResult.Outcome(kind: .ok))
270+
271+
/// The code failed to execute.
272+
public static let failed =
273+
CodeExecutionResultPart.Outcome(CodeExecutionResult.Outcome(kind: .failed))
274+
275+
/// The code took too long to execute.
276+
public static let deadlineExceeded =
277+
CodeExecutionResultPart.Outcome(CodeExecutionResult.Outcome(kind: .deadlineExceeded))
278+
279+
public var description: String { internalOutcome.rawValue }
280+
281+
init(_ outcome: CodeExecutionResult.Outcome) {
282+
internalOutcome = outcome
283+
}
284+
}
285+
286+
let codeExecutionResult: CodeExecutionResult
287+
let _isThought: Bool?
288+
let thoughtSignature: String?
289+
290+
/// The outcome of the code execution.
291+
public var outcome: CodeExecutionResultPart.Outcome {
292+
CodeExecutionResultPart.Outcome(
293+
// Fallback to "OUTCOME_UNSPECIFIED" if this value is ever omitted by the backend; this should
294+
// never happen.
295+
AILog.safeUnwrap(
296+
codeExecutionResult.outcome, fallback: CodeExecutionResult.Outcome(kind: .unspecified)
297+
)
298+
)
299+
}
300+
301+
/// The output of the code execution.
302+
public var output: String? { codeExecutionResult.output }
303+
304+
public var isThought: Bool { _isThought ?? false }
305+
306+
public init(outcome: CodeExecutionResultPart.Outcome, output: String) {
307+
self.init(
308+
codeExecutionResult: CodeExecutionResult(outcome: outcome.internalOutcome, output: output),
309+
isThought: nil,
310+
thoughtSignature: nil
311+
)
312+
}
313+
314+
init(codeExecutionResult: CodeExecutionResult, isThought: Bool?, thoughtSignature: String?) {
315+
self.codeExecutionResult = codeExecutionResult
316+
_isThought = isThought
317+
self.thoughtSignature = thoughtSignature
318+
}
319+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/// A tool that allows the model to execute code.
16+
///
17+
/// This tool can be used to solve complex problems, for example, by generating and executing Python
18+
/// code to solve a math problem.
19+
public struct CodeExecution: Sendable, Encodable {
20+
init() {}
21+
}

FirebaseAI/Tests/TestApp/Sources/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public enum ModelNames {
2626
public static let gemini2FlashPreviewImageGeneration = "gemini-2.0-flash-preview-image-generation"
2727
public static let gemini2_5_FlashImagePreview = "gemini-2.5-flash-image-preview"
2828
public static let gemini2_5_Flash = "gemini-2.5-flash"
29+
public static let gemini2_5_FlashLite = "gemini-2.5-flash-lite"
2930
public static let gemini2_5_Pro = "gemini-2.5-pro"
3031
public static let gemma3_4B = "gemma-3-4b-it"
3132
}

FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,35 @@ struct GenerateContentIntegrationTests {
424424
}
425425
}
426426

427+
@Test(arguments: InstanceConfig.allConfigs)
428+
func generateContent_codeExecution_succeeds(_ config: InstanceConfig) async throws {
429+
let model = FirebaseAI.componentInstance(config).generativeModel(
430+
modelName: ModelNames.gemini2_5_FlashLite,
431+
generationConfig: generationConfig,
432+
tools: [.codeExecution()]
433+
)
434+
let prompt = """
435+
What is the sum of the first 5 prime numbers? Generate and run code for the calculation.
436+
"""
437+
438+
let response = try await model.generateContent(prompt)
439+
440+
let candidate = try #require(response.candidates.first)
441+
let executableCodeParts = candidate.content.parts.compactMap { $0 as? ExecutableCodePart }
442+
#expect(executableCodeParts.count == 1)
443+
let executableCodePart = try #require(executableCodeParts.first)
444+
#expect(executableCodePart.language == .python)
445+
#expect(executableCodePart.code.contains("sum"))
446+
let codeExecutionResults = candidate.content.parts.compactMap { $0 as? CodeExecutionResultPart }
447+
#expect(codeExecutionResults.count == 1)
448+
let codeExecutionResultPart = try #require(codeExecutionResults.first)
449+
#expect(codeExecutionResultPart.outcome == .ok)
450+
let output = try #require(codeExecutionResultPart.output)
451+
#expect(output.contains("28")) // 2 + 3 + 5 + 7 + 11 = 28
452+
let text = try #require(response.text)
453+
#expect(text.contains("28"))
454+
}
455+
427456
// MARK: Streaming Tests
428457

429458
@Test(arguments: [

0 commit comments

Comments
 (0)