Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/OpenAI.net8.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,7 @@ public class ChatCompletionMessageCollectionOptions : IJsonModel<ChatCompletionM
public class ChatCompletionMessageListDatum : IJsonModel<ChatCompletionMessageListDatum>, IPersistableModel<ChatCompletionMessageListDatum> {
public IReadOnlyList<ChatMessageAnnotation> Annotations { get; }
public string Content { get; }
public IList<ChatMessageContentPart> ContentParts { get; }
public string Id { get; }
public ChatOutputAudio OutputAudio { get; }
[EditorBrowsable(EditorBrowsableState.Never)]
Expand Down Expand Up @@ -2232,6 +2233,7 @@ public static class OpenAIChatModelFactory {
public static ChatCompletion ChatCompletion(string id = null, ChatFinishReason finishReason = ChatFinishReason.Stop, ChatMessageContent content = null, string refusal = null, IEnumerable<ChatToolCall> toolCalls = null, ChatMessageRole role = ChatMessageRole.System, ChatFunctionCall functionCall = null, IEnumerable<ChatTokenLogProbabilityDetails> contentTokenLogProbabilities = null, IEnumerable<ChatTokenLogProbabilityDetails> refusalTokenLogProbabilities = null, DateTimeOffset createdAt = default, string model = null, ChatServiceTier? serviceTier = null, string systemFingerprint = null, ChatTokenUsage usage = null, ChatOutputAudio outputAudio = null, IEnumerable<ChatMessageAnnotation> messageAnnotations = null);
[EditorBrowsable(EditorBrowsableState.Never)]
public static ChatCompletion ChatCompletion(string id, ChatFinishReason finishReason, ChatMessageContent content, string refusal, IEnumerable<ChatToolCall> toolCalls, ChatMessageRole role, ChatFunctionCall functionCall, IEnumerable<ChatTokenLogProbabilityDetails> contentTokenLogProbabilities, IEnumerable<ChatTokenLogProbabilityDetails> refusalTokenLogProbabilities, DateTimeOffset createdAt, string model, string systemFingerprint, ChatTokenUsage usage);
public static ChatCompletionMessageListDatum ChatCompletionMessageListDatum(string id, string content, string refusal, ChatMessageRole role, IList<ChatMessageContentPart> contentParts = null, IList<ChatToolCall> toolCalls = null, IList<ChatMessageAnnotation> annotations = null, string functionName = null, string functionArguments = null, ChatOutputAudio outputAudio = null);
public static ChatInputTokenUsageDetails ChatInputTokenUsageDetails(int audioTokenCount = 0, int cachedTokenCount = 0);
[Experimental("OPENAI001")]
public static ChatMessageAnnotation ChatMessageAnnotation(int startIndex = 0, int endIndex = 0, Uri webResourceUri = null, string webResourceTitle = null);
Expand Down
2 changes: 2 additions & 0 deletions api/OpenAI.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,7 @@ public class ChatCompletionMessageCollectionOptions : IJsonModel<ChatCompletionM
public class ChatCompletionMessageListDatum : IJsonModel<ChatCompletionMessageListDatum>, IPersistableModel<ChatCompletionMessageListDatum> {
public IReadOnlyList<ChatMessageAnnotation> Annotations { get; }
public string Content { get; }
public IList<ChatMessageContentPart> ContentParts { get; }
public string Id { get; }
public ChatOutputAudio OutputAudio { get; }
[EditorBrowsable(EditorBrowsableState.Never)]
Expand Down Expand Up @@ -1921,6 +1922,7 @@ public static class OpenAIChatModelFactory {
public static ChatCompletion ChatCompletion(string id = null, ChatFinishReason finishReason = ChatFinishReason.Stop, ChatMessageContent content = null, string refusal = null, IEnumerable<ChatToolCall> toolCalls = null, ChatMessageRole role = ChatMessageRole.System, ChatFunctionCall functionCall = null, IEnumerable<ChatTokenLogProbabilityDetails> contentTokenLogProbabilities = null, IEnumerable<ChatTokenLogProbabilityDetails> refusalTokenLogProbabilities = null, DateTimeOffset createdAt = default, string model = null, ChatServiceTier? serviceTier = null, string systemFingerprint = null, ChatTokenUsage usage = null, ChatOutputAudio outputAudio = null, IEnumerable<ChatMessageAnnotation> messageAnnotations = null);
[EditorBrowsable(EditorBrowsableState.Never)]
public static ChatCompletion ChatCompletion(string id, ChatFinishReason finishReason, ChatMessageContent content, string refusal, IEnumerable<ChatToolCall> toolCalls, ChatMessageRole role, ChatFunctionCall functionCall, IEnumerable<ChatTokenLogProbabilityDetails> contentTokenLogProbabilities, IEnumerable<ChatTokenLogProbabilityDetails> refusalTokenLogProbabilities, DateTimeOffset createdAt, string model, string systemFingerprint, ChatTokenUsage usage);
public static ChatCompletionMessageListDatum ChatCompletionMessageListDatum(string id, string content, string refusal, ChatMessageRole role, IList<ChatMessageContentPart> contentParts = null, IList<ChatToolCall> toolCalls = null, IList<ChatMessageAnnotation> annotations = null, string functionName = null, string functionArguments = null, ChatOutputAudio outputAudio = null);
public static ChatInputTokenUsageDetails ChatInputTokenUsageDetails(int audioTokenCount = 0, int cachedTokenCount = 0);
public static ChatMessageAnnotation ChatMessageAnnotation(int startIndex = 0, int endIndex = 0, Uri webResourceUri = null, string webResourceTitle = null);
public static ChatOutputAudio ChatOutputAudio(BinaryData audioBytes, string id = null, string transcript = null, DateTimeOffset expiresAt = default);
Expand Down
2 changes: 2 additions & 0 deletions specification/base/typespec/chat/models.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ alias CreateChatCompletionStreamResponseChoice = {
alias ChatCompletionMessageListData = {
...ChatCompletionResponseMessage;

content_parts?: ChatCompletionRequestMessageContentPart[] | null;

/** The identifier of the chat message. */
id: string;
};
Expand Down
43 changes: 40 additions & 3 deletions src/Custom/Chat/OpenAIChatModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using OpenAI.Responses;

namespace OpenAI.Chat;

Expand All @@ -29,7 +30,7 @@ public static ChatCompletion ChatCompletion(
ChatCompletion(
id: id,
finishReason: finishReason,
content:content,
content: content,
refusal: refusal,
toolCalls: toolCalls,
role: role,
Expand Down Expand Up @@ -70,13 +71,13 @@ public static ChatCompletion ChatCompletion(
messageAnnotations ??= new List<ChatMessageAnnotation>();

InternalChatCompletionResponseMessage message = new(
content: content,
refusal: refusal,
toolCalls: toolCalls.ToList(),
annotations: messageAnnotations.ToList(),
audio: outputAudio,
role: role,
content: content,
functionCall: functionCall,
audio: outputAudio,
patch: default);

InternalCreateChatCompletionResponseChoiceLogprobs logprobs = new InternalCreateChatCompletionResponseChoiceLogprobs(
Expand Down Expand Up @@ -372,4 +373,40 @@ public static StreamingChatToolCallUpdate StreamingChatToolCallUpdate(int index
toolCallId: toolCallId,
patch: default);
}

/// <summary> Initializes a new instance of <see cref="OpenAI.Chat.ChatCompletionMessageListDatum"/>. </summary>
/// <returns> A new <see cref="OpenAI.Chat.ChatCompletionMessageListDatum"/> instance for mocking.</returns>
public static ChatCompletionMessageListDatum ChatCompletionMessageListDatum(
string id,
string content,
string refusal,
ChatMessageRole role,
IList<ChatMessageContentPart> contentParts = null,
IList<ChatToolCall> toolCalls = null,
IList<ChatMessageAnnotation> annotations = null,
string functionName = null,
string functionArguments = null,
ChatOutputAudio outputAudio = null)
{
InternalChatCompletionResponseMessageFunctionCall functionCall = null;
if (functionName != null && functionArguments != null)
{
functionCall = new(
name: functionName,
arguments: functionArguments,
patch: default);
}

return new ChatCompletionMessageListDatum(
content: content,
contentParts: contentParts,
refusal: refusal,
toolCalls: toolCalls.ToList().AsReadOnly(),
annotations: annotations.ToList().AsReadOnly(),
role: role,
functionCall: functionCall,
outputAudio: outputAudio,
id: id,
patch: default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace OpenAI.Chat
{
public partial class ChatCompletionMessageListDatum : IJsonModel<ChatCompletionMessageListDatum>
{
internal ChatCompletionMessageListDatum() : this(null, null, null, null, default, null, null, null, default)
internal ChatCompletionMessageListDatum() : this(null, null, null, null, default, null, null, null, null, default)
{
}

Expand Down Expand Up @@ -119,6 +119,29 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
writer.WritePropertyName("audio"u8);
writer.WriteObjectValue(OutputAudio, options);
}
if (Patch.Contains("$.content_parts"u8))
{
if (!Patch.IsRemoved("$.content_parts"u8))
{
writer.WritePropertyName("content_parts"u8);
writer.WriteRawValue(Patch.GetJson("$.content_parts"u8));
}
}
else if (Optional.IsCollectionDefined(ContentParts))
{
writer.WritePropertyName("content_parts"u8);
writer.WriteStartArray();
for (int i = 0; i < ContentParts.Count; i++)
{
if (ContentParts[i].Patch.IsRemoved("$"u8))
{
continue;
}
writer.WriteObjectValue(ContentParts[i], options);
}
Patch.WriteTo(writer, "$.content_parts"u8);
writer.WriteEndArray();
}
if (!Patch.Contains("$.id"u8))
{
writer.WritePropertyName("id"u8);
Expand Down Expand Up @@ -155,6 +178,7 @@ internal static ChatCompletionMessageListDatum DeserializeChatCompletionMessageL
ChatMessageRole role = default;
InternalChatCompletionResponseMessageFunctionCall functionCall = default;
ChatOutputAudio outputAudio = default;
IList<ChatMessageContentPart> contentParts = default;
string id = default;
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
JsonPatch patch = new JsonPatch(data is null ? ReadOnlyMemory<byte>.Empty : data.ToMemory());
Expand Down Expand Up @@ -233,6 +257,20 @@ internal static ChatCompletionMessageListDatum DeserializeChatCompletionMessageL
outputAudio = ChatOutputAudio.DeserializeChatOutputAudio(prop.Value, prop.Value.GetUtf8Bytes(), options);
continue;
}
if (prop.NameEquals("content_parts"u8))
{
if (prop.Value.ValueKind == JsonValueKind.Null)
{
continue;
}
List<ChatMessageContentPart> array = new List<ChatMessageContentPart>();
foreach (var item in prop.Value.EnumerateArray())
{
array.Add(ChatMessageContentPart.DeserializeChatMessageContentPart(item, item.GetUtf8Bytes(), options));
}
contentParts = array;
continue;
}
if (prop.NameEquals("id"u8))
{
id = prop.Value.GetString();
Expand All @@ -248,6 +286,7 @@ internal static ChatCompletionMessageListDatum DeserializeChatCompletionMessageL
role,
functionCall,
outputAudio,
contentParts ?? new ChangeTrackingList<ChatMessageContentPart>(),
id,
patch);
}
Expand Down Expand Up @@ -315,6 +354,16 @@ private bool PropagateGet(ReadOnlySpan<byte> jsonPath, out JsonPatch.EncodedValu
}
return Annotations[index].Patch.TryGetEncodedValue([.. "$"u8, .. currentSlice.Slice(bytesConsumed)], out value);
}
if (local.StartsWith("content_parts"u8))
{
int propertyLength = "content_parts"u8.Length;
ReadOnlySpan<byte> currentSlice = local.Slice(propertyLength);
if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed))
{
return false;
}
return ContentParts[index].Patch.TryGetEncodedValue([.. "$"u8, .. currentSlice.Slice(bytesConsumed)], out value);
}
return false;
}
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Expand Down Expand Up @@ -351,6 +400,17 @@ private bool PropagateSet(ReadOnlySpan<byte> jsonPath, JsonPatch.EncodedValue va
Annotations[index].Patch.Set([.. "$"u8, .. currentSlice.Slice(bytesConsumed)], value);
return true;
}
if (local.StartsWith("content_parts"u8))
{
int propertyLength = "content_parts"u8.Length;
ReadOnlySpan<byte> currentSlice = local.Slice(propertyLength);
if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed))
{
return false;
}
ContentParts[index].Patch.Set([.. "$"u8, .. currentSlice.Slice(bytesConsumed)], value);
return true;
}
return false;
}
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ internal ChatCompletionMessageListDatum(string content, string refusal, string i
Refusal = refusal;
ToolCalls = new ChangeTrackingList<ChatToolCall>();
Annotations = new ChangeTrackingList<ChatMessageAnnotation>();
ContentParts = new ChangeTrackingList<ChatMessageContentPart>();
Id = id;
}

#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
internal ChatCompletionMessageListDatum(string content, string refusal, IReadOnlyList<ChatToolCall> toolCalls, IReadOnlyList<ChatMessageAnnotation> annotations, ChatMessageRole role, InternalChatCompletionResponseMessageFunctionCall functionCall, ChatOutputAudio outputAudio, string id, in JsonPatch patch)
internal ChatCompletionMessageListDatum(string content, string refusal, IReadOnlyList<ChatToolCall> toolCalls, IReadOnlyList<ChatMessageAnnotation> annotations, ChatMessageRole role, InternalChatCompletionResponseMessageFunctionCall functionCall, ChatOutputAudio outputAudio, IList<ChatMessageContentPart> contentParts, string id, in JsonPatch patch)
{
// Plugin customization: ensure initialization of collections
Content = content;
Expand All @@ -36,6 +37,7 @@ internal ChatCompletionMessageListDatum(string content, string refusal, IReadOnl
Role = role;
FunctionCall = functionCall;
OutputAudio = outputAudio;
ContentParts = contentParts ?? new ChangeTrackingList<ChatMessageContentPart>();
Id = id;
_patch = patch;
_patch.SetPropagators(PropagateSet, PropagateGet);
Expand All @@ -56,6 +58,8 @@ internal ChatCompletionMessageListDatum(string content, string refusal, IReadOnl

internal InternalChatCompletionResponseMessageFunctionCall FunctionCall { get; }

public IList<ChatMessageContentPart> ContentParts { get; }

public string Id { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ internal InternalChatCompletionRequestMessageContentPartAudio(in JsonPatch patch
}
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.

internal InternalChatCompletionRequestMessageContentPartAudioInputAudio InputAudio { get; }
internal InternalChatCompletionRequestMessageContentPartAudioInputAudio InputAudio { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ internal InternalChatCompletionRequestMessageContentPartAudioInputAudio(BinaryDa
[Experimental("SCME0001")]
public ref JsonPatch Patch => ref _patch;

public BinaryData Data { get; }
public BinaryData Data { get; set; }

public ChatInputAudioFormat Format { get; }
public ChatInputAudioFormat Format { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ internal InternalChatCompletionRequestMessageContentPartFile(in JsonPatch patch,
}
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.

internal InternalChatCompletionRequestMessageContentPartFileFile File { get; }
internal InternalChatCompletionRequestMessageContentPartFileFile File { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ internal InternalChatCompletionRequestMessageContentPartImage(in JsonPatch patch
}
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.

internal InternalChatCompletionRequestMessageContentPartImageImageUrl ImageUrl { get; }
internal InternalChatCompletionRequestMessageContentPartImageImageUrl ImageUrl { get; set; }
}
}
16 changes: 15 additions & 1 deletion tests/Chat/ChatStoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -696,12 +696,15 @@ public async Task GetChatCompletionMessagesWithPagination()
ChatClient client = GetTestClient();

// Create completion with multiple messages (conversation with tool calls)
// and one with multiple content parts
List<ChatMessage> conversationMessages = new()
{
new UserChatMessage("What's the weather like today? Use the weather tool."),
new UserChatMessage("Name something I could do outside in this weather."),
new UserChatMessage("Name something else I could do outside in this weather."),
new UserChatMessage("Name something yet another thing I could do outside in this weather.")
new UserChatMessage([
ChatMessageContentPart.CreateTextPart("Whose logo is this?: "),
ChatMessageContentPart.CreateImagePart(new Uri("https://upload.wikimedia.org/wikipedia/commons/c/c3/Openai.png"))]),
};

// Add function definition to trigger more back-and-forth
Expand Down Expand Up @@ -743,15 +746,26 @@ await RetryWithExponentialBackoffAsync(async () =>
PageSizeLimit = 2
};

bool foundContentParts = false;

await foreach (var message in client.GetChatCompletionMessagesAsync(completion.Id, options))
{
totalMessages++;
lastMessageId = message.Id;

// Check if the message contains any content parts
if (message.ContentParts.Count > 0)
{
foundContentParts = true;
Assert.That(message.ContentParts[0].Text, Is.EqualTo("Whose logo is this?: "));
Assert.That(message.ContentParts[1].ImageBytes, Is.Not.Null);
}
Assert.That(message.Id, Is.Not.Null.And.Not.Empty);

if (totalMessages >= 4) break; // Get a few pages worth
}

Assert.That(foundContentParts, Is.True);
Assert.That(totalMessages, Is.GreaterThan(3));
Assert.That(lastMessageId, Is.Not.Null);
});
Expand Down
Loading