diff --git a/Sources/AgentRunKit/LLM/GeminiClient.swift b/Sources/AgentRunKit/LLM/GeminiClient.swift index 7c6967d..8c8b62c 100644 --- a/Sources/AgentRunKit/LLM/GeminiClient.swift +++ b/Sources/AgentRunKit/LLM/GeminiClient.swift @@ -133,10 +133,12 @@ extension GeminiClient { let level = effortToLevel(config.effort) let budget = config.budgetTokens + // Gemini requires exactly one of thinkingBudget or thinkingLevel. + // If an explicit budget is set, prefer it; otherwise use the level. return GeminiThinkingConfig( includeThoughts: true, thinkingBudget: budget, - thinkingLevel: level + thinkingLevel: budget == nil ? level : nil ) } diff --git a/Sources/AgentRunKit/LLM/GeminiClientTypes.swift b/Sources/AgentRunKit/LLM/GeminiClientTypes.swift index 8258e59..27a8514 100644 --- a/Sources/AgentRunKit/LLM/GeminiClientTypes.swift +++ b/Sources/AgentRunKit/LLM/GeminiClientTypes.swift @@ -151,6 +151,17 @@ struct GeminiThinkingConfig: Encodable { let includeThoughts: Bool let thinkingBudget: Int? let thinkingLevel: String? + + private enum CodingKeys: String, CodingKey { + case includeThoughts, thinkingBudget, thinkingLevel + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(includeThoughts, forKey: .includeThoughts) + try container.encodeIfPresent(thinkingBudget, forKey: .thinkingBudget) + try container.encodeIfPresent(thinkingLevel, forKey: .thinkingLevel) + } } struct GeminiResponse: Decodable { diff --git a/Tests/AgentRunKitTests/GeminiClientTests.swift b/Tests/AgentRunKitTests/GeminiClientTests.swift index 612009a..ca8aff3 100644 --- a/Tests/AgentRunKitTests/GeminiClientTests.swift +++ b/Tests/AgentRunKitTests/GeminiClientTests.swift @@ -135,6 +135,7 @@ struct GeminiRequestSerializationTests { let thinking = genConfig?["thinkingConfig"] as? [String: Any] #expect(thinking?["includeThoughts"] as? Bool == true) #expect(thinking?["thinkingLevel"] as? String == "HIGH") + #expect(thinking?["thinkingBudget"] == nil) } @Test @@ -167,6 +168,22 @@ struct GeminiRequestSerializationTests { let thinking = genConfig?["thinkingConfig"] as? [String: Any] #expect(thinking?["includeThoughts"] as? Bool == true) #expect(thinking?["thinkingBudget"] as? Int == 10000) + #expect(thinking?["thinkingLevel"] == nil) + } + + @Test + func effortWithBudgetSendsOnlyBudget() throws { + // Simulates what ProviderService produces: effort + budgetTokens. + let config = ReasoningConfig(effort: .medium, budgetTokens: 32000) + let client = makeClient(reasoningConfig: config) + let request = try client.buildRequest(messages: [.user("Hi")], tools: []) + let json = try encodeRequest(request) + + let genConfig = json["generationConfig"] as? [String: Any] + let thinking = genConfig?["thinkingConfig"] as? [String: Any] + #expect(thinking?["includeThoughts"] as? Bool == true) + #expect(thinking?["thinkingBudget"] as? Int == 32000) + #expect(thinking?["thinkingLevel"] == nil) } @Test