Skip to content

Commit eb58fa3

Browse files
authored
Add support for Gemini structured outputs (#115)
1 parent 6acf519 commit eb58fa3

File tree

3 files changed

+162
-8
lines changed

3 files changed

+162
-8
lines changed

README.md

+73
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,79 @@ Use the file URL returned from the snippet above.
16351635
}
16361636

16371637

1638+
### How to use structured ouputs with Gemini
1639+
1640+
```swift
1641+
import AIProxy
1642+
1643+
/* Uncomment for BYOK use cases */
1644+
// let geminiService = AIProxy.geminiDirectService(
1645+
// unprotectedAPIKey: "your-gemini-key"
1646+
// )
1647+
1648+
/* Uncomment for all other production use cases */
1649+
// let geminiService = AIProxy.geminiService(
1650+
// partialKey: "partial-key-from-your-developer-dashboard",
1651+
// serviceURL: "service-url-from-your-developer-dashboard"
1652+
// )
1653+
1654+
let schema: [String: AIProxyJSONValue] = [
1655+
"description": "List of recipes",
1656+
"type": "array",
1657+
"items": [
1658+
"type": "object",
1659+
"properties": [
1660+
"recipeName": [
1661+
"type": "string",
1662+
"description": "Name of the recipe",
1663+
"nullable": false
1664+
]
1665+
],
1666+
"required": ["recipeName"]
1667+
]
1668+
]
1669+
do {
1670+
let requestBody = GeminiGenerateContentRequestBody(
1671+
contents: [
1672+
.init(
1673+
parts: [
1674+
.text("List a few popular cookie recipes."),
1675+
]
1676+
)
1677+
],
1678+
generationConfig: .init(
1679+
responseMimeType: "application/json",
1680+
responseSchema: schema
1681+
)
1682+
)
1683+
let response = try await geminiService.generateContentRequest(
1684+
body: requestBody,
1685+
model: "gemini-2.0-flash"
1686+
)
1687+
for part in response.candidates?.first?.content?.parts ?? [] {
1688+
if case .text(let text) = part {
1689+
print("Gemini sent: \(text)")
1690+
}
1691+
}
1692+
if let usage = response.usageMetadata {
1693+
print(
1694+
"""
1695+
Used:
1696+
\(usage.promptTokenCount ?? 0) prompt tokens
1697+
\(usage.cachedContentTokenCount ?? 0) cached tokens
1698+
\(usage.candidatesTokenCount ?? 0) candidate tokens
1699+
\(usage.totalTokenCount ?? 0) total tokens
1700+
"""
1701+
)
1702+
}
1703+
} catch AIProxyError.unsuccessfulRequest(let statusCode, let responseBody) {
1704+
print("Received \(statusCode) status code with response body: \(responseBody)")
1705+
} catch {
1706+
print("Could not create Gemini generate content request: \(error.localizedDescription)")
1707+
}
1708+
```
1709+
1710+
16381711
***
16391712

16401713

Sources/AIProxy/Gemini/GeminiGenerateContentRequestBody.swift

+8-8
Original file line numberDiff line numberDiff line change
@@ -418,15 +418,15 @@ extension GeminiGenerateContentRequestBody {
418418
}
419419

420420
private enum CodingKeys: String, CodingKey {
421-
case maxOutputTokens = "max_output_tokens"
421+
case maxOutputTokens
422422
case temperature
423-
case topP = "top_p"
424-
case topK = "top_k"
425-
case presencePenalty = "presence_penalty"
426-
case frequencyPenalty = "frequency_penalty"
427-
case responseModalities = "response_modalities"
428-
case responseMimeType = "response_mime_type"
429-
case responseSchema = "response_schema"
423+
case topP
424+
case topK
425+
case presencePenalty
426+
case frequencyPenalty
427+
case responseModalities
428+
case responseMimeType
429+
case responseSchema
430430
}
431431
}
432432
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// GeminiStructuredOutputsRequestTests.swift
3+
// AIProxy
4+
//
5+
// Created by Lou Zell on 3/15/25.
6+
//
7+
8+
import XCTest
9+
import Foundation
10+
@testable import AIProxy
11+
12+
13+
final class GeminiStructuredOutputsRequestTests: XCTestCase {
14+
// This example is from generative-ai-js/samples/controlled_generation.js
15+
func testRequestIsEncodableToJson() throws {
16+
let schema: [String: AIProxyJSONValue] = [
17+
"description": "List of recipes",
18+
"type": "array",
19+
"items": [
20+
"type": "object",
21+
"properties": [
22+
"recipeName": [
23+
"type": "string",
24+
"description": "Name of the recipe",
25+
"nullable": false
26+
]
27+
],
28+
"required": ["recipeName"]
29+
]
30+
]
31+
32+
let requestBody = GeminiGenerateContentRequestBody(
33+
contents: [
34+
.init(
35+
parts: [.text("List a few popular cookie recipes.")],
36+
role: "user"
37+
)
38+
],
39+
generationConfig: .init(
40+
responseMimeType: "application/json",
41+
responseSchema: schema
42+
)
43+
)
44+
XCTAssertEqual(#"""
45+
{
46+
"contents" : [
47+
{
48+
"parts" : [
49+
{
50+
"text" : "List a few popular cookie recipes."
51+
}
52+
],
53+
"role" : "user"
54+
}
55+
],
56+
"generationConfig" : {
57+
"responseMimeType" : "application\/json",
58+
"responseSchema" : {
59+
"description" : "List of recipes",
60+
"items" : {
61+
"properties" : {
62+
"recipeName" : {
63+
"description" : "Name of the recipe",
64+
"nullable" : false,
65+
"type" : "string"
66+
}
67+
},
68+
"required" : [
69+
"recipeName"
70+
],
71+
"type" : "object"
72+
},
73+
"type" : "array"
74+
}
75+
}
76+
}
77+
"""#,
78+
try requestBody.serialize(pretty: true)
79+
)
80+
}
81+
}

0 commit comments

Comments
 (0)