Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions Dayflow/Dayflow/Core/AI/OllamaProvider+Networking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
75 changes: 75 additions & 0 deletions Dayflow/DayflowTests/OllamaProviderMaxTokensTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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)
}

// 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
)
}
}