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..0b8ec891d94e 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..dc2a54ceeb5d 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 6fb6266ccbdd..e75b258f4151 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 34f6fec1e9d5..61bc1616e548 100644
--- a/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs
+++ b/extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs
@@ -163,8 +163,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);
}
@@ -424,15 +425,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);
@@ -467,8 +464,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()
{
@@ -569,6 +565,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)
{
@@ -651,32 +651,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 b9dff182a517..c5338f161b53 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,3565 @@ 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_AdditionalProperties_AllTypes()
{
- // 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"
- }
- }
- }
- ]
- }
- },
- "stopReason": "tool_use",
- "usage": {
- "inputTokens": 10,
- "outputTokens": 5,
- "totalTokens": 15
- }
+ 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");
}
- """;
+ };
- 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");
- var request = new ConverseRequest();
- var options = new ChatOptions
+ ChatOptions options = new()
{
- ResponseFormat = ChatResponseFormat.Json,
- RawRepresentationFactory = _ => request
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["boolProp"] = true,
+ ["intProp"] = 42,
+ ["longProp"] = 9999999999L,
+ ["doubleProp"] = 1.5,
+ ["stringProp"] = "hello",
+ ["nullProp"] = null
+ }
};
- var webResponseData = new StubWebResponseData(converseResponse);
- InjectMockedResponse(request, webResponseData);
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")], options);
+ Assert.NotNull(result);
+ }
- // Act
- var response = await chatClient.GetResponseAsync(messages, options);
+ [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"));
- // 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());
+ 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 ResponseFormat_Json_WithComplexSchema_ValidatesStructure()
+ public async Task IChatClient_GetResponseAsync_StopSequences_MergesWithExisting()
{
- // Arrange - Test with detailed schema validation
- var converseResponse = """
+ 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
+ // 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": 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));
+ 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 =>
{
- "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));
+ 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 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));
+ 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()
{
- ResponseFormat = ChatResponseFormat.ForJsonSchema(schemaElement,
- schemaName: "UserProfile",
- schemaDescription: "Complete user profile with preferences"),
- RawRepresentationFactory = _ => request
+ 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) }
+ }
+ }
+ ]
+ }
+ },
+ 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));
+ // Unknown format defaults to text/plain
+ Assert.Equal("text/plain", dataContent.MediaType);
+ }
- // Assert
- Assert.NotNull(response.Text);
- var json = JsonDocument.Parse(response.Text);
+ [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 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")]);
- // 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());
+ 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);
+ }
- // 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());
+ [Fact]
+ [Trait("UnitTest", "BedrockRuntime")]
+ public async Task IChatClient_GetResponseAsync_ReceivesVideo_UnknownFormat()
+ {
+ byte[] videoData = [5, 6, 7, 8];
+ VideoFormat format = new("unknown_format");
- // Verify nested preferences
- var preferences = profile.GetProperty("preferences");
- Assert.Equal("dark", preferences.GetProperty("theme").GetString());
- Assert.True(preferences.GetProperty("notifications").GetBoolean());
+ 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;
+ }
+ };
- // 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());
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ ChatResponse result = await chatClient.GetResponseAsync([new(ChatRole.User, "Test")]);
- // Verify boolean
- Assert.True(json.RootElement.GetProperty("active").GetBoolean());
+ 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);
}
- #endregion
+ [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")])
+ ];
- ///
- /// 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()
{
- private readonly MockPipelineCustomizer _customizer;
+ string reasoningText = "Let me think step by step...";
+ string signature = "sig123";
- public HttpMockFixture()
+ MockBedrockRuntime mock = new()
{
- // Register the mock pipeline customizer globally
- _customizer = new MockPipelineCustomizer();
- Runtime.Internal.RuntimePipelineCustomizerRegistry.Instance.Register(_customizer);
+ 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 }
+ };
- // Create the Bedrock Runtime client - it will use the mocked pipeline
- BedrockRuntimeClient = new AmazonBedrockRuntimeClient();
- }
+ return response;
+ }
+ };
+
+ IChatClient chatClient = mock.AsIChatClient("claude");
+ ChatMessage[] messages = [new(ChatRole.User, "Think step by step about this problem.")];
- public IAmazonBedrockRuntime BedrockRuntimeClient { get; private set; }
+ ChatResponse result = await chatClient.GetResponseAsync(messages);
- public void Dispose()
+ 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()
{
- // Clean up
- Runtime.Internal.RuntimePipelineCustomizerRegistry.Instance.Deregister(_customizer);
- BedrockRuntimeClient?.Dispose();
- }
+ 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 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", ((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_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