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..95596501546a 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..a64f9223fe24 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 11d12c364bc4..e77cd1288587 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
@@ -14,18 +14,18 @@
-
-
+
+
-
-
+
+
-
-
+
+
diff --git a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs
index bfe33dbda368..3ca35f8490ba 100644
--- a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs
+++ b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs
@@ -108,8 +108,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);
}
@@ -398,8 +399,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()
{
@@ -500,6 +500,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)
{
@@ -582,32 +586,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)
{
diff --git a/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs b/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs
index 8f5099c973d8..2fc25876a504 100644
--- a/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs
+++ b/extensions/test/BedrockMEAITests/BedrockChatClientTests.cs
@@ -1,5 +1,14 @@
-using Microsoft.Extensions.AI;
+using Amazon.BedrockRuntime.Model;
+using Amazon.Runtime.Documents;
+using Amazon.Runtime.EventStreams;
+using Microsoft.Extensions.AI;
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
using Xunit;
namespace Amazon.BedrockRuntime;
@@ -44,4 +53,3564 @@ public void AsIChatClient_GetService()
Assert.Null(client.GetService("key"));
Assert.Null(client.GetService("key"));
}
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public void AsIChatClient_ValidArguments_CreatesIChatClientSuccessfully()
+ {
+ MockBedrockRuntime mock = new();
+ IChatClient chatClient = mock.AsIChatClient();
+ Assert.NotNull(chatClient);
+ Assert.Same(mock, chatClient.GetService());
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public void IChatClient_GetService_InvalidArguments_Throws()
+ {
+ MockBedrockRuntime mock = new();
+ IChatClient chatClient = mock.AsIChatClient();
+ Assert.NotNull(chatClient);
+
+ Assert.Throws("serviceType", () => chatClient.GetService(null!));
+ }
+
+ [Theory]
+ [Trait("UnitTest", "BedrockRuntime")]
+ [InlineData(null)]
+ [InlineData("anthropic.claude-3-sonnet-20240229-v1:0")]
+ public void IChatClient_GetService_ReturnsExpectedInstance(string defaultModelId)
+ {
+ MockBedrockRuntime mock = new();
+ IChatClient chatClient = mock.AsIChatClient(defaultModelId);
+ Assert.NotNull(chatClient);
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public void IChatClient_Dispose_Nop()
+ {
+ MockBedrockRuntime mock = new();
+ IChatClient chatClient = mock.AsIChatClient();
+ Assert.NotNull(chatClient);
+
+ chatClient.Dispose();
+
+ Assert.Same(mock, chatClient.GetService());
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_BasicRequest()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request => CreateResponse("Hello")
+ };
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_TextContent()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ 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);
+
+ 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?")];
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_EmptyMessages_CreatesDefaultMessage()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ 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);
+
+ return CreateResponse("Empty input received");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ ChatMessage[] messages = [];
+
+ ChatResponse result = await chatClient.GetResponseAsync(messages);
+
+ Assert.NotNull(result);
+ Assert.Single(result.Messages);
+ Assert.Equal("Empty input received", ((TextContent)result.Messages[0].Contents[0]).Text);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_NullMessages_Throws()
+ {
+ MockBedrockRuntime mock = new();
+ IChatClient chatClient = mock.AsIChatClient("claude");
+
+ await Assert.ThrowsAsync("messages", () => chatClient.GetResponseAsync(null!));
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_DataContent_Image()
+ {
+ byte[] imageData = [0x89, 0x50, 0x4E, 0x47];
+
+ MockBedrockRuntime mock = new()
+ {
+ 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.");
+ }
+ };
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_DataContent_AllImageFormats()
+ {
+ var formats = new[]
+ {
+ ("image/jpeg", ImageFormat.Jpeg),
+ ("image/png", ImageFormat.Png),
+ ("image/gif", ImageFormat.Gif),
+ ("image/webp", ImageFormat.Webp)
+ };
+
+ foreach (var (mimeType, expectedFormat) in formats)
+ {
+ byte[] imageData = [1, 2, 3, 4];
+ bool verified = false;
+
+ 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");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ await chatClient.GetResponseAsync([new(ChatRole.User, [new DataContent(imageData, mimeType)])]);
+ Assert.True(verified, $"Format {mimeType} not verified");
+ }
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_DataContent_Document()
+ {
+ byte[] pdfData = [0x25, 0x50, 0x44, 0x46];
+
+ MockBedrockRuntime mock = new()
+ {
+ 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.");
+ }
+ };
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_DataContent_DocumentWithName()
+ {
+ byte[] pdfData = [1, 2, 3];
+
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ Assert.NotNull(request.Messages[0].Content[0].Document);
+ Assert.Equal("report.pdf", request.Messages[0].Content[0].Document.Name);
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ DataContent dataContent = new(pdfData, "application/pdf") { Name = "report.pdf" };
+ await chatClient.GetResponseAsync([new(ChatRole.User, [dataContent])]);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_DataContent_Video()
+ {
+ byte[] videoData = [0x00, 0x00, 0x00, 0x18];
+
+ MockBedrockRuntime mock = new()
+ {
+ 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);
+
+ return CreateResponse("Video processed.");
+ }
+ };
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_ReceivesImageContent()
+ {
+ byte[] imageData = [1, 2, 3, 4];
+
+ MockBedrockRuntime mock = new()
+ {
+ 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 System.IO.MemoryStream(imageData)
+ }
+ }
+ }
+ ]
+ }
+ },
+ 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")]);
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_ReceivesVideoContent()
+ {
+ byte[] videoData = [5, 6, 7, 8];
+
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ 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 System.IO.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, "Send me a video")]);
+
+ 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 IChatClient_GetResponseAsync_ReceivesDocumentContent()
+ {
+ byte[] docData = [9, 10, 11];
+
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ 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 System.IO.MemoryStream(docData)
+ }
+ }
+ }
+ ]
+ }
+ },
+ 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")]);
+
+ 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);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public void IChatClient_GetService_WithServiceKey_ReturnsNull()
+ {
+ MockBedrockRuntime mock = new();
+ IChatClient chatClient = mock.AsIChatClient();
+
+ // When serviceKey is not null, should return null
+ Assert.Null(chatClient.GetService(typeof(IAmazonBedrockRuntime), "someKey"));
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public void IChatClient_GetService_UnknownType_ReturnsNull()
+ {
+ MockBedrockRuntime mock = new();
+ IChatClient chatClient = mock.AsIChatClient();
+
+ // Unknown type should return null
+ Assert.Null(chatClient.GetService());
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_UsageWithCacheTokens()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ var response = CreateResponse("OK");
+ response.Usage = new TokenUsage
+ {
+ 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")]);
+
+ Assert.NotNull(result.Usage);
+ Assert.Equal(100, result.Usage.InputTokenCount);
+ Assert.Equal(50, result.Usage.OutputTokenCount);
+ Assert.Equal(150, result.Usage.TotalTokenCount);
+ Assert.NotNull(result.Usage.AdditionalCounts);
+ Assert.Equal(25, result.Usage.AdditionalCounts["CacheReadInputTokens"]);
+ Assert.Equal(10, result.Usage.AdditionalCounts["CacheWriteInputTokens"]);
+ }
+
+ [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;
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]);
+
+ Assert.Equal("custom_reason", result.FinishReason?.Value);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_AdditionalProperties_AllTypes()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ var dict = request.AdditionalModelRequestFields.AsDictionary();
+
+ // Verify all types were converted
+ Assert.True(dict["boolProp"].AsBool());
+ Assert.Equal(42, dict["intProp"].AsInt());
+ Assert.Equal(9999999999L, dict["longProp"].AsLong());
+ Assert.Equal(1.5, dict["doubleProp"].AsDouble(), 1);
+ Assert.Equal("hello", dict["stringProp"].AsString());
+ Assert.True(dict["nullProp"].IsNull());
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+
+ ChatOptions options = new()
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["boolProp"] = true,
+ ["intProp"] = 42,
+ ["longProp"] = 9999999999L,
+ ["doubleProp"] = 1.5,
+ ["stringProp"] = "hello",
+ ["nullProp"] = null
+ }
+ };
+
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options);
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_AdditionalProperties_JsonElement()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ var dict = request.AdditionalModelRequestFields.AsDictionary();
+ Assert.True(dict.ContainsKey("jsonProp"));
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+
+ JsonDocument jsonDoc = System.Text.Json.JsonDocument.Parse("{\"nested\": true}");
+ ChatOptions options = new()
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["jsonProp"] = jsonDoc.RootElement
+ }
+ };
+
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options);
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_StopSequences_MergesWithExisting()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ // 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 System.IO.MemoryStream(docData) }
+ }
+ }
+ ]
+ }
+ },
+ 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);
+ }
+
+ [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()
+ {
+ 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 System.IO.MemoryStream(imageData) }
+ }
+ }
+ ]
+ }
+ },
+ 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);
+ }
+
+ [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 System.IO.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 IChatClient_GetResponseAsync_ReceivesDocument_UnknownFormat()
+ {
+ byte[] docData = [9, 10, 11];
+ DocumentFormat format = new("unknown_format");
+
+ 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 System.IO.MemoryStream(docData) }
+ }
+ }
+ ]
+ }
+ },
+ 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 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 =>
+ {
+ ConverseResponse response = new()
+ {
+ Output = new ConverseOutput
+ {
+ Message = new Message
+ {
+ Role = ConversationRole.Assistant,
+ Content =
+ [
+ new() {
+ Image = new ImageBlock
+ {
+ Format = format,
+ Source = new ImageSource { Bytes = new System.IO.MemoryStream(imageData) }
+ }
+ }
+ ]
+ }
+ },
+ 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 System.IO.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));
+ // 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()
+ {
+ OnConverseRequest = request =>
+ {
+ // 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 =
+ [
+ new(ChatRole.User, [new DataContent(data, "application/unknown-type")])
+ ];
+
+ 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";
+
+ 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 }
+ };
+
+ return response;
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ ChatMessage[] messages = [new(ChatRole.User, "Think step by step about this problem.")];
+
+ ChatResponse result = await chatClient.GetResponseAsync(messages);
+
+ Assert.NotNull(result);
+ Assert.Single(result.Messages);
+ Assert.IsType(Assert.Single(result.Messages[0].Contents));
+
+ TextReasoningContent reasoningContent = (TextReasoningContent)result.Messages[0].Contents[0];
+ Assert.Equal(reasoningText, reasoningContent.Text);
+ Assert.Equal(signature, reasoningContent.ProtectedData);
+ Assert.NotNull(reasoningContent.RawRepresentation);
+ }
+
+ [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 }])
+ ];
+
+ ChatResponse result = await chatClient.GetResponseAsync(messages);
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_TextReasoningContent_WithRedactedContent()
+ {
+ byte[] redactedData = [1, 2, 3, 4];
+
+ MockBedrockRuntime mock = new()
+ {
+ 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));
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+
+ TextReasoningContent reasoningContent = new("Reasoning")
+ {
+ 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 System.IO.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_WithAdditionalModelRequestFields()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ Assert.True(request.AdditionalModelRequestFields.IsDictionary());
+
+ var dict = request.AdditionalModelRequestFields.AsDictionary();
+ Assert.Equal(40, dict["k"].AsInt());
+ Assert.Equal(0.5, dict["frequency_penalty"].AsDouble(), 5); // tolerance for float precision
+ Assert.Equal(0.3, dict["presence_penalty"].AsDouble(), 5); // tolerance for float precision
+ Assert.Equal(42, dict["seed"].AsLong());
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ ChatMessage[] messages = [new(ChatRole.User, "Test")];
+
+ ChatOptions options = new()
+ {
+ TopK = 40,
+ FrequencyPenalty = 0.5f,
+ PresencePenalty = 0.3f,
+ Seed = 42
+ };
+
+ ChatResponse result = await chatClient.GetResponseAsync(messages, options);
+ Assert.NotNull(result);
+ }
+
+ [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", ((System.Text.Json.JsonElement)result.Messages[0].AdditionalProperties["custom_field"]).GetString());
+ Assert.Equal(123, ((System.Text.Json.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 System.IO.MemoryStream(imageData) }
+ }
+ };
+ ContentBlock videoBlock = new()
+ {
+ Video = new VideoBlock
+ {
+ Format = VideoFormat.Mp4,
+ Source = new VideoSource { Bytes = new System.IO.MemoryStream(videoData) }
+ }
+ };
+ ContentBlock docBlock = new()
+ {
+ Document = new DocumentBlock
+ {
+ Format = DocumentFormat.Pdf,
+ Name = "file.pdf",
+ Source = new DocumentSource { Bytes = new System.IO.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_AdditionalProperties_InChatOptions()
+ {
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ var dict = request.AdditionalModelRequestFields.AsDictionary();
+
+ Assert.Equal("string_value", dict["string_prop"].AsString());
+ Assert.Equal(42, dict["int_prop"].AsInt());
+ Assert.Equal(3.14, dict["double_prop"].AsDouble(), 2);
+ Assert.True(dict["bool_prop"].AsBool());
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+
+ ChatOptions options = new()
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary()
+ {
+ ["string_prop"] = "string_value",
+ ["int_prop"] = 42,
+ ["double_prop"] = 3.14,
+ ["bool_prop"] = true,
+ ["null_prop"] = null
+ }
+ };
+
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options);
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_AdditionalProperties_WithJsonElement()
+ {
+ var jsonObject = JsonSerializer.SerializeToElement(new { nested = new { value = 123 } });
+
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ var dict = request.AdditionalModelRequestFields.AsDictionary();
+
+ Assert.True(dict.ContainsKey("json_prop"));
+ var nested = dict["json_prop"].AsDictionary();
+ Assert.True(nested.ContainsKey("nested"));
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+
+ ChatOptions options = new()
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary()
+ {
+ ["json_prop"] = jsonObject
+ }
+ };
+
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options);
+ 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_AdditionalProperties_WithJsonArray()
+ {
+ var jsonArray = JsonSerializer.SerializeToElement(new[] { 1, 2, 3 });
+
+ MockBedrockRuntime mock = new()
+ {
+ OnConverseRequest = request =>
+ {
+ var dict = request.AdditionalModelRequestFields.AsDictionary();
+ Assert.True(dict["array_prop"].IsList());
+ var list = dict["array_prop"].AsList();
+ Assert.Equal(3, list.Count);
+
+ return CreateResponse("OK");
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+
+ ChatOptions options = new()
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary()
+ {
+ ["array_prop"] = jsonArray
+ }
+ };
+
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options);
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_AdditionalProperties_WithJsonNull()
+ {
+ var jsonNull = JsonSerializer.SerializeToElement