From 651221e8323ac8caedd5dfeeb59e5d1f7cf99b10 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 13 Mar 2026 20:48:08 -0400 Subject: [PATCH 1/3] Update IChatClient with support from latest bedrock runtime / M.E.AI - Updates to M.E.AI.Abstractions 10.4.0 - Adds support for multi-modal tool returns. - Adds support for citations with URIs. - Adds support for cached tokens in usage details. - Adds support for reasoning. - Adds a ton of tests verifying IChatClient behavior around the underlying IAmazonBedrockRuntime. --- ...xtensions.Bedrock.MEAI.NetFramework.csproj | 2 +- ...Extensions.Bedrock.MEAI.NetStandard.csproj | 2 +- .../AWSSDK.Extensions.Bedrock.MEAI.nuspec | 6 +- .../BedrockChatClient.cs | 165 +- .../BedrockChatClientTests.cs | 4224 ++++++++++++++--- .../BedrockMEAITests.NetFramework.csproj | 2 +- .../BedrockMEAITests/MockBedrockRuntime.cs | 57 + 7 files changed, 3832 insertions(+), 626 deletions(-) create mode 100644 extensions/test/BedrockMEAITests/MockBedrockRuntime.cs diff --git a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetFramework.csproj b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetFramework.csproj index 446626bb8afa..42eed55ae7d4 100644 --- a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetFramework.csproj +++ b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetFramework.csproj @@ -37,7 +37,7 @@ - + diff --git a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetStandard.csproj b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetStandard.csproj index 97b1a20a55e0..d93192a20d3b 100644 --- a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetStandard.csproj +++ b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetStandard.csproj @@ -41,7 +41,7 @@ - + diff --git a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.nuspec b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.nuspec index 8ee03f932450..a66517dfde6c 100644 --- a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.nuspec +++ b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.nuspec @@ -15,17 +15,17 @@ - + - + - + diff --git a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs index 9a2140d3f21e..2550712589ec 100644 --- a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs +++ b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs @@ -70,11 +70,18 @@ public void Dispose() /// /// + /// /// When is specified, the model must support /// the ToolChoice feature. Models without this support will return an error from the Bedrock API /// (typically with ErrorCode "ValidationException"). /// If the model fails to return the expected structured output, /// is thrown. + /// + /// + /// When is specified with a non- effort, + /// the model must support extended thinking (e.g. Anthropic Claude). Models without this support will return + /// an error from the Bedrock API. + /// /// public async Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) @@ -90,6 +97,7 @@ public async Task GetResponseAsync( request.System = CreateSystem(request.System, messages, options); request.ToolConfig = CreateToolConfig(request.ToolConfig, options); request.InferenceConfig = CreateInferenceConfiguration(request.InferenceConfig, options); + request.AdditionalModelRequestFields = ApplyReasoningConfig(request.AdditionalModelRequestFields, request.InferenceConfig, options); ConverseResponse response = await _runtime.ConverseAsync(request, cancellationToken).ConfigureAwait(false); @@ -162,8 +170,9 @@ public async Task GetResponseAsync( TextContent tc = new(citations.Content[i]?.Text) { RawRepresentation = citations.Content[i] }; tc.Annotations = [new CitationAnnotation() { + Snippet = citations.Citations[i].SourceContent?.Select(c => c.Text).FirstOrDefault() ?? citations.Citations[i].Source, Title = citations.Citations[i].Title, - Snippet = citations.Citations[i].SourceContent?.Select(c => c.Text).FirstOrDefault(), + Url = Uri.TryCreate(citations.Citations[i].Location?.Web?.Url, UriKind.Absolute, out Uri? uri) ? uri : null, }]; result.Contents.Add(tc); } @@ -228,6 +237,13 @@ public async Task GetResponseAsync( } /// + /// + /// + /// When is specified with a non- effort, + /// the model must support extended thinking (e.g. Anthropic Claude). Models without this support will return + /// an error from the Bedrock API. + /// + /// public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -256,6 +272,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( request.System = CreateSystem(request.System, messages, options); request.ToolConfig = CreateToolConfig(request.ToolConfig, options); request.InferenceConfig = CreateInferenceConfiguration(request.InferenceConfig, options); + request.AdditionalModelRequestFields = ApplyReasoningConfig(request.AdditionalModelRequestFields, request.InferenceConfig, options); var result = await _runtime.ConverseStreamAsync(request, cancellationToken).ConfigureAwait(false); @@ -422,15 +439,11 @@ private static UsageDetails CreateUsageDetails(TokenUsage usage) UsageDetails ud = new() { InputTokenCount = usage.InputTokens, + CachedInputTokenCount = usage.CacheReadInputTokens, OutputTokenCount = usage.OutputTokens, TotalTokenCount = usage.TotalTokens, }; - if (usage.CacheReadInputTokens is int cacheReadTokens) - { - (ud.AdditionalCounts ??= []).Add(nameof(usage.CacheReadInputTokens), cacheReadTokens); - } - if (usage.CacheWriteInputTokens is int cacheWriteTokens) { (ud.AdditionalCounts ??= []).Add(nameof(usage.CacheWriteInputTokens), cacheWriteTokens); @@ -465,8 +478,7 @@ private static List CreateSystem(List? r }); } - foreach (var message in messages - .Where(m => m.Role == ChatRole.System && m.Contents.Any(c => c is TextContent))) + foreach (var message in messages.Where(m => m.Role == ChatRole.System && m.Contents.Any(c => c is TextContent))) { system.Add(new SystemContentBlock() { @@ -567,6 +579,10 @@ private static List CreateContents(ChatMessage message) { switch (content) { + case AIContent when content.RawRepresentation is ContentBlock cb: + contents.Add(cb); + break; + case TextContent tc: if (message.Role == ChatRole.Assistant) { @@ -649,32 +665,54 @@ private static List CreateContents(ChatMessage message) break; case FunctionResultContent frc: - Document result = frc.Result switch - { - int i => i, - long l => l, - float f => f, - double d => d, - string s => s, - bool b => b, - JsonElement json => ToDocument(json), - { } other => ToDocument(JsonSerializer.SerializeToElement(other, BedrockJsonContext.DefaultOptions.GetTypeInfo(other.GetType()))), - _ => default, - }; - contents.Add(new() { ToolResult = new() { ToolUseId = frc.CallId, - Content = [new() { Json = new Document(new Dictionary() { ["result"] = result }) }], + Content = ToToolResultContentBlocks(frc.Result), }, }); break; } + static List ToToolResultContentBlocks(object? result) => + result switch + { + AIContent aic => [ToolResultContentBlockFromAIContent(aic)], + IEnumerable aics => [.. aics.Select(ToolResultContentBlockFromAIContent)], + string s => [new () { Text = s }], + _ => [new() + { + Json = new Document(new Dictionary() + { + ["result"] = result switch + { + int i => i, + long l => l, + float f => f, + double d => d, + bool b => b, + JsonElement json => ToDocument(json), + { } other => ToDocument(JsonSerializer.SerializeToElement(other, BedrockJsonContext.DefaultOptions.GetTypeInfo(other.GetType()))), + _ => default, + } + }) + }], + }; + + static ToolResultContentBlock ToolResultContentBlockFromAIContent(AIContent aic) => + aic switch + { + TextContent tc => new() { Text = tc.Text }, + TextReasoningContent trc => new() { Text = trc.Text }, + DataContent dc when GetImageFormat(dc.MediaType) is { } imageFormat => new() { Image = new() { Source = new() { Bytes = new(dc.Data.ToArray()) }, Format = imageFormat } }, + DataContent dc when GetVideoFormat(dc.MediaType) is { } videoFormat => new() { Video = new() { Source = new() { Bytes = new(dc.Data.ToArray()) }, Format = videoFormat } }, + DataContent dc when GetDocumentFormat(dc.MediaType) is { } docFormat => new() { Document = new() { Source = new() { Bytes = new(dc.Data.ToArray()) }, Format = docFormat, Name = dc.Name ?? "file" } }, + _ => ToToolResultContentBlocks(JsonSerializer.SerializeToElement(aic, BedrockJsonContext.DefaultOptions.GetTypeInfo(typeof(object)))).First(), + }; - if (content.AdditionalProperties?.TryGetValue(nameof(ContentBlock.CachePoint), out var maybeCachePoint) == true) + if (content.AdditionalProperties?.TryGetValue(nameof(ContentBlock.CachePoint), out var maybeCachePoint) is true) { if (maybeCachePoint is CachePointBlock cachePointBlock) { @@ -1098,4 +1136,85 @@ private static InferenceConfiguration CreateInferenceConfiguration(InferenceConf return config; } + + /// Applies reasoning configuration from ChatOptions to the AdditionalModelRequestFields. + /// + /// Maps to Bedrock's extended thinking configuration + /// via the thinking key in AdditionalModelRequestFields. + /// Budget tokens are computed as a ratio of MaxTokens when available, following the + /// approach used by the AWS bedrock-access-gateway. The constraint budget_tokens < max_tokens + /// is always enforced. + /// See https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html + /// + private static Document ApplyReasoningConfig(Document additionalModelRequestFields, InferenceConfiguration inferenceConfig, ChatOptions? options) + { + // If Effort is not set or is None, there's nothing to configure. + if (options?.Reasoning is not { } reasoning || + reasoning.Effort is not { } effort || + effort is ReasoningEffort.None) + { + return additionalModelRequestFields; + } + + // Don't override if the user already configured thinking via AdditionalModelRequestFields. + if (additionalModelRequestFields.IsDictionary() && + additionalModelRequestFields.AsDictionary().ContainsKey("thinking")) + { + return additionalModelRequestFields; + } + + // budget_tokens must be >= 1024 and < max_tokens. + // When max_tokens is known, compute budget_tokens as a ratio (similar to + // https://github.com/aws-samples/bedrock-access-gateway). When it isn't, + // pick fixed budget values and set max_tokens to satisfy the constraint, + // since the model-specific default for max_tokens is unspecified. + int budgetTokens; + if (inferenceConfig.MaxTokens is int maxTokens) + { + double ratio = effort switch + { + ReasoningEffort.Low => 0.25, + ReasoningEffort.Medium => 0.5, + ReasoningEffort.High => 0.75, + _ => 1.0, // ExtraHigh + }; + + budgetTokens = Math.Max(1024, (int)(maxTokens * ratio)); + if (budgetTokens >= maxTokens) + { + budgetTokens = maxTokens - 1; + } + } + else + { + budgetTokens = effort switch + { + ReasoningEffort.Low => 1024, + ReasoningEffort.Medium => 8192, + ReasoningEffort.High => 16384, + _ => 32768, // ExtraHigh + }; + inferenceConfig.MaxTokens = budgetTokens * 4; + } + + Document thinkingConfig = new(new Dictionary + { + ["type"] = new("enabled"), + ["budget_tokens"] = new(budgetTokens), + }); + + if (additionalModelRequestFields.IsDictionary()) + { + additionalModelRequestFields.AsDictionary()["thinking"] = thinkingConfig; + } + else + { + additionalModelRequestFields = new(new Dictionary + { + ["thinking"] = thinkingConfig, + }); + } + + return additionalModelRequestFields; + } } diff --git a/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs b/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs index b9dff182a517..c72d52648544 100644 --- a/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs +++ b/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs @@ -1,17 +1,12 @@ using Amazon.BedrockRuntime.Model; -using Amazon.Runtime; using Amazon.Runtime.Documents; -using Amazon.Runtime.Internal; -using Amazon.Runtime.Internal.Transform; +using Amazon.Runtime.EventStreams; using Microsoft.Extensions.AI; using Moq; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; -using System.Net.Http; -using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; @@ -37,8 +32,6 @@ public TestAIFunction(string name, string description, JsonElement jsonSchema) public class BedrockChatClientTests { - #region Basic Client Tests - [Fact] [Trait("UnitTest", "BedrockRuntime")] public void AsIChatClient_InvalidArguments_Throws() @@ -73,10 +66,6 @@ public void AsIChatClient_GetService() Assert.Null(client.GetService("key")); } - #endregion - - #region ResponseFormat Tests - [Fact] [Trait("UnitTest", "BedrockRuntime")] public async Task ResponseFormat_Json_WithSchema_CreatesSyntheticToolWithCorrectSchema() @@ -451,749 +440,3790 @@ public async Task ResponseFormat_Json_NullToolInput_ThrowsInvalidOperationExcept Assert.Contains("did not return structured output", ex.Message); } - #endregion -} -/// -/// Tests using HTTP-layer mocking to test actual Converse API response scenarios. -/// This allows testing beyond the happy path with realistic service responses. -/// Based on Peter's (peterrsongg) suggestion to test different response structures. -/// -public class BedrockChatClientHttpMockedTests : IClassFixture -{ - private readonly HttpMockFixture _fixture; - public BedrockChatClientHttpMockedTests(HttpMockFixture fixture) + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public void AsIChatClient_ValidArguments_CreatesIChatClientSuccessfully() { - _fixture = fixture; + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient(); + Assert.NotNull(chatClient); + Assert.Same(mock, chatClient.GetService()); } - /// - /// Helper method to inject stubbed web response data into a request's state - /// - private static void InjectMockedResponse(ConverseRequest request, StubWebResponseData webResponseData) + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public void IChatClient_GetService_InvalidArguments_Throws() { - var interfaceType = typeof(IAmazonWebServiceRequest); - var requestStatePropertyInfo = interfaceType.GetProperty("RequestState"); - var requestState = (Dictionary)requestStatePropertyInfo.GetValue(request); - requestState["response"] = webResponseData; - } + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient(); + Assert.NotNull(chatClient); - #region HTTP Mocking Infrastructure (Based on Peter's Working Code) + Assert.Throws("serviceType", () => chatClient.GetService(null!)); + } - /// - /// Pipeline customizer that replaces the HTTP handler with a mock implementation - /// - private class MockPipelineCustomizer : IRuntimePipelineCustomizer + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(null)] + [InlineData("anthropic.claude-3-sonnet-20240229-v1:0")] + public void IChatClient_GetService_ReturnsExpectedInstance(string defaultModelId) { - public string UniqueName => "BedrockMEAIMockPipeline"; + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient(defaultModelId); + Assert.NotNull(chatClient); - public void Customize(Type type, RuntimePipeline pipeline) - { -#if NETFRAMEWORK - // On .NET Framework, use Stream - pipeline.ReplaceHandler>( - new HttpHandler(new MockHttpRequestFactory(), new object())); -#else - // On .NET Core/.NET 5+, use HttpContent - pipeline.ReplaceHandler>( - new HttpHandler(new MockHttpRequestFactory(), new object())); -#endif - } + Assert.Same(mock, chatClient.GetService()); + Assert.Same(chatClient, chatClient.GetService()); + + ChatClientMetadata metadata = chatClient.GetService(); + Assert.NotNull(metadata); + Assert.Equal("aws.bedrock", metadata.ProviderName); + Assert.Equal(defaultModelId, metadata.DefaultModelId); } - /// - /// Factory for creating mock HTTP requests - /// -#if NETFRAMEWORK - private class MockHttpRequestFactory : IHttpRequestFactory - { - public IHttpRequest CreateHttpRequest(Uri requestUri) - { - return new MockHttpRequest(requestUri); - } -#else - private class MockHttpRequestFactory : IHttpRequestFactory + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public void IChatClient_Dispose_Nop() { - public IHttpRequest CreateHttpRequest(Uri requestUri) - { - return new MockHttpRequest(requestUri); - } -#endif + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient(); + Assert.NotNull(chatClient); - public void Dispose() - { - // No resources to dispose - } + chatClient.Dispose(); + + Assert.Same(mock, chatClient.GetService()); } - /// - /// Mock HTTP request that retrieves stubbed response data from request state - /// -#if NETFRAMEWORK - private class MockHttpRequest : IHttpRequest -#else - private class MockHttpRequest : IHttpRequest -#endif + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_BasicRequest() { - private IWebResponseData _webResponseData; - - public MockHttpRequest(Uri requestUri) + MockBedrockRuntime mock = new() { - RequestUri = requestUri; - } + OnConverseRequest = request => CreateResponse("Hello") + }; - public string Method { get; set; } - public Uri RequestUri { get; set; } - public Version HttpProtocolVersion { get; set; } + IChatClient chatClient = mock.AsIChatClient("anthropic.claude-3-sonnet-20240229-v1:0"); + ChatResponse result = await chatClient.GetResponseAsync("Hello"); + Assert.NotNull(result); + Assert.NotNull(result.Messages); + Assert.Single(result.Messages); + Assert.Equal(ChatRole.Assistant, result.Messages[0].Role); + Assert.NotNull(result.Messages[0].MessageId); + Assert.NotNull(result.ResponseId); + Assert.NotNull(result.CreatedAt); + Assert.Equal("Hello", ((TextContent)result.Messages[0].Contents[0]).Text); + } - public void ConfigureRequest(IRequestContext requestContext) + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_TextContent() + { + MockBedrockRuntime mock = new() { - // Retrieve the stubbed response from request state - // This is the critical line that Peter identified (line 60 in his comment) - var request = requestContext.OriginalRequest as IAmazonWebServiceRequest; - if (request != null && request.RequestState.ContainsKey("response")) + OnConverseRequest = request => { - _webResponseData = request.RequestState["response"] as IWebResponseData; + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Single(request.Messages[0].Content); + Assert.Equal("What is the weather like?", request.Messages[0].Content[0].Text); + + var response = CreateResponse("It's sunny today."); + response.StopReason = StopReason.End_turn; + return response; } - } + }; - public void SetRequestHeaders(IDictionary headers) - { - // Not needed for mock - } + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "What is the weather like?")]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal(ChatRole.Assistant, result.Messages[0].Role); + Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.Equal("It's sunny today.", ((TextContent)result.Messages[0].Contents[0]).Text); + Assert.Equal(ChatFinishReason.Stop, result.FinishReason); + Assert.NotNull(result.Messages[0].RawRepresentation); + Assert.NotNull(((TextContent)result.Messages[0].Contents[0]).RawRepresentation); + Assert.NotNull(result.RawRepresentation); + Assert.NotNull(result.Usage); + Assert.Equal(10, result.Usage.InputTokenCount); + Assert.Equal(5, result.Usage.OutputTokenCount); + Assert.Equal(15, result.Usage.TotalTokenCount); + } -#if NETFRAMEWORK - public Stream GetRequestContent() - { - return new MemoryStream(); - } -#else - public HttpContent GetRequestContent() + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_EmptyMessages_CreatesDefaultMessage() + { + MockBedrockRuntime mock = new() { - return null; - } -#endif + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Single(request.Messages[0].Content); + Assert.Equal("\u200B", request.Messages[0].Content[0].Text); - public IWebResponseData GetResponse() - { - return GetResponseAsync(CancellationToken.None).Result; - } + return CreateResponse("Empty input received"); + } + }; - public Task GetResponseAsync(CancellationToken cancellationToken) - { - return Task.FromResult(_webResponseData); - } + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = []; -#if NETFRAMEWORK - public void WriteToRequestBody(Stream requestContent, Stream contentStream, - IDictionary contentHeaders, IRequestContext requestContext) - { - // Not needed for mock - } + ChatResponse result = await chatClient.GetResponseAsync(messages); - public void WriteToRequestBody(Stream requestContent, byte[] content, - IDictionary contentHeaders) - { - // Not needed for mock - } + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("Empty input received", ((TextContent)result.Messages[0].Contents[0]).Text); + } - public Task WriteToRequestBodyAsync(Stream requestContent, Stream contentStream, - IDictionary contentHeaders, IRequestContext requestContext) - { - return Task.CompletedTask; - } + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_NullMessages_Throws() + { + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient("claude"); - public Task WriteToRequestBodyAsync(Stream requestContent, byte[] content, - IDictionary contentHeaders, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } -#else - public void WriteToRequestBody(HttpContent requestContent, Stream contentStream, - IDictionary contentHeaders, IRequestContext requestContext) - { - // Not needed for mock - } + await Assert.ThrowsAsync("messages", () => chatClient.GetResponseAsync(null!)); + } - public void WriteToRequestBody(HttpContent requestContent, byte[] content, - IDictionary contentHeaders) - { - // Not needed for mock - } + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_DataContent_Image() + { + byte[] imageData = [0x89, 0x50, 0x4E, 0x47]; - public Task WriteToRequestBodyAsync(HttpContent requestContent, Stream contentStream, - IDictionary contentHeaders, IRequestContext requestContext) + MockBedrockRuntime mock = new() { - return Task.CompletedTask; - } + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.Equal("Describe this image", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].Image); + Assert.Equal(ImageFormat.Png, request.Messages[0].Content[1].Image.Format); + Assert.True(request.Messages[0].Content[1].Image.Source.Bytes.ToArray().SequenceEqual(imageData)); + + return CreateResponse("I see an image."); + } + }; - public Task WriteToRequestBodyAsync(HttpContent requestContent, byte[] content, - IDictionary contentHeaders, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } -#endif + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new TextContent("Describe this image"), + new DataContent(imageData, "image/png") + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("I see an image.", ((TextContent)result.Messages[0].Contents[0]).Text); + } - public IHttpRequestStreamHandle SetupHttpRequestStreamPublisher( - IDictionary contentHeaders, IHttpRequestStreamPublisher publisher) + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_DataContent_AllImageFormats() + { + var formats = new[] { - throw new NotImplementedException(); - } + ("image/jpeg", ImageFormat.Jpeg), + ("image/png", ImageFormat.Png), + ("image/gif", ImageFormat.Gif), + ("image/webp", ImageFormat.Webp) + }; - public void Abort() + foreach (var (mimeType, expectedFormat) in formats) { - // Not needed for mock - } + byte[] imageData = [1, 2, 3, 4]; + bool verified = false; -#if NETFRAMEWORK - public Task GetRequestContentAsync() - { - return Task.FromResult(new MemoryStream()); - } + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.Messages[0].Content[0].Image); + Assert.Equal(expectedFormat, request.Messages[0].Content[0].Image.Format); + verified = true; + return CreateResponse("OK"); + } + }; - public Task GetRequestContentAsync(CancellationToken cancellationToken) - { - return Task.FromResult(new MemoryStream()); - } -#else - public Task GetRequestContentAsync() - { - return Task.FromResult(null); + IChatClient chatClient = mock.AsIChatClient("claude"); + await chatClient.GetResponseAsync([new(ChatRole.User, [new DataContent(imageData, mimeType)])]); + Assert.True(verified, $"Format {mimeType} not verified"); } + } - public Task GetRequestContentAsync(CancellationToken cancellationToken) - { - return Task.FromResult(null); - } -#endif + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_DataContent_Document() + { + byte[] pdfData = [0x25, 0x50, 0x44, 0x46]; - public Stream SetupProgressListeners(Stream originalStream, long progressUpdateInterval, - object sender, EventHandler callback) + MockBedrockRuntime mock = new() { - return originalStream; - } + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.Equal("Analyze this document", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].Document); + Assert.Equal(DocumentFormat.Pdf, request.Messages[0].Content[1].Document.Format); + Assert.True(request.Messages[0].Content[1].Document.Source.Bytes.ToArray().SequenceEqual(pdfData)); + Assert.Equal("file", request.Messages[0].Content[1].Document.Name); + + return CreateResponse("Document analyzed."); + } + }; - public void Dispose() - { - // Nothing to dispose - } + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new TextContent("Analyze this document"), + new DataContent(pdfData, "application/pdf") + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("Document analyzed.", ((TextContent)result.Messages[0].Contents[0]).Text); } - /// - /// Stubbed web response data for testing different response scenarios - /// - private class StubWebResponseData : IWebResponseData + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_DataContent_DocumentWithName() { - private readonly IHttpResponseBody _httpResponseBody; + byte[] pdfData = [1, 2, 3]; - public StubWebResponseData(string jsonResponse, Dictionary headers = null, - HttpStatusCode statusCode = HttpStatusCode.OK) + MockBedrockRuntime mock = new() { - StatusCode = statusCode; - IsSuccessStatusCode = (int)statusCode >= 200 && (int)statusCode < 300; - JsonResponse = jsonResponse; - Headers = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - ContentType = "application/json"; - ContentLength = jsonResponse?.Length ?? 0; + OnConverseRequest = request => + { + Assert.NotNull(request.Messages[0].Content[0].Document); + Assert.Equal("report.pdf", request.Messages[0].Content[0].Document.Name); - _httpResponseBody = new HttpResponseBody(jsonResponse); - } + return CreateResponse("OK"); + } + }; - public Dictionary Headers { get; set; } - public string JsonResponse { get; } - public long ContentLength { get; set; } - public string ContentType { get; set; } - public HttpStatusCode StatusCode { get; set; } - public bool IsSuccessStatusCode { get; set; } + IChatClient chatClient = mock.AsIChatClient("claude"); + DataContent dataContent = new(pdfData, "application/pdf") { Name = "report.pdf" }; + await chatClient.GetResponseAsync([new(ChatRole.User, [dataContent])]); + } - public IHttpResponseBody ResponseBody => _httpResponseBody; + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_DataContent_Video() + { + byte[] videoData = [0x00, 0x00, 0x00, 0x18]; - public string[] GetHeaderNames() + MockBedrockRuntime mock = new() { - return Headers.Keys.ToArray(); - } + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.NotNull(request.Messages[0].Content[1].Video); + Assert.Equal(VideoFormat.Mp4, request.Messages[0].Content[1].Video.Format); - public bool IsHeaderPresent(string headerName) - { - return Headers.ContainsKey(headerName); - } + return CreateResponse("Video processed."); + } + }; - public string GetHeaderValue(string headerName) - { - return Headers.ContainsKey(headerName) ? Headers[headerName] : null; - } + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new TextContent("Analyze this video"), + new DataContent(videoData, "video/mp4") + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("Video processed.", ((TextContent)result.Messages[0].Contents[0]).Text); } - /// - /// HTTP response body implementation for stubbed responses - /// - private class HttpResponseBody : IHttpResponseBody + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReceivesImageContent() { - private readonly string _jsonResponse; - private Stream _stream; - - public HttpResponseBody(string jsonResponse) - { - _jsonResponse = jsonResponse ?? string.Empty; - } + byte[] imageData = [1, 2, 3, 4]; - public void Dispose() + MockBedrockRuntime mock = new() { - _stream?.Dispose(); - } + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Image = new ImageBlock + { + Format = ImageFormat.Png, + Source = new ImageSource + { + Bytes = new MemoryStream(imageData) + } + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; - public Stream OpenResponse() - { - _stream = new MemoryStream(Encoding.UTF8.GetBytes(_jsonResponse)); - return _stream; - } + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Send me an image")]); - public Task OpenResponseAsync() - { - return Task.FromResult(OpenResponse()); - } + Assert.NotNull(result); + Assert.Single(result.Messages); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.Equal("image/png", dataContent.MediaType); + Assert.True(dataContent.Data.ToArray().SequenceEqual(imageData)); + Assert.NotNull(dataContent.RawRepresentation); } - #endregion - - #region ResponseFormat with HTTP Mocking Tests - [Fact] [Trait("UnitTest", "BedrockRuntime")] - public async Task ResponseFormat_Json_WithActualConverseResponse_ParsesCorrectly() + public async Task IChatClient_GetResponseAsync_ReceivesVideoContent() { - // Arrange - This is a real Converse API response with tool_use - var converseResponse = """ + byte[] videoData = [5, 6, 7, 8]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => { - "output": { - "message": { - "role": "assistant", - "content": [ - { - "toolUse": { - "toolUseId": "tooluse_12345", - "name": "generate_response", - "input": { - "name": "Alice Johnson", - "age": 28, - "city": "Seattle" + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Video = new VideoBlock + { + Format = VideoFormat.Mp4, + Source = new VideoSource + { + Bytes = new MemoryStream(videoData) + } } } - } - ] - } - }, - "stopReason": "tool_use", - "usage": { - "inputTokens": 125, - "outputTokens": 45, - "totalTokens": 170 - } - } - """; - - var chatClient = _fixture.BedrockRuntimeClient.AsIChatClient("anthropic.claude-3-sonnet-20240229-v1:0"); - var messages = new[] { new ChatMessage(ChatRole.User, "Generate a person") }; - - var schemaJson = """ - { - "type": "object", - "properties": { - "name": { "type": "string" }, - "age": { "type": "number" }, - "city": { "type": "string" } - }, - "required": ["name", "age"] + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; } - """; - var schemaElement = JsonDocument.Parse(schemaJson).RootElement; - - var request = new ConverseRequest(); - var options = new ChatOptions - { - ResponseFormat = ChatResponseFormat.ForJsonSchema(schemaElement, - schemaName: "PersonSchema", - schemaDescription: "A person with demographic information"), - RawRepresentationFactory = _ => request }; - // Inject the stubbed response - var webResponseData = new StubWebResponseData(converseResponse); - InjectMockedResponse(request, webResponseData); - - // Act - var response = await chatClient.GetResponseAsync(messages, options); - - // Assert - Assert.NotNull(response); - Assert.NotNull(response.Text); - - // Verify the JSON structure - var json = JsonDocument.Parse(response.Text); - Assert.Equal("Alice Johnson", json.RootElement.GetProperty("name").GetString()); - Assert.Equal(28, json.RootElement.GetProperty("age").GetInt32()); - Assert.Equal("Seattle", json.RootElement.GetProperty("city").GetString()); + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Send me a video")]); - // Verify usage metadata - var usage = response.Usage; - Assert.NotNull(usage); - Assert.Equal(125, usage.InputTokenCount); - Assert.Equal(45, usage.OutputTokenCount); - Assert.Equal(170, usage.TotalTokenCount); + Assert.NotNull(result); + Assert.Single(result.Messages); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.Equal("video/mp4", dataContent.MediaType); + Assert.True(dataContent.Data.ToArray().SequenceEqual(videoData)); + Assert.NotNull(dataContent.RawRepresentation); } [Fact] [Trait("UnitTest", "BedrockRuntime")] - public async Task ResponseFormat_Json_WithNestedObjects_ParsesCorrectly() + public async Task IChatClient_GetResponseAsync_ReceivesDocumentContent() { - // Arrange - Test with nested JSON structure - var converseResponse = """ + byte[] docData = [9, 10, 11]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => { - "output": { - "message": { - "role": "assistant", - "content": [ - { - "toolUse": { - "toolUseId": "tooluse_nested", - "name": "generate_response", - "input": { - "user": { - "name": "Bob Smith", - "contact": { - "email": "bob@example.com", - "phone": "555-0123" - } - }, - "metadata": { - "timestamp": "2024-01-15T10:30:00Z", - "version": 1 + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Document = new DocumentBlock + { + Format = DocumentFormat.Pdf, + Name = "result.pdf", + Source = new DocumentSource + { + Bytes = new MemoryStream(docData) } } } - } - ] - } - }, - "stopReason": "tool_use", - "usage": { - "inputTokens": 200, - "outputTokens": 80, - "totalTokens": 280 - } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; } - """; - - var chatClient = _fixture.BedrockRuntimeClient.AsIChatClient("anthropic.claude-3-sonnet-20240229-v1:0"); - var messages = new[] { new ChatMessage(ChatRole.User, "Generate user data") }; - - var request = new ConverseRequest(); - var options = new ChatOptions - { - ResponseFormat = ChatResponseFormat.Json, - RawRepresentationFactory = _ => request }; - var webResponseData = new StubWebResponseData(converseResponse); - InjectMockedResponse(request, webResponseData); + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Send me a document")]); - // Act - var response = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + Assert.Single(result.Messages); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.Equal("application/pdf", dataContent.MediaType); + Assert.Equal("result.pdf", dataContent.Name); + Assert.True(dataContent.Data.ToArray().SequenceEqual(docData)); + Assert.NotNull(dataContent.RawRepresentation); + } - // Assert - Assert.NotNull(response.Text); - var json = JsonDocument.Parse(response.Text); + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public void IChatClient_GetService_WithServiceKey_ReturnsNull() + { + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient(); - var user = json.RootElement.GetProperty("user"); - Assert.Equal("Bob Smith", user.GetProperty("name").GetString()); + // When serviceKey is not null, should return null + Assert.Null(chatClient.GetService(typeof(IAmazonBedrockRuntime), "someKey")); + } - var contact = user.GetProperty("contact"); - Assert.Equal("bob@example.com", contact.GetProperty("email").GetString()); - Assert.Equal("555-0123", contact.GetProperty("phone").GetString()); + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public void IChatClient_GetService_UnknownType_ReturnsNull() + { + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient(); - var metadata = json.RootElement.GetProperty("metadata"); - Assert.Equal("2024-01-15T10:30:00Z", metadata.GetProperty("timestamp").GetString()); - Assert.Equal(1, metadata.GetProperty("version").GetInt32()); + // Unknown type should return null + Assert.Null(chatClient.GetService()); } [Fact] [Trait("UnitTest", "BedrockRuntime")] - public async Task ResponseFormat_Json_WithArrayData_ParsesCorrectly() + public async Task IChatClient_GetResponseAsync_UsageWithCacheTokens() { - // Arrange - Test with arrays in JSON response - var converseResponse = """ + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => { - "output": { - "message": { - "role": "assistant", - "content": [ - { - "toolUse": { - "toolUseId": "tooluse_array", - "name": "generate_response", - "input": { - "items": ["apple", "banana", "orange"], - "prices": [1.99, 0.99, 2.49], - "inventory": { - "warehouse": "A", - "quantities": [100, 250, 75] - } - } - } - } - ] - } - }, - "stopReason": "tool_use", - "usage": { - "inputTokens": 50, - "outputTokens": 30, - "totalTokens": 80 - } + var response = CreateResponse("OK"); + response.Usage = new TokenUsage + { + InputTokens = 100, + OutputTokens = 50, + TotalTokens = 150, + CacheReadInputTokens = 25, + CacheWriteInputTokens = 10 + }; + return response; } - """; - - var chatClient = _fixture.BedrockRuntimeClient.AsIChatClient("anthropic.claude-3-sonnet-20240229-v1:0"); - var messages = new[] { new ChatMessage(ChatRole.User, "List items") }; - - var request = new ConverseRequest(); - var options = new ChatOptions - { - ResponseFormat = ChatResponseFormat.Json, - RawRepresentationFactory = _ => request }; - var webResponseData = new StubWebResponseData(converseResponse); - InjectMockedResponse(request, webResponseData); + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); - // Act - var response = await chatClient.GetResponseAsync(messages, options); - - // Assert - Assert.NotNull(response.Text); - var json = JsonDocument.Parse(response.Text); + Assert.NotNull(result.Usage); + Assert.Equal(100, result.Usage.InputTokenCount); + Assert.Equal(25, result.Usage.CachedInputTokenCount); + Assert.Equal(50, result.Usage.OutputTokenCount); + Assert.Equal(150, result.Usage.TotalTokenCount); + Assert.NotNull(result.Usage.AdditionalCounts); + Assert.Equal(10, result.Usage.AdditionalCounts["CacheWriteInputTokens"]); + } - var items = json.RootElement.GetProperty("items"); - Assert.Equal(JsonValueKind.Array, items.ValueKind); - Assert.Equal(3, items.GetArrayLength()); - Assert.Equal("apple", items[0].GetString()); - Assert.Equal("banana", items[1].GetString()); - Assert.Equal("orange", items[2].GetString()); + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_CustomFinishReason() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var response = CreateResponse("Custom"); + response.StopReason = new StopReason("custom_reason"); + return response; + } + }; - var prices = json.RootElement.GetProperty("prices"); - Assert.Equal(3, prices.GetArrayLength()); - Assert.Equal(1.99, prices[0].GetDouble(), precision: 2); + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); - var inventory = json.RootElement.GetProperty("inventory"); - var quantities = inventory.GetProperty("quantities"); - Assert.Equal(3, quantities.GetArrayLength()); - Assert.Equal(100, quantities[0].GetInt32()); + Assert.Equal("custom_reason", result.FinishReason?.Value); } [Fact] [Trait("UnitTest", "BedrockRuntime")] - public async Task ResponseFormat_Json_WithMinimalSchema_ParsesCorrectly() + public async Task IChatClient_GetResponseAsync_StopSequences_MergesWithExisting() { - // Arrange - Test simple JSON response - var converseResponse = """ + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => { - "output": { - "message": { - "role": "assistant", - "content": [ - { - "toolUse": { - "toolUseId": "tooluse_simple", - "name": "generate_response", - "input": { - "message": "Hello, World!", - "status": "success" - } + // Should have merged stop sequences + Assert.Contains("STOP1", request.InferenceConfig.StopSequences); + Assert.Contains("STOP2", request.InferenceConfig.StopSequences); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + ChatOptions options = new() + { + StopSequences = ["STOP1", "STOP2"] + }; + + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData("text/csv")] + [InlineData("text/html")] + [InlineData("text/markdown")] + [InlineData("text/plain")] + [InlineData("application/msword")] + [InlineData("application/vnd.openxmlformats-officedocument.wordprocessingml.document")] + [InlineData("application/vnd.ms-excel")] + [InlineData("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")] + public async Task IChatClient_GetResponseAsync_SendsDocumentContent_AllFormats(string mimeType) + { + byte[] docData = [1, 2, 3]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.Messages[0].Content[0].Document); + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, [new DataContent(docData, mimeType) { Name = "file" }]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData("image/gif")] + [InlineData("image/webp")] + public async Task IChatClient_GetResponseAsync_SendsImageContent_AllFormats(string mimeType) + { + byte[] imageData = [1, 2, 3]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.Messages[0].Content[0].Image); + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, [new DataContent(imageData, mimeType)]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData("video/x-flv")] + [InlineData("video/x-matroska")] + [InlineData("video/quicktime")] + [InlineData("video/mpeg")] + [InlineData("video/webm")] + [InlineData("video/3gpp")] + public async Task IChatClient_GetResponseAsync_SendsVideoContent_AllFormats(string mimeType) + { + byte[] videoData = [1, 2, 3]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.Messages[0].Content[0].Video); + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, [new DataContent(videoData, mimeType)]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_SendsFunctionCallContent() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Equal(2, request.Messages.Count); + + // First message is user + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + + // Second message is assistant with tool use + Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); + var toolUse = request.Messages[1].Content[0].ToolUse; + Assert.NotNull(toolUse); + Assert.Equal("call_123", toolUse.ToolUseId); + Assert.Equal("get_weather", toolUse.Name); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + FunctionCallContent funcCallContent = new("call_123", "get_weather", + new Dictionary { ["location"] = "Seattle" }); + + ChatMessage[] messages = + [ + new(ChatRole.User, "What's the weather?"), + new(ChatRole.Assistant, [funcCallContent]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData("csv", "text/csv")] + [InlineData("html", "text/html")] + [InlineData("md", "text/markdown")] + [InlineData("doc", "application/msword")] + [InlineData("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")] + [InlineData("xls", "application/vnd.ms-excel")] + [InlineData("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")] + public async Task IChatClient_GetResponseAsync_ReceivesDocumentContent_AllFormats(string formatValue, string expectedMimeType) + { + byte[] docData = [9, 10, 11]; + DocumentFormat format = new(formatValue); + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Document = new DocumentBlock + { + Format = format, + Name = "result.doc", + Source = new DocumentSource { Bytes = new MemoryStream(docData) } + } } - } - ] - } - }, - "stopReason": "tool_use", - "usage": { - "inputTokens": 10, - "outputTokens": 5, - "totalTokens": 15 - } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; } - """; + }; - var chatClient = _fixture.BedrockRuntimeClient.AsIChatClient("anthropic.claude-3-haiku-20240307-v1:0"); - var messages = new[] { new ChatMessage(ChatRole.User, "Say hello") }; + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); - var request = new ConverseRequest(); - var options = new ChatOptions + Assert.NotNull(result); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.Equal(expectedMimeType, dataContent.MediaType); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData("gif", "image/gif")] + [InlineData("webp", "image/webp")] + public async Task IChatClient_GetResponseAsync_ReceivesImageContent_AllFormats(string formatValue, string expectedMimeType) + { + byte[] imageData = [1, 2, 3]; + ImageFormat format = new(formatValue); + + MockBedrockRuntime mock = new() { - ResponseFormat = ChatResponseFormat.Json, - RawRepresentationFactory = _ => request + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Image = new ImageBlock + { + Format = format, + Source = new ImageSource { Bytes = new MemoryStream(imageData) } + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } }; - var webResponseData = new StubWebResponseData(converseResponse); - InjectMockedResponse(request, webResponseData); + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); - // Act - var response = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.Equal(expectedMimeType, dataContent.MediaType); + } - // Assert - Assert.NotNull(response.Text); - var json = JsonDocument.Parse(response.Text); - Assert.Equal("Hello, World!", json.RootElement.GetProperty("message").GetString()); - Assert.Equal("success", json.RootElement.GetProperty("status").GetString()); + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData("flv", "video/x-flv")] + [InlineData("mkv", "video/x-matroska")] + [InlineData("mov", "video/quicktime")] + [InlineData("mpeg", "video/mpeg")] + [InlineData("webm", "video/webm")] + [InlineData("three_gp", "video/3gpp")] + public async Task IChatClient_GetResponseAsync_ReceivesVideoContent_AllFormats(string formatValue, string expectedMimeType) + { + byte[] videoData = [5, 6, 7, 8]; + VideoFormat format = new(formatValue); + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Video = new VideoBlock + { + Format = format, + Source = new VideoSource { Bytes = new MemoryStream(videoData) } + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.Equal(expectedMimeType, dataContent.MediaType); } [Fact] [Trait("UnitTest", "BedrockRuntime")] - public async Task ResponseFormat_Json_WithComplexSchema_ValidatesStructure() + public async Task IChatClient_GetResponseAsync_ReceivesDocument_UnknownFormat() { - // Arrange - Test with detailed schema validation - var converseResponse = """ + byte[] docData = [9, 10, 11]; + DocumentFormat format = new("unknown_format"); + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => { - "output": { - "message": { - "role": "assistant", - "content": [ - { - "toolUse": { - "toolUseId": "tooluse_complex", - "name": "generate_response", - "input": { - "id": "usr_123", - "username": "testuser", - "email": "test@example.com", - "profile": { - "firstName": "Test", - "lastName": "User", - "age": 25, - "preferences": { - "theme": "dark", - "notifications": true - } - }, - "roles": ["admin", "user"], - "active": true + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Document = new DocumentBlock + { + Format = format, + Name = "result.doc", + Source = new DocumentSource { Bytes = new MemoryStream(docData) } } } - } - ] - } - }, - "stopReason": "tool_use", - "usage": { - "inputTokens": 300, - "outputTokens": 150, - "totalTokens": 450 - } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; } - """; + }; - var chatClient = _fixture.BedrockRuntimeClient.AsIChatClient("anthropic.claude-3-sonnet-20240229-v1:0"); - var messages = new[] { new ChatMessage(ChatRole.User, "Generate user profile") }; + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); - var schemaJson = """ + Assert.NotNull(result); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + // Unknown format defaults to text/plain + Assert.Equal("text/plain", dataContent.MediaType); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReceivesImage_UnknownFormat() + { + byte[] imageData = [1, 2, 3]; + ImageFormat format = new("unknown_format"); + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => { - "type": "object", - "properties": { - "id": { "type": "string" }, - "username": { "type": "string" }, - "email": { "type": "string", "format": "email" }, - "profile": { - "type": "object", - "properties": { - "firstName": { "type": "string" }, - "lastName": { "type": "string" }, - "age": { "type": "number" }, - "preferences": { "type": "object" } - }, - "required": ["firstName", "lastName"] + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Image = new ImageBlock + { + Format = format, + Source = new ImageSource { Bytes = new MemoryStream(imageData) } + } + } + ] + } }, - "roles": { - "type": "array", - "items": { "type": "string" } + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + // Unknown format defaults to image/jpeg + Assert.Equal("image/jpeg", dataContent.MediaType); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReceivesVideo_UnknownFormat() + { + byte[] videoData = [5, 6, 7, 8]; + VideoFormat format = new("unknown_format"); + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + Video = new VideoBlock + { + Format = format, + Source = new VideoSource { Bytes = new MemoryStream(videoData) } + } + } + ] + } }, - "active": { "type": "boolean" } - }, - "required": ["id", "username", "email"] + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; } - """; - var schemaElement = JsonDocument.Parse(schemaJson).RootElement; + }; - var request = new ConverseRequest(); - var options = new ChatOptions + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + var dataContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + // Unknown format defaults to video/mp4 + Assert.Equal("video/mp4", dataContent.MediaType); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_SendsUnknownMimeType_SkipsContent() + { + byte[] data = [1, 2, 3]; + + MockBedrockRuntime mock = new() { - ResponseFormat = ChatResponseFormat.ForJsonSchema(schemaElement, - schemaName: "UserProfile", - schemaDescription: "Complete user profile with preferences"), - RawRepresentationFactory = _ => request + OnConverseRequest = request => + { + // Unknown MIME type content should not be in the request + // since it doesn't match any known format + return CreateResponse("OK"); + } }; - var webResponseData = new StubWebResponseData(converseResponse); - InjectMockedResponse(request, webResponseData); + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, [new DataContent(data, "application/unknown-type")]) + ]; - // Act - var response = await chatClient.GetResponseAsync(messages, options); + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_TextReasoningContent() + { + string reasoningText = "Let me think step by step..."; + string signature = "sig123"; - // Assert - Assert.NotNull(response.Text); - var json = JsonDocument.Parse(response.Text); + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + ReasoningContent = new ReasoningContentBlock + { + ReasoningText = new ReasoningTextBlock + { + Text = reasoningText, + Signature = signature + } + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; - // Verify required fields - Assert.Equal("usr_123", json.RootElement.GetProperty("id").GetString()); - Assert.Equal("testuser", json.RootElement.GetProperty("username").GetString()); - Assert.Equal("test@example.com", json.RootElement.GetProperty("email").GetString()); + return response; + } + }; - // Verify nested profile - var profile = json.RootElement.GetProperty("profile"); - Assert.Equal("Test", profile.GetProperty("firstName").GetString()); - Assert.Equal("User", profile.GetProperty("lastName").GetString()); - Assert.Equal(25, profile.GetProperty("age").GetInt32()); + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think step by step about this problem.")]; - // Verify nested preferences - var preferences = profile.GetProperty("preferences"); - Assert.Equal("dark", preferences.GetProperty("theme").GetString()); - Assert.True(preferences.GetProperty("notifications").GetBoolean()); + ChatResponse result = await chatClient.GetResponseAsync(messages); - // Verify array - var roles = json.RootElement.GetProperty("roles"); - Assert.Equal(2, roles.GetArrayLength()); - Assert.Equal("admin", roles[0].GetString()); - Assert.Equal("user", roles[1].GetString()); + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.IsType(Assert.Single(result.Messages[0].Contents)); - // Verify boolean - Assert.True(json.RootElement.GetProperty("active").GetBoolean()); + TextReasoningContent reasoningContent = (TextReasoningContent)result.Messages[0].Contents[0]; + Assert.Equal(reasoningText, reasoningContent.Text); + Assert.Equal(signature, reasoningContent.ProtectedData); + Assert.NotNull(reasoningContent.RawRepresentation); } - #endregion + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_SendsTextReasoningContent() + { + string reasoningText = "I reasoned about this"; + string signature = "sig456"; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Equal(2, request.Messages.Count); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); + Assert.Single(request.Messages[1].Content); + + var reasoningBlock = request.Messages[1].Content[0]; + Assert.NotNull(reasoningBlock.ReasoningContent); + Assert.Equal(reasoningText, reasoningBlock.ReasoningContent.ReasoningText.Text); + Assert.Equal(signature, reasoningBlock.ReasoningContent.ReasoningText.Signature); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + ChatMessage[] messages = + [ + new(ChatRole.User, "Question"), + new(ChatRole.Assistant, [new TextReasoningContent(reasoningText) { ProtectedData = signature }]) + ]; - /// - /// Test fixture that registers the HTTP mocking pipeline customizer - /// - public class HttpMockFixture : IDisposable + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_TextReasoningContent_WithRedactedContent() { - private readonly MockPipelineCustomizer _customizer; + byte[] redactedData = [1, 2, 3, 4]; - public HttpMockFixture() + MockBedrockRuntime mock = new() { - // Register the mock pipeline customizer globally - _customizer = new MockPipelineCustomizer(); - Runtime.Internal.RuntimePipelineCustomizerRegistry.Instance.Register(_customizer); + OnConverseRequest = request => + { + var reasoningBlock = request.Messages[0].Content[0]; + Assert.NotNull(reasoningBlock.ReasoningContent); + Assert.NotNull(reasoningBlock.ReasoningContent.RedactedContent); + Assert.True(reasoningBlock.ReasoningContent.RedactedContent.ToArray().SequenceEqual(redactedData)); - // Create the Bedrock Runtime client - it will use the mocked pipeline - BedrockRuntimeClient = new AmazonBedrockRuntimeClient(); - } + return CreateResponse("OK"); + } + }; - public IAmazonBedrockRuntime BedrockRuntimeClient { get; private set; } + IChatClient chatClient = mock.AsIChatClient("claude"); - public void Dispose() + TextReasoningContent reasoningContent = new("Reasoning") { - // Clean up - Runtime.Internal.RuntimePipelineCustomizerRegistry.Instance.Deregister(_customizer); - BedrockRuntimeClient?.Dispose(); - } + ProtectedData = "sig", + AdditionalProperties = new AdditionalPropertiesDictionary() + { + [nameof(ReasoningContentBlock.RedactedContent)] = redactedData + } + }; + + ChatMessage[] messages = [new(ChatRole.User, [reasoningContent])]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReceivesReasoningContent_WithRedactedContent() + { + byte[] redactedData = [5, 6, 7]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + ReasoningContent = new ReasoningContentBlock + { + ReasoningText = new ReasoningTextBlock { Text = "Thinking...", Signature = "sig" }, + RedactedContent = new MemoryStream(redactedData) + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Think")]); + + Assert.NotNull(result); + var reasoningContent = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + Assert.NotNull(reasoningContent.AdditionalProperties); + Assert.True(reasoningContent.AdditionalProperties.ContainsKey(nameof(ReasoningContentBlock.RedactedContent))); + + var received = (byte[])reasoningContent.AdditionalProperties[nameof(ReasoningContentBlock.RedactedContent)]; + Assert.True(received.SequenceEqual(redactedData)); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithCitationMetadata() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "This is cited content." } + ], + Citations = + [ + new() { + Title = "Example Source", + Source = "https://example.com", + Location = new CitationLocation + { + Web = new WebLocation + { + Url = "https://example.com" + } + }, + SourceContent = + [ + new() { Text = "Source snippet" } + ] + } + ] + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Cite your sources")]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Single(result.Messages); + TextContent textContent = Assert.IsType(result.Messages[0].Contents[0]); + Assert.Equal("This is cited content.", textContent.Text); + Assert.NotNull(textContent.RawRepresentation); + Assert.NotNull(textContent.Annotations); + Assert.Single(textContent.Annotations); + + CitationAnnotation citation = Assert.IsType(textContent.Annotations[0]); + Assert.Equal("Example Source", citation.Title); + Assert.Equal("https://example.com/", citation.Url?.ToString()); + Assert.Equal("Source snippet", citation.Snippet); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithCitation_NoSourceContent() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "Cited text." } + ], + Citations = + [ + new() { + Title = "My Source", + Source = "fallback-source" + } + ] + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + TextContent textContent = Assert.IsType(result.Messages[0].Contents[0]); + CitationAnnotation citation = Assert.IsType(Assert.Single(textContent.Annotations)); + Assert.Equal("fallback-source", citation.Snippet); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithSystemInstructions() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.System); + Assert.Single(request.System); + Assert.Equal("You are a helpful assistant.", request.System[0].Text); + + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + + return CreateResponse("I'm here to help!"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.System, "You are a helpful assistant."), + new(ChatRole.User, "Hello") + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("I'm here to help!", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithInstructions_InOptions() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.System); + Assert.Single(request.System); + Assert.Equal("Be concise.", request.System[0].Text); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Hello")]; + ChatOptions options = new() { Instructions = "Be concise." }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithChatOptions() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Equal("custom-model", request.ModelId); + + Assert.NotNull(request.InferenceConfig); + Assert.Equal(0.7f, request.InferenceConfig.Temperature); + Assert.Equal(0.9f, request.InferenceConfig.TopP); + Assert.Equal(100, request.InferenceConfig.MaxTokens); + Assert.NotNull(request.InferenceConfig.StopSequences); + Assert.Contains("STOP", request.InferenceConfig.StopSequences); + + return CreateResponse("Response with options applied."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("default-model"); + ChatMessage[] messages = [new(ChatRole.User, "Test message")]; + + ChatOptions options = new() + { + ModelId = "custom-model", + Temperature = 0.7f, + TopP = 0.9f, + MaxOutputTokens = 100, + StopSequences = ["STOP"] + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("Response with options applied.", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithFinishReasons() + { + var finishReasons = new[] + { + (StopReason.End_turn, ChatFinishReason.Stop), + (StopReason.Max_tokens, ChatFinishReason.Length), + (StopReason.Stop_sequence, ChatFinishReason.Stop), + (StopReason.Tool_use, ChatFinishReason.ToolCalls), + (StopReason.Content_filtered, ChatFinishReason.ContentFilter), + (StopReason.Guardrail_intervened, ChatFinishReason.ContentFilter) + }; + + foreach (var (stopReason, expectedFinishReason) in finishReasons) + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var response = CreateResponse("Test"); + response.StopReason = stopReason; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Test")]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.Equal(expectedFinishReason, result.FinishReason); + } + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithAdditionalModelResponseFields() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var response = CreateResponse("Test"); + response.AdditionalModelResponseFields = new Document(new Dictionary + { + ["custom_field"] = "custom_value", + ["number_field"] = 123 + }); + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + Assert.NotNull(result.Messages[0].AdditionalProperties); + // Values are JsonElement when deserialized from Document + Assert.Equal("custom_value", ((JsonElement)result.Messages[0].AdditionalProperties["custom_field"]).GetString()); + Assert.Equal(123, ((JsonElement)result.Messages[0].AdditionalProperties["number_field"]).GetInt32()); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_SystemMessageWithCachePoint() + { + CachePointBlock cachePoint = new() { Type = CachePointType.Default }; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + // Should have system messages including cache point + Assert.True(request.System.Count >= 2); + Assert.NotNull(request.System.Last().CachePoint); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + ChatMessage systemMessage = new(ChatRole.System, "System instruction") + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(ContentBlock.CachePoint)] = cachePoint + } + }; + + ChatMessage[] messages = [systemMessage, new(ChatRole.User, "Test")]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ToolWithoutProperties() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.ToolConfig); + Assert.Single(request.ToolConfig.Tools); + Assert.Equal("simple_tool", request.ToolConfig.Tools[0].ToolSpec.Name); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + // Create a simple tool with no properties + var tool = AIFunctionFactory.Create(() => "result", "simple_tool"); + + ChatOptions options = new() + { + Tools = [tool] + }; + + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithRawRepresentationFactory() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + // Verify the custom model ID was used + Assert.Equal("custom-model", request.ModelId); + + // Return empty stream + MemoryStream stream = new(); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("default-model"); + + ChatOptions options = new() + { + RawRepresentationFactory = client => new ConverseStreamRequest { ModelId = "custom-model" } + }; + + // Should not throw + await foreach (var _ in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Test")], options)) + { + // Consume stream + } + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_AllContentTypesHaveRawRepresentation() + { + byte[] imageData = [1, 2, 3, 4]; + byte[] videoData = [5, 6, 7, 8]; + byte[] docData = [9, 10, 11]; + string reasoningText = "Thinking..."; + string signature = "sig123"; + + ContentBlock textBlock = new() { Text = "Hello" }; + ContentBlock imageBlock = new() + { + Image = new ImageBlock + { + Format = ImageFormat.Png, + Source = new ImageSource { Bytes = new MemoryStream(imageData) } + } + }; + ContentBlock videoBlock = new() + { + Video = new VideoBlock + { + Format = VideoFormat.Mp4, + Source = new VideoSource { Bytes = new MemoryStream(videoData) } + } + }; + ContentBlock docBlock = new() + { + Document = new DocumentBlock + { + Format = DocumentFormat.Pdf, + Name = "file.pdf", + Source = new DocumentSource { Bytes = new MemoryStream(docData) } + } + }; + ContentBlock toolUseBlock = new() + { + ToolUse = new ToolUseBlock + { + ToolUseId = "tool_1", + Name = "func", + Input = new Document(new Dictionary()) + } + }; + ContentBlock citationBlock = new() + { + CitationsContent = new CitationsContentBlock + { + Content = [new() { Text = "Cited" }], + Citations = [new() { Title = "Source" }] + } + }; + ContentBlock reasoningBlock = new() + { + ReasoningContent = new ReasoningContentBlock + { + ReasoningText = new ReasoningTextBlock + { + Text = reasoningText, + Signature = signature + } + } + }; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + textBlock, + imageBlock, + videoBlock, + docBlock, + toolUseBlock, + citationBlock, + reasoningBlock + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + Assert.Equal(7, result.Messages[0].Contents.Count); + + Assert.Same(textBlock, result.Messages[0].Contents[0].RawRepresentation); + Assert.Same(imageBlock, result.Messages[0].Contents[1].RawRepresentation); + Assert.Same(videoBlock, result.Messages[0].Contents[2].RawRepresentation); + Assert.Same(docBlock, result.Messages[0].Contents[3].RawRepresentation); + Assert.Same(toolUseBlock, result.Messages[0].Contents[4].RawRepresentation); + // Citation content RawRepresentation is the CitationGeneratedContent, not the ContentBlock + Assert.Same(citationBlock.CitationsContent.Content[0], result.Messages[0].Contents[5].RawRepresentation); + Assert.Same(reasoningBlock, result.Messages[0].Contents[6].RawRepresentation); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_RawRepresentation_Message() + { + Message rawMessage = null; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new(); + rawMessage = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { Text = "Test" } + ] + }; + response.Output = new ConverseOutput + { + Message = rawMessage + }; + response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + Assert.Same(rawMessage, result.Messages[0].RawRepresentation); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_RawRepresentation_Response() + { + ConverseResponse rawResponse = null; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + rawResponse = new ConverseResponse + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { Text = "Test" } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return rawResponse; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + Assert.Same(rawResponse, result.RawRepresentation); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_UsesRawRepresentation_WhenSending() + { + ContentBlock originalContentBlock = new() { Text = "Original text from raw" }; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Single(request.Messages[0].Content); + Assert.Same(originalContentBlock, request.Messages[0].Content[0]); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + TextContent content = new("This text should be ignored") + { + RawRepresentation = originalContentBlock + }; + + ChatMessage[] messages = [new(ChatRole.User, [content])]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_HandlesWhitespaceOnlyText() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Single(request.Messages[0].Content); + Assert.Equal("\u200b", request.Messages[0].Content[0].Text); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, " ")]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_TrimsAssistantText() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Equal(2, request.Messages.Count); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); + + Assert.Single(request.Messages[1].Content); + Assert.Equal("Trimmed text", request.Messages[1].Content[0].Text); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, "Trimmed text \n\n") + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_SkipsEmptyAssistantText() + { + // When an assistant message contains only whitespace, it should be skipped entirely + // because sending an assistant message with empty content would fail the service. + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + // Only the user message should be sent; the whitespace-only assistant message is dropped + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, "Hello"), + new(ChatRole.Assistant, " \n\n ") + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_CachePointBlock_InMessages() + { + CachePointBlock cachePoint = new() { Type = CachePointType.Default }; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.Equal("Text before cache", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].CachePoint); + Assert.Equal(CachePointType.Default, request.Messages[0].Content[1].CachePoint.Type); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + ChatMessage chatMessage = new(ChatRole.User, "Text before cache") + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(ContentBlock.CachePoint)] = cachePoint + } + }; + + ChatResponse result = await chatClient.GetResponseAsync([chatMessage]); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_CachePointBlock_InSystemMessages() + { + CachePointBlock cachePoint = new() { Type = CachePointType.Default }; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.System); + Assert.Equal(2, request.System.Count); + Assert.Equal("System instruction", request.System[0].Text); + Assert.NotNull(request.System[1].CachePoint); + Assert.Equal(CachePointType.Default, request.System[1].CachePoint.Type); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + ChatMessage systemMessage = new(ChatRole.System, "System instruction") + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(ContentBlock.CachePoint)] = cachePoint + } + }; + + ChatResponse result = await chatClient.GetResponseAsync([systemMessage, new(ChatRole.User, "Hello")]); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_CachePointBlock_InContent() + { + CachePointBlock cachePoint = new() { Type = CachePointType.Default }; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Equal(3, request.Messages[0].Content.Count); + Assert.Equal("Text 1", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].CachePoint); + Assert.Equal("Text 2", request.Messages[0].Content[2].Text); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + TextContent content1 = new("Text 1") + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(ContentBlock.CachePoint)] = cachePoint + } + }; + + TextContent content2 = new("Text 2"); + + ChatMessage[] messages = [new(ChatRole.User, [content1, content2])]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithRawRepresentationFactory() + { + ConverseRequest factoryRequest = null; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Same(factoryRequest, request); + Assert.Equal("factory-model", request.ModelId); + Assert.NotNull(request.InferenceConfig); + Assert.Equal(0.5f, request.InferenceConfig.Temperature); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("default-model"); + + ChatOptions options = new() + { + RawRepresentationFactory = (client) => + { + factoryRequest = new ConverseRequest + { + ModelId = "factory-model", + InferenceConfig = new InferenceConfiguration { Temperature = 0.5f } + }; + return factoryRequest; + } + }; + + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_MultipleContentInCitations() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "Content 1" }, + new() { Text = "Content 2" } + ], + Citations = + [ + new() { Title = "Citation 1" }, + new() { Title = "Citation 2" } + ] + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + Assert.Equal(2, result.Messages[0].Contents.Count); + + var content1 = Assert.IsType(result.Messages[0].Contents[0]); + Assert.Equal("Content 1", content1.Text); + var citation1 = Assert.IsType(Assert.Single(content1.Annotations)); + Assert.Equal("Citation 1", citation1.Title); + + var content2 = Assert.IsType(result.Messages[0].Contents[1]); + Assert.Equal("Content 2", content2.Text); + var citation2 = Assert.IsType(Assert.Single(content2.Annotations)); + Assert.Equal("Citation 2", citation2.Title); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_MismatchedCitationCounts_UsesMinimum() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "Content 1" }, + new() { Text = "Content 2" }, + new() { Text = "Content 3" } + ], + Citations = + [ + new() { Title = "Citation 1" } + ] + } + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + Assert.Single(result.Messages[0].Contents); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_SendsFunctionCall_WithComplexArguments() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + // Verify the tool definition was created correctly + var toolSpec = request.ToolConfig?.Tools?[0]?.ToolSpec; + Assert.NotNull(toolSpec); + + return CreateResponse("OK"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + // Create a function with array parameters + var tool = AIFunctionFactory.Create( + (string[] items, int count) => "result", + "process_items", + "Processes an array of items"); + + ChatOptions options = new() + { + Tools = [tool] + }; + + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_DocumentWithArrayValues() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var response = CreateResponse("OK"); + response.Output.Message.Content.Add(new ContentBlock + { + ToolUse = new ToolUseBlock + { + ToolUseId = "tool_arr", + Name = "array_func", + Input = new Document(new Dictionary + { + ["items"] = new Document(new List { "a", "b", "c" }) + }) + } + }); + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + var funcCall = result.Messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(funcCall); + Assert.NotNull(funcCall.Arguments); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReceivesNestedDictionary() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var response = CreateResponse("OK"); + response.AdditionalModelResponseFields = new Document(new Dictionary + { + ["outer"] = new Document(new Dictionary + { + ["inner"] = "nested_value" + }) + }); + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + Assert.NotNull(result.Messages[0].AdditionalProperties); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionCallContent() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new(); + Document document = new(new Dictionary + { + ["location"] = "San Francisco" + }); + + response.Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + ToolUse = new ToolUseBlock + { + ToolUseId = "tool_123", + Name = "get_weather", + Input = document + } + } + ] + } + }; + response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; + + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "What's the weather in San Francisco?")]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.IsType(Assert.Single(result.Messages[0].Contents)); + + FunctionCallContent functionCall = (FunctionCallContent)result.Messages[0].Contents[0]; + Assert.Equal("get_weather", functionCall.Name); + Assert.Equal("tool_123", functionCall.CallId); + Assert.NotNull(functionCall.Arguments); + Assert.Null(functionCall.Exception); + Assert.NotNull(functionCall.RawRepresentation); + // Arguments values are JsonElement when deserialized from Document + Assert.Equal("San Francisco", ((JsonElement)functionCall.Arguments["location"]).GetString()); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionCallContent_WithDeeplyNestedDocument() + { + // Note: JSON serialization has a default max depth of 64. Documents nested deeper than that + // will fail during conversion. This test uses depth 50 which is within limits. + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + ConverseResponse response = new(); + var document = CreateDeeplyNestedDocument(50); + + response.Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = + [ + new() { + ToolUse = new ToolUseBlock + { + ToolUseId = "tool_nested", + Name = "nested_tool", + Input = document + } + } + ] + } + }; + response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; + + return response; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); + + Assert.NotNull(result); + FunctionCallContent functionCall = Assert.IsType(Assert.Single(result.Messages[0].Contents)); + + Assert.Equal("nested_tool", functionCall.Name); + Assert.Equal("tool_nested", functionCall.CallId); + Assert.NotNull(functionCall.Arguments); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Single(request.Messages[0].Content); + Assert.NotNull(request.Messages[0].Content[0].ToolResult); + Assert.Equal("call_123", request.Messages[0].Content[0].ToolResult.ToolUseId); + + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult.Content); + Assert.Single(toolResult.Content); + + return CreateResponse("Based on the weather data, it's sunny."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_123", new { temperature = 72, condition = "sunny" }) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + + Assert.NotNull(result); + Assert.Equal("Based on the weather data, it's sunny.", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithString() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_str", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + Assert.Equal("Result text", toolResult.Content[0].Text); + + return CreateResponse("Got your result"); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_str", "Result text") + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithDataContent() + { + byte[] imageData = [0x89, 0x50, 0x4E, 0x47]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.Single(request.Messages); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_456", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + Assert.NotNull(toolResult.Content[0].Image); + Assert.Equal(ImageFormat.Png, toolResult.Content[0].Image.Format); + Assert.True(toolResult.Content[0].Image.Source.Bytes.ToArray().SequenceEqual(imageData)); + + return CreateResponse("Image processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_456", new DataContent(imageData, "image/png")) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + Assert.Equal("Image processed.", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithTextContent() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_text", toolResult.ToolUseId); + Assert.NotNull(toolResult.Content); + Assert.Single(toolResult.Content); + + // TextContent should be converted to ToolResultContentBlock with Text property + Assert.Equal("Simple text result", toolResult.Content[0].Text); + + return CreateResponse("Text result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_text", new TextContent("Simple text result")) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + Assert.Equal("Text result processed.", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithMultipleAIContents() + { + byte[] data = [1, 2, 3]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_multi", toolResult.ToolUseId); + Assert.Equal(2, toolResult.Content.Count); + + Assert.NotNull(toolResult.Content[0].Image); + Assert.True(toolResult.Content[0].Image.Source.Bytes.ToArray().SequenceEqual(data)); + + Assert.NotNull(toolResult.Content[1].Document); + Assert.Equal(DocumentFormat.Pdf, toolResult.Content[1].Document.Format); + + return CreateResponse("Multi-content processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List multiContent = + [ + new DataContent(data, "image/png"), + new DataContent(new byte[] { 4, 5 }, "application/pdf") + ]; + + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_multi", multiContent) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + Assert.Equal("Multi-content processed.", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithTools() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.ToolConfig); + Assert.NotNull(request.ToolConfig.Tools); + Assert.Single(request.ToolConfig.Tools); + + var tool = request.ToolConfig.Tools[0]; + Assert.NotNull(tool.ToolSpec); + Assert.Equal("get_weather", tool.ToolSpec.Name); + Assert.Equal("Gets weather information", tool.ToolSpec.Description); + Assert.NotNull(tool.ToolSpec.InputSchema); + + var json = tool.ToolSpec.InputSchema.Json; + Assert.True(json.IsDictionary()); + var dict = json.AsDictionary(); + Assert.Equal("object", dict["type"].AsString()); + + return CreateResponse("I can use tools to help you."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "What tools do you have?")]; + + ChatOptions options = new() + { + Tools = + [ + AIFunctionFactory.Create((string location) => "the weather", "get_weather", "Gets weather information") + ] + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("I can use tools to help you.", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithToolMode_RequireSpecific() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.ToolConfig); + Assert.NotNull(request.ToolConfig.ToolChoice); + Assert.NotNull(request.ToolConfig.ToolChoice.Tool); + Assert.Equal("get_weather", request.ToolConfig.ToolChoice.Tool.Name); + + return CreateResponse("Required mode with specific function."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Test required tool mode")]; + + ChatOptions options = new() + { + Tools = [AIFunctionFactory.Create((string location) => "the weather", "get_weather", "Gets weather information")], + ToolMode = ChatToolMode.RequireSpecific("get_weather") + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + + Assert.NotNull(result); + Assert.Single(result.Messages); + Assert.Equal("Required mode with specific function.", ((TextContent)result.Messages[0].Contents[0]).Text); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_WithToolMode_RequireAny() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.NotNull(request.ToolConfig); + Assert.NotNull(request.ToolConfig.ToolChoice); + Assert.NotNull(request.ToolConfig.ToolChoice.Any); + + return CreateResponse("Required mode any function."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Test")]; + + ChatOptions options = new() + { + Tools = [AIFunctionFactory.Create((string location) => "the weather", "get_weather", "Gets weather")], + ToolMode = ChatToolMode.RequireAny + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + private static Document CreateDeeplyNestedDocument(int depth) + { + Dictionary dict = []; + var current = dict; + + for (int i = 0; i < depth; i++) + { + Dictionary next = []; + current[$"level{i}"] = new Document(next); + current = next; + } + + current["value"] = "final"; + return new Document(dict); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithReasoningContent() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_reason", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + Assert.NotNull(toolResult.Content[0].Text); + + return CreateResponse("Reasoning result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_reason", new TextReasoningContent("Here's my reasoning...")) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithVideoContent() + { + byte[] videoData = [1, 2, 3, 4, 5]; + + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.NotNull(toolResult.Content[0].Video); + Assert.True(toolResult.Content[0].Video.Source.Bytes.ToArray().SequenceEqual(videoData)); + + return CreateResponse("Video result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_video", new DataContent(videoData, "video/mp4")) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithIntResult() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + var dict = toolResult.Content[0].Json.AsDictionary(); + // The value is stored as double since JsonSerializer uses double for numbers + Assert.True(dict["result"].IsDouble() || dict["result"].IsInt()); + + return CreateResponse("Int result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_int", 42) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithBoolResult() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + var dict = toolResult.Content[0].Json.AsDictionary(); + Assert.True(dict["result"].AsBool()); + + return CreateResponse("Bool result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_bool", true) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithNullResult() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + + return CreateResponse("Null result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_null", (object)null) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithJsonElementResult() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + + return CreateResponse("JsonElement result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + JsonDocument jsonDoc = System.Text.Json.JsonDocument.Parse("{\"key\": \"value\"}"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_json", jsonDoc.RootElement) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithLongResult() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + var dict = toolResult.Content[0].Json.AsDictionary(); + Assert.True(dict["result"].IsLong() || dict["result"].IsDouble()); + + return CreateResponse("Long result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_long", 9999999999L) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(3.14f)] + [InlineData(2.718281828)] + public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithFloatingPointResult(double value) + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + + return CreateResponse("Floating point result processed."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = + [ + new(ChatRole.User, + [ + new FunctionResultContent("call_fp", value) + ]) + ]; + + ChatResponse result = await chatClient.GetResponseAsync(messages); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_NullMessages_Throws() + { + MockBedrockRuntime mock = new(); + IChatClient chatClient = mock.AsIChatClient("claude"); + + var enumerator = chatClient.GetStreamingResponseAsync(null).GetAsyncEnumerator(); + await Assert.ThrowsAsync("messages", () => enumerator.MoveNextAsync().AsTask()); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_BasicTextStreaming() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Hello"), + CreateContentBlockDeltaEvent(0, " world"), + CreateContentBlockDeltaEvent(0, "!"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Say hello")]; + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync(messages)) + { + updates.Add(update); + } + + Assert.NotEmpty(updates); + + // Verify all updates have consistent messageId and responseId + var messageIds = updates.Select(u => u.MessageId).Distinct().ToList(); + Assert.Single(messageIds); + Assert.NotNull(messageIds[0]); + + var responseIds = updates.Select(u => u.ResponseId).Distinct().ToList(); + Assert.Single(responseIds); + Assert.NotNull(responseIds[0]); + + // Verify role is set on updates + Assert.All(updates.Where(u => u.Role.HasValue), u => Assert.Equal(ChatRole.Assistant, u.Role)); + + List textUpdates = updates.Where(u => u.Contents.Any(c => c is TextContent)).ToList(); + Assert.Equal(3, textUpdates.Count); + + string fullText = string.Concat(textUpdates.Select(u => ((TextContent)u.Contents[0]).Text)); + Assert.Equal("Hello world!", fullText); + + Assert.Equal(ChatFinishReason.Stop, updates.Last(u => u.FinishReason != null).FinishReason); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithUsageMetadata() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Test"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(100, 50) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Test")])) + { + updates.Add(update); + } + + var usageUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is UsageContent)); + Assert.NotNull(usageUpdate); + + UsageContent usageContent = (UsageContent)usageUpdate.Contents.First(c => c is UsageContent); + Assert.Equal(100, usageContent.Details.InputTokenCount); + Assert.Equal(50, usageContent.Details.OutputTokenCount); + // TotalTokenCount is only set if the API returns it; the streaming API may not include it + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithToolUse() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEventWithToolUse(0, "tool_123", "get_weather"), + CreateContentBlockDeltaEventWithToolUse(0, "{\"location\":"), + CreateContentBlockDeltaEventWithToolUse(0, "\"Seattle\"}"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("tool_use"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Weather?")])) + { + updates.Add(update); + } + + var functionCallUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is FunctionCallContent)); + Assert.NotNull(functionCallUpdate); + + FunctionCallContent functionCall = (FunctionCallContent)functionCallUpdate.Contents.First(c => c is FunctionCallContent); + Assert.Equal("get_weather", functionCall.Name); + Assert.Equal("tool_123", functionCall.CallId); + Assert.NotNull(functionCall.Arguments); + Assert.Null(functionCall.Exception); + Assert.Equal("Seattle", ((JsonElement)functionCall.Arguments["location"]).GetString()); + + // Verify finish reason is ToolCalls + Assert.Equal(ChatFinishReason.ToolCalls, updates.Last(u => u.FinishReason != null).FinishReason); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithCitation() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEventWithCitation(0, "Cited text", "Source Title", "Source snippet"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Test")])) + { + updates.Add(update); + } + + var textUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is TextContent tc && tc.Text == "Cited text")); + Assert.NotNull(textUpdate); + + TextContent textContent = (TextContent)textUpdate.Contents.First(c => c is TextContent); + Assert.NotNull(textContent.Annotations); + Assert.Single(textContent.Annotations); + var citation = Assert.IsType(textContent.Annotations[0]); + Assert.Equal("Source Title", citation.Title); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithReasoningContent() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEventWithReasoning(0, "Thinking...", "sig123", null), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Test")])) + { + updates.Add(update); + } + + var reasoningUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is TextReasoningContent)); + Assert.NotNull(reasoningUpdate); + + TextReasoningContent reasoningContent = (TextReasoningContent)reasoningUpdate.Contents.First(c => c is TextReasoningContent); + Assert.Equal("Thinking...", reasoningContent.Text); + Assert.Equal("sig123", reasoningContent.ProtectedData); + Assert.Equal(ChatRole.Assistant, reasoningUpdate.Role); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithReasoningContentAndRedacted() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEventWithReasoning(0, "Thinking...", null, "cmVkYWN0ZWQ="), // base64 "redacted" + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Test")])) + { + updates.Add(update); + } + + var reasoningUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is TextReasoningContent)); + Assert.NotNull(reasoningUpdate); + + TextReasoningContent reasoningContent = (TextReasoningContent)reasoningUpdate.Contents.First(c => c is TextReasoningContent); + Assert.NotNull(reasoningContent.AdditionalProperties); + Assert.True(reasoningContent.AdditionalProperties.ContainsKey("RedactedContent")); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithInvalidToolJson() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEventWithToolUse(0, "tool_err", "bad_tool"), + CreateContentBlockDeltaEventWithToolUse(0, "not valid json {{{"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("tool_use"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Test")])) + { + updates.Add(update); + } + + var functionCallUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is FunctionCallContent)); + Assert.NotNull(functionCallUpdate); + + FunctionCallContent functionCall = (FunctionCallContent)functionCallUpdate.Contents.First(c => c is FunctionCallContent); + Assert.Equal("bad_tool", functionCall.Name); + Assert.NotNull(functionCall.Exception); // Should have parse error + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_WithAdditionalResponseFields() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Test"), + CreateContentBlockStopEvent(0), + CreateMessageStopEventWithAdditionalFields("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + + List updates = []; + await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync([new(ChatRole.User, "Test")])) + { + updates.Add(update); + } + + // Should have received an update with additional properties + var stopUpdate = updates.FirstOrDefault(u => u.FinishReason == ChatFinishReason.Stop && u.AdditionalProperties != null); + Assert.NotNull(stopUpdate); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(ReasoningEffort.Low, 1024, 4096)] + [InlineData(ReasoningEffort.Medium, 8192, 32768)] + [InlineData(ReasoningEffort.High, 16384, 65536)] + [InlineData(ReasoningEffort.ExtraHigh, 32768, 131072)] + public async Task IChatClient_GetResponseAsync_ReasoningEffort_SetsThinkingConfig_NoMaxTokens(ReasoningEffort effort, int expectedBudget, int expectedMaxTokens) + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + Assert.Equal(expectedMaxTokens, request.InferenceConfig.MaxTokens); + + return CreateResponse("Thinking response."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = effort } + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(ReasoningEffort.Low, 10000, 2500)] + [InlineData(ReasoningEffort.Medium, 10000, 5000)] + [InlineData(ReasoningEffort.High, 10000, 7500)] + [InlineData(ReasoningEffort.ExtraHigh, 10000, 9999)] + [InlineData(ReasoningEffort.Low, 2000, 1024)] // clamped to minimum of 1024 + [InlineData(ReasoningEffort.ExtraHigh, 1025, 1024)] // clamped to maxTokens - 1 + public async Task IChatClient_GetResponseAsync_ReasoningEffort_SetsThinkingConfig_WithMaxTokens(ReasoningEffort effort, int maxTokens, int expectedBudget) + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var thinking = request.AdditionalModelRequestFields.AsDictionary()["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + + return CreateResponse("Thinking response."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; + ChatOptions options = new() + { + MaxOutputTokens = maxTokens, + Reasoning = new() { Effort = effort } + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReasoningEffortNone_DoesNotSetThinkingConfig() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsNull()); + return CreateResponse("No thinking."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Hello")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.None } + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_NoReasoning_DoesNotSetThinkingConfig() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsNull()); + return CreateResponse("No thinking."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Hello")]; + ChatOptions options = new(); + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReasoningDoesNotOverrideExistingThinkingConfig() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var fields = request.AdditionalModelRequestFields.AsDictionary(); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(99999, thinking["budget_tokens"].AsInt()); + + return CreateResponse("Custom thinking."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.Low }, + RawRepresentationFactory = _ => + { + var req = new ConverseRequest(); + req.AdditionalModelRequestFields = new Document(new Dictionary + { + ["thinking"] = new Document(new Dictionary + { + ["type"] = new Document("enabled"), + ["budget_tokens"] = new Document(99999) + }) + }); + return req; + } + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetResponseAsync_ReasoningMergesWithExistingAdditionalFields() + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.Equal("bar", fields["foo"].AsString()); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + + return CreateResponse("Merged fields."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.High }, + RawRepresentationFactory = _ => + { + var req = new ConverseRequest(); + req.AdditionalModelRequestFields = new Document(new Dictionary + { + ["foo"] = new Document("bar") + }); + return req; + } + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(ReasoningOutput.Summary)] + [InlineData(ReasoningOutput.Full)] + public async Task IChatClient_GetResponseAsync_ReasoningOutputSummaryOrFull_EnablesThinking(ReasoningOutput output) + { + MockBedrockRuntime mock = new() + { + OnConverseRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + + return CreateResponse("Thinking response."); + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.High, Output = output } + }; + + ChatResponse result = await chatClient.GetResponseAsync(messages, options); + Assert.NotNull(result); + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(ReasoningEffort.Low, 1024, 4096)] + [InlineData(ReasoningEffort.Medium, 8192, 32768)] + [InlineData(ReasoningEffort.High, 16384, 65536)] + [InlineData(ReasoningEffort.ExtraHigh, 32768, 131072)] + public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffort_SetsThinkingConfig_NoMaxTokens(ReasoningEffort effort, int expectedBudget, int expectedMaxTokens) + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + Assert.Equal(expectedMaxTokens, request.InferenceConfig.MaxTokens); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Response."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = effort } + }; + + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) { } + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(ReasoningEffort.Low, 10000, 2500)] + [InlineData(ReasoningEffort.Medium, 10000, 5000)] + [InlineData(ReasoningEffort.High, 10000, 7500)] + [InlineData(ReasoningEffort.ExtraHigh, 10000, 9999)] + [InlineData(ReasoningEffort.Low, 2000, 1024)] // clamped to minimum of 1024 + [InlineData(ReasoningEffort.ExtraHigh, 1025, 1024)] // clamped to maxTokens - 1 + public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffort_SetsThinkingConfig_WithMaxTokens(ReasoningEffort effort, int maxTokens, int expectedBudget) + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var thinking = request.AdditionalModelRequestFields.AsDictionary()["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Response."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; + ChatOptions options = new() + { + MaxOutputTokens = maxTokens, + Reasoning = new() { Effort = effort } + }; + + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) { } + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffortNone_DoesNotSetThinkingConfig() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsNull()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "No thinking."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Hello")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.None } + }; + + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) { } + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_NoReasoning_DoesNotSetThinkingConfig() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsNull()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "No thinking."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Hello")]; + ChatOptions options = new(); + + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) { } + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_ReasoningDoesNotOverrideExistingThinkingConfig() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var fields = request.AdditionalModelRequestFields.AsDictionary(); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(99999, thinking["budget_tokens"].AsInt()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Custom thinking."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.Low }, + RawRepresentationFactory = _ => + { + var req = new ConverseStreamRequest(); + req.AdditionalModelRequestFields = new Document(new Dictionary + { + ["thinking"] = new Document(new Dictionary + { + ["type"] = new Document("enabled"), + ["budget_tokens"] = new Document(99999) + }) + }); + return req; + } + }; + + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) { } + } + + [Fact] + [Trait("UnitTest", "BedrockRuntime")] + public async Task IChatClient_GetStreamingResponseAsync_ReasoningMergesWithExistingAdditionalFields() + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.Equal("bar", fields["foo"].AsString()); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Merged."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.High }, + RawRepresentationFactory = _ => + { + var req = new ConverseStreamRequest(); + req.AdditionalModelRequestFields = new Document(new Dictionary + { + ["foo"] = new Document("bar") + }); + return req; + } + }; + + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) { } + } + + [Theory] + [Trait("UnitTest", "BedrockRuntime")] + [InlineData(ReasoningOutput.Summary)] + [InlineData(ReasoningOutput.Full)] + public async Task IChatClient_GetStreamingResponseAsync_ReasoningOutputSummaryOrFull_EnablesThinking(ReasoningOutput output) + { + MockBedrockRuntime mock = new() + { + OnConverseStreamRequest = request => + { + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Thinking response."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + } + }; + + IChatClient chatClient = mock.AsIChatClient("claude"); + ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; + ChatOptions options = new() + { + Reasoning = new() { Effort = ReasoningEffort.High, Output = output } + }; + + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) { } + } + + private static byte[] CreateContentBlockDeltaEventWithCitation(int contentBlockIndex, string text, string title, string snippet) + { + return CreateEventMessage("ContentBlockDelta", Encoding.UTF8.GetBytes($"{{\"contentBlockIndex\":{contentBlockIndex},\"delta\":{{\"text\":\"{text}\",\"citation\":{{\"title\":\"{title}\",\"sourceContent\":[{{\"text\":\"{snippet}\"}}]}}}}}}")); + } + + private static byte[] CreateContentBlockDeltaEventWithReasoning(int contentBlockIndex, string text, string signature, string redactedContentBase64) + { + var sigPart = signature != null ? $",\"signature\":\"{signature}\"" : ""; + var redactedPart = redactedContentBase64 != null ? $",\"redactedContent\":\"{redactedContentBase64}\"" : ""; + return CreateEventMessage("ContentBlockDelta", Encoding.UTF8.GetBytes($"{{\"contentBlockIndex\":{contentBlockIndex},\"delta\":{{\"reasoningContent\":{{\"text\":\"{text}\"{sigPart}{redactedPart}}}}}}}")); + } + + private static byte[] CreateMessageStopEventWithAdditionalFields(string stopReason) => + CreateEventMessage("MessageStop", Encoding.UTF8.GetBytes($"{{\"stopReason\":\"{stopReason}\",\"additionalModelResponseFields\":{{\"custom\":\"value\"}}}}")); + + private static Stream CreateEventStream(params byte[][] events) + { + MemoryStream ms = new(); + foreach (var evt in events) + { + ms.Write(evt, 0, evt.Length); + } + ms.Position = 0; + return ms; + } + + private static byte[] CreateMessageStartEvent() => + CreateEventMessage("MessageStart", Encoding.UTF8.GetBytes("""{"role":"assistant"}""")); + + private static byte[] CreateContentBlockStartEvent(int contentBlockIndex) => + CreateEventMessage("ContentBlockStart", Encoding.UTF8.GetBytes($"{{\"contentBlockIndex\":{contentBlockIndex},\"start\":{{\"text\":\"\"}}}}")); + + private static byte[] CreateContentBlockStartEventWithToolUse(int contentBlockIndex, string toolUseId, string name) + { + return CreateEventMessage("ContentBlockStart", Encoding.UTF8.GetBytes($"{{\"contentBlockIndex\":{contentBlockIndex},\"start\":{{\"toolUse\":{{\"toolUseId\":\"{toolUseId}\",\"name\":\"{name}\"}}}}}}")); + } + + private static byte[] CreateContentBlockDeltaEvent(int contentBlockIndex, string text) + { + var escapedText = text.Replace("\"", "\\\""); + return CreateEventMessage("ContentBlockDelta", Encoding.UTF8.GetBytes($"{{\"contentBlockIndex\":{contentBlockIndex},\"delta\":{{\"text\":\"{escapedText}\"}}}}")); + } + + private static byte[] CreateContentBlockDeltaEventWithToolUse(int contentBlockIndex, string input) + { + var escapedInput = input.Replace("\"", "\\\""); + return CreateEventMessage("ContentBlockDelta", Encoding.UTF8.GetBytes($"{{\"contentBlockIndex\":{contentBlockIndex},\"delta\":{{\"toolUse\":{{\"input\":\"{escapedInput}\"}}}}}}")); + } + + private static byte[] CreateContentBlockStopEvent(int contentBlockIndex) => + CreateEventMessage("ContentBlockStop", Encoding.UTF8.GetBytes($"{{\"contentBlockIndex\":{contentBlockIndex}}}")); + + private static byte[] CreateMessageStopEvent(string stopReason) => + CreateEventMessage("MessageStop", Encoding.UTF8.GetBytes($"{{\"stopReason\":\"{stopReason}\"}}")); + + private static byte[] CreateMetadataEvent(int inputTokens, int outputTokens) => + CreateEventMessage("Metadata", Encoding.UTF8.GetBytes($"{{\"usage\":{{\"inputTokens\":{inputTokens},\"outputTokens\":{outputTokens}}}}}")); + + private static byte[] GetUtf8(string s) => Encoding.UTF8.GetBytes(s); + + private static byte[] CreateEventMessage(string eventType, byte[] payload) + { + EventStreamHeader messageTypeHeader = new(":message-type"); + messageTypeHeader.SetString("event"); + + EventStreamHeader eventTypeHeader = new(":event-type"); + eventTypeHeader.SetString(eventType); + + EventStreamHeader contentTypeHeader = new(":content-type"); + contentTypeHeader.SetString("application/json"); + + List headers = + [ + messageTypeHeader, + eventTypeHeader, + contentTypeHeader + ]; + + return new EventStreamMessage(headers, payload).ToByteArray(); + } + + private static ConverseResponse CreateResponse(string text) + { + ConverseResponse response = new() + { + Output = new ConverseOutput + { + Message = new Message + { + Role = ConversationRole.Assistant, + Content = [new() { Text = text }] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; } } \ No newline at end of file diff --git a/extensions/test/BedrockMEAITests/BedrockMEAITests.NetFramework.csproj b/extensions/test/BedrockMEAITests/BedrockMEAITests.NetFramework.csproj index f81ac215ea79..1615ed9514cb 100644 --- a/extensions/test/BedrockMEAITests/BedrockMEAITests.NetFramework.csproj +++ b/extensions/test/BedrockMEAITests/BedrockMEAITests.NetFramework.csproj @@ -18,7 +18,7 @@ - + diff --git a/extensions/test/BedrockMEAITests/MockBedrockRuntime.cs b/extensions/test/BedrockMEAITests/MockBedrockRuntime.cs new file mode 100644 index 000000000000..06e470c8c07a --- /dev/null +++ b/extensions/test/BedrockMEAITests/MockBedrockRuntime.cs @@ -0,0 +1,57 @@ +using Amazon.BedrockRuntime.Model; +using Amazon.Runtime; +using Amazon.Runtime.Endpoints; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.BedrockRuntime; + +internal sealed class MockBedrockRuntime : IAmazonBedrockRuntime +{ + public Func OnConverseRequest { get; set; } + public Func OnConverseStreamRequest { get; set; } + + public IClientConfig Config => throw new NotImplementedException(); + public IBedrockRuntimePaginatorFactory Paginators => throw new NotImplementedException(); + + public Task ConverseAsync(ConverseRequest request, CancellationToken cancellationToken = default) + { + if (OnConverseRequest is null) + { + throw new NotSupportedException($"{nameof(ConverseAsync)} was invoked but no {nameof(OnConverseRequest)} was provided."); + } + + return Task.FromResult(OnConverseRequest(request)); + } + + public Task ConverseStreamAsync(ConverseStreamRequest request, CancellationToken cancellationToken = default) + { + if (OnConverseStreamRequest is null) + { + throw new NotSupportedException($"{nameof(ConverseStreamAsync)} was invoked but no {nameof(OnConverseStreamRequest)} was provided."); + } + + return Task.FromResult(OnConverseStreamRequest(request)); + } + + public void Dispose() { } + + public ConverseResponse Converse(ConverseRequest request) => throw new NotImplementedException(); + public ConverseStreamResponse ConverseStream(ConverseStreamRequest request) => throw new NotImplementedException(); + public ApplyGuardrailResponse ApplyGuardrail(ApplyGuardrailRequest request) => throw new NotImplementedException(); + public Task ApplyGuardrailAsync(ApplyGuardrailRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public CountTokensResponse CountTokens(CountTokensRequest request) => throw new NotImplementedException(); + public Task CountTokensAsync(CountTokensRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public GetAsyncInvokeResponse GetAsyncInvoke(GetAsyncInvokeRequest request) => throw new NotImplementedException(); + public Task GetAsyncInvokeAsync(GetAsyncInvokeRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public InvokeModelResponse InvokeModel(InvokeModelRequest request) => throw new NotImplementedException(); + public Task InvokeModelAsync(InvokeModelRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public InvokeModelWithResponseStreamResponse InvokeModelWithResponseStream(InvokeModelWithResponseStreamRequest request) => throw new NotImplementedException(); + public Task InvokeModelWithResponseStreamAsync(InvokeModelWithResponseStreamRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public ListAsyncInvokesResponse ListAsyncInvokes(ListAsyncInvokesRequest request) => throw new NotImplementedException(); + public Task ListAsyncInvokesAsync(ListAsyncInvokesRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public StartAsyncInvokeResponse StartAsyncInvoke(StartAsyncInvokeRequest request) => throw new NotImplementedException(); + public Task StartAsyncInvokeAsync(StartAsyncInvokeRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Endpoint DetermineServiceOperationEndpoint(AmazonWebServiceRequest request) => throw new NotImplementedException(); +} From cc9d06b2f87396ce0dfb84f94c250550954c3c63 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 13 Mar 2026 21:14:34 -0400 Subject: [PATCH 2/3] Update tests to use Mock --- .../BedrockChatClientTests.cs | 2540 ++++++++--------- .../BedrockMEAITests/MockBedrockRuntime.cs | 57 - 2 files changed, 1138 insertions(+), 1459 deletions(-) delete mode 100644 extensions/test/BedrockMEAITests/MockBedrockRuntime.cs diff --git a/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs b/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs index c72d52648544..cd4fa0c900ae 100644 --- a/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs +++ b/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs @@ -1,4 +1,4 @@ -using Amazon.BedrockRuntime.Model; +using Amazon.BedrockRuntime.Model; using Amazon.Runtime.Documents; using Amazon.Runtime.EventStreams; using Microsoft.Extensions.AI; @@ -446,7 +446,7 @@ public async Task ResponseFormat_Json_NullToolInput_ThrowsInvalidOperationExcept [Trait("UnitTest", "BedrockRuntime")] public void AsIChatClient_ValidArguments_CreatesIChatClientSuccessfully() { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient(); Assert.NotNull(chatClient); Assert.Same(mock, chatClient.GetService()); @@ -456,7 +456,7 @@ public void AsIChatClient_ValidArguments_CreatesIChatClientSuccessfully() [Trait("UnitTest", "BedrockRuntime")] public void IChatClient_GetService_InvalidArguments_Throws() { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient(); Assert.NotNull(chatClient); @@ -469,7 +469,7 @@ public void IChatClient_GetService_InvalidArguments_Throws() [InlineData("anthropic.claude-3-sonnet-20240229-v1:0")] public void IChatClient_GetService_ReturnsExpectedInstance(string defaultModelId) { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient(defaultModelId); Assert.NotNull(chatClient); @@ -486,7 +486,7 @@ public void IChatClient_GetService_ReturnsExpectedInstance(string defaultModelId [Trait("UnitTest", "BedrockRuntime")] public void IChatClient_Dispose_Nop() { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient(); Assert.NotNull(chatClient); @@ -499,10 +499,7 @@ public void IChatClient_Dispose_Nop() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_BasicRequest() { - MockBedrockRuntime mock = new() - { - OnConverseRequest = request => CreateResponse("Hello") - }; + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => CreateResponse("Hello")); IChatClient chatClient = mock.AsIChatClient("anthropic.claude-3-sonnet-20240229-v1:0"); ChatResponse result = await chatClient.GetResponseAsync("Hello"); @@ -520,20 +517,17 @@ public async Task IChatClient_GetResponseAsync_BasicRequest() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_TextContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); - Assert.Single(request.Messages[0].Content); - Assert.Equal("What is the weather like?", request.Messages[0].Content[0].Text); + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Single(request.Messages[0].Content); + Assert.Equal("What is the weather like?", request.Messages[0].Content[0].Text); - var response = CreateResponse("It's sunny today."); - response.StopReason = StopReason.End_turn; - return response; - } - }; + var response = CreateResponse("It's sunny today."); + response.StopReason = StopReason.End_turn; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "What is the weather like?")]; @@ -559,18 +553,15 @@ public async Task IChatClient_GetResponseAsync_TextContent() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_EmptyMessages_CreatesDefaultMessage() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); - Assert.Single(request.Messages[0].Content); - Assert.Equal("\u200B", request.Messages[0].Content[0].Text); + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Single(request.Messages[0].Content); + Assert.Equal("\u200B", request.Messages[0].Content[0].Text); - return CreateResponse("Empty input received"); - } - }; + return CreateResponse("Empty input received"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = []; @@ -586,7 +577,7 @@ public async Task IChatClient_GetResponseAsync_EmptyMessages_CreatesDefaultMessa [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_NullMessages_Throws() { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient("claude"); await Assert.ThrowsAsync("messages", () => chatClient.GetResponseAsync(null!)); @@ -598,21 +589,18 @@ public async Task IChatClient_GetResponseAsync_DataContent_Image() { byte[] imageData = [0x89, 0x50, 0x4E, 0x47]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); - Assert.Equal(2, request.Messages[0].Content.Count); - Assert.Equal("Describe this image", request.Messages[0].Content[0].Text); - Assert.NotNull(request.Messages[0].Content[1].Image); - Assert.Equal(ImageFormat.Png, request.Messages[0].Content[1].Image.Format); - Assert.True(request.Messages[0].Content[1].Image.Source.Bytes.ToArray().SequenceEqual(imageData)); - - return CreateResponse("I see an image."); - } - }; + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.Equal("Describe this image", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].Image); + Assert.Equal(ImageFormat.Png, request.Messages[0].Content[1].Image.Format); + Assert.True(request.Messages[0].Content[1].Image.Source.Bytes.ToArray().SequenceEqual(imageData)); + + return CreateResponse("I see an image."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -648,16 +636,13 @@ public async Task IChatClient_GetResponseAsync_DataContent_AllImageFormats() byte[] imageData = [1, 2, 3, 4]; bool verified = false; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.Messages[0].Content[0].Image); - Assert.Equal(expectedFormat, request.Messages[0].Content[0].Image.Format); - verified = true; - return CreateResponse("OK"); - } - }; + Assert.NotNull(request.Messages[0].Content[0].Image); + Assert.Equal(expectedFormat, request.Messages[0].Content[0].Image.Format); + verified = true; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); await chatClient.GetResponseAsync([new(ChatRole.User, [new DataContent(imageData, mimeType)])]); @@ -671,21 +656,18 @@ public async Task IChatClient_GetResponseAsync_DataContent_Document() { byte[] pdfData = [0x25, 0x50, 0x44, 0x46]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(2, request.Messages[0].Content.Count); - Assert.Equal("Analyze this document", request.Messages[0].Content[0].Text); - Assert.NotNull(request.Messages[0].Content[1].Document); - Assert.Equal(DocumentFormat.Pdf, request.Messages[0].Content[1].Document.Format); - Assert.True(request.Messages[0].Content[1].Document.Source.Bytes.ToArray().SequenceEqual(pdfData)); - Assert.Equal("file", request.Messages[0].Content[1].Document.Name); - - return CreateResponse("Document analyzed."); - } - }; + Assert.Single(request.Messages); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.Equal("Analyze this document", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].Document); + Assert.Equal(DocumentFormat.Pdf, request.Messages[0].Content[1].Document.Format); + Assert.True(request.Messages[0].Content[1].Document.Source.Bytes.ToArray().SequenceEqual(pdfData)); + Assert.Equal("file", request.Messages[0].Content[1].Document.Name); + + return CreateResponse("Document analyzed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -710,16 +692,13 @@ public async Task IChatClient_GetResponseAsync_DataContent_DocumentWithName() { byte[] pdfData = [1, 2, 3]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.Messages[0].Content[0].Document); - Assert.Equal("report.pdf", request.Messages[0].Content[0].Document.Name); + Assert.NotNull(request.Messages[0].Content[0].Document); + Assert.Equal("report.pdf", request.Messages[0].Content[0].Document.Name); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); DataContent dataContent = new(pdfData, "application/pdf") { Name = "report.pdf" }; @@ -732,18 +711,15 @@ public async Task IChatClient_GetResponseAsync_DataContent_Video() { byte[] videoData = [0x00, 0x00, 0x00, 0x18]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(2, request.Messages[0].Content.Count); - Assert.NotNull(request.Messages[0].Content[1].Video); - Assert.Equal(VideoFormat.Mp4, request.Messages[0].Content[1].Video.Format); + Assert.Single(request.Messages); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.NotNull(request.Messages[0].Content[1].Video); + Assert.Equal(VideoFormat.Mp4, request.Messages[0].Content[1].Video.Format); - return CreateResponse("Video processed."); - } - }; + return CreateResponse("Video processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -768,37 +744,34 @@ public async Task IChatClient_GetResponseAsync_ReceivesImageContent() { byte[] imageData = [1, 2, 3, 4]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Image = new ImageBlock + Role = ConversationRole.Assistant, + Content = + [ + new() { + Image = new ImageBlock + { + Format = ImageFormat.Png, + Source = new ImageSource { - Format = ImageFormat.Png, - Source = new ImageSource - { - Bytes = new MemoryStream(imageData) - } + Bytes = new MemoryStream(imageData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Send me an image")]); @@ -817,37 +790,34 @@ public async Task IChatClient_GetResponseAsync_ReceivesVideoContent() { byte[] videoData = [5, 6, 7, 8]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Video = new VideoBlock + Role = ConversationRole.Assistant, + Content = + [ + new() { + Video = new VideoBlock + { + Format = VideoFormat.Mp4, + Source = new VideoSource { - Format = VideoFormat.Mp4, - Source = new VideoSource - { - Bytes = new MemoryStream(videoData) - } + Bytes = new MemoryStream(videoData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Send me a video")]); @@ -866,38 +836,35 @@ public async Task IChatClient_GetResponseAsync_ReceivesDocumentContent() { byte[] docData = [9, 10, 11]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Document = new DocumentBlock + Role = ConversationRole.Assistant, + Content = + [ + new() { + Document = new DocumentBlock + { + Format = DocumentFormat.Pdf, + Name = "result.pdf", + Source = new DocumentSource { - Format = DocumentFormat.Pdf, - Name = "result.pdf", - Source = new DocumentSource - { - Bytes = new MemoryStream(docData) - } + Bytes = new MemoryStream(docData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Send me a document")]); @@ -915,7 +882,7 @@ public async Task IChatClient_GetResponseAsync_ReceivesDocumentContent() [Trait("UnitTest", "BedrockRuntime")] public void IChatClient_GetService_WithServiceKey_ReturnsNull() { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient(); // When serviceKey is not null, should return null @@ -926,7 +893,7 @@ public void IChatClient_GetService_WithServiceKey_ReturnsNull() [Trait("UnitTest", "BedrockRuntime")] public void IChatClient_GetService_UnknownType_ReturnsNull() { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient(); // Unknown type should return null @@ -937,22 +904,19 @@ public void IChatClient_GetService_UnknownType_ReturnsNull() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_UsageWithCacheTokens() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + var response = CreateResponse("OK"); + response.Usage = new TokenUsage { - var response = CreateResponse("OK"); - response.Usage = new TokenUsage - { - InputTokens = 100, - OutputTokens = 50, - TotalTokens = 150, - CacheReadInputTokens = 25, - CacheWriteInputTokens = 10 - }; - return response; - } - }; + InputTokens = 100, + OutputTokens = 50, + TotalTokens = 150, + CacheReadInputTokens = 25, + CacheWriteInputTokens = 10 + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -970,15 +934,12 @@ public async Task IChatClient_GetResponseAsync_UsageWithCacheTokens() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_CustomFinishReason() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var response = CreateResponse("Custom"); - response.StopReason = new StopReason("custom_reason"); - return response; - } - }; + var response = CreateResponse("Custom"); + response.StopReason = new StopReason("custom_reason"); + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -990,17 +951,14 @@ public async Task IChatClient_GetResponseAsync_CustomFinishReason() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_StopSequences_MergesWithExisting() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - // Should have merged stop sequences - Assert.Contains("STOP1", request.InferenceConfig.StopSequences); - Assert.Contains("STOP2", request.InferenceConfig.StopSequences); + // Should have merged stop sequences + Assert.Contains("STOP1", request.InferenceConfig.StopSequences); + Assert.Contains("STOP2", request.InferenceConfig.StopSequences); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -1027,14 +985,11 @@ public async Task IChatClient_GetResponseAsync_SendsDocumentContent_AllFormats(s { byte[] docData = [1, 2, 3]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.Messages[0].Content[0].Document); - return CreateResponse("OK"); - } - }; + Assert.NotNull(request.Messages[0].Content[0].Document); + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -1054,14 +1009,11 @@ public async Task IChatClient_GetResponseAsync_SendsImageContent_AllFormats(stri { byte[] imageData = [1, 2, 3]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.Messages[0].Content[0].Image); - return CreateResponse("OK"); - } - }; + Assert.NotNull(request.Messages[0].Content[0].Image); + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -1085,14 +1037,11 @@ public async Task IChatClient_GetResponseAsync_SendsVideoContent_AllFormats(stri { byte[] videoData = [1, 2, 3]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.Messages[0].Content[0].Video); - return CreateResponse("OK"); - } - }; + Assert.NotNull(request.Messages[0].Content[0].Video); + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -1108,25 +1057,22 @@ public async Task IChatClient_GetResponseAsync_SendsVideoContent_AllFormats(stri [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_SendsFunctionCallContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Equal(2, request.Messages.Count); + Assert.Equal(2, request.Messages.Count); - // First message is user - Assert.Equal(ConversationRole.User, request.Messages[0].Role); + // First message is user + Assert.Equal(ConversationRole.User, request.Messages[0].Role); - // Second message is assistant with tool use - Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); - var toolUse = request.Messages[1].Content[0].ToolUse; - Assert.NotNull(toolUse); - Assert.Equal("call_123", toolUse.ToolUseId); - Assert.Equal("get_weather", toolUse.Name); + // Second message is assistant with tool use + Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); + var toolUse = request.Messages[1].Content[0].ToolUse; + Assert.NotNull(toolUse); + Assert.Equal("call_123", toolUse.ToolUseId); + Assert.Equal("get_weather", toolUse.Name); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -1157,35 +1103,32 @@ public async Task IChatClient_GetResponseAsync_ReceivesDocumentContent_AllFormat byte[] docData = [9, 10, 11]; DocumentFormat format = new(formatValue); - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Document = new DocumentBlock - { - Format = format, - Name = "result.doc", - Source = new DocumentSource { Bytes = new MemoryStream(docData) } - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + Document = new DocumentBlock + { + Format = format, + Name = "result.doc", + Source = new DocumentSource { Bytes = new MemoryStream(docData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1204,34 +1147,31 @@ public async Task IChatClient_GetResponseAsync_ReceivesImageContent_AllFormats(s byte[] imageData = [1, 2, 3]; ImageFormat format = new(formatValue); - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Image = new ImageBlock - { - Format = format, - Source = new ImageSource { Bytes = new MemoryStream(imageData) } - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + Image = new ImageBlock + { + Format = format, + Source = new ImageSource { Bytes = new MemoryStream(imageData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1254,34 +1194,31 @@ public async Task IChatClient_GetResponseAsync_ReceivesVideoContent_AllFormats(s byte[] videoData = [5, 6, 7, 8]; VideoFormat format = new(formatValue); - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Video = new VideoBlock - { - Format = format, - Source = new VideoSource { Bytes = new MemoryStream(videoData) } - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + Video = new VideoBlock + { + Format = format, + Source = new VideoSource { Bytes = new MemoryStream(videoData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1298,35 +1235,32 @@ public async Task IChatClient_GetResponseAsync_ReceivesDocument_UnknownFormat() byte[] docData = [9, 10, 11]; DocumentFormat format = new("unknown_format"); - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Document = new DocumentBlock - { - Format = format, - Name = "result.doc", - Source = new DocumentSource { Bytes = new MemoryStream(docData) } - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + Document = new DocumentBlock + { + Format = format, + Name = "result.doc", + Source = new DocumentSource { Bytes = new MemoryStream(docData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1344,34 +1278,31 @@ public async Task IChatClient_GetResponseAsync_ReceivesImage_UnknownFormat() byte[] imageData = [1, 2, 3]; ImageFormat format = new("unknown_format"); - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Image = new ImageBlock - { - Format = format, - Source = new ImageSource { Bytes = new MemoryStream(imageData) } - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + Image = new ImageBlock + { + Format = format, + Source = new ImageSource { Bytes = new MemoryStream(imageData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1389,34 +1320,31 @@ public async Task IChatClient_GetResponseAsync_ReceivesVideo_UnknownFormat() byte[] videoData = [5, 6, 7, 8]; VideoFormat format = new("unknown_format"); - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - Video = new VideoBlock - { - Format = format, - Source = new VideoSource { Bytes = new MemoryStream(videoData) } - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + Video = new VideoBlock + { + Format = format, + Source = new VideoSource { Bytes = new MemoryStream(videoData) } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1433,15 +1361,12 @@ public async Task IChatClient_GetResponseAsync_SendsUnknownMimeType_SkipsContent { byte[] data = [1, 2, 3]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - // Unknown MIME type content should not be in the request - // since it doesn't match any known format - return CreateResponse("OK"); - } - }; + // Unknown MIME type content should not be in the request + // since it doesn't match any known format + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -1459,38 +1384,35 @@ public async Task IChatClient_GetResponseAsync_TextReasoningContent() string reasoningText = "Let me think step by step..."; string signature = "sig123"; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - ReasoningContent = new ReasoningContentBlock + Role = ConversationRole.Assistant, + Content = + [ + new() { + ReasoningContent = new ReasoningContentBlock + { + ReasoningText = new ReasoningTextBlock { - ReasoningText = new ReasoningTextBlock - { - Text = reasoningText, - Signature = signature - } + Text = reasoningText, + Signature = signature } } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; - return response; - } - }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think step by step about this problem.")]; @@ -1514,23 +1436,20 @@ public async Task IChatClient_GetResponseAsync_SendsTextReasoningContent() string reasoningText = "I reasoned about this"; string signature = "sig456"; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Equal(2, request.Messages.Count); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); - Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); - Assert.Single(request.Messages[1].Content); + Assert.Equal(2, request.Messages.Count); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); + Assert.Single(request.Messages[1].Content); - var reasoningBlock = request.Messages[1].Content[0]; - Assert.NotNull(reasoningBlock.ReasoningContent); - Assert.Equal(reasoningText, reasoningBlock.ReasoningContent.ReasoningText.Text); - Assert.Equal(signature, reasoningBlock.ReasoningContent.ReasoningText.Signature); + var reasoningBlock = request.Messages[1].Content[0]; + Assert.NotNull(reasoningBlock.ReasoningContent); + Assert.Equal(reasoningText, reasoningBlock.ReasoningContent.ReasoningText.Text); + Assert.Equal(signature, reasoningBlock.ReasoningContent.ReasoningText.Signature); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -1550,18 +1469,15 @@ public async Task IChatClient_GetResponseAsync_TextReasoningContent_WithRedacted { byte[] redactedData = [1, 2, 3, 4]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var reasoningBlock = request.Messages[0].Content[0]; - Assert.NotNull(reasoningBlock.ReasoningContent); - Assert.NotNull(reasoningBlock.ReasoningContent.RedactedContent); - Assert.True(reasoningBlock.ReasoningContent.RedactedContent.ToArray().SequenceEqual(redactedData)); + var reasoningBlock = request.Messages[0].Content[0]; + Assert.NotNull(reasoningBlock.ReasoningContent); + Assert.NotNull(reasoningBlock.ReasoningContent.RedactedContent); + Assert.True(reasoningBlock.ReasoningContent.RedactedContent.ToArray().SequenceEqual(redactedData)); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -1586,34 +1502,31 @@ public async Task IChatClient_GetResponseAsync_ReceivesReasoningContent_WithReda { byte[] redactedData = [5, 6, 7]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - ReasoningContent = new ReasoningContentBlock - { - ReasoningText = new ReasoningTextBlock { Text = "Thinking...", Signature = "sig" }, - RedactedContent = new MemoryStream(redactedData) - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + ReasoningContent = new ReasoningContentBlock + { + ReasoningText = new ReasoningTextBlock { Text = "Thinking...", Signature = "sig" }, + RedactedContent = new MemoryStream(redactedData) } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Think")]); @@ -1631,55 +1544,52 @@ public async Task IChatClient_GetResponseAsync_ReceivesReasoningContent_WithReda [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithCitationMetadata() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - CitationsContent = new CitationsContentBlock - { - Content = - [ - new() { Text = "This is cited content." } - ], - Citations = - [ - new() { - Title = "Example Source", - Source = "https://example.com", - Location = new CitationLocation + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "This is cited content." } + ], + Citations = + [ + new() { + Title = "Example Source", + Source = "https://example.com", + Location = new CitationLocation + { + Web = new WebLocation { - Web = new WebLocation - { - Url = "https://example.com" - } - }, - SourceContent = - [ - new() { Text = "Source snippet" } - ] - } - ] - } + Url = "https://example.com" + } + }, + SourceContent = + [ + new() { Text = "Source snippet" } + ] + } + ] } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; - return response; - } - }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Cite your sources")]; @@ -1704,43 +1614,40 @@ public async Task IChatClient_GetResponseAsync_WithCitationMetadata() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithCitation_NoSourceContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - CitationsContent = new CitationsContentBlock - { - Content = - [ - new() { Text = "Cited text." } - ], - Citations = - [ - new() { - Title = "My Source", - Source = "fallback-source" - } - ] - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "Cited text." } + ], + Citations = + [ + new() { + Title = "My Source", + Source = "fallback-source" + } + ] } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1755,20 +1662,17 @@ public async Task IChatClient_GetResponseAsync_WithCitation_NoSourceContent() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithSystemInstructions() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.System); - Assert.Single(request.System); - Assert.Equal("You are a helpful assistant.", request.System[0].Text); + Assert.NotNull(request.System); + Assert.Single(request.System); + Assert.Equal("You are a helpful assistant.", request.System[0].Text); - Assert.Single(request.Messages); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); - return CreateResponse("I'm here to help!"); - } - }; + return CreateResponse("I'm here to help!"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -1788,17 +1692,14 @@ public async Task IChatClient_GetResponseAsync_WithSystemInstructions() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithInstructions_InOptions() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.System); - Assert.Single(request.System); - Assert.Equal("Be concise.", request.System[0].Text); + Assert.NotNull(request.System); + Assert.Single(request.System); + Assert.Equal("Be concise.", request.System[0].Text); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Hello")]; @@ -1812,22 +1713,19 @@ public async Task IChatClient_GetResponseAsync_WithInstructions_InOptions() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithChatOptions() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Equal("custom-model", request.ModelId); + Assert.Equal("custom-model", request.ModelId); - Assert.NotNull(request.InferenceConfig); - Assert.Equal(0.7f, request.InferenceConfig.Temperature); - Assert.Equal(0.9f, request.InferenceConfig.TopP); - Assert.Equal(100, request.InferenceConfig.MaxTokens); - Assert.NotNull(request.InferenceConfig.StopSequences); - Assert.Contains("STOP", request.InferenceConfig.StopSequences); + Assert.NotNull(request.InferenceConfig); + Assert.Equal(0.7f, request.InferenceConfig.Temperature); + Assert.Equal(0.9f, request.InferenceConfig.TopP); + Assert.Equal(100, request.InferenceConfig.MaxTokens); + Assert.NotNull(request.InferenceConfig.StopSequences); + Assert.Contains("STOP", request.InferenceConfig.StopSequences); - return CreateResponse("Response with options applied."); - } - }; + return CreateResponse("Response with options applied."); + }); IChatClient chatClient = mock.AsIChatClient("default-model"); ChatMessage[] messages = [new(ChatRole.User, "Test message")]; @@ -1864,15 +1762,12 @@ public async Task IChatClient_GetResponseAsync_WithFinishReasons() foreach (var (stopReason, expectedFinishReason) in finishReasons) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var response = CreateResponse("Test"); - response.StopReason = stopReason; - return response; - } - }; + var response = CreateResponse("Test"); + response.StopReason = stopReason; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Test")]; @@ -1886,19 +1781,16 @@ public async Task IChatClient_GetResponseAsync_WithFinishReasons() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithAdditionalModelResponseFields() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + var response = CreateResponse("Test"); + response.AdditionalModelResponseFields = new Document(new Dictionary { - var response = CreateResponse("Test"); - response.AdditionalModelResponseFields = new Document(new Dictionary - { - ["custom_field"] = "custom_value", - ["number_field"] = 123 - }); - return response; - } - }; + ["custom_field"] = "custom_value", + ["number_field"] = 123 + }); + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -1916,17 +1808,14 @@ public async Task IChatClient_GetResponseAsync_SystemMessageWithCachePoint() { CachePointBlock cachePoint = new() { Type = CachePointType.Default }; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - // Should have system messages including cache point - Assert.True(request.System.Count >= 2); - Assert.NotNull(request.System.Last().CachePoint); + // Should have system messages including cache point + Assert.True(request.System.Count >= 2); + Assert.NotNull(request.System.Last().CachePoint); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -1948,17 +1837,14 @@ public async Task IChatClient_GetResponseAsync_SystemMessageWithCachePoint() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_ToolWithoutProperties() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.ToolConfig); - Assert.Single(request.ToolConfig.Tools); - Assert.Equal("simple_tool", request.ToolConfig.Tools[0].ToolSpec.Name); + Assert.NotNull(request.ToolConfig); + Assert.Single(request.ToolConfig.Tools); + Assert.Equal("simple_tool", request.ToolConfig.Tools[0].ToolSpec.Name); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -1978,18 +1864,15 @@ public async Task IChatClient_GetResponseAsync_ToolWithoutProperties() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithRawRepresentationFactory() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - // Verify the custom model ID was used - Assert.Equal("custom-model", request.ModelId); + // Verify the custom model ID was used + Assert.Equal("custom-model", request.ModelId); - // Return empty stream - MemoryStream stream = new(); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + // Return empty stream + MemoryStream stream = new(); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("default-model"); @@ -2070,34 +1953,31 @@ public async Task IChatClient_GetResponseAsync_AllContentTypesHaveRawRepresentat } }; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - textBlock, - imageBlock, - videoBlock, - docBlock, - toolUseBlock, - citationBlock, - reasoningBlock - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + Role = ConversationRole.Assistant, + Content = + [ + textBlock, + imageBlock, + videoBlock, + docBlock, + toolUseBlock, + citationBlock, + reasoningBlock + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2121,27 +2001,24 @@ public async Task IChatClient_GetResponseAsync_RawRepresentation_Message() { Message rawMessage = null; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new(); + rawMessage = new Message { - ConverseResponse response = new(); - rawMessage = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { Text = "Test" } - ] - }; - response.Output = new ConverseOutput - { - Message = rawMessage - }; - response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; - return response; - } - }; + Role = ConversationRole.Assistant, + Content = + [ + new() { Text = "Test" } + ] + }; + response.Output = new ConverseOutput + { + Message = rawMessage + }; + response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2156,28 +2033,25 @@ public async Task IChatClient_GetResponseAsync_RawRepresentation_Response() { ConverseResponse rawResponse = null; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + rawResponse = new ConverseResponse { - rawResponse = new ConverseResponse + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { Text = "Test" } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return rawResponse; - } - }; + Role = ConversationRole.Assistant, + Content = + [ + new() { Text = "Test" } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return rawResponse; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2192,17 +2066,14 @@ public async Task IChatClient_GetResponseAsync_UsesRawRepresentation_WhenSending { ContentBlock originalContentBlock = new() { Text = "Original text from raw" }; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Single(request.Messages[0].Content); - Assert.Same(originalContentBlock, request.Messages[0].Content[0]); + Assert.Single(request.Messages); + Assert.Single(request.Messages[0].Content); + Assert.Same(originalContentBlock, request.Messages[0].Content[0]); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -2221,17 +2092,14 @@ public async Task IChatClient_GetResponseAsync_UsesRawRepresentation_WhenSending [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_HandlesWhitespaceOnlyText() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Single(request.Messages[0].Content); - Assert.Equal("\u200b", request.Messages[0].Content[0].Text); + Assert.Single(request.Messages); + Assert.Single(request.Messages[0].Content); + Assert.Equal("\u200b", request.Messages[0].Content[0].Text); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, " ")]; @@ -2244,20 +2112,17 @@ public async Task IChatClient_GetResponseAsync_HandlesWhitespaceOnlyText() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_TrimsAssistantText() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Equal(2, request.Messages.Count); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); - Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); + Assert.Equal(2, request.Messages.Count); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Equal(ConversationRole.Assistant, request.Messages[1].Role); - Assert.Single(request.Messages[1].Content); - Assert.Equal("Trimmed text", request.Messages[1].Content[0].Text); + Assert.Single(request.Messages[1].Content); + Assert.Equal("Trimmed text", request.Messages[1].Content[0].Text); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -2276,17 +2141,14 @@ public async Task IChatClient_GetResponseAsync_SkipsEmptyAssistantText() { // When an assistant message contains only whitespace, it should be skipped entirely // because sending an assistant message with empty content would fail the service. - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - // Only the user message should be sent; the whitespace-only assistant message is dropped - Assert.Single(request.Messages); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); + // Only the user message should be sent; the whitespace-only assistant message is dropped + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -2305,19 +2167,16 @@ public async Task IChatClient_GetResponseAsync_CachePointBlock_InMessages() { CachePointBlock cachePoint = new() { Type = CachePointType.Default }; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(2, request.Messages[0].Content.Count); - Assert.Equal("Text before cache", request.Messages[0].Content[0].Text); - Assert.NotNull(request.Messages[0].Content[1].CachePoint); - Assert.Equal(CachePointType.Default, request.Messages[0].Content[1].CachePoint.Type); + Assert.Single(request.Messages); + Assert.Equal(2, request.Messages[0].Content.Count); + Assert.Equal("Text before cache", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].CachePoint); + Assert.Equal(CachePointType.Default, request.Messages[0].Content[1].CachePoint.Type); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -2339,19 +2198,16 @@ public async Task IChatClient_GetResponseAsync_CachePointBlock_InSystemMessages( { CachePointBlock cachePoint = new() { Type = CachePointType.Default }; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.System); - Assert.Equal(2, request.System.Count); - Assert.Equal("System instruction", request.System[0].Text); - Assert.NotNull(request.System[1].CachePoint); - Assert.Equal(CachePointType.Default, request.System[1].CachePoint.Type); + Assert.NotNull(request.System); + Assert.Equal(2, request.System.Count); + Assert.Equal("System instruction", request.System[0].Text); + Assert.NotNull(request.System[1].CachePoint); + Assert.Equal(CachePointType.Default, request.System[1].CachePoint.Type); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -2373,19 +2229,16 @@ public async Task IChatClient_GetResponseAsync_CachePointBlock_InContent() { CachePointBlock cachePoint = new() { Type = CachePointType.Default }; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(3, request.Messages[0].Content.Count); - Assert.Equal("Text 1", request.Messages[0].Content[0].Text); - Assert.NotNull(request.Messages[0].Content[1].CachePoint); - Assert.Equal("Text 2", request.Messages[0].Content[2].Text); - - return CreateResponse("OK"); - } - }; + Assert.Single(request.Messages); + Assert.Equal(3, request.Messages[0].Content.Count); + Assert.Equal("Text 1", request.Messages[0].Content[0].Text); + Assert.NotNull(request.Messages[0].Content[1].CachePoint); + Assert.Equal("Text 2", request.Messages[0].Content[2].Text); + + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -2411,18 +2264,15 @@ public async Task IChatClient_GetResponseAsync_WithRawRepresentationFactory() { ConverseRequest factoryRequest = null; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Same(factoryRequest, request); - Assert.Equal("factory-model", request.ModelId); - Assert.NotNull(request.InferenceConfig); - Assert.Equal(0.5f, request.InferenceConfig.Temperature); + Assert.Same(factoryRequest, request); + Assert.Equal("factory-model", request.ModelId); + Assert.NotNull(request.InferenceConfig); + Assert.Equal(0.5f, request.InferenceConfig.Temperature); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("default-model"); @@ -2447,42 +2297,39 @@ public async Task IChatClient_GetResponseAsync_WithRawRepresentationFactory() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_MultipleContentInCitations() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - CitationsContent = new CitationsContentBlock - { - Content = - [ - new() { Text = "Content 1" }, - new() { Text = "Content 2" } - ], - Citations = - [ - new() { Title = "Citation 1" }, - new() { Title = "Citation 2" } - ] - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "Content 1" }, + new() { Text = "Content 2" } + ], + Citations = + [ + new() { Title = "Citation 1" }, + new() { Title = "Citation 2" } + ] } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2505,42 +2352,39 @@ public async Task IChatClient_GetResponseAsync_MultipleContentInCitations() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_MismatchedCitationCounts_UsesMinimum() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new() { - ConverseResponse response = new() + Output = new ConverseOutput { - Output = new ConverseOutput + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - CitationsContent = new CitationsContentBlock - { - Content = - [ - new() { Text = "Content 1" }, - new() { Text = "Content 2" }, - new() { Text = "Content 3" } - ], - Citations = - [ - new() { Title = "Citation 1" } - ] - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + CitationsContent = new CitationsContentBlock + { + Content = + [ + new() { Text = "Content 1" }, + new() { Text = "Content 2" }, + new() { Text = "Content 3" } + ], + Citations = + [ + new() { Title = "Citation 1" } + ] } - ] - } - }, - Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } - }; - return response; - } - }; + } + ] + } + }, + Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 } + }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2553,17 +2397,14 @@ public async Task IChatClient_GetResponseAsync_MismatchedCitationCounts_UsesMini [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_SendsFunctionCall_WithComplexArguments() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - // Verify the tool definition was created correctly - var toolSpec = request.ToolConfig?.Tools?[0]?.ToolSpec; - Assert.NotNull(toolSpec); + // Verify the tool definition was created correctly + var toolSpec = request.ToolConfig?.Tools?[0]?.ToolSpec; + Assert.NotNull(toolSpec); - return CreateResponse("OK"); - } - }; + return CreateResponse("OK"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -2586,26 +2427,23 @@ public async Task IChatClient_GetResponseAsync_SendsFunctionCall_WithComplexArgu [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_DocumentWithArrayValues() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + var response = CreateResponse("OK"); + response.Output.Message.Content.Add(new ContentBlock { - var response = CreateResponse("OK"); - response.Output.Message.Content.Add(new ContentBlock + ToolUse = new ToolUseBlock { - ToolUse = new ToolUseBlock + ToolUseId = "tool_arr", + Name = "array_func", + Input = new Document(new Dictionary { - ToolUseId = "tool_arr", - Name = "array_func", - Input = new Document(new Dictionary - { - ["items"] = new Document(new List { "a", "b", "c" }) - }) - } - }); - return response; - } - }; + ["items"] = new Document(new List { "a", "b", "c" }) + }) + } + }); + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2620,21 +2458,18 @@ public async Task IChatClient_GetResponseAsync_DocumentWithArrayValues() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_ReceivesNestedDictionary() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + var response = CreateResponse("OK"); + response.AdditionalModelResponseFields = new Document(new Dictionary { - var response = CreateResponse("OK"); - response.AdditionalModelResponseFields = new Document(new Dictionary + ["outer"] = new Document(new Dictionary { - ["outer"] = new Document(new Dictionary - { - ["inner"] = "nested_value" - }) - }); - return response; - } - }; + ["inner"] = "nested_value" + }) + }); + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2647,39 +2482,36 @@ public async Task IChatClient_GetResponseAsync_ReceivesNestedDictionary() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionCallContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => + ConverseResponse response = new(); + Document document = new(new Dictionary { - ConverseResponse response = new(); - Document document = new(new Dictionary - { - ["location"] = "San Francisco" - }); + ["location"] = "San Francisco" + }); - response.Output = new ConverseOutput + response.Output = new ConverseOutput + { + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - ToolUse = new ToolUseBlock - { - ToolUseId = "tool_123", - Name = "get_weather", - Input = document - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + ToolUse = new ToolUseBlock + { + ToolUseId = "tool_123", + Name = "get_weather", + Input = document } - ] - } - }; - response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; + } + ] + } + }; + response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; - return response; - } - }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "What's the weather in San Francisco?")]; @@ -2706,36 +2538,33 @@ public async Task IChatClient_GetResponseAsync_FunctionCallContent_WithDeeplyNes { // Note: JSON serialization has a default max depth of 64. Documents nested deeper than that // will fail during conversion. This test uses depth 50 which is within limits. - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - ConverseResponse response = new(); - var document = CreateDeeplyNestedDocument(50); + ConverseResponse response = new(); + var document = CreateDeeplyNestedDocument(50); - response.Output = new ConverseOutput + response.Output = new ConverseOutput + { + Message = new Message { - Message = new Message - { - Role = ConversationRole.Assistant, - Content = - [ - new() { - ToolUse = new ToolUseBlock - { - ToolUseId = "tool_nested", - Name = "nested_tool", - Input = document - } + Role = ConversationRole.Assistant, + Content = + [ + new() { + ToolUse = new ToolUseBlock + { + ToolUseId = "tool_nested", + Name = "nested_tool", + Input = document } - ] - } - }; - response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; + } + ] + } + }; + response.Usage = new TokenUsage { InputTokens = 10, OutputTokens = 5, TotalTokens = 15 }; - return response; - } - }; + return response; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]); @@ -2752,23 +2581,20 @@ public async Task IChatClient_GetResponseAsync_FunctionCallContent_WithDeeplyNes [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - Assert.Equal(ConversationRole.User, request.Messages[0].Role); - Assert.Single(request.Messages[0].Content); - Assert.NotNull(request.Messages[0].Content[0].ToolResult); - Assert.Equal("call_123", request.Messages[0].Content[0].ToolResult.ToolUseId); + Assert.Single(request.Messages); + Assert.Equal(ConversationRole.User, request.Messages[0].Role); + Assert.Single(request.Messages[0].Content); + Assert.NotNull(request.Messages[0].Content[0].ToolResult); + Assert.Equal("call_123", request.Messages[0].Content[0].ToolResult.ToolUseId); - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult.Content); - Assert.Single(toolResult.Content); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult.Content); + Assert.Single(toolResult.Content); - return CreateResponse("Based on the weather data, it's sunny."); - } - }; + return CreateResponse("Based on the weather data, it's sunny."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -2789,19 +2615,16 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithString() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.Equal("call_str", toolResult.ToolUseId); - Assert.Single(toolResult.Content); - Assert.Equal("Result text", toolResult.Content[0].Text); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_str", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + Assert.Equal("Result text", toolResult.Content[0].Text); - return CreateResponse("Got your result"); - } - }; + return CreateResponse("Got your result"); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -2822,22 +2645,19 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithDataCon { byte[] imageData = [0x89, 0x50, 0x4E, 0x47]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.Single(request.Messages); - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.Equal("call_456", toolResult.ToolUseId); - Assert.Single(toolResult.Content); - Assert.NotNull(toolResult.Content[0].Image); - Assert.Equal(ImageFormat.Png, toolResult.Content[0].Image.Format); - Assert.True(toolResult.Content[0].Image.Source.Bytes.ToArray().SequenceEqual(imageData)); - - return CreateResponse("Image processed."); - } - }; + Assert.Single(request.Messages); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_456", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + Assert.NotNull(toolResult.Content[0].Image); + Assert.Equal(ImageFormat.Png, toolResult.Content[0].Image.Format); + Assert.True(toolResult.Content[0].Image.Source.Bytes.ToArray().SequenceEqual(imageData)); + + return CreateResponse("Image processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -2857,22 +2677,19 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithDataCon [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithTextContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.Equal("call_text", toolResult.ToolUseId); - Assert.NotNull(toolResult.Content); - Assert.Single(toolResult.Content); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_text", toolResult.ToolUseId); + Assert.NotNull(toolResult.Content); + Assert.Single(toolResult.Content); - // TextContent should be converted to ToolResultContentBlock with Text property - Assert.Equal("Simple text result", toolResult.Content[0].Text); + // TextContent should be converted to ToolResultContentBlock with Text property + Assert.Equal("Simple text result", toolResult.Content[0].Text); - return CreateResponse("Text result processed."); - } - }; + return CreateResponse("Text result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -2894,24 +2711,21 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithMultipl { byte[] data = [1, 2, 3]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.Equal("call_multi", toolResult.ToolUseId); - Assert.Equal(2, toolResult.Content.Count); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_multi", toolResult.ToolUseId); + Assert.Equal(2, toolResult.Content.Count); - Assert.NotNull(toolResult.Content[0].Image); - Assert.True(toolResult.Content[0].Image.Source.Bytes.ToArray().SequenceEqual(data)); + Assert.NotNull(toolResult.Content[0].Image); + Assert.True(toolResult.Content[0].Image.Source.Bytes.ToArray().SequenceEqual(data)); - Assert.NotNull(toolResult.Content[1].Document); - Assert.Equal(DocumentFormat.Pdf, toolResult.Content[1].Document.Format); + Assert.NotNull(toolResult.Content[1].Document); + Assert.Equal(DocumentFormat.Pdf, toolResult.Content[1].Document.Format); - return CreateResponse("Multi-content processed."); - } - }; + return CreateResponse("Multi-content processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -2938,28 +2752,25 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithMultipl [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithTools() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.ToolConfig); - Assert.NotNull(request.ToolConfig.Tools); - Assert.Single(request.ToolConfig.Tools); - - var tool = request.ToolConfig.Tools[0]; - Assert.NotNull(tool.ToolSpec); - Assert.Equal("get_weather", tool.ToolSpec.Name); - Assert.Equal("Gets weather information", tool.ToolSpec.Description); - Assert.NotNull(tool.ToolSpec.InputSchema); - - var json = tool.ToolSpec.InputSchema.Json; - Assert.True(json.IsDictionary()); - var dict = json.AsDictionary(); - Assert.Equal("object", dict["type"].AsString()); - - return CreateResponse("I can use tools to help you."); - } - }; + Assert.NotNull(request.ToolConfig); + Assert.NotNull(request.ToolConfig.Tools); + Assert.Single(request.ToolConfig.Tools); + + var tool = request.ToolConfig.Tools[0]; + Assert.NotNull(tool.ToolSpec); + Assert.Equal("get_weather", tool.ToolSpec.Name); + Assert.Equal("Gets weather information", tool.ToolSpec.Description); + Assert.NotNull(tool.ToolSpec.InputSchema); + + var json = tool.ToolSpec.InputSchema.Json; + Assert.True(json.IsDictionary()); + var dict = json.AsDictionary(); + Assert.Equal("object", dict["type"].AsString()); + + return CreateResponse("I can use tools to help you."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "What tools do you have?")]; @@ -2983,18 +2794,15 @@ public async Task IChatClient_GetResponseAsync_WithTools() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithToolMode_RequireSpecific() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.ToolConfig); - Assert.NotNull(request.ToolConfig.ToolChoice); - Assert.NotNull(request.ToolConfig.ToolChoice.Tool); - Assert.Equal("get_weather", request.ToolConfig.ToolChoice.Tool.Name); + Assert.NotNull(request.ToolConfig); + Assert.NotNull(request.ToolConfig.ToolChoice); + Assert.NotNull(request.ToolConfig.ToolChoice.Tool); + Assert.Equal("get_weather", request.ToolConfig.ToolChoice.Tool.Name); - return CreateResponse("Required mode with specific function."); - } - }; + return CreateResponse("Required mode with specific function."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Test required tool mode")]; @@ -3016,17 +2824,14 @@ public async Task IChatClient_GetResponseAsync_WithToolMode_RequireSpecific() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_WithToolMode_RequireAny() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.NotNull(request.ToolConfig); - Assert.NotNull(request.ToolConfig.ToolChoice); - Assert.NotNull(request.ToolConfig.ToolChoice.Any); + Assert.NotNull(request.ToolConfig); + Assert.NotNull(request.ToolConfig.ToolChoice); + Assert.NotNull(request.ToolConfig.ToolChoice.Any); - return CreateResponse("Required mode any function."); - } - }; + return CreateResponse("Required mode any function."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Test")]; @@ -3061,19 +2866,16 @@ private static Document CreateDeeplyNestedDocument(int depth) [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithReasoningContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.Equal("call_reason", toolResult.ToolUseId); - Assert.Single(toolResult.Content); - Assert.NotNull(toolResult.Content[0].Text); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.Equal("call_reason", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + Assert.NotNull(toolResult.Content[0].Text); - return CreateResponse("Reasoning result processed."); - } - }; + return CreateResponse("Reasoning result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -3094,18 +2896,15 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithVideoCo { byte[] videoData = [1, 2, 3, 4, 5]; - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.NotNull(toolResult.Content[0].Video); - Assert.True(toolResult.Content[0].Video.Source.Bytes.ToArray().SequenceEqual(videoData)); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.NotNull(toolResult.Content[0].Video); + Assert.True(toolResult.Content[0].Video.Source.Bytes.ToArray().SequenceEqual(videoData)); - return CreateResponse("Video result processed."); - } - }; + return CreateResponse("Video result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -3124,20 +2923,17 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithVideoCo [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithIntResult() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.True(toolResult.Content[0].Json.IsDictionary()); - var dict = toolResult.Content[0].Json.AsDictionary(); - // The value is stored as double since JsonSerializer uses double for numbers - Assert.True(dict["result"].IsDouble() || dict["result"].IsInt()); - - return CreateResponse("Int result processed."); - } - }; + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + var dict = toolResult.Content[0].Json.AsDictionary(); + // The value is stored as double since JsonSerializer uses double for numbers + Assert.True(dict["result"].IsDouble() || dict["result"].IsInt()); + + return CreateResponse("Int result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -3156,19 +2952,16 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithIntResu [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithBoolResult() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.True(toolResult.Content[0].Json.IsDictionary()); - var dict = toolResult.Content[0].Json.AsDictionary(); - Assert.True(dict["result"].AsBool()); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + var dict = toolResult.Content[0].Json.AsDictionary(); + Assert.True(dict["result"].AsBool()); - return CreateResponse("Bool result processed."); - } - }; + return CreateResponse("Bool result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -3187,17 +2980,14 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithBoolRes [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithNullResult() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.True(toolResult.Content[0].Json.IsDictionary()); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); - return CreateResponse("Null result processed."); - } - }; + return CreateResponse("Null result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -3216,17 +3006,14 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithNullRes [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithJsonElementResult() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.True(toolResult.Content[0].Json.IsDictionary()); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); - return CreateResponse("JsonElement result processed."); - } - }; + return CreateResponse("JsonElement result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3247,19 +3034,16 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithJsonEle [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithLongResult() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.True(toolResult.Content[0].Json.IsDictionary()); - var dict = toolResult.Content[0].Json.AsDictionary(); - Assert.True(dict["result"].IsLong() || dict["result"].IsDouble()); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); + var dict = toolResult.Content[0].Json.AsDictionary(); + Assert.True(dict["result"].IsLong() || dict["result"].IsDouble()); - return CreateResponse("Long result processed."); - } - }; + return CreateResponse("Long result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -3280,17 +3064,14 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithLongRes [InlineData(2.718281828)] public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithFloatingPointResult(double value) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var toolResult = request.Messages[0].Content[0].ToolResult; - Assert.NotNull(toolResult); - Assert.True(toolResult.Content[0].Json.IsDictionary()); + var toolResult = request.Messages[0].Content[0].ToolResult; + Assert.NotNull(toolResult); + Assert.True(toolResult.Content[0].Json.IsDictionary()); - return CreateResponse("Floating point result processed."); - } - }; + return CreateResponse("Floating point result processed."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = @@ -3309,7 +3090,7 @@ public async Task IChatClient_GetResponseAsync_FunctionResultContent_WithFloatin [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_NullMessages_Throws() { - MockBedrockRuntime mock = new(); + IAmazonBedrockRuntime mock = CreateMock(); IChatClient chatClient = mock.AsIChatClient("claude"); var enumerator = chatClient.GetStreamingResponseAsync(null).GetAsyncEnumerator(); @@ -3320,23 +3101,20 @@ public async Task IChatClient_GetStreamingResponseAsync_NullMessages_Throws() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_BasicTextStreaming() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Hello"), - CreateContentBlockDeltaEvent(0, " world"), - CreateContentBlockDeltaEvent(0, "!"), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Hello"), + CreateContentBlockDeltaEvent(0, " world"), + CreateContentBlockDeltaEvent(0, "!"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Say hello")]; @@ -3374,21 +3152,18 @@ public async Task IChatClient_GetStreamingResponseAsync_BasicTextStreaming() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithUsageMetadata() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Test"), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(100, 50) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Test"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(100, 50) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3411,22 +3186,19 @@ public async Task IChatClient_GetStreamingResponseAsync_WithUsageMetadata() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithToolUse() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEventWithToolUse(0, "tool_123", "get_weather"), - CreateContentBlockDeltaEventWithToolUse(0, "{\"location\":"), - CreateContentBlockDeltaEventWithToolUse(0, "\"Seattle\"}"), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("tool_use"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEventWithToolUse(0, "tool_123", "get_weather"), + CreateContentBlockDeltaEventWithToolUse(0, "{\"location\":"), + CreateContentBlockDeltaEventWithToolUse(0, "\"Seattle\"}"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("tool_use"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3454,21 +3226,18 @@ public async Task IChatClient_GetStreamingResponseAsync_WithToolUse() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithCitation() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEventWithCitation(0, "Cited text", "Source Title", "Source snippet"), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEventWithCitation(0, "Cited text", "Source Title", "Source snippet"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3492,21 +3261,18 @@ public async Task IChatClient_GetStreamingResponseAsync_WithCitation() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithReasoningContent() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEventWithReasoning(0, "Thinking...", "sig123", null), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEventWithReasoning(0, "Thinking...", "sig123", null), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3529,21 +3295,18 @@ public async Task IChatClient_GetStreamingResponseAsync_WithReasoningContent() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithReasoningContentAndRedacted() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEventWithReasoning(0, "Thinking...", null, "cmVkYWN0ZWQ="), // base64 "redacted" - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEventWithReasoning(0, "Thinking...", null, "cmVkYWN0ZWQ="), // base64 "redacted" + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3565,21 +3328,18 @@ public async Task IChatClient_GetStreamingResponseAsync_WithReasoningContentAndR [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithInvalidToolJson() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEventWithToolUse(0, "tool_err", "bad_tool"), - CreateContentBlockDeltaEventWithToolUse(0, "not valid json {{{"), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("tool_use"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEventWithToolUse(0, "tool_err", "bad_tool"), + CreateContentBlockDeltaEventWithToolUse(0, "not valid json {{{"), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("tool_use"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3601,21 +3361,18 @@ public async Task IChatClient_GetStreamingResponseAsync_WithInvalidToolJson() [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_WithAdditionalResponseFields() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Test"), - CreateContentBlockStopEvent(0), - CreateMessageStopEventWithAdditionalFields("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Test"), + CreateContentBlockStopEvent(0), + CreateMessageStopEventWithAdditionalFields("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); @@ -3638,21 +3395,18 @@ public async Task IChatClient_GetStreamingResponseAsync_WithAdditionalResponseFi [InlineData(ReasoningEffort.ExtraHigh, 32768, 131072)] public async Task IChatClient_GetResponseAsync_ReasoningEffort_SetsThinkingConfig_NoMaxTokens(ReasoningEffort effort, int expectedBudget, int expectedMaxTokens) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsDictionary()); - var fields = request.AdditionalModelRequestFields.AsDictionary(); - Assert.True(fields.ContainsKey("thinking")); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); - Assert.Equal(expectedMaxTokens, request.InferenceConfig.MaxTokens); - - return CreateResponse("Thinking response."); - } - }; + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + Assert.Equal(expectedMaxTokens, request.InferenceConfig.MaxTokens); + + return CreateResponse("Thinking response."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; @@ -3675,17 +3429,14 @@ public async Task IChatClient_GetResponseAsync_ReasoningEffort_SetsThinkingConfi [InlineData(ReasoningEffort.ExtraHigh, 1025, 1024)] // clamped to maxTokens - 1 public async Task IChatClient_GetResponseAsync_ReasoningEffort_SetsThinkingConfig_WithMaxTokens(ReasoningEffort effort, int maxTokens, int expectedBudget) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var thinking = request.AdditionalModelRequestFields.AsDictionary()["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + var thinking = request.AdditionalModelRequestFields.AsDictionary()["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); - return CreateResponse("Thinking response."); - } - }; + return CreateResponse("Thinking response."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; @@ -3703,14 +3454,11 @@ public async Task IChatClient_GetResponseAsync_ReasoningEffort_SetsThinkingConfi [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_ReasoningEffortNone_DoesNotSetThinkingConfig() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsNull()); - return CreateResponse("No thinking."); - } - }; + Assert.True(request.AdditionalModelRequestFields.IsNull()); + return CreateResponse("No thinking."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Hello")]; @@ -3727,14 +3475,11 @@ public async Task IChatClient_GetResponseAsync_ReasoningEffortNone_DoesNotSetThi [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_NoReasoning_DoesNotSetThinkingConfig() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsNull()); - return CreateResponse("No thinking."); - } - }; + Assert.True(request.AdditionalModelRequestFields.IsNull()); + return CreateResponse("No thinking."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Hello")]; @@ -3748,18 +3493,15 @@ public async Task IChatClient_GetResponseAsync_NoReasoning_DoesNotSetThinkingCon [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_ReasoningDoesNotOverrideExistingThinkingConfig() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var fields = request.AdditionalModelRequestFields.AsDictionary(); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - Assert.Equal(99999, thinking["budget_tokens"].AsInt()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(99999, thinking["budget_tokens"].AsInt()); - return CreateResponse("Custom thinking."); - } - }; + return CreateResponse("Custom thinking."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think")]; @@ -3789,19 +3531,16 @@ public async Task IChatClient_GetResponseAsync_ReasoningDoesNotOverrideExistingT [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetResponseAsync_ReasoningMergesWithExistingAdditionalFields() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - var fields = request.AdditionalModelRequestFields.AsDictionary(); - Assert.Equal("bar", fields["foo"].AsString()); - Assert.True(fields.ContainsKey("thinking")); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.Equal("bar", fields["foo"].AsString()); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); - return CreateResponse("Merged fields."); - } - }; + return CreateResponse("Merged fields."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think")]; @@ -3829,19 +3568,16 @@ public async Task IChatClient_GetResponseAsync_ReasoningMergesWithExistingAdditi [InlineData(ReasoningOutput.Full)] public async Task IChatClient_GetResponseAsync_ReasoningOutputSummaryOrFull_EnablesThinking(ReasoningOutput output) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseRequest: request => { - OnConverseRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsDictionary()); - var fields = request.AdditionalModelRequestFields.AsDictionary(); - Assert.True(fields.ContainsKey("thinking")); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); - return CreateResponse("Thinking response."); - } - }; + return CreateResponse("Thinking response."); + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; @@ -3862,29 +3598,26 @@ public async Task IChatClient_GetResponseAsync_ReasoningOutputSummaryOrFull_Enab [InlineData(ReasoningEffort.ExtraHigh, 32768, 131072)] public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffort_SetsThinkingConfig_NoMaxTokens(ReasoningEffort effort, int expectedBudget, int expectedMaxTokens) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsDictionary()); - var fields = request.AdditionalModelRequestFields.AsDictionary(); - Assert.True(fields.ContainsKey("thinking")); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); - Assert.Equal(expectedMaxTokens, request.InferenceConfig.MaxTokens); - - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Response."), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + Assert.Equal(expectedMaxTokens, request.InferenceConfig.MaxTokens); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Response."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; @@ -3906,25 +3639,22 @@ public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffort_SetsThin [InlineData(ReasoningEffort.ExtraHigh, 1025, 1024)] // clamped to maxTokens - 1 public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffort_SetsThinkingConfig_WithMaxTokens(ReasoningEffort effort, int maxTokens, int expectedBudget) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var thinking = request.AdditionalModelRequestFields.AsDictionary()["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); - - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Response."), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var thinking = request.AdditionalModelRequestFields.AsDictionary()["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(expectedBudget, thinking["budget_tokens"].AsInt()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Response."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; @@ -3941,23 +3671,20 @@ public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffort_SetsThin [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffortNone_DoesNotSetThinkingConfig() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsNull()); - - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "No thinking."), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + Assert.True(request.AdditionalModelRequestFields.IsNull()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "No thinking."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Hello")]; @@ -3973,23 +3700,20 @@ public async Task IChatClient_GetStreamingResponseAsync_ReasoningEffortNone_Does [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_NoReasoning_DoesNotSetThinkingConfig() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsNull()); - - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "No thinking."), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + Assert.True(request.AdditionalModelRequestFields.IsNull()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "No thinking."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Hello")]; @@ -4002,26 +3726,23 @@ public async Task IChatClient_GetStreamingResponseAsync_NoReasoning_DoesNotSetTh [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_ReasoningDoesNotOverrideExistingThinkingConfig() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var fields = request.AdditionalModelRequestFields.AsDictionary(); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - Assert.Equal(99999, thinking["budget_tokens"].AsInt()); - - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Custom thinking."), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var fields = request.AdditionalModelRequestFields.AsDictionary(); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + Assert.Equal(99999, thinking["budget_tokens"].AsInt()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Custom thinking."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think")]; @@ -4050,27 +3771,24 @@ public async Task IChatClient_GetStreamingResponseAsync_ReasoningDoesNotOverride [Trait("UnitTest", "BedrockRuntime")] public async Task IChatClient_GetStreamingResponseAsync_ReasoningMergesWithExistingAdditionalFields() { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - var fields = request.AdditionalModelRequestFields.AsDictionary(); - Assert.Equal("bar", fields["foo"].AsString()); - Assert.True(fields.ContainsKey("thinking")); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Merged."), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.Equal("bar", fields["foo"].AsString()); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Merged."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think")]; @@ -4097,27 +3815,24 @@ public async Task IChatClient_GetStreamingResponseAsync_ReasoningMergesWithExist [InlineData(ReasoningOutput.Full)] public async Task IChatClient_GetStreamingResponseAsync_ReasoningOutputSummaryOrFull_EnablesThinking(ReasoningOutput output) { - MockBedrockRuntime mock = new() + IAmazonBedrockRuntime mock = CreateMock(onConverseStreamRequest: request => { - OnConverseStreamRequest = request => - { - Assert.True(request.AdditionalModelRequestFields.IsDictionary()); - var fields = request.AdditionalModelRequestFields.AsDictionary(); - Assert.True(fields.ContainsKey("thinking")); - var thinking = fields["thinking"].AsDictionary(); - Assert.Equal("enabled", thinking["type"].AsString()); - - var stream = CreateEventStream( - CreateMessageStartEvent(), - CreateContentBlockStartEvent(0), - CreateContentBlockDeltaEvent(0, "Thinking response."), - CreateContentBlockStopEvent(0), - CreateMessageStopEvent("end_turn"), - CreateMetadataEvent(10, 5) - ); - return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; - } - }; + Assert.True(request.AdditionalModelRequestFields.IsDictionary()); + var fields = request.AdditionalModelRequestFields.AsDictionary(); + Assert.True(fields.ContainsKey("thinking")); + var thinking = fields["thinking"].AsDictionary(); + Assert.Equal("enabled", thinking["type"].AsString()); + + var stream = CreateEventStream( + CreateMessageStartEvent(), + CreateContentBlockStartEvent(0), + CreateContentBlockDeltaEvent(0, "Thinking response."), + CreateContentBlockStopEvent(0), + CreateMessageStopEvent("end_turn"), + CreateMetadataEvent(10, 5) + ); + return new ConverseStreamResponse { Stream = new ConverseStreamOutput(stream) }; + }); IChatClient chatClient = mock.AsIChatClient("claude"); ChatMessage[] messages = [new(ChatRole.User, "Think about this")]; @@ -4210,6 +3925,27 @@ private static byte[] CreateEventMessage(string eventType, byte[] payload) return new EventStreamMessage(headers, payload).ToByteArray(); } + private static IAmazonBedrockRuntime CreateMock( + Func onConverseRequest = null, + Func onConverseStreamRequest = null) + { + var mock = new Mock(); + + if (onConverseRequest != null) + { + mock.Setup(m => m.ConverseAsync(It.IsAny(), It.IsAny())) + .Returns((request, ct) => Task.FromResult(onConverseRequest(request))); + } + + if (onConverseStreamRequest != null) + { + mock.Setup(m => m.ConverseStreamAsync(It.IsAny(), It.IsAny())) + .Returns((request, ct) => Task.FromResult(onConverseStreamRequest(request))); + } + + return mock.Object; + } + private static ConverseResponse CreateResponse(string text) { ConverseResponse response = new() diff --git a/extensions/test/BedrockMEAITests/MockBedrockRuntime.cs b/extensions/test/BedrockMEAITests/MockBedrockRuntime.cs deleted file mode 100644 index 06e470c8c07a..000000000000 --- a/extensions/test/BedrockMEAITests/MockBedrockRuntime.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Amazon.BedrockRuntime.Model; -using Amazon.Runtime; -using Amazon.Runtime.Endpoints; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Amazon.BedrockRuntime; - -internal sealed class MockBedrockRuntime : IAmazonBedrockRuntime -{ - public Func OnConverseRequest { get; set; } - public Func OnConverseStreamRequest { get; set; } - - public IClientConfig Config => throw new NotImplementedException(); - public IBedrockRuntimePaginatorFactory Paginators => throw new NotImplementedException(); - - public Task ConverseAsync(ConverseRequest request, CancellationToken cancellationToken = default) - { - if (OnConverseRequest is null) - { - throw new NotSupportedException($"{nameof(ConverseAsync)} was invoked but no {nameof(OnConverseRequest)} was provided."); - } - - return Task.FromResult(OnConverseRequest(request)); - } - - public Task ConverseStreamAsync(ConverseStreamRequest request, CancellationToken cancellationToken = default) - { - if (OnConverseStreamRequest is null) - { - throw new NotSupportedException($"{nameof(ConverseStreamAsync)} was invoked but no {nameof(OnConverseStreamRequest)} was provided."); - } - - return Task.FromResult(OnConverseStreamRequest(request)); - } - - public void Dispose() { } - - public ConverseResponse Converse(ConverseRequest request) => throw new NotImplementedException(); - public ConverseStreamResponse ConverseStream(ConverseStreamRequest request) => throw new NotImplementedException(); - public ApplyGuardrailResponse ApplyGuardrail(ApplyGuardrailRequest request) => throw new NotImplementedException(); - public Task ApplyGuardrailAsync(ApplyGuardrailRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public CountTokensResponse CountTokens(CountTokensRequest request) => throw new NotImplementedException(); - public Task CountTokensAsync(CountTokensRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public GetAsyncInvokeResponse GetAsyncInvoke(GetAsyncInvokeRequest request) => throw new NotImplementedException(); - public Task GetAsyncInvokeAsync(GetAsyncInvokeRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public InvokeModelResponse InvokeModel(InvokeModelRequest request) => throw new NotImplementedException(); - public Task InvokeModelAsync(InvokeModelRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public InvokeModelWithResponseStreamResponse InvokeModelWithResponseStream(InvokeModelWithResponseStreamRequest request) => throw new NotImplementedException(); - public Task InvokeModelWithResponseStreamAsync(InvokeModelWithResponseStreamRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public ListAsyncInvokesResponse ListAsyncInvokes(ListAsyncInvokesRequest request) => throw new NotImplementedException(); - public Task ListAsyncInvokesAsync(ListAsyncInvokesRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public StartAsyncInvokeResponse StartAsyncInvoke(StartAsyncInvokeRequest request) => throw new NotImplementedException(); - public Task StartAsyncInvokeAsync(StartAsyncInvokeRequest request, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Endpoint DetermineServiceOperationEndpoint(AmazonWebServiceRequest request) => throw new NotImplementedException(); -} From ac6f21cc48efd72ee0726e70fa3c09712f733c09 Mon Sep 17 00:00:00 2001 From: Daniel Pinheiro Date: Sun, 15 Mar 2026 20:39:53 -0700 Subject: [PATCH 3/3] Add DevConfig file --- .../db194401-c10a-413d-8ec8-cefee54ad65a.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 generator/.DevConfigs/db194401-c10a-413d-8ec8-cefee54ad65a.json diff --git a/generator/.DevConfigs/db194401-c10a-413d-8ec8-cefee54ad65a.json b/generator/.DevConfigs/db194401-c10a-413d-8ec8-cefee54ad65a.json new file mode 100644 index 000000000000..47d91f3437c5 --- /dev/null +++ b/generator/.DevConfigs/db194401-c10a-413d-8ec8-cefee54ad65a.json @@ -0,0 +1,11 @@ +{ + "extensions": [ + { + "extensionName": "Extensions.Bedrock.MEAI", + "type": "minor", + "changeLogMessages": [ + "Update `IChatClient` with latest `BedrockRuntime` / M.E.AI (adding support for multi-modal tool returns and citations with URIs)" + ] + } + ] +} \ No newline at end of file