Files
ClaudeDo/src/ClaudeDo.Data/Models/ModelPresets.cs
T
Mika Kuns fde9615b34 feat(settings): per-model effort and turn presets
ClaudeDo never passed --effort, so every session inherited whatever effortLevel
the user's Claude Code config happened to carry. Settings -> General now holds one
row per model alias (haiku medium/20, sonnet high/30, opus high/40, fable high/25)
supplying the global effort and turn defaults; list- and task-level max-turns
overrides still win, and the agent editor's inherited badge follows the model.

--effort is applied to autonomous runs and to every ConPTY spec (task session,
planning start/resume, ad-hoc, list handler). The model itself is deliberately not
forced on interactive sessions. The single global 'Max turns' field is replaced by
the table, and 'fable' joins ModelRegistry.Aliases.

The migration also adds the is_manual columns used by the next commit.
2026-07-27 15:02:51 +02:00

92 lines
3.9 KiB
C#

using System.Text.Json;
namespace ClaudeDo.Data.Models;
/// <summary>Per-model run defaults: which reasoning effort and turn budget a session gets when
/// it runs under <paramref name="Model"/> and nothing more specific overrides it.</summary>
public sealed record ModelPreset(string Model, string Effort, int MaxTurns);
/// <summary>
/// Serialization + lookup for <see cref="AppSettingsEntity.ModelPresets"/> (a JSON array on the
/// singleton settings row). One row per <see cref="ModelRegistry.Aliases"/> entry; the row supplies
/// the *global* effort and max-turns defaults, which list- and task-level overrides still beat.
/// </summary>
public static class ModelPresets
{
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
/// <summary>Shipping defaults — one row per known model alias.</summary>
public static IReadOnlyList<ModelPreset> 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<ModelPreset> presets)
=> JsonSerializer.Serialize(Normalize(presets), Json);
/// <summary>Parses the stored JSON, falling back to <see cref="Defaults"/> for anything absent
/// or malformed — a bad settings row must never stop a run from starting.</summary>
public static IReadOnlyList<ModelPreset> Parse(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return Defaults;
try
{
var parsed = JsonSerializer.Deserialize<List<ModelPreset>>(json, Json);
return parsed is { Count: > 0 } ? Normalize(parsed) : Defaults;
}
catch (JsonException)
{
return Defaults;
}
}
/// <summary>The preset for <paramref name="model"/>, or the default row for it. Never null so
/// callers don't have to special-case an unconfigured or unknown model.</summary>
public static ModelPreset For(IReadOnlyList<ModelPreset> 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<ModelPreset> Normalize(IEnumerable<ModelPreset> presets)
{
var byModel = new Dictionary<string, ModelPreset>(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<ModelPreset>();
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; }
}
}