using System.Text.Json; namespace ClaudeDo.Data.Models; /// Per-model run defaults: which reasoning effort and turn budget a session gets when /// it runs under and nothing more specific overrides it. public sealed record ModelPreset(string Model, string Effort, int MaxTurns); /// /// Serialization + lookup for (a JSON array on the /// singleton settings row). One row per entry; the row supplies /// the *global* effort and max-turns defaults, which list- and task-level overrides still beat. /// public static class ModelPresets { private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); /// Shipping defaults — one row per known model alias. public static IReadOnlyList Defaults { get; } = new[] { new ModelPreset("haiku", "medium", 20), new ModelPreset("sonnet", "high", 30), new ModelPreset("opus", "high", 40), new ModelPreset("fable", "high", 25), }; public static string SerializeDefaults() => Serialize(Defaults); public static string Serialize(IEnumerable presets) => JsonSerializer.Serialize(Normalize(presets), Json); /// Parses the stored JSON, falling back to for anything absent /// or malformed — a bad settings row must never stop a run from starting. public static IReadOnlyList Parse(string? json) { if (string.IsNullOrWhiteSpace(json)) return Defaults; try { var parsed = JsonSerializer.Deserialize>(json, Json); return parsed is { Count: > 0 } ? Normalize(parsed) : Defaults; } catch (JsonException) { return Defaults; } } /// The preset for , or the default row for it. Never null so /// callers don't have to special-case an unconfigured or unknown model. public static ModelPreset For(IReadOnlyList presets, string? model) { var alias = (model ?? ModelRegistry.DefaultAlias).Trim(); foreach (var p in presets) if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase)) return p; foreach (var p in Defaults) if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase)) return p; return new ModelPreset(alias, EffortRegistry.DefaultLevel, 30); } public static ModelPreset For(string? json, string? model) => For(Parse(json), model); // One row per known alias, canonical order, clamped values — so an edited or partial list // still round-trips into something every consumer can rely on. private static IReadOnlyList Normalize(IEnumerable presets) { var byModel = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var p in presets) { if (p is null || string.IsNullOrWhiteSpace(p.Model)) continue; byModel[p.Model.Trim()] = p; } var result = new List(); foreach (var fallback in Defaults) { var source = byModel.TryGetValue(fallback.Model, out var found) ? found : fallback; var effort = TryNormalizeEffort(source.Effort) ?? fallback.Effort; var turns = source.MaxTurns is >= 1 and <= 200 ? source.MaxTurns : fallback.MaxTurns; result.Add(new ModelPreset(fallback.Model, effort, turns)); } return result; } private static string? TryNormalizeEffort(string? effort) { try { return EffortRegistry.NormalizeLevel(effort); } catch (ArgumentException) { return null; } } }