Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

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