diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 5307760..5046929 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -172,6 +172,13 @@ public class AnthropicApiClient : IAnthropicApiClient chatResponse is not null ) { + var existingUsage = chatResponse.Usage; + var newUsage = new ChatUsage() + { + InputTokens = existingUsage.InputTokens + msgDeltaData.Usage.InputTokens, + OutputTokens = existingUsage.OutputTokens + msgDeltaData.Usage.OutputTokens, + }; + chatResponse = new ChatResponse() { Id = chatResponse.Id, @@ -180,7 +187,7 @@ public class AnthropicApiClient : IAnthropicApiClient StopReason = msgDeltaData.Delta.StopReason, StopSequence = msgDeltaData.Delta.StopSequence, Type = chatResponse.Type, - Usage = msgDeltaData.Usage, + Usage = newUsage, Content = chatResponse.Content, }; } diff --git a/src/AnthropicClient/Models/EventData.cs b/src/AnthropicClient/Models/EventData.cs index e73d531..d40914a 100644 --- a/src/AnthropicClient/Models/EventData.cs +++ b/src/AnthropicClient/Models/EventData.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace AnthropicClient.Models; public abstract class EventData diff --git a/src/AnthropicClient/Models/MessageCompleteEventData.cs b/src/AnthropicClient/Models/MessageCompleteEventData.cs index 85218ad..bdd62df 100644 --- a/src/AnthropicClient/Models/MessageCompleteEventData.cs +++ b/src/AnthropicClient/Models/MessageCompleteEventData.cs @@ -1,11 +1,9 @@ -using System.Text.Json.Serialization; - namespace AnthropicClient.Models; public class MessageCompleteEventData : EventData { - public AnthropicHeaders Headers { get; init; } = new(); - public ChatResponse Message { get; init; } = new(); + public AnthropicHeaders Headers { get; init; } + public ChatResponse Message { get; init; } public MessageCompleteEventData(ChatResponse message, AnthropicHeaders headers) : base(EventType.MessageComplete) { diff --git a/tests/AnthropicClient.Tests/Unit/Models/AnthropicEventTests.cs b/tests/AnthropicClient.Tests/Unit/Models/AnthropicEventTests.cs new file mode 100644 index 0000000..931616b --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/AnthropicEventTests.cs @@ -0,0 +1,372 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class AnthropicEventTests : SerializationTest +{ + private readonly string _testMessageStartJson = @"{ + ""type"": ""message_start"", + ""data"": { + ""message"": { + ""id"": ""msg_014p7gG3wDgGV9EUtLvnow3U"", + ""type"": ""message"", + ""role"": ""assistant"", + ""model"": ""claude-3-haiku-20240307"", + ""stop_sequence"": """", + ""usage"": { + ""input_tokens"": 472, + ""output_tokens"": 2 + }, + ""content"": [], + ""stop_reason"": """" + }, + ""type"": ""message_start"" + } + }"; + + private readonly string _testContentBlockStartJson = @"{ + ""type"": ""content_block_start"", + ""data"": { + ""index"": 0, + ""content_block"": { + ""type"": ""text"", + ""text"": ""Okay"" + }, + ""type"": ""content_block_start"" + } + }"; + + private readonly string _testPingJson = @"{ + ""type"": ""ping"", + ""data"": { + ""type"": ""ping"" + } + }"; + + private readonly string _testContentBlockDeltaJson = @"{ + ""type"": ""content_block_delta"", + ""data"": { + ""index"": 0, + ""delta"": { + ""type"": ""text_delta"", + ""text"": ""Okay"" + }, + ""type"": ""content_block_delta"" + } + }"; + + private readonly string _testContentBlockStopJson = @"{ + ""type"": ""content_block_stop"", + ""data"": { + ""index"": 0, + ""type"": ""content_block_stop"" + } + }"; + + private readonly string _testMessageDeltaJson = @"{ + ""type"": ""message_delta"", + ""data"": { + ""delta"": { + ""stop_reason"": ""tool_use"", + ""stop_sequence"": """" + }, + ""usage"": { + ""output_tokens"": 89, + ""input_tokens"": 0 + }, + ""type"": ""message_delta"" + } + }"; + + private readonly string _testMessageStopJson = @"{ + ""type"": ""message_stop"", + ""data"": { + ""type"": ""message_stop"" + } + }"; + + private readonly string _testErrorJson = @"{ + ""type"": ""error"", + ""data"": { + ""error"": { + ""type"": ""api_error"", + ""message"": ""An error occurred."" + }, + ""type"": ""error"" + } + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var expectedType = EventType.MessageStart; + var expected = new MessageStartEventData + { + Type = EventType.MessageStart, + Message = new() + }; + + var actual = new AnthropicEvent(expectedType, expected); + + actual.Type.Should().Be(expectedType); + actual.Data.Should().BeSameAs(expected); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var eventData = new MessageStartEventData + { + Type = EventType.MessageStart, + Message = new() + { + Id = "msg_014p7gG3wDgGV9EUtLvnow3U", + Type = "message", + Role = "assistant", + Model = "claude-3-haiku-20240307", + StopSequence = string.Empty, + Usage = new() + { + InputTokens = 472, + OutputTokens = 2 + }, + Content = [], + StopReason = string.Empty + } + }; + + var e = new AnthropicEvent(EventType.MessageStart, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testMessageStartJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenMessageStartEventDeserialized_ItShouldHaveBeExpectedType() + { + var expected = new MessageStartEventData + { + Type = EventType.MessageStart, + Message = new() + { + Id = "msg_014p7gG3wDgGV9EUtLvnow3U", + Type = "message", + Role = "assistant", + Model = "claude-3-haiku-20240307", + StopSequence = string.Empty, + Usage = new() + { + InputTokens = 472, + OutputTokens = 2 + }, + Content = [], + StopReason = string.Empty + } + }; + + var actual = Deserialize(_testMessageStartJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonSerialization_WhenContentBlockStartEventSerialized_ItShouldHaveExpectedShape() + { + var eventData = new ContentStartEventData(0, new TextContent("Okay")); + + var e = new AnthropicEvent(EventType.ContentBlockStart, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testContentBlockStartJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenContentBlockStartEventDeserialized_ItShouldHaveExpectedType() + { + var expected = new ContentStartEventData(0, new TextContent("Okay")); + + var actual = Deserialize(_testContentBlockStartJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonSerialization_WhenPingEventSerialized_ItShouldHaveExpectedShape() + { + var eventData = new PingEventData(); + + var e = new AnthropicEvent(EventType.Ping, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testPingJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenPingEventDeserialized_ItShouldHaveExpectedType() + { + var expected = new PingEventData(); + + var actual = Deserialize(_testPingJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonSerialization_WhenContentBlockDeltaEventSerialized_ItShouldHaveExpectedShape() + { + var eventData = new ContentDeltaEventData(0, new TextDelta("Okay")); + + var e = new AnthropicEvent(EventType.ContentBlockDelta, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testContentBlockDeltaJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenContentBlockDeltaEventDeserialized_ItShouldHaveExpectedType() + { + var expected = new ContentDeltaEventData(0, new TextDelta("Okay")); + + var actual = Deserialize(_testContentBlockDeltaJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonSerialization_WhenContentBlockStopEventSerialized_ItShouldHaveExpectedShape() + { + var eventData = new ContentStopEventData(0); + + var e = new AnthropicEvent(EventType.ContentBlockStop, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testContentBlockStopJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenContentBlockStopEventDeserialized_ItShouldHaveExpectedType() + { + var expected = new ContentStopEventData(0); + + var actual = Deserialize(_testContentBlockStopJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonSerialization_WhenMessageDeltaEventSerialized_ItShouldHaveExpectedShape() + { + var eventData = new MessageDeltaEventData + { + Type = EventType.MessageDelta, + Delta = new() + { + StopReason = "tool_use", + StopSequence = string.Empty, + }, + Usage = new() + { + OutputTokens = 89, + } + }; + + var e = new AnthropicEvent(EventType.MessageDelta, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testMessageDeltaJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenMessageDeltaEventDeserialized_ItShouldHaveExpectedType() + { + var expected = new MessageDeltaEventData + { + Type = EventType.MessageDelta, + Delta = new() + { + StopReason = "tool_use", + StopSequence = string.Empty, + }, + Usage = new() + { + OutputTokens = 89, + InputTokens = 0 + } + }; + + var actual = Deserialize(_testMessageDeltaJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonSerialization_WhenMessageStopEventSerialized_ItShouldHaveExpectedShape() + { + var eventData = new MessageStopEventData(); + + var e = new AnthropicEvent(EventType.MessageStop, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testMessageStopJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenMessageStopEventDeserialized_ItShouldHaveExpectedType() + { + var expected = new MessageStopEventData(); + + var actual = Deserialize(_testMessageStopJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonSerialization_WhenErrorEventSerialized_ItShouldHaveExpectedShape() + { + var eventData = new ErrorEventData(new ApiError("An error occurred.")); + + var e = new AnthropicEvent(EventType.Error, eventData); + + var actual = Serialize(e); + + JsonAssert.Equal(_testErrorJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenErrorEventDeserialized_ItShouldHaveExpectedType() + { + var expected = new ErrorEventData(new ApiError("An error occurred.")); + + var actual = Deserialize(_testErrorJson); + + actual!.Type.Should().Be(expected.Type); + actual.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public void JsonDeserialization_WhenTypeUnknown_ItShouldThrow() + { + var json = @"{ + ""type"": ""unknown"", + ""data"": { + ""type"": ""unknown"" + } + }"; + + Action act = () => Deserialize(json); + + act.Should().Throw(); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ContentDeltaEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ContentDeltaEventDataTests.cs new file mode 100644 index 0000000..1f226c1 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ContentDeltaEventDataTests.cs @@ -0,0 +1,103 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ContentDeltaEventDataTests : SerializationTest +{ + private readonly string _testJsonWithTextDelta = @"{ + ""type"": ""content_block_delta"", + ""index"": 0, + ""delta"": { + ""type"": ""text_delta"", + ""text"": ""Hello World!"" + } + }"; + + private readonly string _testJsonWithInputDelta = @"{ + ""type"": ""content_block_delta"", + ""index"": 0, + ""delta"": { + ""type"": ""input_json_delta"", + ""partial_json"": ""Hello"" + } + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var index = 0; + var text = "Hello World!"; + var textDelta = new TextDelta(text); + var contentDeltaEventData = new ContentDeltaEventData(index, textDelta); + + contentDeltaEventData.Type.Should().Be("content_block_delta"); + contentDeltaEventData.Delta.Should().BeSameAs(textDelta); + } + + [Fact] + public void JsonSerialization_WhenSerializedWithTextDelta_ItShouldHaveExpectedShape() + { + var index = 0; + var text = "Hello World!"; + var textDelta = new TextDelta(text); + var contentDeltaEventData = new ContentDeltaEventData(index, textDelta); + + var actual = Serialize(contentDeltaEventData); + + JsonAssert.Equal(_testJsonWithTextDelta, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserializedWithTextDelta_ItShouldHaveExpectedProperties() + { + var index = 0; + var text = "Hello World!"; + var textDelta = new TextDelta(text); + var contentDeltaEventData = new ContentDeltaEventData(index, textDelta); + + var actual = Deserialize(_testJsonWithTextDelta); + + actual.Should().BeEquivalentTo(contentDeltaEventData); + } + + [Fact] + public void JsonSerialization_WhenSerializedWithInputDelta_ItShouldHaveExpectedShape() + { + var index = 0; + var input = "Hello"; + var inputDelta = new JsonDelta(input); + var contentDeltaEventData = new ContentDeltaEventData(index, inputDelta); + + var actual = Serialize(contentDeltaEventData); + + JsonAssert.Equal(_testJsonWithInputDelta, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserializedWithInputDelta_ItShouldHaveExpectedProperties() + { + var index = 0; + var input = "Hello"; + var inputDelta = new JsonDelta(input); + var contentDeltaEventData = new ContentDeltaEventData(index, inputDelta); + + var actual = Deserialize(_testJsonWithInputDelta); + + actual.Should().BeEquivalentTo(contentDeltaEventData); + } + + [Fact] + public void JsonDeserialization_WhenDeserializedWithUnknownDeltaType_ItShouldThrowJsonException() + { + var json = @"{ + ""type"": ""content_block_delta"", + ""index"": 0, + ""delta"": { + ""type"": ""unknown_delta"", + ""text"": ""Hello World!"" + } + }"; + + var action = () => Deserialize(json); + + action.Should().Throw(); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ContentDeltaTypeTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ContentDeltaTypeTests.cs new file mode 100644 index 0000000..9bddaa2 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ContentDeltaTypeTests.cs @@ -0,0 +1,24 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ContentDeltaTypeTests +{ + [Fact] + public void TextDelta_WhenCalled_ItShouldReturnExpectedValue() + { + var expected = "text_delta"; + + var actual = ContentDeltaType.TextDelta; + + actual.Should().Be(expected); + } + + [Fact] + public void JsonDelta_WhenCalled_ItShouldReturnExpectedValue() + { + var expected = "input_json_delta"; + + var actual = ContentDeltaType.JsonDelta; + + actual.Should().Be(expected); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ContentStartEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ContentStartEventDataTests.cs new file mode 100644 index 0000000..25bf30e --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ContentStartEventDataTests.cs @@ -0,0 +1,77 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ContentStartEventDataTests : SerializationTest +{ + private readonly string _testJsonWithTextBlock = @"{ + ""type"": ""content_block_start"", + ""index"": 0, + ""content_block"": { + ""type"": ""text"", + ""text"": ""Hello World!"" + } + }"; + + private readonly string _testJsonWithToolUseBlock = @"{ + ""type"": ""content_block_start"", + ""index"": 0, + ""content_block"": { + ""type"": ""tool_use"", + ""id"": ""toolu_01T1x1fJ34qAmk2tNTrN7Up6"", + ""name"": ""get_weather"", + ""input"": {}} + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var index = 0; + var textContent = new TextContent("Hello World!"); + var contentStartEventData = new ContentStartEventData(index, textContent); + + contentStartEventData.Type.Should().Be("content_block_start"); + contentStartEventData.Index.Should().Be(index); + contentStartEventData.ContentBlock.Should().BeSameAs(textContent); + } + + [Fact] + public void JsonSerialization_WhenSerializedWithTextContent_ItShouldHaveExpectedShape() + { + var index = 0; + var textContent = new TextContent("Hello World!"); + var contentStartEventData = new ContentStartEventData(index, textContent); + + var actual = Serialize(contentStartEventData); + + JsonAssert.Equal(_testJsonWithTextBlock, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserializedWithTextContent_ItShouldHaveExpectedProperties() + { + var index = 0; + var textContent = new TextContent("Hello World!"); + var contentStartEventData = new ContentStartEventData(index, textContent); + + var actual = Deserialize(_testJsonWithTextBlock); + + actual.Should().BeEquivalentTo(contentStartEventData); + } + + [Fact] + public void JsonSerialization_WhenSerializedWithToolUseContent_ItShouldHaveExpectedShape() + { + var index = 0; + var toolUseContent = new ToolUseContent() + { + Id = "toolu_01T1x1fJ34qAmk2tNTrN7Up6", + Name = "get_weather", + Input = [] + }; + + var contentStartEventData = new ContentStartEventData(index, toolUseContent); + + var actual = Serialize(contentStartEventData); + + JsonAssert.Equal(_testJsonWithToolUseBlock, actual); + } +} diff --git a/tests/AnthropicClient.Tests/Unit/Models/ContentStopEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ContentStopEventDataTests.cs new file mode 100644 index 0000000..03155e6 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ContentStopEventDataTests.cs @@ -0,0 +1,41 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ContentStopEventDataTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""type"": ""content_block_stop"", + ""index"": 0 + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var index = 0; + var contentStopEventData = new ContentStopEventData(index); + + contentStopEventData.Type.Should().Be("content_block_stop"); + contentStopEventData.Index.Should().Be(index); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var index = 0; + var contentStopEventData = new ContentStopEventData(index); + + var actual = Serialize(contentStopEventData); + + JsonAssert.Equal(_testJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var index = 0; + var contentStopEventData = new ContentStopEventData(index); + + var actual = Deserialize(_testJson); + + actual.Should().BeEquivalentTo(contentStopEventData); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ErrorEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ErrorEventDataTests.cs new file mode 100644 index 0000000..b88d83d --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ErrorEventDataTests.cs @@ -0,0 +1,44 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ErrorEventDataTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""api_error"", + ""message"": ""An error occurred."" + } + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var error = new ApiError("An error occurred."); + var errorEventData = new ErrorEventData(error); + + errorEventData.Type.Should().Be("error"); + errorEventData.Error.Should().BeSameAs(error); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var error = new ApiError("An error occurred."); + var errorEventData = new ErrorEventData(error); + + var actual = Serialize(errorEventData); + + JsonAssert.Equal(_testJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var error = new ApiError("An error occurred."); + var errorEventData = new ErrorEventData(error); + + var actual = Deserialize(_testJson); + + actual.Should().BeEquivalentTo(errorEventData); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/JsonDeltaTests.cs b/tests/AnthropicClient.Tests/Unit/Models/JsonDeltaTests.cs new file mode 100644 index 0000000..c153425 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/JsonDeltaTests.cs @@ -0,0 +1,40 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class JsonDeltaTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""type"": ""input_json_delta"", + ""partial_json"": ""{\""key\"": \""value\""}"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var partialJson = "{\"key\": \"value\"}"; + + var jsonDelta = new JsonDelta(partialJson); + + jsonDelta.Type.Should().Be("input_json_delta"); + jsonDelta.PartialJson.Should().Be(partialJson); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var jsonDelta = new JsonDelta("{\"key\": \"value\"}"); + + var actual = Serialize(jsonDelta); + + JsonAssert.Equal(_testJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var expected = new JsonDelta("{\"key\": \"value\"}"); + + var actual = Deserialize(_testJson); + + actual.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageCompleteEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageCompleteEventDataTests.cs new file mode 100644 index 0000000..8b456d7 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageCompleteEventDataTests.cs @@ -0,0 +1,16 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageCompleteEventDataTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var expectedMessage = new ChatResponse(); + var expectedHeaders = new AnthropicHeaders(); + + var messageCompleteEventData = new MessageCompleteEventData(expectedMessage, expectedHeaders); + + messageCompleteEventData.Message.Should().BeSameAs(expectedMessage); + messageCompleteEventData.Headers.Should().BeSameAs(expectedHeaders); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs new file mode 100644 index 0000000..b514fbd --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs @@ -0,0 +1,53 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageDeltaEventDataTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""type"": ""message_delta"", + ""delta"": { + ""stop_reason"": ""max_tokens"", + ""stop_sequence"": ""max_tokens"" + }, + ""usage"": { + ""input_tokens"": 1, + ""output_tokens"": 1 + } + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); + var expectedUsage = new ChatUsage { InputTokens = 1, OutputTokens = 1 }; + + var messageDeltaEventData = new MessageDeltaEventData(expectedDelta, expectedUsage); + + messageDeltaEventData.Delta.Should().BeSameAs(expectedDelta); + messageDeltaEventData.Usage.Should().BeSameAs(expectedUsage); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); + var expectedUsage = new ChatUsage { InputTokens = 1, OutputTokens = 1 }; + + var messageDeltaEventData = new MessageDeltaEventData(expectedDelta, expectedUsage); + + var actualJson = Serialize(messageDeltaEventData); + + JsonAssert.Equal(_testJson, actualJson); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); + var expectedUsage = new ChatUsage { InputTokens = 1, OutputTokens = 1 }; + + var messageDeltaEventData = Deserialize(_testJson); + + messageDeltaEventData!.Delta.Should().BeEquivalentTo(expectedDelta); + messageDeltaEventData.Usage.Should().BeEquivalentTo(expectedUsage); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaTests.cs new file mode 100644 index 0000000..db8b52b --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaTests.cs @@ -0,0 +1,43 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageDeltaTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""stop_reason"": ""max_tokens"", + ""stop_sequence"": ""max_tokens"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var expectedStopReason = "max_tokens"; + var expectedStopSequence = "max_tokens"; + + var messageDelta = new MessageDelta(expectedStopReason, expectedStopSequence); + + messageDelta.StopReason.Should().Be(expectedStopReason); + messageDelta.StopSequence.Should().Be(expectedStopSequence); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var messageDelta = new MessageDelta("max_tokens", "max_tokens"); + + var actualJson = Serialize(messageDelta); + + JsonAssert.Equal(_testJson, actualJson); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var expectedStopReason = "max_tokens"; + var expectedStopSequence = "max_tokens"; + + var messageDelta = Deserialize(_testJson); + + messageDelta!.StopReason.Should().Be(expectedStopReason); + messageDelta.StopSequence.Should().Be(expectedStopSequence); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageStartEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageStartEventDataTests.cs new file mode 100644 index 0000000..2df48d1 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageStartEventDataTests.cs @@ -0,0 +1,83 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageStartEventDataTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""type"": ""message_start"", + ""message"": { + ""id"": ""msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY"", + ""type"": ""message"", + ""role"": ""assistant"", + ""content"": [], + ""model"": ""claude-3-5-sonnet-20240620"", + ""stop_reason"": """", + ""stop_sequence"": """", + ""usage"": { + ""input_tokens"": 25, + ""output_tokens"": 1 + } + } + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var expectedMessage = new ChatResponse + { + Id = "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", + Type = "message", + Role = "assistant", + Content = [], + Model = "claude-3-5-sonnet-20240620", + StopReason = string.Empty, + StopSequence = string.Empty, + Usage = new ChatUsage { InputTokens = 25, OutputTokens = 1 } + }; + + var messageStartEventData = new MessageStartEventData(expectedMessage); + + messageStartEventData.Message.Should().BeSameAs(expectedMessage); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var expectedMessage = new ChatResponse + { + Id = "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", + Type = "message", + Role = "assistant", + Content = [], + Model = "claude-3-5-sonnet-20240620", + StopReason = string.Empty, + StopSequence = string.Empty, + Usage = new ChatUsage { InputTokens = 25, OutputTokens = 1 } + }; + + var messageStartEventData = new MessageStartEventData(expectedMessage); + + var actualJson = Serialize(messageStartEventData); + + JsonAssert.Equal(_testJson, actualJson); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var expectedMessage = new ChatResponse + { + Id = "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", + Type = "message", + Role = "assistant", + Content = [], + Model = "claude-3-5-sonnet-20240620", + StopReason = string.Empty, + StopSequence = string.Empty, + Usage = new ChatUsage { InputTokens = 25, OutputTokens = 1 } + }; + + var messageStartEventData = Deserialize(_testJson); + + messageStartEventData!.Message.Should().BeEquivalentTo(expectedMessage); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/StreamChatMessageRequestTests.cs b/tests/AnthropicClient.Tests/Unit/Models/StreamChatMessageRequestTests.cs new file mode 100644 index 0000000..e2cc8d9 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/StreamChatMessageRequestTests.cs @@ -0,0 +1,237 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class StreamChatMessageRequestTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""model"": ""claude-3-sonnet-20240229"", + ""system"": ""test-system"", + ""messages"": [ + { ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] } + ], + ""max_tokens"": 512, + ""metadata"": { ""test"":""test"" }, + ""stop_sequences"": [], + ""temperature"": 0.5, + ""topK"": 10, + ""topP"": 0.5, + ""tool_choice"": { ""type"":""auto"" }, + ""tools"": [ + { + ""name"": ""test-tool"", + ""description"": ""test-description"", + ""input_schema"": { + ""type"": ""object"", + ""properties"": { + ""test-property"": { + ""type"": ""string"", + ""description"": ""test-description"" + } + }, + ""required"": [""test-property""] + } + } + ], + ""stream"": true + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var model = AnthropicModels.Claude3Sonnet; + var messages = new List { new() }; + var maxTokens = 512; + var system = "test-system"; + var metadata = new Dictionary { ["test"] = "test" }; + var temperature = 0.5m; + var topK = 10; + var topP = 0.5m; + var toolChoice = new AutoToolChoice(); + var tools = new List { new() }; + + var chatMessageRequest = new StreamChatMessageRequest( + model: model, + messages: messages, + maxTokens: maxTokens, + system: system, + metadata: metadata, + temperature: temperature, + topK: topK, + topP: topP, + toolChoice: toolChoice, + tools: tools + ); + + chatMessageRequest.Model.Should().Be(model); + chatMessageRequest.Messages.Should().BeSameAs(messages); + chatMessageRequest.MaxTokens.Should().Be(maxTokens); + chatMessageRequest.System.Should().Be(system); + chatMessageRequest.Metadata.Should().BeSameAs(metadata); + chatMessageRequest.Temperature.Should().Be(temperature); + chatMessageRequest.TopK.Should().Be(topK); + chatMessageRequest.TopP.Should().Be(topP); + chatMessageRequest.ToolChoice.Should().Be(toolChoice); + chatMessageRequest.Tools.Should().BeSameAs(tools); + chatMessageRequest.Stream.Should().BeTrue(); + } + + [Fact] + public void Constructor_WhenCalledAndModelIsNull_ItShouldThrowArgumentNullException() + { + var action = () => new StreamChatMessageRequest( + model: null!, + messages: [new()] + ); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_WhenCalledAndMessagesIsNull_ItShouldThrowArgumentNullException() + { + var action = () => new StreamChatMessageRequest( + model: AnthropicModels.Claude3Sonnet, + messages: null! + ); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_WhenCalledAndModelIsInvalid_ItShouldThrowArgumentException() + { + var action = () => new StreamChatMessageRequest( + model: "invalid-model", + messages: [new()] + ); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_WhenCalledAndMessagesIsEmpty_ItShouldThrowArgumentException() + { + var action = () => new StreamChatMessageRequest( + model: AnthropicModels.Claude3Sonnet, + messages: [] + ); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_WhenCalledAndMaxTokensIsInvalid_ItShouldThrowArgumentException() + { + var action = () => new StreamChatMessageRequest( + model: AnthropicModels.Claude3Sonnet, + messages: [new()], + maxTokens: 0 + ); + + action.Should().Throw(); + } + + [Theory] + [InlineData(-1)] + [InlineData(2)] + public void Constructor_WhenCalledAndTemperatureIsInvalid_ItShouldThrowArgumentException(decimal temperature) + { + var action = () => new StreamChatMessageRequest( + model: AnthropicModels.Claude3Sonnet, + messages: [new()], + temperature: temperature + ); + + action.Should().Throw(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var messages = new List() + { + new() + { + Role = MessageRole.User, + Content = [new TextContent("Hello!")] + } + }; + + var model = AnthropicModels.Claude3Sonnet; + var maxTokens = 512; + var system = "test-system"; + var metadata = new Dictionary + { + ["test"] = "test" + }; + var temperature = 0.5m; + var topK = 10; + var topP = 0.5m; + var toolChoice = new AutoToolChoice(); + var tools = new List + { + new() + { + Name = "test-tool", + Description = "test-description", + InputSchema = new InputSchema( + properties: new Dictionary + { + ["test-property"] = new InputProperty( + type: "string", + description: "test-description" + ) + }, + required: ["test-property"] + ), + } + }; + + var chatMessageRequest = new StreamChatMessageRequest( + model: model, + messages: messages, + maxTokens: maxTokens, + system: system, + metadata: metadata, + temperature: temperature, + topK: topK, + topP: topP, + toolChoice: toolChoice, + tools: tools + ); + + var actual = Serialize(chatMessageRequest); + + JsonAssert.Equal(_testJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape() + { + var chatMessageRequest = Deserialize(_testJson); + + chatMessageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); + chatMessageRequest.System.Should().Be("test-system"); + chatMessageRequest.Messages.Should().HaveCount(1); + chatMessageRequest.MaxTokens.Should().Be(512); + chatMessageRequest.Metadata.Should().HaveCount(1); + + var testValue = chatMessageRequest.Metadata!.GetValueOrDefault("test")!.ToString(); + testValue.Should().Be("test"); + + chatMessageRequest.Temperature.Should().Be(0.5m); + chatMessageRequest.TopK.Should().Be(10); + chatMessageRequest.TopP.Should().Be(0.5m); + chatMessageRequest.ToolChoice.Should().BeOfType(); + chatMessageRequest.ToolChoice!.Type.Should().Be("auto"); + chatMessageRequest.Tools.Should().HaveCount(1); + chatMessageRequest.Tools![0].Name.Should().Be("test-tool"); + chatMessageRequest.Tools[0].Description.Should().Be("test-description"); + chatMessageRequest.Tools[0].InputSchema.Type.Should().Be("object"); + chatMessageRequest.Tools[0].InputSchema.Properties.Should().HaveCount(1); + chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Type.Should().Be("string"); + chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Description.Should().Be("test-description"); + chatMessageRequest.Tools[0].InputSchema.Required.Should().HaveCount(1); + chatMessageRequest.Tools[0].InputSchema.Required[0].Should().Be("test-property"); + chatMessageRequest.Stream.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/TextDeltaTests.cs b/tests/AnthropicClient.Tests/Unit/Models/TextDeltaTests.cs new file mode 100644 index 0000000..9f68a64 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/TextDeltaTests.cs @@ -0,0 +1,40 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class TextDeltaTests : SerializationTest +{ + private readonly string _testJson = @"{ + ""type"": ""text_delta"", + ""text"": ""Hello World!"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeProperties() + { + var text = "Hello World!"; + + var textDelta = new TextDelta(text); + + textDelta.Type.Should().Be("text_delta"); + textDelta.Text.Should().Be(text); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var textDelta = new TextDelta("Hello World!"); + + var actual = Serialize(textDelta); + + JsonAssert.Equal(_testJson, actual); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var expected = new TextDelta("Hello World!"); + + var actual = Deserialize(_testJson); + + actual.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file