From 627395d7bd7d74c3641224259b21af9310f064fd Mon Sep 17 00:00:00 2001 From: Prax Lannister Date: Tue, 7 Jul 2026 05:27:44 +0530 Subject: [PATCH 1/2] local models: make max output tokens configurable (#246) --- .../Core/AI/OllamaProvider+Networking.swift | 17 +++++-- .../OllamaProviderMaxTokensTests.swift | 46 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift diff --git a/Dayflow/Dayflow/Core/AI/OllamaProvider+Networking.swift b/Dayflow/Dayflow/Core/AI/OllamaProvider+Networking.swift index 362af4f35..bcc2cfa80 100644 --- a/Dayflow/Dayflow/Core/AI/OllamaProvider+Networking.swift +++ b/Dayflow/Dayflow/Core/AI/OllamaProvider+Networking.swift @@ -6,11 +6,21 @@ import Foundation extension OllamaProvider { + /// Default output cap kept low because long generations are slow on local hardware. + static let defaultMaxOutputTokens = 4000 + + /// Override for reasoning models that need more headroom (#246): + /// `defaults write teleportlabs.com.Dayflow llmLocalMaxOutputTokens -int 32000` + static var configuredMaxOutputTokens: Int { + let configured = UserDefaults.standard.integer(forKey: "llmLocalMaxOutputTokens") + return configured > 0 ? configured : defaultMaxOutputTokens + } + struct ChatRequest: Codable { let model: String let messages: [ChatMessage] var temperature: Double = 0.7 - var max_tokens: Int = 4000 + var max_tokens: Int = OllamaProvider.configuredMaxOutputTokens var stream: Bool = false } @@ -231,7 +241,7 @@ extension OllamaProvider { // Helper method for text-only requests func callTextAPI( _ prompt: String, operation: String, expectJSON: Bool = false, batchId: Int64? = nil, - maxRetries: Int = 3, maxTokens: Int = 4000 + maxRetries: Int = 3, maxTokens: Int = OllamaProvider.configuredMaxOutputTokens ) async throws -> String { let systemPrompt = expectJSON @@ -269,7 +279,8 @@ extension OllamaProvider { // MARK: - Text Generation extension OllamaProvider { - func generateText(prompt: String, maxTokens: Int = 4000) async throws + func generateText(prompt: String, maxTokens: Int = OllamaProvider.configuredMaxOutputTokens) + async throws -> (text: String, log: LLMCall) { let callStart = Date() diff --git a/Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift b/Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift new file mode 100644 index 000000000..cae565d33 --- /dev/null +++ b/Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift @@ -0,0 +1,46 @@ +import XCTest + +@testable import Dayflow + +final class OllamaProviderMaxTokensTests: XCTestCase { + private let key = "llmLocalMaxOutputTokens" + private var savedValue: Any? + + override func setUp() { + super.setUp() + savedValue = UserDefaults.standard.object(forKey: key) + UserDefaults.standard.removeObject(forKey: key) + } + + override func tearDown() { + UserDefaults.standard.removeObject(forKey: key) + if let savedValue { + UserDefaults.standard.set(savedValue, forKey: key) + } + savedValue = nil + super.tearDown() + } + + func testMaxOutputTokensDefaultsTo4000WhenUnset() { + XCTAssertEqual(OllamaProvider.configuredMaxOutputTokens, 4000) + } + + func testMaxOutputTokensReadsUserDefaultsOverride() { + UserDefaults.standard.set(32000, forKey: key) + XCTAssertEqual(OllamaProvider.configuredMaxOutputTokens, 32000) + } + + func testMaxOutputTokensFallsBackToDefaultForInvalidValues() { + UserDefaults.standard.set(0, forKey: key) + XCTAssertEqual(OllamaProvider.configuredMaxOutputTokens, 4000) + + UserDefaults.standard.set(-100, forKey: key) + XCTAssertEqual(OllamaProvider.configuredMaxOutputTokens, 4000) + } + + func testChatRequestUsesConfiguredMaxOutputTokens() { + UserDefaults.standard.set(16000, forKey: key) + let request = OllamaProvider.ChatRequest(model: "test-model", messages: []) + XCTAssertEqual(request.max_tokens, 16000) + } +} From 9ebc0bb36482543241244140ee80f6f91c9d1b89 Mon Sep 17 00:00:00 2001 From: Prax Lannister Date: Wed, 8 Jul 2026 05:47:48 +0530 Subject: [PATCH 2/2] test: add edge-case coverage for configurable max output tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up hardening pass. Adds 4 tests covering gaps in the original suite: - minimum positive override (exactly 1) pins the `configured > 0` exclusive- zero guard against a future `>= 0` regression - large reasoning-model override (131072) passes through unclamped, both via configuredMaxOutputTokens and into the ChatRequest — the #246 use case - default constant matches the unset fallback (catches drift if one changes) All 8 tests pass (4 original + 4 new); no production code changed. --- .../OllamaProviderMaxTokensTests.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift b/Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift index cae565d33..9e729d37e 100644 --- a/Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift +++ b/Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift @@ -43,4 +43,33 @@ final class OllamaProviderMaxTokensTests: XCTestCase { let request = OllamaProvider.ChatRequest(model: "test-model", messages: []) XCTAssertEqual(request.max_tokens, 16000) } + + // Boundary: exactly 1 is the smallest value the `configured > 0` guard + // treats as a real override (0 falls back, per the test above). Pins the + // guard's exclusive-zero semantics so a future `>= 0` regression is caught. + func testMaxOutputTokensAcceptsMinimumPositiveOverride() { + UserDefaults.standard.set(1, forKey: key) + XCTAssertEqual(OllamaProvider.configuredMaxOutputTokens, 1) + } + + // Reasoning models (the #246 use case) can legitimately need very large + // caps. Confirm a large override passes through unclamped, both via the + // computed property and into the ChatRequest the network layer sends. + func testMaxOutputTokensPassesLargeReasoningModelOverrideThrough() { + UserDefaults.standard.set(131_072, forKey: key) + XCTAssertEqual(OllamaProvider.configuredMaxOutputTokens, 131_072) + let request = OllamaProvider.ChatRequest(model: "test-model", messages: []) + XCTAssertEqual(request.max_tokens, 131_072) + } + + // The default constant and the fallback path must agree — guards against a + // future edit that changes one but not the other. + func testDefaultConstantMatchesUnsetFallback() { + XCTAssertEqual(OllamaProvider.defaultMaxOutputTokens, 4000) + UserDefaults.standard.removeObject(forKey: key) + XCTAssertEqual( + OllamaProvider.configuredMaxOutputTokens, + OllamaProvider.defaultMaxOutputTokens + ) + } }