From 1a51f9495bd508f22868b1e0de6ab65795deb10d Mon Sep 17 00:00:00 2001 From: sukru tikves Date: Sun, 20 Sep 2026 09:37:18 -0700 Subject: [PATCH 1/3] Add reasoning_effort control to the LLM server Adds an OpenAI-compatible reasoning_effort field to the chat completions request and a --reasoning-default server flag, so callers can set the thinking budget of reasoning models. The value maps to chat-template keyword arguments (reasoning_effort, reasoning_strength, enable_thinking) and is applied through applyChatTemplate(additionalContext:). --no-thinking becomes an alias for --reasoning-default none. A request that omits the field keeps the template's own default. --- .../CoreAILMCommon/ReasoningEffort.swift | 44 ++++++++++++++++ .../CoreAILMCommon/ServerAPITypes.swift | 5 ++ .../Tools/llm-server/ChatHandler.swift | 17 +++++-- .../Tools/llm-server/LLMServerMain.swift | 9 +++- .../Tools/llm-server/ServerState.swift | 2 + .../ReasoningEffortTests.swift | 50 +++++++++++++++++++ 6 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 swift/Sources/CoreAILMCommon/ReasoningEffort.swift create mode 100644 swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift diff --git a/swift/Sources/CoreAILMCommon/ReasoningEffort.swift b/swift/Sources/CoreAILMCommon/ReasoningEffort.swift new file mode 100644 index 00000000..46310110 --- /dev/null +++ b/swift/Sources/CoreAILMCommon/ReasoningEffort.swift @@ -0,0 +1,44 @@ +// Copyright 2026 Apple Inc. +// +// Use of this source code is governed by a BSD-3-clause license that can +// be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause + +import Foundation + +/// Resolves a reasoning-effort value into chat-template keyword arguments, which are then passed to +/// `applyChatTemplate(additionalContext:)`. +/// +/// Reasoning models expose their thinking budget through different chat-template variables (for +/// example `reasoning_effort`, `reasoning_strength`, or a boolean `enable_thinking`). Binding the +/// canonical value to each known variable lets one request field drive them all: a chat template +/// reads only the variables it references, so setting the others alongside is safe. +public enum ReasoningEffort { + /// Canonical value that requests no reasoning. + public static let none = "none" + + /// Maps a canonical effort value to chat-template keyword arguments. + /// + /// - `nil` or empty returns an empty dictionary, so the template keeps its own default. + /// - `"none"` sets `enable_thinking` to `false` for templates that support disabling reasoning. + /// - any level (for example `low`, `medium`, `high`) binds the level to `reasoning_effort` and + /// `reasoning_strength`, and sets `enable_thinking` to `true`. + public static func templateContext(_ effort: String?) -> [String: any Sendable] { + guard let value = effort?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { + return [:] + } + if value.lowercased() == none { + return ["enable_thinking": false] + } + return [ + "reasoning_effort": value, + "reasoning_strength": value, + "enable_thinking": true, + ] + } + + /// Resolves the effort for a request: the per-request value takes precedence, then the server + /// default, then `nil` (leaving the template default in place). + public static func resolve(request: String?, default defaultEffort: String?) -> String? { + request ?? defaultEffort + } +} diff --git a/swift/Sources/CoreAILMCommon/ServerAPITypes.swift b/swift/Sources/CoreAILMCommon/ServerAPITypes.swift index 399c1e79..fb8d3069 100644 --- a/swift/Sources/CoreAILMCommon/ServerAPITypes.swift +++ b/swift/Sources/CoreAILMCommon/ServerAPITypes.swift @@ -22,6 +22,9 @@ public struct ChatCompletionRequest: Decodable, Sendable { public let toolChoice: ToolChoice? public let parallelToolCalls: Bool? public let raw: Bool? + /// Reasoning-effort control, OpenAI-compatible. Canonical values are none, low, medium, high; + /// other values pass through to the model's chat template. + public let reasoningEffort: String? enum CodingKeys: String, CodingKey { case model, messages, temperature, stream, stop, tools, raw @@ -32,6 +35,7 @@ public struct ChatCompletionRequest: Decodable, Sendable { case responseFormat = "response_format" case toolChoice = "tool_choice" case parallelToolCalls = "parallel_tool_calls" + case reasoningEffort = "reasoning_effort" } public init(from decoder: Decoder) throws { @@ -49,6 +53,7 @@ public struct ChatCompletionRequest: Decodable, Sendable { toolChoice = try container.decodeIfPresent(ToolChoice.self, forKey: .toolChoice) parallelToolCalls = try container.decodeIfPresent(Bool.self, forKey: .parallelToolCalls) raw = try container.decodeIfPresent(Bool.self, forKey: .raw) + reasoningEffort = try container.decodeIfPresent(String.self, forKey: .reasoningEffort) if let arr = try? container.decode([String].self, forKey: .stop) { stop = arr diff --git a/swift/Sources/Tools/llm-server/ChatHandler.swift b/swift/Sources/Tools/llm-server/ChatHandler.swift index 2820c4f3..7fec4ef1 100644 --- a/swift/Sources/Tools/llm-server/ChatHandler.swift +++ b/swift/Sources/Tools/llm-server/ChatHandler.swift @@ -186,7 +186,10 @@ private func handleNonStreamingRequest(chatRequest: ChatCompletionRequest, state minP: nil ) - let promptTokens = tokenizeMessages(chatRequest.messages, tools: chatRequest.tools, state: state) + let reasoningEffort = ReasoningEffort.resolve( + request: chatRequest.reasoningEffort, default: state.config.defaultReasoningEffort) + let promptTokens = tokenizeMessages( + chatRequest.messages, tools: chatRequest.tools, reasoningEffort: reasoningEffort, state: state) let stopSequences = buildStopSequences(from: chatRequest, state: state) let input: Input = .tokens(promptTokens) @@ -358,7 +361,10 @@ private func handleStreamingRequest( minP: nil ) - let promptTokens = tokenizeMessages(chatRequest.messages, tools: chatRequest.tools, state: state) + let reasoningEffort = ReasoningEffort.resolve( + request: chatRequest.reasoningEffort, default: state.config.defaultReasoningEffort) + let promptTokens = tokenizeMessages( + chatRequest.messages, tools: chatRequest.tools, reasoningEffort: reasoningEffort, state: state) let stopSequences = buildStopSequences(from: chatRequest, state: state) let input: Input = .tokens(promptTokens) @@ -556,7 +562,8 @@ private func handleStreamingRequest( // MARK: - Helpers private func tokenizeMessages( - _ messages: [ChatMessage], tools: [ToolDefinition]? = nil, state: ServerState + _ messages: [ChatMessage], tools: [ToolDefinition]? = nil, + reasoningEffort: String? = nil, state: ServerState ) -> [Int] { var templateMessages: [[String: any Sendable]] = [] for msg in messages { @@ -608,8 +615,10 @@ private func tokenizeMessages( } do { + let additionalContext = ReasoningEffort.templateContext(reasoningEffort) let tokens = try state.tokenizer.applyChatTemplate( - messages: templateMessages, tools: toolSpecs) + messages: templateMessages, tools: toolSpecs, + additionalContext: additionalContext.isEmpty ? nil : additionalContext) return tokens } catch { CLILogger.log("applyChatTemplate failed: \(error)", component: "Server") diff --git a/swift/Sources/Tools/llm-server/LLMServerMain.swift b/swift/Sources/Tools/llm-server/LLMServerMain.swift index 41cd72fb..df17fd86 100644 --- a/swift/Sources/Tools/llm-server/LLMServerMain.swift +++ b/swift/Sources/Tools/llm-server/LLMServerMain.swift @@ -68,9 +68,15 @@ struct LLMServer: AsyncParsableCommand { @Option(name: .customLong("max-queue-depth"), help: "Max requests queued before returning 429 (default: 16)") var maxQueueDepth: Int = 16 - @Flag(name: .customLong("no-thinking"), help: "Disable thinking/reasoning (appends /no_think or sets template)") + @Flag(name: .customLong("no-thinking"), help: "Disable thinking/reasoning (alias for --reasoning-default none)") var noThinking: Bool = false + @Option( + name: .customLong("reasoning-default"), + help: "Default reasoning_effort when a request omits it (none, low, medium, high)." + ) + var reasoningDefault: String? + @Flag( name: .customLong("clear-coreai-cache"), help: "Clear Core AI cached specialization for this model before loading (forces re-specialization)" @@ -194,6 +200,7 @@ struct LLMServer: AsyncParsableCommand { defaultTopK: topK, defaultMinP: minP, noThinking: noThinking, + defaultReasoningEffort: reasoningDefault ?? (noThinking ? ReasoningEffort.none : nil), supportsLogprobs: supportsLogprobs, maxContextLength: bundle.maxContextLength, vocabSize: bundle.vocabSize, diff --git a/swift/Sources/Tools/llm-server/ServerState.swift b/swift/Sources/Tools/llm-server/ServerState.swift index 8b0cf1ca..7b9b0fba 100644 --- a/swift/Sources/Tools/llm-server/ServerState.swift +++ b/swift/Sources/Tools/llm-server/ServerState.swift @@ -20,6 +20,8 @@ struct ServerConfig: Sendable { let defaultTopK: Int? let defaultMinP: Double? let noThinking: Bool + /// Default `reasoning_effort` applied when a request omits it. `nil` leaves the template default. + let defaultReasoningEffort: String? let supportsLogprobs: Bool let maxContextLength: Int let vocabSize: Int? diff --git a/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift b/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift new file mode 100644 index 00000000..d26bf0bb --- /dev/null +++ b/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift @@ -0,0 +1,50 @@ +// Copyright 2026 Apple Inc. +// +// Use of this source code is governed by a BSD-3-clause license that can +// be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause + +import Foundation +import Testing + +@testable import CoreAILMCommon + +@Suite("Reasoning Effort") +struct ReasoningEffortTests { + @Test("An absent or empty effort injects no template context") + func emptyInjectsNothing() { + #expect(ReasoningEffort.templateContext(nil).isEmpty) + #expect(ReasoningEffort.templateContext("").isEmpty) + } + + @Test("none disables thinking") + func noneDisablesThinking() { + let context = ReasoningEffort.templateContext("none") + #expect((context["enable_thinking"] as? Bool) == false) + } + + @Test("A level binds each reasoning variable") + func levelBindsVariables() { + for level in ["low", "medium", "high"] { + let context = ReasoningEffort.templateContext(level) + #expect((context["reasoning_effort"] as? String) == level) + #expect((context["reasoning_strength"] as? String) == level) + #expect((context["enable_thinking"] as? Bool) == true) + } + } + + @Test("A request value takes precedence over the default") + func precedence() { + #expect(ReasoningEffort.resolve(request: "high", default: "low") == "high") + #expect(ReasoningEffort.resolve(request: nil, default: "low") == "low") + #expect(ReasoningEffort.resolve(request: nil, default: nil) == nil) + } + + @Test("reasoning_effort decodes from a chat completion request") + func requestDecodes() throws { + let json = """ + {"messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high"} + """.data(using: .utf8)! + let request = try JSONDecoder().decode(ChatCompletionRequest.self, from: json) + #expect(request.reasoningEffort == "high") + } +} From d1012935b735f4124c4934371a0d51a67e0e66d6 Mon Sep 17 00:00:00 2001 From: sukru tikves Date: Tue, 22 Sep 2026 06:58:07 -0700 Subject: [PATCH 2/3] Unify no-think mechanisms behind resolved reasoning effort Make --no-thinking an alias for --reasoning-default none and reject the contradictory combination of --no-thinking with a non-none default. Gate the legacy /no_think literal injection on the resolved effort being none instead of an independent noThinking flag, so the two paths cannot disagree. Drop the ungrounded reasoning_strength template var (no template in the repo consumes it); keep reasoning_effort and enable_thinking. Add resolution/precedence tests for disablesThinking and resolveDefault, including the contradictory-flags case. --- .../CoreAILMCommon/ReasoningEffort.swift | 38 ++++++++++++++++--- .../Tools/llm-server/ChatHandler.swift | 7 +++- .../Tools/llm-server/LLMServerMain.swift | 13 +++++-- .../Tools/llm-server/ServerState.swift | 3 +- .../ReasoningEffortTests.swift | 28 +++++++++++++- 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/swift/Sources/CoreAILMCommon/ReasoningEffort.swift b/swift/Sources/CoreAILMCommon/ReasoningEffort.swift index 46310110..05539d04 100644 --- a/swift/Sources/CoreAILMCommon/ReasoningEffort.swift +++ b/swift/Sources/CoreAILMCommon/ReasoningEffort.swift @@ -5,13 +5,26 @@ import Foundation +/// Thrown when server flags request contradictory reasoning defaults. +public enum ReasoningEffortError: Error, CustomStringConvertible { + /// `--no-thinking` was combined with a non-`none` `--reasoning-default`. + case contradiction(default: String) + + public var description: String { + switch self { + case .contradiction(let value): + return "--no-thinking conflicts with --reasoning-default \(value); --no-thinking means none" + } + } +} + /// Resolves a reasoning-effort value into chat-template keyword arguments, which are then passed to /// `applyChatTemplate(additionalContext:)`. /// /// Reasoning models expose their thinking budget through different chat-template variables (for -/// example `reasoning_effort`, `reasoning_strength`, or a boolean `enable_thinking`). Binding the -/// canonical value to each known variable lets one request field drive them all: a chat template -/// reads only the variables it references, so setting the others alongside is safe. +/// example `reasoning_effort` or a boolean `enable_thinking`). Binding the canonical value to each +/// known variable lets one request field drive them all: a chat template reads only the variables +/// it references, so setting the others alongside is safe. public enum ReasoningEffort { /// Canonical value that requests no reasoning. public static let none = "none" @@ -21,7 +34,7 @@ public enum ReasoningEffort { /// - `nil` or empty returns an empty dictionary, so the template keeps its own default. /// - `"none"` sets `enable_thinking` to `false` for templates that support disabling reasoning. /// - any level (for example `low`, `medium`, `high`) binds the level to `reasoning_effort` and - /// `reasoning_strength`, and sets `enable_thinking` to `true`. + /// sets `enable_thinking` to `true`. public static func templateContext(_ effort: String?) -> [String: any Sendable] { guard let value = effort?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return [:] @@ -31,7 +44,6 @@ public enum ReasoningEffort { } return [ "reasoning_effort": value, - "reasoning_strength": value, "enable_thinking": true, ] } @@ -41,4 +53,20 @@ public enum ReasoningEffort { public static func resolve(request: String?, default defaultEffort: String?) -> String? { request ?? defaultEffort } + + /// Whether a resolved effort disables model thinking (canonical `none`). Drives both the + /// `enable_thinking:false` template var and the legacy `/no_think` literal injection. + public static func disablesThinking(_ effort: String?) -> Bool { + effort?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == none + } + + /// Folds the `--no-thinking` alias into the server's `--reasoning-default`. `--no-thinking` + /// means `none`; combining it with a non-`none` default is a contradiction. + public static func resolveDefault(reasoningDefault: String?, noThinking: Bool) throws -> String? { + guard noThinking else { return reasoningDefault } + if let value = reasoningDefault, !disablesThinking(value) { + throw ReasoningEffortError.contradiction(default: value) + } + return none + } } diff --git a/swift/Sources/Tools/llm-server/ChatHandler.swift b/swift/Sources/Tools/llm-server/ChatHandler.swift index 7fec4ef1..933c71f9 100644 --- a/swift/Sources/Tools/llm-server/ChatHandler.swift +++ b/swift/Sources/Tools/llm-server/ChatHandler.swift @@ -566,6 +566,9 @@ private func tokenizeMessages( reasoningEffort: String? = nil, state: ServerState ) -> [Int] { var templateMessages: [[String: any Sendable]] = [] + // Resolved effort is the single source of truth: `none` also injects the legacy `/no_think` + // literal for models (e.g. Qwen3) that honor it in the system prompt. + let noThink = ReasoningEffort.disablesThinking(reasoningEffort) for msg in messages { var dict: [String: any Sendable] = ["role": msg.role] @@ -587,7 +590,7 @@ private func tokenizeMessages( dict["content"] = msg.content.textContent } else { var content = msg.content.textContent - if msg.role == "system" && state.config.noThinking { + if msg.role == "system" && noThink { content += "\n/no_think" } dict["content"] = content @@ -595,7 +598,7 @@ private func tokenizeMessages( templateMessages.append(dict) } - if state.config.noThinking && !messages.contains(where: { $0.role == "system" }) { + if noThink && !messages.contains(where: { $0.role == "system" }) { templateMessages.insert(["role": "system", "content": "/no_think"], at: 0) } diff --git a/swift/Sources/Tools/llm-server/LLMServerMain.swift b/swift/Sources/Tools/llm-server/LLMServerMain.swift index df17fd86..703e8436 100644 --- a/swift/Sources/Tools/llm-server/LLMServerMain.swift +++ b/swift/Sources/Tools/llm-server/LLMServerMain.swift @@ -90,6 +90,11 @@ struct LLMServer: AsyncParsableCommand { guard maxQueueDepth >= 0 else { throw ValidationError("--max-queue-depth must be >= 0 (got \(maxQueueDepth))") } + do { + _ = try ReasoningEffort.resolveDefault(reasoningDefault: reasoningDefault, noThinking: noThinking) + } catch { + throw ValidationError("\(error)") + } } func run() async throws { @@ -192,6 +197,9 @@ struct LLMServer: AsyncParsableCommand { print(" done in \(String(format: "%.3f", prepareElapsed))s\(cacheSuffix)\n") } + let resolvedReasoningDefault = try ReasoningEffort.resolveDefault( + reasoningDefault: reasoningDefault, noThinking: noThinking) + let config = ServerConfig( modelName: modelName, defaultMaxTokens: maxTokens, @@ -199,8 +207,7 @@ struct LLMServer: AsyncParsableCommand { defaultTopP: topP, defaultTopK: topK, defaultMinP: minP, - noThinking: noThinking, - defaultReasoningEffort: reasoningDefault ?? (noThinking ? ReasoningEffort.none : nil), + defaultReasoningEffort: resolvedReasoningDefault, supportsLogprobs: supportsLogprobs, maxContextLength: bundle.maxContextLength, vocabSize: bundle.vocabSize, @@ -219,7 +226,7 @@ struct LLMServer: AsyncParsableCommand { print(" Engine: \(type(of: engine))") print(" Logprobs: \(supportsLogprobs ? "supported" : "not supported (use --variant coreai-sequential)")") print(" Context: \(bundle.maxContextLength) tokens") - print(" No-thinking: \(noThinking)") + print(" Reasoning default: \(config.defaultReasoningEffort ?? "template default")") print(" Max queue depth: \(maxQueueDepth)") let topKStr = topK.map { "\($0)" } ?? "nil" let topPStr = topP.map { "\($0)" } ?? "nil" diff --git a/swift/Sources/Tools/llm-server/ServerState.swift b/swift/Sources/Tools/llm-server/ServerState.swift index 7b9b0fba..daad5660 100644 --- a/swift/Sources/Tools/llm-server/ServerState.swift +++ b/swift/Sources/Tools/llm-server/ServerState.swift @@ -19,8 +19,9 @@ struct ServerConfig: Sendable { let defaultTopP: Double? let defaultTopK: Int? let defaultMinP: Double? - let noThinking: Bool /// Default `reasoning_effort` applied when a request omits it. `nil` leaves the template default. + /// `--no-thinking` folds into this as `none`, which drives both `enable_thinking:false` and the + /// legacy `/no_think` literal injection. let defaultReasoningEffort: String? let supportsLogprobs: Bool let maxContextLength: Int diff --git a/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift b/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift index d26bf0bb..eb4eab2e 100644 --- a/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift +++ b/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift @@ -27,8 +27,8 @@ struct ReasoningEffortTests { for level in ["low", "medium", "high"] { let context = ReasoningEffort.templateContext(level) #expect((context["reasoning_effort"] as? String) == level) - #expect((context["reasoning_strength"] as? String) == level) #expect((context["enable_thinking"] as? Bool) == true) + #expect(context["reasoning_strength"] == nil) } } @@ -39,6 +39,32 @@ struct ReasoningEffortTests { #expect(ReasoningEffort.resolve(request: nil, default: nil) == nil) } + @Test("disablesThinking is true only for the canonical none") + func disablesThinking() { + #expect(ReasoningEffort.disablesThinking("none")) + #expect(ReasoningEffort.disablesThinking("None")) + #expect(ReasoningEffort.disablesThinking(" none ")) + #expect(!ReasoningEffort.disablesThinking("low")) + #expect(!ReasoningEffort.disablesThinking(nil)) + #expect(!ReasoningEffort.disablesThinking("")) + } + + @Test("--no-thinking folds into the reasoning default as none") + func resolveDefaultNoThinking() throws { + #expect(try ReasoningEffort.resolveDefault(reasoningDefault: nil, noThinking: true) == "none") + #expect(try ReasoningEffort.resolveDefault(reasoningDefault: nil, noThinking: false) == nil) + #expect(try ReasoningEffort.resolveDefault(reasoningDefault: "low", noThinking: false) == "low") + // --no-thinking plus an explicit none default is consistent, not a conflict. + #expect(try ReasoningEffort.resolveDefault(reasoningDefault: "none", noThinking: true) == "none") + } + + @Test("--no-thinking with a non-none default is rejected") + func resolveDefaultContradiction() { + #expect(throws: ReasoningEffortError.self) { + try ReasoningEffort.resolveDefault(reasoningDefault: "low", noThinking: true) + } + } + @Test("reasoning_effort decodes from a chat completion request") func requestDecodes() throws { let json = """ From c8f2d828d87a406d931232ddb33131f630d9ff75 Mon Sep 17 00:00:00 2001 From: sukru tikves Date: Tue, 22 Sep 2026 13:54:54 -0700 Subject: [PATCH 3/3] Rename --reasoning-default flag to --default-reasoning-effort --- .../Sources/CoreAILMCommon/ReasoningEffort.swift | 12 ++++++------ .../Sources/Tools/llm-server/LLMServerMain.swift | 16 ++++++++++------ .../ReasoningEffortTests.swift | 10 +++++----- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/swift/Sources/CoreAILMCommon/ReasoningEffort.swift b/swift/Sources/CoreAILMCommon/ReasoningEffort.swift index 05539d04..3b509143 100644 --- a/swift/Sources/CoreAILMCommon/ReasoningEffort.swift +++ b/swift/Sources/CoreAILMCommon/ReasoningEffort.swift @@ -7,13 +7,13 @@ import Foundation /// Thrown when server flags request contradictory reasoning defaults. public enum ReasoningEffortError: Error, CustomStringConvertible { - /// `--no-thinking` was combined with a non-`none` `--reasoning-default`. + /// `--no-thinking` was combined with a non-`none` `--default-reasoning-effort`. case contradiction(default: String) public var description: String { switch self { case .contradiction(let value): - return "--no-thinking conflicts with --reasoning-default \(value); --no-thinking means none" + return "--no-thinking conflicts with --default-reasoning-effort \(value); --no-thinking means none" } } } @@ -60,11 +60,11 @@ public enum ReasoningEffort { effort?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == none } - /// Folds the `--no-thinking` alias into the server's `--reasoning-default`. `--no-thinking` + /// Folds the `--no-thinking` alias into the server's `--default-reasoning-effort`. `--no-thinking` /// means `none`; combining it with a non-`none` default is a contradiction. - public static func resolveDefault(reasoningDefault: String?, noThinking: Bool) throws -> String? { - guard noThinking else { return reasoningDefault } - if let value = reasoningDefault, !disablesThinking(value) { + public static func resolveDefault(defaultReasoningEffort: String?, noThinking: Bool) throws -> String? { + guard noThinking else { return defaultReasoningEffort } + if let value = defaultReasoningEffort, !disablesThinking(value) { throw ReasoningEffortError.contradiction(default: value) } return none diff --git a/swift/Sources/Tools/llm-server/LLMServerMain.swift b/swift/Sources/Tools/llm-server/LLMServerMain.swift index 703e8436..e6affd3c 100644 --- a/swift/Sources/Tools/llm-server/LLMServerMain.swift +++ b/swift/Sources/Tools/llm-server/LLMServerMain.swift @@ -68,14 +68,17 @@ struct LLMServer: AsyncParsableCommand { @Option(name: .customLong("max-queue-depth"), help: "Max requests queued before returning 429 (default: 16)") var maxQueueDepth: Int = 16 - @Flag(name: .customLong("no-thinking"), help: "Disable thinking/reasoning (alias for --reasoning-default none)") + @Flag( + name: .customLong("no-thinking"), + help: "Disable thinking/reasoning (alias for --default-reasoning-effort none)" + ) var noThinking: Bool = false @Option( - name: .customLong("reasoning-default"), + name: .customLong("default-reasoning-effort"), help: "Default reasoning_effort when a request omits it (none, low, medium, high)." ) - var reasoningDefault: String? + var defaultReasoningEffort: String? @Flag( name: .customLong("clear-coreai-cache"), @@ -91,7 +94,8 @@ struct LLMServer: AsyncParsableCommand { throw ValidationError("--max-queue-depth must be >= 0 (got \(maxQueueDepth))") } do { - _ = try ReasoningEffort.resolveDefault(reasoningDefault: reasoningDefault, noThinking: noThinking) + _ = try ReasoningEffort.resolveDefault( + defaultReasoningEffort: defaultReasoningEffort, noThinking: noThinking) } catch { throw ValidationError("\(error)") } @@ -198,7 +202,7 @@ struct LLMServer: AsyncParsableCommand { } let resolvedReasoningDefault = try ReasoningEffort.resolveDefault( - reasoningDefault: reasoningDefault, noThinking: noThinking) + defaultReasoningEffort: defaultReasoningEffort, noThinking: noThinking) let config = ServerConfig( modelName: modelName, @@ -226,7 +230,7 @@ struct LLMServer: AsyncParsableCommand { print(" Engine: \(type(of: engine))") print(" Logprobs: \(supportsLogprobs ? "supported" : "not supported (use --variant coreai-sequential)")") print(" Context: \(bundle.maxContextLength) tokens") - print(" Reasoning default: \(config.defaultReasoningEffort ?? "template default")") + print(" Default reasoning effort: \(config.defaultReasoningEffort ?? "template default")") print(" Max queue depth: \(maxQueueDepth)") let topKStr = topK.map { "\($0)" } ?? "nil" let topPStr = topP.map { "\($0)" } ?? "nil" diff --git a/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift b/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift index eb4eab2e..67608a16 100644 --- a/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift +++ b/swift/Tests/CoreAILMCommonTests/ReasoningEffortTests.swift @@ -51,17 +51,17 @@ struct ReasoningEffortTests { @Test("--no-thinking folds into the reasoning default as none") func resolveDefaultNoThinking() throws { - #expect(try ReasoningEffort.resolveDefault(reasoningDefault: nil, noThinking: true) == "none") - #expect(try ReasoningEffort.resolveDefault(reasoningDefault: nil, noThinking: false) == nil) - #expect(try ReasoningEffort.resolveDefault(reasoningDefault: "low", noThinking: false) == "low") + #expect(try ReasoningEffort.resolveDefault(defaultReasoningEffort: nil, noThinking: true) == "none") + #expect(try ReasoningEffort.resolveDefault(defaultReasoningEffort: nil, noThinking: false) == nil) + #expect(try ReasoningEffort.resolveDefault(defaultReasoningEffort: "low", noThinking: false) == "low") // --no-thinking plus an explicit none default is consistent, not a conflict. - #expect(try ReasoningEffort.resolveDefault(reasoningDefault: "none", noThinking: true) == "none") + #expect(try ReasoningEffort.resolveDefault(defaultReasoningEffort: "none", noThinking: true) == "none") } @Test("--no-thinking with a non-none default is rejected") func resolveDefaultContradiction() { #expect(throws: ReasoningEffortError.self) { - try ReasoningEffort.resolveDefault(reasoningDefault: "low", noThinking: true) + try ReasoningEffort.resolveDefault(defaultReasoningEffort: "low", noThinking: true) } }