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