189 lines
7.8 KiB
Swift
189 lines
7.8 KiB
Swift
// The Profiles tab's three-state model (named / working / defaults), plus the
|
|
// helpers that translate between the per-field VM state and the loose
|
|
// `settings` dict the server wants on /admin/api/profile-templates and
|
|
// /admin/api/models/{id}/profiles.
|
|
//
|
|
// The "Working profile" is a client-only construct: as soon as the user
|
|
// edits any field in Basic/Advanced, the active named profile detaches
|
|
// into an in-memory scratch the user can Apply / Save as new / Update or
|
|
// Revert. The server never sees a working profile — only the result of
|
|
// one of those four actions.
|
|
|
|
import Foundation
|
|
|
|
/// What the Profiles banner + chip group show as the model's current
|
|
/// attach state. The view layer renders one of three banner variants
|
|
/// from this enum.
|
|
enum ActiveProfileState: Equatable {
|
|
/// The model is using a named profile (preset/global/model). No
|
|
/// unsaved edits.
|
|
case named(scope: ProfileScope, name: String)
|
|
|
|
/// The user has edited fields since the last load. `basedOn` is the
|
|
/// profile the edits forked from (nil when no profile was active).
|
|
case working(basedOn: NamedProfileRef?)
|
|
|
|
/// No profile assigned and no edits — the model falls back to the
|
|
/// server's `GlobalSettings.sampling` defaults.
|
|
case defaults
|
|
|
|
struct NamedProfileRef: Equatable, Hashable {
|
|
let scope: ProfileScope
|
|
let name: String
|
|
}
|
|
}
|
|
|
|
/// Snapshot of all editable model-settings fields used to detect dirty
|
|
/// state. The VM stores one of these on `load()` and compares the live
|
|
/// observable values against it to decide whether to surface the
|
|
/// Working banner.
|
|
///
|
|
/// Equality is intentionally permissive: empty string == nil, since the
|
|
/// editor uses `""` for "unset" and the server sends `null`. Same for
|
|
/// numeric strings parsing as the same number.
|
|
struct ModelSettingsSnapshot: Equatable {
|
|
var alias: String
|
|
var modelTypeOverride: String
|
|
var contextLength: String
|
|
var maxTokens: String
|
|
var temperature: String
|
|
var topP: String
|
|
var topK: String
|
|
var minP: String
|
|
var repetitionPenalty: String
|
|
var presencePenalty: String
|
|
var ttlSeconds: String
|
|
|
|
var enableThinking: Bool
|
|
var thinkingBudgetEnabled: Bool
|
|
var thinkingBudgetTokens: String
|
|
var limitToolResults: Bool
|
|
var toolResultLimitTokens: String
|
|
var forceSampling: Bool
|
|
var isPinned: Bool
|
|
|
|
var trustRemoteCode: Bool
|
|
var reasoningParser: String
|
|
|
|
var chatTemplateEntries: [ChatTemplateKwargEntry]
|
|
|
|
var turboquantKvEnabled: Bool
|
|
var turboquantKvBits: String
|
|
var indexCacheEnabled: Bool
|
|
var indexCacheFreq: String
|
|
var specprefillEnabled: Bool
|
|
var specprefillDraftModel: String
|
|
var specprefillKeepPct: String
|
|
var specprefillThreshold: String
|
|
var dflashEnabled: Bool
|
|
var dflashDraftModel: String
|
|
var dflashDraftQuantEnabled: Bool
|
|
var dflashDraftQuantWeightBits: String
|
|
var dflashDraftQuantActivationBits: String
|
|
var dflashDraftQuantGroupSize: String
|
|
var dflashMaxCtx: String
|
|
var dflashVerifyMode: String
|
|
var dflashDraftWindowSize: String
|
|
var dflashDraftSinkSize: String
|
|
var dflashInMemoryCache: Bool
|
|
var dflashInMemoryCacheGib: String
|
|
var dflashInMemoryCacheMaxEntries: String
|
|
var dflashSsdCache: Bool
|
|
var dflashSsdCacheGib: String
|
|
var mtpEnabled: Bool
|
|
}
|
|
|
|
/// Settings keys the server understands inside the free-form `settings`
|
|
/// dict on /profile-templates and /models/{id}/profiles. Mirrors
|
|
/// `UNIVERSAL_PROFILE_FIELDS` + `MODEL_SPECIFIC_PROFILE_FIELDS` from
|
|
/// `omlx/model_profiles.py`. Each key is snake_case; values are JSON-
|
|
/// compatible scalars.
|
|
///
|
|
/// The list is kept here (not derived from the snapshot) so a field
|
|
/// renamed on either side surfaces as a build break rather than a
|
|
/// silent drop on save.
|
|
enum ProfileSettingsKey {
|
|
// Universal — both templates and model profiles
|
|
static let modelAlias = "model_alias"
|
|
static let maxContextWindow = "max_context_window"
|
|
static let maxTokens = "max_tokens"
|
|
static let temperature = "temperature"
|
|
static let topP = "top_p"
|
|
static let topK = "top_k"
|
|
static let minP = "min_p"
|
|
static let repetitionPenalty = "repetition_penalty"
|
|
static let presencePenalty = "presence_penalty"
|
|
static let ttlSeconds = "ttl_seconds"
|
|
static let enableThinking = "enable_thinking"
|
|
static let thinkingBudgetEnabled = "thinking_budget_enabled"
|
|
static let thinkingBudgetTokens = "thinking_budget_tokens"
|
|
static let reasoningParser = "reasoning_parser"
|
|
static let maxToolResultTokens = "max_tool_result_tokens"
|
|
static let forceSampling = "force_sampling"
|
|
static let isPinned = "is_pinned"
|
|
static let chatTemplateKwargs = "chat_template_kwargs"
|
|
static let forcedCtKwargs = "forced_ct_kwargs"
|
|
|
|
// Model-specific
|
|
static let modelTypeOverride = "model_type_override"
|
|
static let trustRemoteCode = "trust_remote_code"
|
|
static let turboquantKvEnabled = "turboquant_kv_enabled"
|
|
static let turboquantKvBits = "turboquant_kv_bits"
|
|
static let indexCacheFreq = "index_cache_freq"
|
|
static let specprefillEnabled = "specprefill_enabled"
|
|
static let specprefillDraftModel = "specprefill_draft_model"
|
|
static let specprefillKeepPct = "specprefill_keep_pct"
|
|
static let specprefillThreshold = "specprefill_threshold"
|
|
static let dflashEnabled = "dflash_enabled"
|
|
static let dflashDraftModel = "dflash_draft_model"
|
|
static let dflashDraftQuantEnabled = "dflash_draft_quant_enabled"
|
|
static let dflashDraftQuantWeightBits = "dflash_draft_quant_weight_bits"
|
|
static let dflashDraftQuantActivationBits = "dflash_draft_quant_activation_bits"
|
|
static let dflashDraftQuantGroupSize = "dflash_draft_quant_group_size"
|
|
static let dflashMaxCtx = "dflash_max_ctx"
|
|
static let dflashVerifyMode = "dflash_verify_mode"
|
|
static let dflashDraftWindowSize = "dflash_draft_window_size"
|
|
static let dflashDraftSinkSize = "dflash_draft_sink_size"
|
|
static let dflashInMemoryCache = "dflash_in_memory_cache"
|
|
static let dflashInMemoryCacheMaxBytes = "dflash_in_memory_cache_max_bytes"
|
|
static let dflashInMemoryCacheMaxEntries = "dflash_in_memory_cache_max_entries"
|
|
static let dflashSsdCache = "dflash_ssd_cache"
|
|
static let dflashSsdCacheMaxBytes = "dflash_ssd_cache_max_bytes"
|
|
static let mtpEnabled = "mtp_enabled"
|
|
static let vlmMtpEnabled = "vlm_mtp_enabled"
|
|
static let vlmMtpDraftModel = "vlm_mtp_draft_model"
|
|
static let vlmMtpDraftBlockSize = "vlm_mtp_draft_block_size"
|
|
}
|
|
|
|
/// Resolves the *display* scope for the model's currently active profile.
|
|
/// The server stores active state as a per-model profile reference, but the
|
|
/// user-visible scope follows the profile's `source_template` so applying
|
|
/// the "Balanced" preset chip lights up the Preset row, not the Model row.
|
|
///
|
|
/// - Parameters:
|
|
/// - activeName: server's `active_profile_name` for this model.
|
|
/// - modelProfiles: list of per-model profiles for this model.
|
|
/// - templates: list of profile templates (preset + global).
|
|
/// - Returns: scope + the display name (template name if source-template
|
|
/// resolves, profile name otherwise), or nil when no profile is active.
|
|
func resolveActiveProfileDisplay(
|
|
activeName: String?,
|
|
modelProfiles: [ProfileDTO],
|
|
templates: [ProfileDTO]
|
|
) -> (scope: ProfileScope, name: String)? {
|
|
guard let activeName, !activeName.isEmpty else { return nil }
|
|
|
|
if let profile = modelProfiles.first(where: { $0.name == activeName }),
|
|
let source = profile.sourceTemplate,
|
|
let template = templates.first(where: { $0.name == source }) {
|
|
return (template.templateScope, template.name)
|
|
}
|
|
if let template = templates.first(where: { $0.name == activeName }) {
|
|
return (template.templateScope, template.name)
|
|
}
|
|
if modelProfiles.contains(where: { $0.name == activeName }) {
|
|
return (.model, activeName)
|
|
}
|
|
return (.model, activeName)
|
|
}
|