mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 16:24:02 +00:00
216 lines
8.9 KiB
Swift
216 lines
8.9 KiB
Swift
import Foundation
|
|
|
|
// Thinking-level normalization and option resolution. Session entries,
|
|
// session defaults, and free-form user aliases all feed the picker; this
|
|
// extension owns collapsing them into the canonical option list.
|
|
|
|
extension OpenClawChatViewModel {
|
|
/// `agent-command.ts` throws for explicit unsupported levels, so hidden controls must send `off`.
|
|
var effectiveThinkingLevelForSend: String {
|
|
self.effectiveThinkingLevelForSend(self.thinkingLevel)
|
|
}
|
|
|
|
func effectiveThinkingLevelForSend(_ storedLevel: String, sessionKey: String? = nil) -> String {
|
|
let showsPicker: Bool
|
|
if let sessionKey, sessionKey != self.sessionKey {
|
|
// Sessions absent from the loaded list resolve to no metadata and fail
|
|
// open, preserving the queued level — matches shipped flush behavior;
|
|
// downgrading unknown sessions to "off" would silently strip levels
|
|
// users explicitly queued for reasoning-capable models.
|
|
let session = self.sessions.first(where: { $0.key == sessionKey })
|
|
showsPicker = self.thinkingPickerIsAvailable(
|
|
for: session,
|
|
modelChoice: self.sessionModelChoice(for: session))
|
|
} else {
|
|
showsPicker = self.showsThinkingPicker
|
|
}
|
|
return showsPicker ? storedLevel : "off"
|
|
}
|
|
|
|
func syncThinkingLevelOptions() {
|
|
let currentSession = self.sessions.first(where: { $0.key == self.sessionKey })
|
|
self.showsThinkingPicker = self.thinkingPickerIsAvailable(
|
|
for: currentSession,
|
|
modelChoice: self.selectedModelChoice(for: currentSession))
|
|
|
|
var options = self.resolvedThinkingLevelOptions(for: currentSession).options
|
|
if let current = Self.normalizedThinkingLevel(thinkingLevel) {
|
|
options = Self.withCurrentThinkingOption(options, current: current)
|
|
}
|
|
self.thinkingLevelOptions = options
|
|
}
|
|
|
|
private func thinkingPickerIsAvailable(
|
|
for session: OpenClawChatSessionEntry?,
|
|
modelChoice: OpenClawChatModelChoice?) -> Bool
|
|
{
|
|
let resolved = self.resolvedThinkingLevelOptions(for: session)
|
|
let gatewayAllowsOnlyOff = resolved.isGatewayMetadata &&
|
|
resolved.options.allSatisfy { $0.id == "off" }
|
|
return !gatewayAllowsOnlyOff && modelChoice?.reasoning != false
|
|
}
|
|
|
|
private struct ThinkingLevelOptionsResolution {
|
|
let options: [OpenClawChatThinkingLevelOption]
|
|
let isGatewayMetadata: Bool
|
|
}
|
|
|
|
private func resolvedThinkingLevelOptions(
|
|
for currentSession: OpenClawChatSessionEntry?) -> ThinkingLevelOptionsResolution
|
|
{
|
|
if let levels = Self.normalizedThinkingLevelOptions(currentSession?.thinkingLevels), !levels.isEmpty {
|
|
return ThinkingLevelOptionsResolution(options: levels, isGatewayMetadata: true)
|
|
}
|
|
|
|
let defaultsMatch = currentSession.map {
|
|
Self.sessionModelMatchesDefaults($0, defaults: self.sessionDefaults)
|
|
} ?? true
|
|
|
|
if defaultsMatch,
|
|
let levels = Self.normalizedThinkingLevelOptions(sessionDefaults?.thinkingLevels),
|
|
!levels.isEmpty
|
|
{
|
|
return ThinkingLevelOptionsResolution(options: levels, isGatewayMetadata: true)
|
|
}
|
|
|
|
if let options = Self.thinkingOptions(from: currentSession?.thinkingOptions), !options.isEmpty {
|
|
return ThinkingLevelOptionsResolution(options: options, isGatewayMetadata: true)
|
|
}
|
|
|
|
if defaultsMatch,
|
|
let options = Self.thinkingOptions(from: sessionDefaults?.thinkingOptions),
|
|
!options.isEmpty
|
|
{
|
|
return ThinkingLevelOptionsResolution(options: options, isGatewayMetadata: true)
|
|
}
|
|
|
|
return ThinkingLevelOptionsResolution(options: Self.baseThinkingLevelOptions, isGatewayMetadata: false)
|
|
}
|
|
|
|
private func selectedModelChoice(
|
|
for currentSession: OpenClawChatSessionEntry?) -> OpenClawChatModelChoice?
|
|
{
|
|
if self.modelSelectionID != Self.defaultModelSelectionID {
|
|
return self.modelChoices.first(where: { $0.selectionID == self.modelSelectionID })
|
|
}
|
|
|
|
return self.sessionModelChoice(for: currentSession)
|
|
}
|
|
|
|
private func sessionModelChoice(
|
|
for currentSession: OpenClawChatSessionEntry?) -> OpenClawChatModelChoice?
|
|
{
|
|
if Self.normalizedModelID(currentSession?.model) != nil {
|
|
return self.modelChoice(modelID: currentSession?.model, provider: currentSession?.modelProvider)
|
|
}
|
|
return self.modelChoice(modelID: self.sessionDefaults?.model, provider: self.sessionDefaults?.modelProvider)
|
|
}
|
|
|
|
private func modelChoice(modelID: String?, provider: String?) -> OpenClawChatModelChoice? {
|
|
guard let modelID = Self.normalizedModelID(modelID) else { return nil }
|
|
let provider = provider?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let provider, !provider.isEmpty {
|
|
let prefix = "\(provider)/"
|
|
let selectionID = modelID.hasPrefix(prefix) ? modelID : "\(prefix)\(modelID)"
|
|
return self.modelChoices.first(where: {
|
|
$0.selectionID == selectionID ||
|
|
($0.modelID == modelID && $0.provider == provider)
|
|
})
|
|
}
|
|
|
|
let matches = self.modelChoices.filter { $0.selectionID == modelID || $0.modelID == modelID }
|
|
return matches.count == 1 ? matches[0] : nil
|
|
}
|
|
|
|
private static func normalizedModelID(_ modelID: String?) -> String? {
|
|
let trimmed = modelID?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard let trimmed, !trimmed.isEmpty else { return nil }
|
|
return trimmed
|
|
}
|
|
|
|
private static func sessionModelMatchesDefaults(
|
|
_ session: OpenClawChatSessionEntry,
|
|
defaults: OpenClawChatSessionsDefaults?) -> Bool
|
|
{
|
|
let providerMatches = session.modelProvider == nil || session.modelProvider == defaults?.modelProvider
|
|
let modelMatches = session.model == nil || session.model == defaults?.model
|
|
return providerMatches && modelMatches
|
|
}
|
|
|
|
private static func normalizedThinkingLevelOptions(
|
|
_ levels: [OpenClawChatThinkingLevelOption]?) -> [OpenClawChatThinkingLevelOption]?
|
|
{
|
|
guard let levels else { return nil }
|
|
return Self.dedupedThinkingOptions(
|
|
levels.compactMap { level in
|
|
guard let id = Self.normalizedThinkingLevel(level.id) else { return nil }
|
|
let label = level.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return OpenClawChatThinkingLevelOption(id: id, label: label.isEmpty ? id : label)
|
|
})
|
|
}
|
|
|
|
private static func thinkingOptions(from labels: [String]?) -> [OpenClawChatThinkingLevelOption]? {
|
|
guard let labels else { return nil }
|
|
return Self.dedupedThinkingOptions(
|
|
labels.compactMap { label in
|
|
guard let id = Self.normalizedThinkingLevel(label) else { return nil }
|
|
let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return OpenClawChatThinkingLevelOption(id: id, label: trimmed.isEmpty ? id : trimmed)
|
|
})
|
|
}
|
|
|
|
static func withCurrentThinkingOption(
|
|
_ options: [OpenClawChatThinkingLevelOption],
|
|
current: String) -> [OpenClawChatThinkingLevelOption]
|
|
{
|
|
guard !options.contains(where: { $0.id == current }) else { return options }
|
|
return options + [OpenClawChatThinkingLevelOption(id: current, label: current)]
|
|
}
|
|
|
|
private static func dedupedThinkingOptions(
|
|
_ options: [OpenClawChatThinkingLevelOption]) -> [OpenClawChatThinkingLevelOption]
|
|
{
|
|
var result: [OpenClawChatThinkingLevelOption] = []
|
|
var seen = Set<String>()
|
|
for option in options {
|
|
guard !option.id.isEmpty, !seen.contains(option.id) else { continue }
|
|
seen.insert(option.id)
|
|
result.append(option)
|
|
}
|
|
return result
|
|
}
|
|
|
|
static func normalizedThinkingLevel(_ level: String?) -> String? {
|
|
guard let level else { return nil }
|
|
let trimmed = level.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
guard !trimmed.isEmpty else { return nil }
|
|
let collapsed = trimmed.replacingOccurrences(
|
|
of: "[\\s_-]+",
|
|
with: "",
|
|
options: .regularExpression)
|
|
|
|
switch collapsed {
|
|
case "adaptive", "auto":
|
|
return "adaptive"
|
|
case "max":
|
|
return "max"
|
|
case "xhigh", "extrahigh":
|
|
return "xhigh"
|
|
case "off", "none":
|
|
return "off"
|
|
case "on", "enable", "enabled":
|
|
return "low"
|
|
case "min", "minimal", "think":
|
|
return "minimal"
|
|
case "low", "thinkhard":
|
|
return "low"
|
|
case "mid", "med", "medium", "thinkharder", "harder":
|
|
return "medium"
|
|
case "high", "ultra", "ultrathink", "thinkhardest", "highest":
|
|
return "high"
|
|
default:
|
|
return trimmed
|
|
}
|
|
}
|
|
}
|