mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 13:11:17 +00:00
* feat(clients): provider-grouped model picker with fast, verbosity, and inherited-thinking controls * chore(i18n): translate model-control strings and regenerate platform catalogs * feat(clients): inherited-default restore for fast mode and verbosity controls * fix(clients): preserve control state in thinking snapshots and persist inherited verbosity * chore(i18n): resync native inventory after rebase onto message-reader main * chore(i18n): realign locale artifacts after rebase onto message-reader main * refactor(clients): drop superseded fast-mode accessors and picker convenience flagged by periphery * chore(i18n): resync inventory line refs after dead-code removal
307 lines
14 KiB
Swift
307 lines
14 KiB
Swift
import Foundation
|
|
|
|
extension OpenClawChatViewModel {
|
|
public static let verboseLevelOptions = ["off", "on", "full"]
|
|
|
|
public var thinkingSelectionID: String {
|
|
self.thinkingOverrideIsInherited ? Self.inheritedThinkingSelectionID : self.thinkingLevel
|
|
}
|
|
|
|
public var thinkingOverrideIsInherited: Bool {
|
|
if self.hasAppliedLiveSessions {
|
|
return self.currentSessionEntry()?.thinkingLevel == nil
|
|
}
|
|
return !self.prefersExplicitThinkingLevel
|
|
}
|
|
|
|
public var verboseLevel: String {
|
|
if self.hasAppliedLiveSessions {
|
|
return Self.normalizedVerboseLevel(self.currentSessionEntry()?.verboseLevel)
|
|
?? Self.inheritedThinkingSelectionID
|
|
}
|
|
return self.prefersExplicitVerboseLevel
|
|
? self.preferredVerboseLevel
|
|
: Self.inheritedThinkingSelectionID
|
|
}
|
|
|
|
public var fastModeSelectionID: String {
|
|
guard let session = self.currentSessionEntry(), session.fastMode != nil else {
|
|
return Self.inheritedThinkingSelectionID
|
|
}
|
|
return (session.effectiveFastMode ?? session.fastMode)?.isEnabled == true ? "on" : "off"
|
|
}
|
|
|
|
/// `models.list` currently has no fast-support capability field. Keep the
|
|
/// control available and let the gateway validate the session patch.
|
|
public var selectedModelSupportsFastMode: Bool {
|
|
true
|
|
}
|
|
|
|
public var isUpdatingSessionSettings: Bool {
|
|
self.inFlightSettingsPatchCountsByTarget[self.currentModelPatchTarget()] != nil
|
|
}
|
|
|
|
static func normalizedVerboseLevel(_ level: String?) -> String? {
|
|
let normalized = level?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return Self.verboseLevelOptions.contains(normalized ?? "") ? normalized : nil
|
|
}
|
|
|
|
func performSelectVerboseLevel(_ level: String) {
|
|
let clearsOverride = level == Self.inheritedThinkingSelectionID
|
|
let next = clearsOverride ? nil : Self.normalizedVerboseLevel(level)
|
|
guard clearsOverride || next != nil else { return }
|
|
let target = self.currentModelPatchTarget()
|
|
let sessionKey = self.sessionKey
|
|
let baselineSessionLevel = self.currentSessionEntry()?.verboseLevel
|
|
guard clearsOverride ? baselineSessionLevel != nil : Self.normalizedVerboseLevel(baselineSessionLevel) != next
|
|
else { return }
|
|
|
|
if self.acceptedVerboseLevelsByTarget[target] == nil {
|
|
self.acceptedVerboseLevelsByTarget[target] = baselineSessionLevel.map(VerboseLevelState.value)
|
|
?? VerboseLevelState.none
|
|
}
|
|
|
|
self.updateCurrentSessionVerboseLevel(next, sessionKey: sessionKey)
|
|
self.nextVerboseSelectionRequestID &+= 1
|
|
let verboseRequestID = self.nextVerboseSelectionRequestID
|
|
let requestedPreference = VerbosePreferenceState(
|
|
level: next ?? self.preferredVerboseLevel,
|
|
isExplicit: !clearsOverride)
|
|
self.verbosePreferenceRequests[verboseRequestID] = .pending(requestedPreference)
|
|
self.reconcileVerbosePreferenceRequests()
|
|
let requestID = self.reserveSessionSettingsRequest(for: target)
|
|
self.enqueueSessionSettingsPatch(requestID: requestID, target: target) { [weak self] routeLease in
|
|
guard let self else { return }
|
|
do {
|
|
guard let routeLease else { throw OpenClawChatTransportSendError.notDispatched }
|
|
let result = try await routeLease.patchSessionSettings(
|
|
sessionKey: target.canonicalSessionKey,
|
|
agentID: target.agentID,
|
|
patch: OpenClawChatSessionSettingsPatch(verboseLevel: .some(next)))
|
|
let accepted = clearsOverride ? nil : (Self.normalizedVerboseLevel(result?.verboseLevel) ?? next)
|
|
self.acceptedVerboseLevelsByTarget[target] = accepted.map(VerboseLevelState.value)
|
|
?? VerboseLevelState.none
|
|
self.recordModelControlPatchSuccess(
|
|
result: result,
|
|
requestID: requestID,
|
|
target: target,
|
|
verboseLevelOverride: .some(accepted))
|
|
self.verbosePreferenceRequests[verboseRequestID] = .succeeded(VerbosePreferenceState(
|
|
level: accepted ?? requestedPreference.level,
|
|
isExplicit: !clearsOverride))
|
|
self.reconcileVerbosePreferenceRequests()
|
|
if let state = self.modelControlState(for: target, originalSessionKey: sessionKey) {
|
|
self.updateCurrentSessionVerboseLevel(
|
|
accepted,
|
|
sessionKey: state.key,
|
|
exactMatchOnly: state.exactMatchOnly)
|
|
}
|
|
} catch {
|
|
self.verbosePreferenceRequests[verboseRequestID] = .failed
|
|
self.reconcileVerbosePreferenceRequests()
|
|
if let state = self.modelControlState(for: target, originalSessionKey: sessionKey) {
|
|
self.updateCurrentSessionVerboseLevel(
|
|
self.acceptedVerboseLevelsByTarget[target]?.level,
|
|
sessionKey: state.key,
|
|
exactMatchOnly: state.exactMatchOnly)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func reconcileVerbosePreferenceRequests() {
|
|
let resolved = self.verbosePreferenceRequests.keys.sorted(by: >).compactMap { requestID
|
|
-> VerbosePreferenceState? in
|
|
switch self.verbosePreferenceRequests[requestID] {
|
|
case let .pending(state), let .succeeded(state): state
|
|
case .failed, .none: nil
|
|
}
|
|
}.first ?? self.confirmedVerbosePreference
|
|
if resolved.level != self.preferredVerboseLevel {
|
|
self.preferredVerboseLevel = resolved.level
|
|
self.onVerboseLevelChanged?(resolved.level)
|
|
}
|
|
self.prefersExplicitVerboseLevel = resolved.isExplicit
|
|
if resolved != self.emittedVerbosePreference {
|
|
self.emittedVerbosePreference = resolved
|
|
self.onVerbosePreferenceChanged?(resolved.isExplicit ? resolved.level : nil)
|
|
}
|
|
guard !self.verbosePreferenceRequests.values.contains(where: {
|
|
if case .pending = $0 { return true }
|
|
return false
|
|
}) else { return }
|
|
self.confirmedVerbosePreference = resolved
|
|
self.verbosePreferenceRequests.removeAll()
|
|
}
|
|
|
|
func performSelectFastMode(_ selectionID: String) {
|
|
let next: OpenClawChatFastMode?
|
|
switch selectionID {
|
|
case Self.inheritedThinkingSelectionID: next = nil
|
|
case "on": next = .on
|
|
case "off": next = .off
|
|
default: return
|
|
}
|
|
let target = self.currentModelPatchTarget()
|
|
let sessionKey = self.sessionKey
|
|
let baselineFastMode = self.currentSessionEntry()?.fastMode
|
|
let baselineEffectiveFastMode = self.currentSessionEntry()?.effectiveFastMode
|
|
guard baselineFastMode != next else { return }
|
|
|
|
if self.acceptedFastModesByTarget[target] == nil {
|
|
self.acceptedFastModesByTarget[target] = FastModeState(
|
|
override: baselineFastMode,
|
|
effective: baselineEffectiveFastMode)
|
|
}
|
|
|
|
self.updateCurrentSessionFastMode(
|
|
next,
|
|
effective: next ?? baselineEffectiveFastMode,
|
|
sessionKey: sessionKey)
|
|
let requestID = self.reserveSessionSettingsRequest(for: target)
|
|
self.enqueueSessionSettingsPatch(requestID: requestID, target: target) { [weak self] routeLease in
|
|
guard let self else { return }
|
|
do {
|
|
guard let routeLease else { throw OpenClawChatTransportSendError.notDispatched }
|
|
let result = try await routeLease.patchSessionSettings(
|
|
sessionKey: target.canonicalSessionKey,
|
|
agentID: target.agentID,
|
|
patch: OpenClawChatSessionSettingsPatch(fastMode: .some(next)))
|
|
let acceptedOverride = next == nil ? nil : (result?.fastMode ?? next)
|
|
let acceptedEffective = result?.effectiveFastMode
|
|
?? result?.fastMode
|
|
?? acceptedOverride
|
|
?? baselineEffectiveFastMode
|
|
self.acceptedFastModesByTarget[target] = FastModeState(
|
|
override: acceptedOverride,
|
|
effective: acceptedEffective)
|
|
self.recordModelControlPatchSuccess(
|
|
result: result,
|
|
requestID: requestID,
|
|
target: target,
|
|
fastModeOverride: .some(acceptedOverride),
|
|
effectiveFastMode: acceptedEffective)
|
|
if let state = self.modelControlState(for: target, originalSessionKey: sessionKey) {
|
|
self.updateCurrentSessionFastMode(
|
|
acceptedOverride,
|
|
effective: acceptedEffective,
|
|
sessionKey: state.key,
|
|
exactMatchOnly: state.exactMatchOnly)
|
|
}
|
|
} catch {
|
|
if let state = self.modelControlState(for: target, originalSessionKey: sessionKey) {
|
|
let accepted = self.acceptedFastModesByTarget[target]
|
|
self.updateCurrentSessionFastMode(
|
|
accepted?.override,
|
|
effective: accepted?.effective,
|
|
sessionKey: state.key,
|
|
exactMatchOnly: state.exactMatchOnly)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyModelControlPatchResult(
|
|
_ result: OpenClawChatModelPatchResult,
|
|
sessionKey: String,
|
|
fastOverrideCleared: Bool = false,
|
|
verboseOverrideCleared: Bool = false)
|
|
{
|
|
let session = self.currentSessionEntry()
|
|
if fastOverrideCleared {
|
|
self.updateCurrentSessionFastMode(
|
|
nil,
|
|
effective: result.effectiveFastMode ?? result.fastMode ?? session?.effectiveFastMode,
|
|
sessionKey: sessionKey)
|
|
} else if let fastMode = result.fastMode {
|
|
self.updateCurrentSessionFastMode(
|
|
fastMode,
|
|
effective: result.effectiveFastMode ?? fastMode,
|
|
sessionKey: sessionKey)
|
|
} else if let effectiveFastMode = result.effectiveFastMode {
|
|
self.updateCurrentSessionFastMode(
|
|
session?.fastMode,
|
|
effective: effectiveFastMode,
|
|
sessionKey: sessionKey)
|
|
}
|
|
if verboseOverrideCleared {
|
|
self.updateCurrentSessionVerboseLevel(nil, sessionKey: sessionKey)
|
|
} else if let verboseLevel = Self.normalizedVerboseLevel(result.verboseLevel) {
|
|
self.updateCurrentSessionVerboseLevel(verboseLevel, sessionKey: sessionKey)
|
|
}
|
|
}
|
|
|
|
private func recordModelControlPatchSuccess(
|
|
result: OpenClawChatModelPatchResult?,
|
|
requestID: UInt64,
|
|
target: ModelPatchTarget,
|
|
fastModeOverride: OpenClawChatFastMode?? = nil,
|
|
effectiveFastMode: OpenClawChatFastMode? = nil,
|
|
verboseLevelOverride: String?? = nil)
|
|
{
|
|
let previous = self.lastSuccessfulSettingsPatchResultsByTarget[target]
|
|
let recordedFastMode: OpenClawChatFastMode? = if let fastModeOverride {
|
|
fastModeOverride
|
|
} else {
|
|
result?.fastMode ?? previous?.fastMode
|
|
}
|
|
let recordedVerboseLevel: String? = if let verboseLevelOverride {
|
|
verboseLevelOverride
|
|
} else {
|
|
result?.verboseLevel ?? previous?.verboseLevel
|
|
}
|
|
if let fastModeOverride {
|
|
self.lastSuccessfulFastOverrideClearedByTarget[target] = fastModeOverride == nil
|
|
}
|
|
if let verboseLevelOverride {
|
|
self.lastSuccessfulVerboseOverrideClearedByTarget[target] = verboseLevelOverride == nil
|
|
}
|
|
self.lastSuccessfulSettingsPatchRequestIDsByTarget[target] = requestID
|
|
self.lastSuccessfulSettingsPatchResultsByTarget[target] = OpenClawChatModelPatchResult(
|
|
key: result?.key ?? previous?.key ?? target.canonicalSessionKey,
|
|
modelProvider: result?.modelProvider ?? previous?.modelProvider,
|
|
model: result?.model ?? previous?.model,
|
|
thinkingLevel: result?.thinkingLevel ?? previous?.thinkingLevel,
|
|
thinkingLevels: result?.thinkingLevels ?? previous?.thinkingLevels,
|
|
fastMode: recordedFastMode,
|
|
effectiveFastMode: result?.effectiveFastMode ?? effectiveFastMode ?? previous?.effectiveFastMode,
|
|
verboseLevel: recordedVerboseLevel)
|
|
}
|
|
|
|
private func modelControlState(for target: ModelPatchTarget, originalSessionKey: String)
|
|
-> (key: String, exactMatchOnly: Bool)?
|
|
{
|
|
if target == self.currentModelPatchTarget() {
|
|
return (originalSessionKey, false)
|
|
}
|
|
guard let key = self.inactiveSettingsStateKey(for: target) else { return nil }
|
|
return (key, true)
|
|
}
|
|
|
|
private func updateCurrentSessionVerboseLevel(
|
|
_ level: String?,
|
|
sessionKey: String,
|
|
exactMatchOnly: Bool = false)
|
|
{
|
|
let index = exactMatchOnly
|
|
? self.sessions.firstIndex(where: { $0.key == sessionKey })
|
|
: self.sessionIndexForModelState(sessionKey: sessionKey)
|
|
guard let index else { return }
|
|
self.sessions[index].verboseLevel = level
|
|
}
|
|
|
|
private func updateCurrentSessionFastMode(
|
|
_ mode: OpenClawChatFastMode?,
|
|
effective: OpenClawChatFastMode?,
|
|
sessionKey: String,
|
|
exactMatchOnly: Bool = false)
|
|
{
|
|
let index = exactMatchOnly
|
|
? self.sessions.firstIndex(where: { $0.key == sessionKey })
|
|
: self.sessionIndexForModelState(sessionKey: sessionKey)
|
|
guard let index else { return }
|
|
self.sessions[index].fastMode = mode
|
|
self.sessions[index].effectiveFastMode = effective
|
|
}
|
|
}
|