diff --git a/Directory.Packages.props b/Directory.Packages.props index 1fcda75..b9ec2ce 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + diff --git a/docs/providers.md b/docs/providers.md index b7362e5..425e17c 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -103,3 +103,7 @@ Hard failures use **`ProviderExecutionException`** with **`ProviderFailureKind`* 4. Extend **`ProviderCatalogOptions`** (defaults / aliases) via **`IConfiguration`** or **`Configure`** / **`PostConfigure`**. **Test pattern:** **`SharpClaw.Code.MockProvider`** registers **`DeterministicMockModelProvider`** with **`PostConfigure`** so **`default`** maps to provider name **`mock`** (`MockProviderServiceCollectionExtensions`). + +Anthropic requests omit the deprecated `temperature` field, including when the shared +provider request specifies a value. This avoids rejection by newer Claude models; +the shared request contract and other providers retain temperature support. diff --git a/src/SharpClaw.Code.Providers/AnthropicProvider.cs b/src/SharpClaw.Code.Providers/AnthropicProvider.cs index 511a354..abee896 100644 --- a/src/SharpClaw.Code.Providers/AnthropicProvider.cs +++ b/src/SharpClaw.Code.Providers/AnthropicProvider.cs @@ -50,8 +50,40 @@ public async Task StartStreamAsync(ProviderRequest request var client = CreateClient(resolved.ApiKey); var modelId = Internal.ProviderHttpHelpers.ResolveModelOrDefault(request.Model, _options.DefaultModel); + var parameters = CreateMessageParameters(request, modelId); + + logger.LogInformation("Starting Anthropic SDK stream for request {RequestId}.", request.Id); + + IAsyncEnumerable stream; + try + { + stream = client.Messages.CreateStreaming(parameters, cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) when (ProviderStreamFailureClassifier.IsAuthenticationFailure(exception)) + { + throw new ProviderExecutionException( + ProviderName, + modelId, + ProviderFailureKind.AuthenticationUnavailable, + $"Provider '{ProviderName}' authentication failed while starting the stream.", + exception); + } + + logger.LogInformation("Started Anthropic SDK stream for request {RequestId}.", request.Id); + + return new ProviderStreamHandle(request, AnthropicSdkStreamAdapter.AdaptAsync(stream, request.Id, systemClock, cancellationToken)); + } + + /// + /// Builds SDK parameters without the obsolete temperature field, which newer models reject. + /// + internal static MessageCreateParams CreateMessageParameters(ProviderRequest request, string modelId) + { var systemPrompt = string.IsNullOrWhiteSpace(request.SystemPrompt) ? null : request.SystemPrompt; - float? temperature = request.Temperature.HasValue ? (float)request.Temperature.Value : null; MessageCreateParams parameters; @@ -63,7 +95,6 @@ public async Task StartStreamAsync(ProviderRequest request MaxTokens = request.MaxTokens ?? 1024, Model = modelId, Messages = messages, - Temperature = temperature, }; if (request.Tools is { Count: > 0 } tools) @@ -86,7 +117,6 @@ public async Task StartStreamAsync(ProviderRequest request Content = request.Prompt, }, ], - Temperature = temperature, }; } @@ -95,30 +125,7 @@ public async Task StartStreamAsync(ProviderRequest request parameters = parameters with { System = systemPrompt }; } - logger.LogInformation("Starting Anthropic SDK stream for request {RequestId}.", request.Id); - - IAsyncEnumerable stream; - try - { - stream = client.Messages.CreateStreaming(parameters, cancellationToken); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception exception) when (ProviderStreamFailureClassifier.IsAuthenticationFailure(exception)) - { - throw new ProviderExecutionException( - ProviderName, - modelId, - ProviderFailureKind.AuthenticationUnavailable, - $"Provider '{ProviderName}' authentication failed while starting the stream.", - exception); - } - - logger.LogInformation("Started Anthropic SDK stream for request {RequestId}.", request.Id); - - return new ProviderStreamHandle(request, AnthropicSdkStreamAdapter.AdaptAsync(stream, request.Id, systemClock, cancellationToken)); + return parameters; } private AnthropicClient CreateClient(string? resolvedApiKey) diff --git a/tests/SharpClaw.Code.UnitTests/Providers/AnthropicRequestCompatibilityTests.cs b/tests/SharpClaw.Code.UnitTests/Providers/AnthropicRequestCompatibilityTests.cs new file mode 100644 index 0000000..2402cad --- /dev/null +++ b/tests/SharpClaw.Code.UnitTests/Providers/AnthropicRequestCompatibilityTests.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using SharpClaw.Code.Protocol.Enums; +using SharpClaw.Code.Protocol.Models; +using SharpClaw.Code.Providers; + +namespace SharpClaw.Code.UnitTests.Providers; + +/// Verifies wire compatibility with Anthropic models that reject temperature. +public sealed class AnthropicRequestCompatibilityTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Requests_omit_temperature_and_preserve_message_options(bool useHistory) + { + var request = new ProviderRequest( + "request", "session", "turn", "anthropic", "claude-model", "Hello", "Be concise.", + OutputFormat.Text, 0.2m, null, + Messages: useHistory ? [new ChatMessage("user", [new ContentBlock(ContentBlockKind.Text, "Hello", null, null, null, null)])] : null, + Tools: useHistory ? [new ProviderToolDefinition("read_file", "Read a file", "{\"type\":\"object\"}")] : null, + MaxTokens: 2048); + + var parameters = AnthropicProvider.CreateMessageParameters(request, request.Model); + using var json = JsonDocument.Parse(JsonSerializer.Serialize(parameters.RawBodyData)); + var root = json.RootElement; + + Assert.False(root.TryGetProperty("temperature", out _)); + Assert.Equal("claude-model", root.GetProperty("model").GetString()); + Assert.Equal(2048, root.GetProperty("max_tokens").GetInt32()); + Assert.Equal("Be concise.", root.GetProperty("system").GetString()); + Assert.Single(root.GetProperty("messages").EnumerateArray()); + if (useHistory) + { + Assert.Equal("read_file", root.GetProperty("tools")[0].GetProperty("name").GetString()); + } + } +}