diff --git a/ClaudeRevit.PaneTests/ClaudeRevit.PaneTests.csproj b/ClaudeRevit.PaneTests/ClaudeRevit.PaneTests.csproj
index 2ed0eba..6b3de4d 100644
--- a/ClaudeRevit.PaneTests/ClaudeRevit.PaneTests.csproj
+++ b/ClaudeRevit.PaneTests/ClaudeRevit.PaneTests.csproj
@@ -31,6 +31,7 @@
+
diff --git a/ClaudeRevit.PaneTests/Program.cs b/ClaudeRevit.PaneTests/Program.cs
index 4af8346..633450d 100644
--- a/ClaudeRevit.PaneTests/Program.cs
+++ b/ClaudeRevit.PaneTests/Program.cs
@@ -46,7 +46,7 @@ private static void Main(string[] args)
effort.SelectedValue = "high";
Check(SettingsStore.GetAgentSelection("codex").Effort == "high", "Codex effort was not saved");
agent.SelectedIndex = 1;
- model.SelectedValue = "opus";
+ model.SelectedValue = "claude-opus-5-5";
effort.SelectedValue = "medium";
agent.SelectedIndex = 2;
Check((string)model.SelectedValue == "gpt-test" && (string)effort.SelectedValue == "high", "Codex choices were not restored");
@@ -58,7 +58,7 @@ private static void Main(string[] args)
await (Task)send.Invoke(pane, null)!;
Check(ChatService.SentSelection?.Model == "custom-model-id", "Typed model was ignored on send");
Check(ChatService.SentSelection?.Agent == "codex", "Wrong agent on send");
- Check(SettingsStore.GetAgentSelection("claudecode").Model == "opus", "Claude model was overwritten by Codex");
+ Check(SettingsStore.GetAgentSelection("claudecode").Model == "claude-opus-5-5", "Claude model was overwritten by Codex");
model.SelectedValue = "gpt-test";
effort.SelectedValue = "high";
ChatService.Pending = new();
@@ -117,7 +117,7 @@ private static void Main(string[] args)
Check(((ItemsControl)pane.FindName("AttachmentList")).Items.Count == 0, "A's attachments leaked into B");
Check(pane.Messages.Count == 0, "Another project's transcript was shown in B");
agent.SelectedIndex=1;
- model.SelectedValue="sonnet";
+ model.SelectedValue="claude-sonnet-5-5";
effort.SelectedValue="low";
pane.Messages.Add(new ChatMessage { Role = "user", Text = "Project B" });
DocumentHarness.Change("a", false);
@@ -125,7 +125,7 @@ private static void Main(string[] args)
Check(pane.Messages.Single().Text == "Project A", "Returning to A lost its transcript");
Check((string)model.SelectedValue=="gpt-test" && (string)effort.SelectedValue=="high","A's selected model/effort leaked from B");
agent.SelectedIndex=1;
- Check((string)model.SelectedValue=="opus" && (string)effort.SelectedValue=="medium","A's other provider choices leaked from B");
+ Check((string)model.SelectedValue=="claude-opus-5-5" && (string)effort.SelectedValue=="medium","A's other provider choices leaked from B");
agent.SelectedIndex=2;
ChatService.Pending = new();
((TextBox)pane.FindName("InputBox")).Text = "Pending A turn";
diff --git a/ClaudeRevit.PaneTests/Stubs.cs b/ClaudeRevit.PaneTests/Stubs.cs
index 72688d0..9c9ef4b 100644
--- a/ClaudeRevit.PaneTests/Stubs.cs
+++ b/ClaudeRevit.PaneTests/Stubs.cs
@@ -46,6 +46,12 @@ public void ClearHistory() { }
public void RecreateClient() { }
public void Activate() { }
}
+ public static class ClaudeModelCatalog
+ {
+ public static bool LastWasLive { get; private set; }
+ public static Task> ReadAsync(CancellationToken ct, bool forceRefresh = false) =>
+ Task.FromResult(ClaudeModels.Current);
+ }
public static class CodexModelCatalog
{
public static Task> ReadAsync(string exe, string workDir, CancellationToken ct, bool forceRefresh = false) =>
diff --git a/ClaudeRevit.Tests/ClaudeModelsTests.cs b/ClaudeRevit.Tests/ClaudeModelsTests.cs
index de9bd91..32a4695 100644
--- a/ClaudeRevit.Tests/ClaudeModelsTests.cs
+++ b/ClaudeRevit.Tests/ClaudeModelsTests.cs
@@ -10,7 +10,9 @@ public class ClaudeModelsTests
[InlineData("sonnet-5-5", "claude-sonnet-5-5")]
[InlineData("haiku-5-5", "claude-haiku-5-5")]
[InlineData("fable-5-1", "claude-fable-5-1")]
- [InlineData("auto", "claude-sonnet-5")]
+ [InlineData("fable-5-2", "claude-fable-5-2")]
+ [InlineData("claude-opus-5-5", "claude-opus-5-5")]
+ [InlineData("auto", "claude-sonnet-5-5")]
public void TagsMapToApiIds(string tag, string id) => Assert.Equal(id, ClaudeModels.ApiId(tag));
// Served ids come back dashed or in the SDK's enum spelling; the newer ids must not fall
@@ -27,12 +29,40 @@ public class ClaudeModelsTests
[InlineData("claude-fable-5", "fable-5")]
[InlineData("claude-opus-4-8", "opus-4-8")]
[InlineData("claude-sonnet-5", "sonnet-5")]
+ [InlineData("claude-fable-5-2", "fable-5-2")]
+ [InlineData("claude-opus-4-5-20251101", "opus-4-5")]
+ [InlineData("claude-sonnet-5-20260101", "sonnet-5")]
public void ServedModelsMapToRateTags(string id, string tag) => Assert.Equal(tag, ClaudeModels.TagFromModel(id));
+ // The pickers follow the Models API: Claude ids only, in the API's order, without repeats,
+ // and a missing display name is derived from the id.
+ [Fact]
+ public void ServedCatalogBecomesPickerEntries()
+ {
+ var list = ClaudeModels.FromServed(new (string, string?)[]
+ {
+ ("claude-fable-5-2", "Claude Fable 5.2"), ("claude-opus-5-5", null), ("gpt-5", "GPT"),
+ ("claude-opus-5-5", "dup"), ("claude-haiku-4-5", "Claude Haiku 4.5"),
+ });
+ Assert.Equal(new[] { "fable-5-2", "opus-5-5", "haiku-4-5" }, list.Select(m => m.Tag));
+ Assert.Equal("Claude Opus 5.5", list[1].Name);
+ Assert.Equal(new[] { "fable-5-2", "opus-5-5", "sonnet-5-5", "haiku-5-5" }, ClaudeModels.Current.Select(m => m.Tag));
+ }
+
+ [Theory]
+ [InlineData("opus-5-5", true)]
+ [InlineData("claude-sonnet-4-6", true)]
+ [InlineData("fable-5-2", true)]
+ [InlineData("haiku-4-5", false)]
+ [InlineData("opus-4-5-20251101", false)]
+ [InlineData("sonnet-4-5", false)]
+ public void AdaptiveThinkingFromThe46GenerationOn(string tag, bool expected) =>
+ Assert.Equal(expected, ClaudeModels.SupportsAdaptiveThinking(tag));
+
[Fact]
public void PreservedThinkingModelsKeepAppendOnlyHistory()
{
- foreach (var tag in new[] { "fable-5-1", "opus-5-5", "sonnet-5-5", "haiku-5-5" })
+ foreach (var tag in new[] { "fable-5-2", "fable-5-1", "opus-5-5", "sonnet-5-5", "haiku-5-5", "claude-opus-5-5" })
Assert.True(ClaudeModels.AppendOnlyHistory(tag), tag);
foreach (var tag in new[] { "sonnet-5", "opus-5", "opus-4-8", "fable-5", "haiku-4-5", "auto" })
Assert.False(ClaudeModels.AppendOnlyHistory(tag), tag);
@@ -57,6 +87,9 @@ public void PricesMatchTheCurrentListAndUnknownTagsOverestimate()
// Fable 5.1 reads its cache at $0.25 per million, Opus 5.5 at $0.20.
Assert.True(ClaudeModels.TryPrice("fable-5-1", 1000, out var fable51));
Assert.Equal(0.25m, ClaudeModels.Cost(fable51, 0, 0, 0, 1_000_000));
+ Assert.True(ClaudeModels.TryPrice("fable-5-2", 1000, out var fable52));
+ Assert.Equal(40m, ClaudeModels.Cost(fable52, 0, 1_000_000, 0, 0));
+ Assert.Equal(0.20m, ClaudeModels.Cost(fable52, 0, 0, 0, 1_000_000));
Assert.True(ClaudeModels.TryPrice("opus-5-5", 1000, out var opus55));
Assert.Equal(0.20m, ClaudeModels.Cost(opus55, 0, 0, 0, 1_000_000));
// 1-hour cache writes are twice the base input rate.
diff --git a/ClaudeRevit/Services/ChatService.cs b/ClaudeRevit/Services/ChatService.cs
index 4dc444b..7adcf93 100644
--- a/ClaudeRevit/Services/ChatService.cs
+++ b/ClaudeRevit/Services/ChatService.cs
@@ -863,8 +863,8 @@ private async Task SendCoreAsync(
// invalidate the 1h prompt-cache prefix for the rest of the turn, and rebuilding
// the tool list would let a Settings change yank tools out from under the model
// mid-plan (plus ~130 tool schemas re-serialized per round for nothing).
- // "Auto" model mode. DEFAULT (advisorMode): Sonnet 5 runs every round and consults Opus
- // 4.8 via the advisor tool only when it needs a plan — Sonnet's prompt cache stays warm
+ // "Auto" model mode. DEFAULT (advisorMode): Sonnet 5.5 runs every round and consults Opus
+ // 5.5 via the advisor tool only when it needs a plan — Sonnet's prompt cache stays warm
// all session, Opus is billed only for the short advice sub-inference. The legacy
// alternative (Settings → AutoUseAdvisor off) switches the whole turn to Opus on a hard
// task/error/deep-loop; see the escalation vars below.
@@ -926,7 +926,7 @@ List ClaudeTools()
// to a stronger one when the task looks hard up front, or once the fast model starts
// erroring / the task turns into a deep multi-step loop. Two paths share this machinery:
// • Alt path — fast alt model → the configured "reasoning" alt model.
- // • Claude "Auto" — Sonnet 5 (cheap) → Opus 4.8 (strong). This is the big cost lever:
+ // • Claude "Auto" — Sonnet 5.5 (cheap) → Opus 5.5 (strong). This is the big cost lever:
// Sonnet is $3/$15 vs Opus $5/$25 per 1M, so routine turns run at ~60% of the price
// and Opus is reserved for the turns that actually need it.
// Legacy model-switch path (alt reasoning model, or Auto with the advisor turned off).
@@ -1005,11 +1005,11 @@ await ui.InvokeAsync(() => conversation.Add(new ChatMessage
// the reasoning model still takes precedence, since that is a deliberate switch.
var altModelForTurn = escalate ? reasoningModel : AltModelOverride(model);
// Claude Auto: in advisor mode the executor is the configured cheap/fast model
- // (Sonnet 5 default, or Haiku 4.5 for the cheapest tier); the advisor is reached
+ // (Sonnet 5.5 default, or Haiku 5.5 for the cheapest tier); the advisor is reached
// via the tool, not by switching. In legacy mode: Sonnet until escalated to Opus.
var claudeModelForTurn = advisorMode
? SettingsStore.AutoExecutorModel
- : (claudeAuto ? (escalate ? "opus-4-8" : "sonnet-5") : model);
+ : (claudeAuto ? (escalate ? ClaudeModels.DefaultAdvisor : ClaudeModels.DefaultExecutor) : model);
BackendTurn turn;
try
{
@@ -1741,7 +1741,7 @@ private static BackendTurn ToBackendTurn(BetaMessage message)
// The advisor tool definition for Auto mode: the executor consults the advisor model at most
// once per request (a per-turn total cap is enforced by the caller), with the advisor's own
// transcript cached for an hour so repeated consults in a session don't re-bill the whole
- // prefix. Advisor model is user-configurable (Opus 4.8 default, or Fable 5 for hardest tasks).
+ // prefix. Advisor model is user-configurable (Opus 5.5 default, or Fable 5.2 for hardest tasks).
private static BetaToolUnion AdvisorTool(string advisorModelId) => new BetaAdvisorTool20260301
{
Model = advisorModelId,
diff --git a/ClaudeRevit/Services/ClaudeCodeBackend.cs b/ClaudeRevit/Services/ClaudeCodeBackend.cs
index 1d82a0e..cd02cd8 100644
--- a/ClaudeRevit/Services/ClaudeCodeBackend.cs
+++ b/ClaudeRevit/Services/ClaudeCodeBackend.cs
@@ -42,26 +42,17 @@ public sealed class Result
public string? Subtype;
}
- // Map our internal model tag to what Claude Code accepts for `--model`.
- //
- // The older tags use the coarse family aliases ("opus"/"sonnet"/"haiku"), which resolve to
- // whatever the CLI currently considers that family's model. That is fine for them, but it can't
- // express "Opus 5 specifically" — the family alias would just pick the CLI's current Opus and
- // make two dropdown entries behave identically. So the newer models pass their FULL model id,
- // which Claude Code also accepts, and the pane's choice actually decides.
+ // Map our internal model tag to what Claude Code accepts for `--model`: the full model id,
+ // so the pane's choice decides exactly which model runs (a family alias such as "opus"
+ // would silently pick whatever the CLI currently considers that family's model). Tags come
+ // from the Models API catalog, so any model the account can use maps without a table here.
//
// Returns null for "auto" — there we let the CLI pick, because the advisor-escalation that
- // "auto" means on the API has no equivalent inside Claude Code's own loop. Fable 5 used to
- // map to null too, which silently ran the CLI's default model instead of the one chosen.
- // A tag the CLI doesn't recognise surfaces as a CLI error rather than a silent fallback.
- public static string? ModelAlias(string? tag) => tag switch
- {
- "opus-5-5" or "sonnet-5-5" or "haiku-5-5" or "opus-5" or "fable-5" or "fable-5-1" => ClaudeModels.ApiId(tag),
- "opus-4-8" or "opus-4-7" or "opus-4-6" => "opus",
- "sonnet-5" or "sonnet-4-6" => "sonnet",
- "haiku-4-5" => "haiku",
- _ => null
- };
+ // "auto" means on the API has no equivalent inside Claude Code's own loop. A tag the CLI
+ // doesn't recognise surfaces as a CLI error rather than a silent fallback.
+ public static string? ModelAlias(string? tag) =>
+ string.IsNullOrWhiteSpace(tag) || tag == "auto" || tag!.StartsWith("alt", StringComparison.Ordinal)
+ ? null : ClaudeModels.ApiId(tag);
public static async Task RunAsync(
string exe, string prompt, string workDir, string mcpConfigPath, string? resumeSessionId,
diff --git a/ClaudeRevit/Services/ClaudeModelCatalog.cs b/ClaudeRevit/Services/ClaudeModelCatalog.cs
new file mode 100644
index 0000000..80e3984
--- /dev/null
+++ b/ClaudeRevit/Services/ClaudeModelCatalog.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Anthropic;
+using Anthropic.Core;
+using Anthropic.Models.Models;
+
+namespace ClaudeRevit.Services;
+
+// The Claude models this account can use, read from the Models API (GET /v1/models) — the
+// counterpart of CodexModelCatalog, so the pickers follow what Anthropic serves instead of a
+// hand-kept list that goes stale with each release. Listing models is free: no inference runs.
+// Without an API key (Claude Code on a subscription) the built-in current generation is used.
+public static class ClaudeModelCatalog
+{
+ private static readonly SemaphoreSlim CacheGate = new(1, 1);
+ private static string? _cacheKey;
+ private static IReadOnlyList? _cached;
+ private static DateTime _expires;
+
+ // True when the last answer came from the Models API rather than the built-in list.
+ public static bool LastWasLive { get; private set; }
+
+ public static async Task> ReadAsync(CancellationToken ct, bool forceRefresh = false)
+ {
+ var apiKey = ApiKeyStore.Load();
+ if (string.IsNullOrWhiteSpace(apiKey)) { LastWasLive = false; return ClaudeModels.Current; }
+ // The key itself is never kept; its hash only tells a changed key from the cached one.
+ var key = Convert.ToBase64String(System.Security.Cryptography.SHA256.Create()
+ .ComputeHash(System.Text.Encoding.UTF8.GetBytes(apiKey)));
+ await CacheGate.WaitAsync(ct);
+ try
+ {
+ ct.ThrowIfCancellationRequested();
+ if (!forceRefresh && _cached != null && _cacheKey == key && DateTime.UtcNow < _expires)
+ { LastWasLive = true; return _cached; }
+ // A failed explicit refresh must not leave stale entries available to the next run.
+ _cached = null;
+ var models = await DiscoverAsync(apiKey!, ct);
+ if (models.Count == 0) throw new InvalidOperationException("The Models API returned no Claude models.");
+ _cacheKey = key; _cached = models; _expires = DateTime.UtcNow.AddMinutes(5);
+ LastWasLive = true;
+ return models;
+ }
+ finally { CacheGate.Release(); }
+ }
+
+ private static async Task> DiscoverAsync(string apiKey, CancellationToken ct)
+ {
+ using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ timeout.CancelAfter(TimeSpan.FromSeconds(30));
+ var client = new AnthropicClient(new ClientOptions { ApiKey = apiKey });
+ var served = new List<(string, string?)>();
+ // Newest first, as the API orders them; bounded so a misbehaving cursor cannot loop.
+ IPage page = await client.Models.List(new ModelListParams { Limit = 100 }, timeout.Token);
+ for (var pages = 0; ; pages++)
+ {
+ foreach (var model in page.Items) served.Add((model.ID, model.DisplayName));
+ if (pages >= 20 || !page.HasNext()) break;
+ page = await page.Next(timeout.Token);
+ }
+ return ClaudeModels.FromServed(served);
+ }
+}
diff --git a/ClaudeRevit/Services/ClaudeModels.cs b/ClaudeRevit/Services/ClaudeModels.cs
index f15ede8..0ad00c2 100644
--- a/ClaudeRevit/Services/ClaudeModels.cs
+++ b/ClaudeRevit/Services/ClaudeModels.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
namespace ClaudeRevit.Services;
@@ -7,54 +8,98 @@ namespace ClaudeRevit.Services;
// the Claude Code backend all read them from here instead of keeping their own lists.
public static class ClaudeModels
{
- public static string ApiId(string tag) => tag switch
+ // A tag is the API id without its "claude-" prefix ("opus-5-5" <-> "claude-opus-5-5"), so
+ // any model the Models API returns maps to a tag without a hand-kept table. A full id is
+ // accepted as-is; "auto" (and an empty tag) resolve to the current default executor.
+ public const string DefaultExecutor = "sonnet-5-5";
+ public const string DefaultAdvisor = "opus-5-5";
+
+ public static string ApiId(string tag)
+ {
+ var t = (tag ?? "").Trim();
+ if (t.Length == 0 || t == "auto") return "claude-" + DefaultExecutor;
+ return t.StartsWith("claude-", StringComparison.OrdinalIgnoreCase) ? t : "claude-" + t;
+ }
+
+ public static string TagOf(string apiId)
+ {
+ var id = (apiId ?? "").Trim();
+ return id.StartsWith("claude-", StringComparison.OrdinalIgnoreCase) ? id.Substring(7) : id;
+ }
+
+ // Normalise a served model id (either "claude-opus-4-8", a dated "claude-opus-4-5-20251101"
+ // or an enum form like "ClaudeOpus4_8") to our rate tag: family, major, optional one-digit
+ // minor. A date suffix is not a minor version. Unknown ids default to the advisor.
+ public static string TagFromModel(string id) => Normalize(id) ?? DefaultAdvisor;
+
+ // Family-major[-minor] of any Claude id or tag, or null when it names no Claude model.
+ private static string? Normalize(string? id)
{
- "opus-5-5" => "claude-opus-5-5",
- "sonnet-5-5" => "claude-sonnet-5-5",
- "haiku-5-5" => "claude-haiku-5-5",
- "opus-5" => "claude-opus-5",
- "fable-5-1" => "claude-fable-5-1",
- "opus-4-8" => "claude-opus-4-8",
- "fable-5" => "claude-fable-5",
- "haiku-4-5" => "claude-haiku-4-5",
- "sonnet-4-6" => "claude-sonnet-4-6",
- "opus-4-7" => "claude-opus-4-7",
- _ => "claude-sonnet-5"
+ var s = (id ?? "").ToLowerInvariant().Replace("_", "-");
+ var m = System.Text.RegularExpressions.Regex.Match(s, @"(fable|mythos|opus|sonnet|haiku)-?(\d+)(?:-(\d)(?!\d))?");
+ if (!m.Success) return null;
+ return m.Groups[1].Value + "-" + m.Groups[2].Value + (m.Groups[3].Success ? "-" + m.Groups[3].Value : "");
+ }
+
+ // Rules below are keyed by the normalised tag, so a dated id from the Models API
+ // ("opus-4-5-20251101") or a full id ("claude-opus-5-5") gets its family's rules.
+ private static string Key(string tag) => Normalize(tag) ?? tag;
+
+ // Display name for a tag the Models API did not name: "opus-5-5" -> "Claude Opus 5.5".
+ public static string DisplayName(string tag)
+ {
+ var parts = TagOf(ApiId(tag)).Split('-');
+ if (parts.Length < 2 || parts[0].Length == 0 || !char.IsDigit(parts[1][0])) return TagOf(ApiId(tag));
+ var version = parts[1] + (parts.Length > 2 && parts[2].Length == 1 ? "." + parts[2] : "");
+ return "Claude " + char.ToUpperInvariant(parts[0][0]) + parts[0].Substring(1) + " " + version;
+ }
+
+ // Shown until the Models API answers (no API key, offline): the current generation only.
+ // Same order as the API: most capable/newest first.
+ public static readonly IReadOnlyList Current = new[]
+ {
+ new ClaudeModelInfo("fable-5-2", "Claude Fable 5.2"),
+ new ClaudeModelInfo("opus-5-5", "Claude Opus 5.5"),
+ new ClaudeModelInfo("sonnet-5-5", "Claude Sonnet 5.5"),
+ new ClaudeModelInfo("haiku-5-5", "Claude Haiku 5.5"),
};
- // Normalise a served model id (either "claude-opus-4-8" or an enum form like "ClaudeOpus4_8")
- // to our rate tag. Most specific first: "opus-5-5" must not fall into "opus-5", and
- // "fable-5-1" must not collapse into "fable-5". Unknown ids default to opus-4-8, the advisor.
- public static string TagFromModel(string id)
+ // The Models API answer as picker entries: Claude ids only, de-duplicated, order kept.
+ public static IReadOnlyList FromServed(IEnumerable<(string Id, string? Name)> served)
{
- var s = id.ToLowerInvariant().Replace("_", "-");
- if (s.Contains("opus-5-5") || s.Contains("opus5-5")) return "opus-5-5";
- if (s.Contains("sonnet-5-5") || s.Contains("sonnet5-5")) return "sonnet-5-5";
- if (s.Contains("haiku-5-5") || s.Contains("haiku5-5")) return "haiku-5-5";
- if (s.Contains("opus-5") || s.Contains("opus5")) return "opus-5";
- if (s.Contains("fable-5-1") || s.Contains("fable5-1")) return "fable-5-1";
- if (s.Contains("opus") && s.Contains("4-7")) return "opus-4-7";
- if (s.Contains("opus")) return "opus-4-8";
- if (s.Contains("sonnet") && s.Contains("4-6")) return "sonnet-4-6";
- if (s.Contains("sonnet")) return "sonnet-5";
- if (s.Contains("haiku")) return "haiku-4-5";
- if (s.Contains("fable")) return "fable-5";
- return "opus-4-8";
+ var list = new List();
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var (id, name) in served)
+ {
+ if (string.IsNullOrWhiteSpace(id) || !id.StartsWith("claude-", StringComparison.OrdinalIgnoreCase)) continue;
+ var tag = TagOf(id);
+ if (!seen.Add(tag)) continue;
+ list.Add(new ClaudeModelInfo(tag, string.IsNullOrWhiteSpace(name) ? DisplayName(tag) : name!.Trim()));
+ }
+ return list;
}
// Preserved thinking: these models bind each thinking block to the exact conversation before
// it, and for accounts created from 31 August 2026 the API answers an edited history with a
// 400. For them the history must be append-only: no tool-result aging, no client-side
// compaction, no per-request text removed again, and system/tools kept stable.
- public static bool AppendOnlyHistory(string tag) => tag is "fable-5-1" or "opus-5-5" or "sonnet-5-5" or "haiku-5-5";
+ public static bool AppendOnlyHistory(string tag) => Key(tag) is "fable-5-2" or "mythos-5-2" or "fable-5-1" or "opus-5-5" or "sonnet-5-5" or "haiku-5-5";
// Models whose safety classifier can decline a request and that accept the server-side
// fallback ("fallbacks": "default"). Haiku 5.5 declines too but has no server-side fallback.
- public static bool SupportsServerFallback(string tag) => tag is "fable-5" or "fable-5-1" or "opus-5" or "opus-5-5" or "sonnet-5-5";
+ public static bool SupportsServerFallback(string tag) => Key(tag) is "fable-5-2" or "mythos-5-2" or "fable-5" or "fable-5-1" or "opus-5" or "opus-5-5" or "sonnet-5-5";
- // Haiku 4.5 predates adaptive thinking and rejects the effort parameter; every other
- // current model takes both.
- public static bool SupportsAdaptiveThinking(string tag) => tag != "haiku-4-5";
+ // Adaptive thinking and effort arrived with the 4.6 generation; Haiku 4.5 and anything older
+ // (which the Models API may still list) reject both. Fable and Mythos always take them.
+ public static bool SupportsAdaptiveThinking(string tag)
+ {
+ var t = Key(tag);
+ if (t.StartsWith("fable-") || t.StartsWith("mythos-")) return true;
+ var parts = t.Split('-');
+ if (parts.Length < 2 || !int.TryParse(parts[1], out var major)) return true;
+ var minor = parts.Length > 2 && int.TryParse(parts[2], out var n) ? n : 0;
+ return major > 4 || (major == 4 && minor >= 6);
+ }
// List prices per million tokens (USD) and the cache-read price as a fraction of base input.
// Cache writes are always 1-hour here (2x base input). Haiku 5.5 is priced by prompt size:
@@ -65,16 +110,17 @@ public static string TagFromModel(string id)
public static bool TryPrice(string tag, long promptTokens, out Price price)
{
- price = tag switch
+ price = Key(tag) switch
{
"opus-5-5" => new(4.0m, 20.0m, 0.05m),
"sonnet-5-5" => new(2.0m, 10.0m, 0.1m),
"haiku-5-5" => promptTokens > 100_000 ? new(0.5m, 2.5m, 0.1m) : new(0.1m, 0.5m, 0.1m),
+ "fable-5-2" or "mythos-5-2" => new(8.0m, 40.0m, 0.025m),
"fable-5-1" => new(10.0m, 50.0m, 0.025m),
"fable-5" => new(10.0m, 50.0m, 0.1m),
"opus-5" => new(5.0m, 25.0m, 0.1m),
"sonnet-5" => new(2.0m, 10.0m, 0.1m),
- "opus-4-8" or "opus-4-7" => new(5.0m, 25.0m, 0.1m),
+ "opus-4-8" or "opus-4-7" or "opus-4-6" => new(5.0m, 25.0m, 0.1m),
"sonnet-4-6" => new(3.0m, 15.0m, 0.1m),
"haiku-4-5" => new(1.0m, 5.0m, 0.1m),
_ => default
@@ -105,3 +151,6 @@ public static decimal Cost(Price price, long input, long output, long cacheCreat
_ => null
};
}
+
+// One entry of the Claude model picker: our tag ("opus-5-5") and the name to show.
+public sealed record ClaudeModelInfo(string Tag, string Name);
diff --git a/ClaudeRevit/Services/SettingsStore.cs b/ClaudeRevit/Services/SettingsStore.cs
index 2642eea..56ab034 100644
--- a/ClaudeRevit/Services/SettingsStore.cs
+++ b/ClaudeRevit/Services/SettingsStore.cs
@@ -108,23 +108,36 @@ public static bool AutoUseAdvisor
}
// "Auto" executor model: the cheap/fast model that runs every round.
- // "sonnet-5" (default) — best balance. "haiku-4-5" — cheapest/fastest, pairs with an
- // advisor for the hard moments (Haiku executor + Opus/Fable advisor is a valid pattern).
+ // "sonnet-5-5" (default) — best balance. "haiku-5-5" — cheapest, pairs with an advisor
+ // for the hard moments. Values saved by older versions (Sonnet 5, Haiku 4.5) move to the
+ // current generation of the same tier.
public static string AutoExecutorModel
{
- get => string.IsNullOrWhiteSpace(Current.AutoExecutorModel) ? "sonnet-5" : Current.AutoExecutorModel;
+ get => CurrentExecutor(Current.AutoExecutorModel);
set { Current.AutoExecutorModel = value; Save(); }
}
// "Auto" advisor model consulted mid-turn.
- // "opus-4-8" (default). "fable-5" — Anthropic's most capable, for the hardest tasks
- // (2x Opus's price, but only the short advice sub-inference is billed at that rate).
+ // "opus-5-5" (default). "fable-5-2" — Anthropic's most capable, for the hardest tasks
+ // (only the short advice sub-inference is billed at its rate). Both are accepted as advisors
+ // by both executors; Opus 4.8 and older are rejected by a Sonnet 5.5 executor, so older
+ // saved values move to the current model of the same tier.
public static string AutoAdvisorModel
{
- get => string.IsNullOrWhiteSpace(Current.AutoAdvisorModel) ? "opus-4-8" : Current.AutoAdvisorModel;
+ get => CurrentAdvisor(Current.AutoAdvisorModel);
set { Current.AutoAdvisorModel = value; Save(); }
}
+ public static string CurrentExecutor(string? saved) =>
+ saved is "sonnet-5-5" or "haiku-5-5" ? saved
+ : saved != null && saved.StartsWith("haiku", StringComparison.Ordinal) ? "haiku-5-5"
+ : ClaudeModels.DefaultExecutor;
+
+ public static string CurrentAdvisor(string? saved) =>
+ saved is "opus-5-5" or "fable-5-2" ? saved
+ : saved != null && (saved.StartsWith("fable", StringComparison.Ordinal) || saved.StartsWith("mythos", StringComparison.Ordinal)) ? "fable-5-2"
+ : ClaudeModels.DefaultAdvisor;
+
// Show a per-task diagnostics line (elapsed time, tokens, rounds) after each answer, to
// compare how efficiently different models solve the same task. On by default.
public static bool ShowTaskDiagnostics
@@ -337,8 +350,8 @@ private sealed class Settings
public int AltContextK { get; set; } = 0;
public bool AltCompactTools { get; set; } = true;
public bool AutoUseAdvisor { get; set; } = true;
- public string AutoExecutorModel { get; set; } = "sonnet-5";
- public string AutoAdvisorModel { get; set; } = "opus-4-8";
+ public string AutoExecutorModel { get; set; } = ClaudeModels.DefaultExecutor;
+ public string AutoAdvisorModel { get; set; } = ClaudeModels.DefaultAdvisor;
public bool ShowTaskDiagnostics { get; set; } = true;
public bool McpEnabled { get; set; }
public int McpPort { get; set; } = 8788;
diff --git a/ClaudeRevit/UI/BenchmarkWindow.xaml b/ClaudeRevit/UI/BenchmarkWindow.xaml
index 3d50cb1..c139104 100644
--- a/ClaudeRevit/UI/BenchmarkWindow.xaml
+++ b/ClaudeRevit/UI/BenchmarkWindow.xaml
@@ -73,9 +73,17 @@
-
-
+
+
+
+
+
+ codex = System.Array.Empty();
if (Backend(BackendBox) == "codex" || Backend(JudgeBackendBox) == "codex")
codex = await CodexModelCatalog.ReadAsync(SettingsStore.CodexExe, McpServer.ClientWorkDir(), _lifetime.Token, forceRefresh: true);
+ // Claude models from the Models API (or the built-in current list without a key).
+ System.Collections.Generic.IReadOnlyList claude = ClaudeModels.Current;
+ string? claudeError = null;
+ if (Backend(BackendBox) != "codex" || Backend(JudgeBackendBox) != "codex")
+ {
+ try { claude = await ClaudeModelCatalog.ReadAsync(_lifetime.Token); }
+ catch (Exception ex) when (ex is not OperationCanceledException) { claudeError = "Claude models (showing current ones): " + ex.Message; }
+ }
if (version != _loading || _lifetime.IsCancellationRequested) return;
void Fill(ComboBox backend, ComboBox box)
{
@@ -79,17 +87,20 @@ void Fill(ComboBox backend, ComboBox box)
var options = Backend(backend) switch
{
"codex" => codex.Select(m => new ModelOption(m.Id, m.Name, m.Efforts, m.DefaultEffort)).ToList(),
- "claudecode" => new[] { "", "opus", "sonnet", "haiku" }.Select(id => new ModelOption(id, id == "" ? "CLI default" : id,
- new[] { "low", "medium", "high" }, "high")).ToList(),
- _ => new[] { "auto", "sonnet-5", "sonnet-5-5", "opus-5-5", "opus-4-8", "opus-5", "haiku-5-5", "haiku-4-5", "alt" }
- .Select(id => new ModelOption(id, id, System.Array.Empty(), "")).ToList()
+ "claudecode" => new[] { new ModelOption("", "CLI default", new[] { "low", "medium", "high" }, "high") }
+ .Concat(claude.Select(m => new ModelOption(ClaudeModels.ApiId(m.Tag), m.Name, new[] { "low", "medium", "high" }, "high"))).ToList(),
+ _ => new[] { new ModelOption("auto", "auto", System.Array.Empty(), "") }
+ .Concat(claude.Select(m => new ModelOption(m.Tag, m.Name, System.Array.Empty(), "")))
+ .Append(new ModelOption("alt", "alt", System.Array.Empty(), "")).ToList()
};
box.ItemsSource = options;
box.SelectedItem = options.FirstOrDefault(m => m.Id == old) ??
options.FirstOrDefault(m => codex.Any(c => c.IsDefault && c.Id == m.Id)) ?? options.FirstOrDefault();
}
Fill(BackendBox, ModelBox); Fill(JudgeBackendBox, JudgeBox);
+ // ModelStatusText gates Codex runs, so a Claude catalog problem goes to the summary.
ModelStatusText.Text = "";
+ if (claudeError != null) SummaryText.Text = claudeError;
}
catch (OperationCanceledException) when (_lifetime.IsCancellationRequested) { }
catch (Exception ex) { if (version == _loading) ModelStatusText.Text = ex.Message; }
@@ -216,6 +227,52 @@ private void Calibrate_Click(object sender, RoutedEventArgs e)
}
private void CancelButton_Click(object sender, RoutedEventArgs e) => _cts?.Cancel();
+ // Per-run JSONL logs live in %AppData%\ClaudeRevit\benchmarks; the cumulative results file
+ // sits one level up. Same folder BenchmarkLog writes to by default.
+ private static string LogsFolder => System.IO.Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ClaudeRevit", "benchmarks");
+
+ // The run in progress or the last one of this session; otherwise the newest log on disk,
+ // so a log from an earlier Revit session can still be opened.
+ private static string? LatestLogPath()
+ {
+ if (BenchmarkRunner.LastLogPath is { } current && System.IO.File.Exists(current)) return current;
+ try
+ {
+ var dir = new System.IO.DirectoryInfo(LogsFolder);
+ return dir.Exists ? dir.GetFiles("*.jsonl").OrderByDescending(f => f.LastWriteTimeUtc).FirstOrDefault()?.FullName : null;
+ }
+ catch { return null; }
+ }
+
+ private void OpenLog_Click(object sender, RoutedEventArgs e)
+ {
+ var path = LatestLogPath();
+ if (path == null) { SummaryText.Text = "No benchmark log yet — run a benchmark first. Logs folder: " + LogsFolder; return; }
+ // .jsonl usually has no file association; fall back to Notepad, which reads a log that
+ // is still being written (the runner opens it with FileShare.ReadWrite).
+ try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); }
+ catch
+ {
+ try { Process.Start(new ProcessStartInfo("notepad.exe") { ArgumentList = { path }, UseShellExecute = false }); }
+ catch (Exception ex) { SummaryText.Text = "Cannot open the log (" + ex.Message + "): " + path; return; }
+ }
+ SummaryText.ToolTip = "Log: " + path;
+ }
+
+ private void LogFolder_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ System.IO.Directory.CreateDirectory(LogsFolder);
+ var latest = LatestLogPath();
+ var start = new ProcessStartInfo("explorer.exe") { UseShellExecute = false };
+ if (latest != null) start.ArgumentList.Add("/select," + latest); else start.ArgumentList.Add(LogsFolder);
+ Process.Start(start);
+ }
+ catch (Exception ex) { SummaryText.Text = "Cannot open " + LogsFolder + ": " + ex.Message; }
+ }
+
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
_cts?.Cancel();
diff --git a/ClaudeRevit/UI/ChatPaneView.xaml b/ClaudeRevit/UI/ChatPaneView.xaml
index bf0ab62..bd681cc 100644
--- a/ClaudeRevit/UI/ChatPaneView.xaml
+++ b/ClaudeRevit/UI/ChatPaneView.xaml
@@ -45,17 +45,6 @@
Width="190" Height="24" VerticalContentAlignment="Center"
SelectionChanged="ModelPicker_SelectionChanged">
Auto (cost-optimized)
- Sonnet 5 (default)
- Sonnet 5.5
- Opus 5.5
- Opus 4.8 (smart)
- Fable 5 (smartest)
- Opus 5
- Fable 5.1
- Haiku 5.5 (cheapest)
- Haiku 4.5 (fastest)
- Sonnet 4.6
- Opus 4.7Alt model (set up in ⚙)
@@ -68,9 +57,9 @@
DisplayMemberPath="Label" SelectedValuePath="Id"
SelectionChanged="McpEffortPicker_SelectionChanged"
ToolTip="Reasoning effort for this agent and model." />
-
+