feat(anthropic): current model catalogue with adaptive thinking and summarized display
This commit is contained in:
@@ -30,7 +30,7 @@ dotnet test
|
||||
dotnet run --project samples/Volcanic.AgenticCore.Sample.Headless -- --faux "Say hello"
|
||||
|
||||
# with a real provider
|
||||
ANTHROPIC_API_KEY=sk-ant-... dotnet run --project samples/Volcanic.AgenticCore.Sample.Headless -- --model anthropic/claude-sonnet-4-5 "List the files here"
|
||||
ANTHROPIC_API_KEY=sk-ant-... dotnet run --project samples/Volcanic.AgenticCore.Sample.Headless -- --model anthropic/claude-opus-5 "List the files here"
|
||||
|
||||
# with a Claude subscription through the claude CLI
|
||||
dotnet run --project samples/Volcanic.AgenticCore.Sample.Headless -- --model claude-code/sonnet "List the files here"
|
||||
@@ -42,7 +42,7 @@ Hosting it yourself:
|
||||
var services = new ServiceCollection();
|
||||
services.AddAgenticCore(o =>
|
||||
{
|
||||
o.DefaultModel = "anthropic/claude-sonnet-4-5";
|
||||
o.DefaultModel = "anthropic/claude-opus-5";
|
||||
o.AddExtension<AnthropicProviderExtension>();
|
||||
o.AddExtension<OpenAIProviderExtension>();
|
||||
o.AddExtension<ClaudeCodeProviderExtension>();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Model keys
|
||||
|
||||
Models are addressed as `provider/model-id`, for example `anthropic/claude-sonnet-4-5`, `openai/gpt-5`,
|
||||
Models are addressed as `provider/model-id`, for example `anthropic/claude-opus-5`, `openai/gpt-5`,
|
||||
`claude-code/sonnet`, `openrouter/deepseek/deepseek-chat`. `AgentSession.SetModelAsync("provider/id")`,
|
||||
`AgenticCoreOptions.DefaultModel` and `AgentSessionOptions.Model` all take this form.
|
||||
|
||||
|
||||
@@ -312,7 +312,8 @@ public sealed class AnthropicApi(HttpClient? http = null)
|
||||
return new ThinkingPlan(new JsonObject { ["type"] = "enabled", ["budget_tokens"] = budget }, budget, null);
|
||||
|
||||
var effort = EffortNames.Contains(mapped) ? mapped.ToLowerInvariant() : null;
|
||||
return new ThinkingPlan(new JsonObject { ["type"] = "adaptive" }, null, effort);
|
||||
// Newer models omit thinking text by default; ask for the readable summary so UIs can show it.
|
||||
return new ThinkingPlan(new JsonObject { ["type"] = "adaptive", ["display"] = "summarized" }, null, effort);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -3,7 +3,7 @@ using Volcanic.AgenticCore.Abstractions.Providers;
|
||||
namespace Volcanic.AgenticCore.Extensions.Anthropic;
|
||||
|
||||
/// <summary>
|
||||
/// Static catalogue of Anthropic models served through the Messages API.
|
||||
/// Static catalogue of Anthropic models served through the Messages API (prices in USD per million tokens).
|
||||
/// Hosts can add more (or override these) by registering their own <see cref="ProviderRegistration"/>.
|
||||
/// </summary>
|
||||
public static class AnthropicModels
|
||||
@@ -13,8 +13,22 @@ public static class AnthropicModels
|
||||
public const string DefaultBaseUrl = "https://api.anthropic.com";
|
||||
|
||||
/// <summary>
|
||||
/// Thinking budgets (in tokens, as strings) for the catalogue models. <see cref="ThinkingLevel.XHigh"/> and
|
||||
/// <see cref="ThinkingLevel.Max"/> are marked unsupported; the core clamps them to <see cref="ThinkingLevel.High"/>.
|
||||
/// Adaptive thinking (Claude 4.6 and later): the level becomes <c>output_config.effort</c> and the request sends
|
||||
/// <c>thinking: {type: "adaptive"}</c>. <see cref="ThinkingLevel.Minimal"/> maps to <c>low</c>.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyDictionary<ThinkingLevel, string?> AdaptiveEffort = new Dictionary<ThinkingLevel, string?>
|
||||
{
|
||||
[ThinkingLevel.Minimal] = "low",
|
||||
[ThinkingLevel.Low] = "low",
|
||||
[ThinkingLevel.Medium] = "medium",
|
||||
[ThinkingLevel.High] = "high",
|
||||
[ThinkingLevel.XHigh] = "xhigh",
|
||||
[ThinkingLevel.Max] = "max",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fixed thinking budgets (tokens, as strings) for models that still take <c>budget_tokens</c> (Haiku 4.5 and older).
|
||||
/// <see cref="ThinkingLevel.XHigh"/> and <see cref="ThinkingLevel.Max"/> are unsupported; the core clamps them to High.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyDictionary<ThinkingLevel, string?> ThinkingBudgets = new Dictionary<ThinkingLevel, string?>
|
||||
{
|
||||
@@ -26,13 +40,22 @@ public static class AnthropicModels
|
||||
[ThinkingLevel.Max] = null,
|
||||
};
|
||||
|
||||
public static readonly Model Opus41 = Create("claude-opus-4-1", "Claude Opus 4.1", new ModelCost(15m, 75m, 1.5m, 18.75m), maxTokens: 32_000);
|
||||
public static readonly Model Sonnet45 = Create("claude-sonnet-4-5", "Claude Sonnet 4.5", new ModelCost(3m, 15m, 0.3m, 3.75m), maxTokens: 64_000);
|
||||
public static readonly Model Haiku45 = Create("claude-haiku-4-5", "Claude Haiku 4.5", new ModelCost(1m, 5m, 0.1m, 1.25m), maxTokens: 64_000);
|
||||
// Current generation (adaptive thinking, 1M context, 128K output).
|
||||
public static readonly Model Opus5 = Create("claude-opus-5", "Claude Opus 5", new ModelCost(5m, 25m, 0.5m, 6.25m), 1_000_000, 128_000, AdaptiveEffort);
|
||||
public static readonly Model Sonnet5 = Create("claude-sonnet-5", "Claude Sonnet 5", new ModelCost(2m, 10m, 0.2m, 2.5m), 1_000_000, 128_000, AdaptiveEffort);
|
||||
public static readonly Model Fable51 = Create("claude-fable-5-1", "Claude Fable 5.1", new ModelCost(10m, 50m, 0.25m, 12.5m), 1_000_000, 128_000, AdaptiveEffort);
|
||||
public static readonly Model Opus48 = Create("claude-opus-4-8", "Claude Opus 4.8", new ModelCost(5m, 25m, 0.5m, 6.25m), 1_000_000, 128_000, AdaptiveEffort);
|
||||
public static readonly Model Sonnet46 = Create("claude-sonnet-4-6", "Claude Sonnet 4.6", new ModelCost(3m, 15m, 0.3m, 3.75m), 1_000_000, 128_000, AdaptiveEffort);
|
||||
public static readonly Model Haiku45 = Create("claude-haiku-4-5", "Claude Haiku 4.5", new ModelCost(1m, 5m, 0.1m, 1.25m), 200_000, 64_000, ThinkingBudgets);
|
||||
|
||||
public static readonly IReadOnlyList<Model> All = [Opus41, Sonnet45, Haiku45];
|
||||
// Previous generation (fixed thinking budgets). Not registered by default; available for hosts that still need them.
|
||||
public static readonly Model Opus41 = Create("claude-opus-4-1", "Claude Opus 4.1", new ModelCost(15m, 75m, 1.5m, 18.75m), 200_000, 32_000, ThinkingBudgets);
|
||||
public static readonly Model Sonnet45 = Create("claude-sonnet-4-5", "Claude Sonnet 4.5", new ModelCost(3m, 15m, 0.3m, 3.75m), 200_000, 64_000, ThinkingBudgets);
|
||||
|
||||
private static Model Create(string id, string name, ModelCost cost, int maxTokens) => new()
|
||||
public static readonly IReadOnlyList<Model> All = [Opus5, Sonnet5, Fable51, Opus48, Sonnet46, Haiku45];
|
||||
public static readonly IReadOnlyList<Model> Legacy = [Opus41, Sonnet45];
|
||||
|
||||
private static Model Create(string id, string name, ModelCost cost, int contextWindow, int maxTokens, IReadOnlyDictionary<ThinkingLevel, string?> thinking) => new()
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
@@ -42,8 +65,8 @@ public static class AnthropicModels
|
||||
Reasoning = true,
|
||||
Input = [ModelInput.Text, ModelInput.Image],
|
||||
Cost = cost,
|
||||
ContextWindow = 200_000,
|
||||
ContextWindow = contextWindow,
|
||||
MaxTokens = maxTokens,
|
||||
ThinkingLevelMap = ThinkingBudgets,
|
||||
ThinkingLevelMap = thinking,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ var services = new ServiceCollection();
|
||||
services.AddLogging(b => b.AddSimpleConsole(o => o.SingleLine = true).SetMinimumLevel(quiet ? LogLevel.Warning : LogLevel.Information));
|
||||
services.AddAgenticCore(o =>
|
||||
{
|
||||
o.DefaultModel = model ?? "anthropic/claude-sonnet-4-5";
|
||||
o.DefaultModel = model ?? "anthropic/claude-opus-5";
|
||||
o.AddExtension<AnthropicProviderExtension>();
|
||||
o.AddExtension<OpenAIProviderExtension>();
|
||||
o.AddExtension<ClaudeCodeProviderExtension>();
|
||||
|
||||
@@ -29,16 +29,16 @@ public class AnthropicExtensionTests
|
||||
Assert.Equal("$ANTHROPIC_API_KEY", registration.ApiKey);
|
||||
Assert.True(registration.RequiresApiKey);
|
||||
Assert.NotNull(registration.Stream);
|
||||
Assert.Equal(["claude-opus-4-1", "claude-sonnet-4-5", "claude-haiku-4-5"], registration.Models.Select(m => m.Id).ToArray());
|
||||
Assert.Equal(["claude-opus-5", "claude-sonnet-5", "claude-fable-5-1", "claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"], registration.Models.Select(m => m.Id).ToArray());
|
||||
Assert.All(registration.Models, m =>
|
||||
{
|
||||
Assert.Equal("anthropic", m.Provider);
|
||||
Assert.Equal("anthropic-messages", m.Api);
|
||||
Assert.True(m.Reasoning);
|
||||
Assert.True(m.SupportsImages);
|
||||
Assert.Equal(200_000, m.ContextWindow);
|
||||
Assert.Null(m.ThinkingLevelMap![ThinkingLevel.XHigh]);
|
||||
Assert.Equal("8192", m.ThinkingLevelMap[ThinkingLevel.Medium]);
|
||||
Assert.True(m.ContextWindow >= 200_000);
|
||||
Assert.NotNull(m.ThinkingLevelMap![ThinkingLevel.Medium]);
|
||||
Assert.NotNull(m.ThinkingLevelMap[ThinkingLevel.High]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,12 +64,12 @@ public class AnthropicExtensionTests
|
||||
await new AnthropicProviderExtension().ActivateAsync(api, CancellationToken.None);
|
||||
registry.Register(api.Providers[0]);
|
||||
|
||||
var model = registry.Find("anthropic/claude-sonnet-4-5");
|
||||
var model = registry.Find("anthropic/claude-opus-5");
|
||||
Assert.NotNull(model);
|
||||
Assert.Equal("Claude Sonnet 4.5", model!.Name);
|
||||
Assert.Equal("Claude Opus 5", model!.Name);
|
||||
Assert.Equal("anthropic-messages", model.Api);
|
||||
Assert.Equal("https://api.anthropic.com", model.BaseUrl);
|
||||
Assert.Equal(3, registry.ListModels().Count);
|
||||
Assert.Equal(6, registry.ListModels().Count);
|
||||
}
|
||||
|
||||
/// <summary>Only <see cref="RegisterProvider"/> is supported; everything else throws.</summary>
|
||||
|
||||
@@ -250,4 +250,23 @@ public class AnthropicRequestTests
|
||||
Assert.Equal(JsonValueKind.Object, doc.RootElement.ValueKind);
|
||||
Assert.IsType<JsonObject>(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Catalogue_models_use_adaptive_thinking_or_budgets_as_appropriate()
|
||||
{
|
||||
var opus = AnthropicApi.BuildRequest(AnthropicModels.Opus5, LlmContext.Simple("s", "hi"), new StreamOptions { Reasoning = ThinkingLevel.XHigh });
|
||||
Assert.Equal("adaptive", opus["thinking"]!["type"]!.GetValue<string>());
|
||||
Assert.Equal("summarized", opus["thinking"]!["display"]!.GetValue<string>());
|
||||
Assert.Equal("xhigh", opus["output_config"]!["effort"]!.GetValue<string>());
|
||||
Assert.Null(opus["thinking"]!["budget_tokens"]);
|
||||
Assert.Equal(128_000, opus["max_tokens"]!.GetValue<int>());
|
||||
|
||||
var haiku = AnthropicApi.BuildRequest(AnthropicModels.Haiku45, LlmContext.Simple("s", "hi"), new StreamOptions { Reasoning = ThinkingLevel.Medium });
|
||||
Assert.Equal("enabled", haiku["thinking"]!["type"]!.GetValue<string>());
|
||||
Assert.Equal(8192, haiku["thinking"]!["budget_tokens"]!.GetValue<int>());
|
||||
Assert.Null(haiku["output_config"]);
|
||||
|
||||
var off = AnthropicApi.BuildRequest(AnthropicModels.Sonnet5, LlmContext.Simple("s", "hi"), new StreamOptions { Reasoning = ThinkingLevel.Off });
|
||||
Assert.Null(off["thinking"]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user