Skip to content

Commit 5fb80bc

Browse files
authored
Revert "Revert "Add custom JSON mode generation option for Gemini language mo…" (#84)
This reverts commit eca7b8f.
1 parent e28bf43 commit 5fb80bc

File tree

2 files changed

+94
-4
lines changed

2 files changed

+94
-4
lines changed

Sources/AnyLanguageModel/Models/GeminiLanguageModel.swift

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,47 @@ public struct GeminiLanguageModel: LanguageModel {
8888
/// (URL context), code execution, and location services (Google Maps).
8989
public var serverTools: [ServerTool]?
9090

91+
/// Configures JSON mode for structured output.
92+
///
93+
/// Use this type to enable JSON mode,
94+
/// which constrains the model to output a valid JSON.
95+
/// Optionally provide a schema for typed JSON output.
96+
public enum JSONMode: Sendable, Hashable, ExpressibleByBooleanLiteral {
97+
/// JSON mode is disabled (default text output).
98+
case disabled
99+
100+
/// JSON mode is enabled without a schema constraint.
101+
case enabled
102+
103+
/// JSON mode is enabled with a schema constraint for typed output.
104+
case schema(JSONSchema)
105+
106+
public init(booleanLiteral value: Bool) {
107+
self = value ? .enabled : .disabled
108+
}
109+
}
110+
111+
/// The JSON mode configuration for structured output.
112+
///
113+
/// When set to `.enabled`, the model will output valid JSON.
114+
/// When set to `.schema(_:)`, the model will output JSON
115+
/// conforming to the provided schema.
116+
public var jsonMode: JSONMode?
117+
91118
/// Creates custom generation options for Gemini models.
92119
///
93120
/// - Parameters:
94121
/// - thinking: The thinking mode configuration. When `nil`, uses the model's default.
95122
/// - serverTools: Server-side tools to enable. When `nil`, uses the model's default.
123+
/// - jsonMode: The JSON mode configuration. When `nil`, uses the model's default.
96124
public init(
97125
thinking: Thinking? = nil,
98-
serverTools: [ServerTool]? = nil
126+
serverTools: [ServerTool]? = nil,
127+
jsonMode: JSONMode? = nil
99128
) {
100129
self.thinking = thinking
101130
self.serverTools = serverTools
131+
self.jsonMode = jsonMode
102132
}
103133
}
104134

@@ -240,6 +270,7 @@ public struct GeminiLanguageModel: LanguageModel {
240270
let customOptions = options[custom: GeminiLanguageModel.self]
241271
let effectiveThinking = customOptions?.thinking ?? _thinking
242272
let effectiveServerTools = customOptions?.serverTools ?? _serverTools
273+
let effectiveJsonMode = customOptions?.jsonMode
243274

244275
let url =
245276
baseURL
@@ -257,7 +288,8 @@ public struct GeminiLanguageModel: LanguageModel {
257288
contents: transcript.toGeminiContent(),
258289
tools: geminiTools,
259290
options: options,
260-
thinking: effectiveThinking
291+
thinking: effectiveThinking,
292+
jsonMode: effectiveJsonMode
261293
)
262294

263295
let body = try JSONEncoder().encode(params)
@@ -326,6 +358,7 @@ public struct GeminiLanguageModel: LanguageModel {
326358
let customOptions = options[custom: GeminiLanguageModel.self]
327359
let effectiveThinking = customOptions?.thinking ?? _thinking
328360
let effectiveServerTools = customOptions?.serverTools ?? _serverTools
361+
let effectiveJsonMode = customOptions?.jsonMode
329362

330363
var streamURL =
331364
baseURL
@@ -346,7 +379,8 @@ public struct GeminiLanguageModel: LanguageModel {
346379
contents: session.transcript.toGeminiContent(),
347380
tools: geminiTools,
348381
options: options,
349-
thinking: effectiveThinking
382+
thinking: effectiveThinking,
383+
jsonMode: effectiveJsonMode
350384
)
351385

352386
let body = try JSONEncoder().encode(params)
@@ -431,7 +465,8 @@ private func createGenerateContentParams(
431465
contents: [GeminiContent],
432466
tools: [GeminiTool]?,
433467
options: GenerationOptions,
434-
thinking: GeminiLanguageModel.CustomGenerationOptions.Thinking
468+
thinking: GeminiLanguageModel.CustomGenerationOptions.Thinking,
469+
jsonMode: GeminiLanguageModel.CustomGenerationOptions.JSONMode?
435470
) throws -> [String: JSONValue] {
436471
var params: [String: JSONValue] = [
437472
"contents": try JSONValue(contents)
@@ -471,6 +506,18 @@ private func createGenerateContentParams(
471506
}
472507
generationConfig["thinkingConfig"] = .object(thinkingConfig)
473508

509+
if let jsonMode {
510+
switch jsonMode {
511+
case .disabled:
512+
break
513+
case .enabled:
514+
generationConfig["responseMimeType"] = .string("application/json")
515+
case .schema(let schema):
516+
generationConfig["responseMimeType"] = .string("application/json")
517+
generationConfig["responseSchema"] = try JSONValue(schema)
518+
}
519+
}
520+
474521
if !generationConfig.isEmpty {
475522
params["generationConfig"] = .object(generationConfig)
476523
}

Tests/AnyLanguageModelTests/GeminiLanguageModelTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import JSONSchema
23
import Testing
34

45
@testable import AnyLanguageModel
@@ -134,4 +135,46 @@ struct GeminiLanguageModelTests {
134135
)
135136
#expect(!response.content.isEmpty)
136137
}
138+
139+
@Test func jsonModeEnabled() async throws {
140+
let session = LanguageModelSession(model: model)
141+
142+
var options = GenerationOptions()
143+
options[custom: GeminiLanguageModel.self] = .init(
144+
thinking: .disabled,
145+
jsonMode: true
146+
)
147+
148+
let response = try await session.respond(
149+
to: "Return a JSON object with a 'greeting' key and value 'hello'",
150+
options: options
151+
)
152+
#expect(response.content.contains("greeting"))
153+
#expect(response.content.contains("hello"))
154+
}
155+
156+
@Test func jsonModeWithSchema() async throws {
157+
let session = LanguageModelSession(model: model)
158+
159+
let schema = JSONSchema.object(
160+
properties: [
161+
"name": .string(),
162+
"age": .integer(),
163+
],
164+
required: ["name", "age"]
165+
)
166+
167+
var options = GenerationOptions()
168+
options[custom: GeminiLanguageModel.self] = .init(
169+
thinking: .disabled,
170+
jsonMode: .schema(schema)
171+
)
172+
173+
let response = try await session.respond(
174+
to: "Generate a person with name 'Alice' and age 30",
175+
options: options
176+
)
177+
#expect(response.content.contains("Alice"))
178+
#expect(response.content.contains("30"))
179+
}
137180
}

0 commit comments

Comments
 (0)