Skip to content

Commit 959f658

Browse files
authored
Fix serialization of response continuation tokens (#7356)
Utilizing the same approach recently utilized in modelcontextprotocol/csharp-sdk.
1 parent b56c778 commit 959f658

6 files changed

Lines changed: 185 additions & 4 deletions

File tree

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,15 @@ protected ChatOptions(ChatOptions? other)
189189
/// </remarks>
190190
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations, UrlFormat = DiagnosticIds.UrlFormat)]
191191
[JsonIgnore]
192-
public bool? AllowBackgroundResponses { get; set; }
192+
public bool? AllowBackgroundResponses
193+
{
194+
get => AllowBackgroundResponsesCore;
195+
set => AllowBackgroundResponsesCore = value;
196+
}
197+
198+
[JsonInclude]
199+
[JsonPropertyName("allowBackgroundResponses")]
200+
internal bool? AllowBackgroundResponsesCore { get; set; }
193201

194202
/// <summary>Gets or sets the continuation token for resuming and getting the result of the chat response identified by this token.</summary>
195203
/// <remarks>
@@ -204,7 +212,15 @@ protected ChatOptions(ChatOptions? other)
204212
/// </remarks>
205213
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations, UrlFormat = DiagnosticIds.UrlFormat)]
206214
[JsonIgnore]
207-
public ResponseContinuationToken? ContinuationToken { get; set; }
215+
public ResponseContinuationToken? ContinuationToken
216+
{
217+
get => ContinuationTokenCore;
218+
set => ContinuationTokenCore = value;
219+
}
220+
221+
[JsonInclude]
222+
[JsonPropertyName("continuationToken")]
223+
internal ResponseContinuationToken? ContinuationTokenCore { get; set; }
208224

209225
/// <summary>
210226
/// Gets or sets a callback responsible for creating the raw representation of the chat options from an underlying implementation.

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,15 @@ public IList<ChatMessage> Messages
103103
/// </remarks>
104104
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations, UrlFormat = DiagnosticIds.UrlFormat)]
105105
[JsonIgnore]
106-
public ResponseContinuationToken? ContinuationToken { get; set; }
106+
public ResponseContinuationToken? ContinuationToken
107+
{
108+
get => ContinuationTokenCore;
109+
set => ContinuationTokenCore = value;
110+
}
111+
112+
[JsonInclude]
113+
[JsonPropertyName("continuationToken")]
114+
internal ResponseContinuationToken? ContinuationTokenCore { get; set; }
107115

108116
/// <summary>Gets or sets the raw representation of the chat response from an underlying implementation.</summary>
109117
/// <remarks>

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseUpdate.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,15 @@ public IList<AIContent> Contents
173173
/// </remarks>
174174
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations, UrlFormat = DiagnosticIds.UrlFormat)]
175175
[JsonIgnore]
176-
public ResponseContinuationToken? ContinuationToken { get; set; }
176+
public ResponseContinuationToken? ContinuationToken
177+
{
178+
get => ContinuationTokenCore;
179+
set => ContinuationTokenCore = value;
180+
}
181+
182+
[JsonInclude]
183+
[JsonPropertyName("continuationToken")]
184+
internal ResponseContinuationToken? ContinuationTokenCore { get; set; }
177185

178186
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
179187
[DebuggerBrowsable(DebuggerBrowsableState.Never)]

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,43 @@ public void JsonSerialization_Roundtrips()
227227
Assert.Equal("value", ((JsonElement)value!).GetString());
228228
}
229229

230+
[Fact]
231+
public void JsonSerialization_Roundtrips_DefaultOptions()
232+
{
233+
ChatOptions original = new()
234+
{
235+
ConversationId = "12345",
236+
Instructions = "Some instructions",
237+
Temperature = 0.1f,
238+
MaxOutputTokens = 2,
239+
ModelId = "modelId",
240+
AllowBackgroundResponses = true,
241+
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
242+
AdditionalProperties = new() { ["key"] = "value" },
243+
};
244+
245+
string json = JsonSerializer.Serialize(original, AIJsonUtilities.DefaultOptions);
246+
247+
ChatOptions? result = JsonSerializer.Deserialize<ChatOptions>(json, AIJsonUtilities.DefaultOptions);
248+
249+
Assert.NotNull(result);
250+
Assert.Equal("12345", result.ConversationId);
251+
Assert.Equal("Some instructions", result.Instructions);
252+
Assert.Equal(0.1f, result.Temperature);
253+
Assert.Equal(2, result.MaxOutputTokens);
254+
Assert.Equal("modelId", result.ModelId);
255+
256+
Assert.True(result.AllowBackgroundResponses);
257+
258+
Assert.NotNull(result.ContinuationToken);
259+
Assert.Equal(new byte[] { 1, 2, 3 }, result.ContinuationToken.ToBytes().ToArray());
260+
261+
Assert.NotNull(result.AdditionalProperties);
262+
Assert.Single(result.AdditionalProperties);
263+
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
264+
Assert.Equal("value", value?.ToString());
265+
}
266+
230267
[Fact]
231268
public void CopyConstructors_EnableHierarchyCloning()
232269
{

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public void JsonSerialization_Roundtrips()
8888
{
8989
ResponseId = "id",
9090
ModelId = "modelId",
91+
ConversationId = "conv123",
9192
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
9293
FinishReason = ChatFinishReason.ContentFilter,
9394
Usage = new UsageDetails(),
@@ -105,6 +106,7 @@ public void JsonSerialization_Roundtrips()
105106

106107
Assert.Equal("id", result.ResponseId);
107108
Assert.Equal("modelId", result.ModelId);
109+
Assert.Equal("conv123", result.ConversationId);
108110
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), result.CreatedAt);
109111
Assert.Equal(ChatFinishReason.ContentFilter, result.FinishReason);
110112
Assert.NotNull(result.Usage);
@@ -116,6 +118,46 @@ public void JsonSerialization_Roundtrips()
116118
Assert.Equal("value", ((JsonElement)value!).GetString());
117119
}
118120

121+
[Fact]
122+
public void JsonSerialization_Roundtrips_DefaultOptions()
123+
{
124+
ChatResponse original = new(new ChatMessage(ChatRole.Assistant, "the message"))
125+
{
126+
ResponseId = "id",
127+
ModelId = "modelId",
128+
ConversationId = "conv123",
129+
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
130+
FinishReason = ChatFinishReason.ContentFilter,
131+
Usage = new UsageDetails(),
132+
RawRepresentation = new(),
133+
AdditionalProperties = new() { ["key"] = "value" },
134+
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
135+
};
136+
137+
string json = JsonSerializer.Serialize(original, AIJsonUtilities.DefaultOptions);
138+
139+
ChatResponse? result = JsonSerializer.Deserialize<ChatResponse>(json, AIJsonUtilities.DefaultOptions);
140+
141+
Assert.NotNull(result);
142+
Assert.Equal(ChatRole.Assistant, result.Messages.Single().Role);
143+
Assert.Equal("the message", result.Messages.Single().Text);
144+
145+
Assert.Equal("id", result.ResponseId);
146+
Assert.Equal("modelId", result.ModelId);
147+
Assert.Equal("conv123", result.ConversationId);
148+
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), result.CreatedAt);
149+
Assert.Equal(ChatFinishReason.ContentFilter, result.FinishReason);
150+
Assert.NotNull(result.Usage);
151+
152+
Assert.NotNull(result.ContinuationToken);
153+
Assert.Equal(new byte[] { 1, 2, 3 }, result.ContinuationToken.ToBytes().ToArray());
154+
155+
Assert.NotNull(result.AdditionalProperties);
156+
Assert.Single(result.AdditionalProperties);
157+
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
158+
Assert.Equal("value", value?.ToString());
159+
}
160+
119161
[Fact]
120162
public void ToString_OutputsText()
121163
{

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ public void JsonSerialization_Roundtrips()
127127
RawRepresentation = new object(),
128128
ResponseId = "id",
129129
MessageId = "messageid",
130+
ModelId = "gpt-4",
131+
ConversationId = "conv123",
130132
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
131133
FinishReason = ChatFinishReason.ContentFilter,
132134
AdditionalProperties = new() { ["key"] = "value" },
@@ -158,6 +160,8 @@ public void JsonSerialization_Roundtrips()
158160
Assert.Equal(ChatRole.Assistant, result.Role);
159161
Assert.Equal("id", result.ResponseId);
160162
Assert.Equal("messageid", result.MessageId);
163+
Assert.Equal("gpt-4", result.ModelId);
164+
Assert.Equal("conv123", result.ConversationId);
161165
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), result.CreatedAt);
162166
Assert.Equal(ChatFinishReason.ContentFilter, result.FinishReason);
163167

@@ -168,6 +172,72 @@ public void JsonSerialization_Roundtrips()
168172
Assert.Equal("value", ((JsonElement)value!).GetString());
169173
}
170174

175+
[Fact]
176+
public void JsonSerialization_Roundtrips_DefaultOptions()
177+
{
178+
ChatResponseUpdate original = new()
179+
{
180+
AuthorName = "author",
181+
Role = ChatRole.Assistant,
182+
Contents =
183+
[
184+
new TextContent("text-1"),
185+
new DataContent("data:image/png;base64,aGVsbG8="),
186+
new FunctionCallContent("callId1", "fc1"),
187+
new DataContent("data"u8.ToArray(), "text/plain"),
188+
new TextContent("text-2"),
189+
],
190+
RawRepresentation = new object(),
191+
ResponseId = "id",
192+
MessageId = "messageid",
193+
ModelId = "gpt-4",
194+
ConversationId = "conv123",
195+
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
196+
FinishReason = ChatFinishReason.ContentFilter,
197+
AdditionalProperties = new() { ["key"] = "value" },
198+
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
199+
};
200+
201+
string json = JsonSerializer.Serialize(original, AIJsonUtilities.DefaultOptions);
202+
203+
ChatResponseUpdate? result = JsonSerializer.Deserialize<ChatResponseUpdate>(json, AIJsonUtilities.DefaultOptions);
204+
205+
Assert.NotNull(result);
206+
Assert.Equal(5, result.Contents.Count);
207+
208+
Assert.IsType<TextContent>(result.Contents[0]);
209+
Assert.Equal("text-1", ((TextContent)result.Contents[0]).Text);
210+
211+
Assert.IsType<DataContent>(result.Contents[1]);
212+
Assert.Equal("data:image/png;base64,aGVsbG8=", ((DataContent)result.Contents[1]).Uri);
213+
214+
Assert.IsType<FunctionCallContent>(result.Contents[2]);
215+
Assert.Equal("fc1", ((FunctionCallContent)result.Contents[2]).Name);
216+
217+
Assert.IsType<DataContent>(result.Contents[3]);
218+
Assert.Equal("data"u8.ToArray(), ((DataContent)result.Contents[3]).Data.ToArray());
219+
220+
Assert.IsType<TextContent>(result.Contents[4]);
221+
Assert.Equal("text-2", ((TextContent)result.Contents[4]).Text);
222+
223+
Assert.Equal("author", result.AuthorName);
224+
Assert.Equal(ChatRole.Assistant, result.Role);
225+
Assert.Equal("id", result.ResponseId);
226+
Assert.Equal("messageid", result.MessageId);
227+
Assert.Equal("gpt-4", result.ModelId);
228+
Assert.Equal("conv123", result.ConversationId);
229+
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), result.CreatedAt);
230+
Assert.Equal(ChatFinishReason.ContentFilter, result.FinishReason);
231+
232+
Assert.NotNull(result.ContinuationToken);
233+
Assert.Equal(new byte[] { 1, 2, 3 }, result.ContinuationToken.ToBytes().ToArray());
234+
235+
Assert.NotNull(result.AdditionalProperties);
236+
Assert.Single(result.AdditionalProperties);
237+
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
238+
Assert.Equal("value", value?.ToString());
239+
}
240+
171241
[Fact]
172242
public void Clone_CreatesShallowCopy()
173243
{

0 commit comments

Comments
 (0)