Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Support for file inputs in chat completions
  • Loading branch information
joseharriaga committed Mar 18, 2025
commit 21a3352f75b880d75551f3a7a068f9f51214da6e
9 changes: 8 additions & 1 deletion api/OpenAI.net8.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,10 @@ public class ChatMessageContent : ObjectModel.Collection<ChatMessageContentPart>
public ChatMessageContent(string content);
}
public class ChatMessageContentPart : IJsonModel<ChatMessageContentPart>, IPersistableModel<ChatMessageContentPart> {
public BinaryData FileBytes { get; }
public string FileBytesMediaType { get; }
public string FileId { get; }
public string Filename { get; }
public BinaryData ImageBytes { get; }
public string ImageBytesMediaType { get; }
public ChatImageDetailLevel? ImageDetailLevel { get; }
Expand All @@ -1421,6 +1425,8 @@ public class ChatMessageContentPart : IJsonModel<ChatMessageContentPart>, IPersi
public ChatMessageContentPartKind Kind { get; }
public string Refusal { get; }
public string Text { get; }
public static ChatMessageContentPart CreateFilePart(BinaryData fileBytes, string fileBytesMediaType, string filename);
public static ChatMessageContentPart CreateFilePart(string fileId);
public static ChatMessageContentPart CreateImagePart(BinaryData imageBytes, string imageBytesMediaType, ChatImageDetailLevel? imageDetailLevel = null);
public static ChatMessageContentPart CreateImagePart(Uri imageUri, ChatImageDetailLevel? imageDetailLevel = null);
public static ChatMessageContentPart CreateInputAudioPart(BinaryData inputAudioBytes, ChatInputAudioFormat inputAudioFormat);
Expand All @@ -1434,7 +1440,8 @@ public enum ChatMessageContentPartKind {
Text = 0,
Refusal = 1,
Image = 2,
InputAudio = 3
InputAudio = 3,
File = 4
}
public enum ChatMessageRole {
System = 0,
Expand Down
9 changes: 8 additions & 1 deletion api/OpenAI.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,10 @@ public class ChatMessageContent : ObjectModel.Collection<ChatMessageContentPart>
public ChatMessageContent(string content);
}
public class ChatMessageContentPart : IJsonModel<ChatMessageContentPart>, IPersistableModel<ChatMessageContentPart> {
public BinaryData FileBytes { get; }
public string FileBytesMediaType { get; }
public string FileId { get; }
public string Filename { get; }
public BinaryData ImageBytes { get; }
public string ImageBytesMediaType { get; }
public ChatImageDetailLevel? ImageDetailLevel { get; }
Expand All @@ -1332,6 +1336,8 @@ public class ChatMessageContentPart : IJsonModel<ChatMessageContentPart>, IPersi
public ChatMessageContentPartKind Kind { get; }
public string Refusal { get; }
public string Text { get; }
public static ChatMessageContentPart CreateFilePart(BinaryData fileBytes, string fileBytesMediaType, string filename);
public static ChatMessageContentPart CreateFilePart(string fileId);
public static ChatMessageContentPart CreateImagePart(BinaryData imageBytes, string imageBytesMediaType, ChatImageDetailLevel? imageDetailLevel = null);
public static ChatMessageContentPart CreateImagePart(Uri imageUri, ChatImageDetailLevel? imageDetailLevel = null);
public static ChatMessageContentPart CreateInputAudioPart(BinaryData inputAudioBytes, ChatInputAudioFormat inputAudioFormat);
Expand All @@ -1345,7 +1351,8 @@ public enum ChatMessageContentPartKind {
Text = 0,
Refusal = 1,
Image = 2,
InputAudio = 3
InputAudio = 3,
File = 4
}
public enum ChatMessageRole {
System = 0,
Expand Down
14 changes: 13 additions & 1 deletion src/Custom/Chat/ChatMessageContentPart.Serialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ internal static void WriteCoreContentPart(ChatMessageContentPart instance, Utf8J
writer.WritePropertyName("input_audio"u8);
writer.WriteObjectValue(instance._inputAudio, options);
}
else if (instance._kind == ChatMessageContentPartKind.File)
{
writer.WritePropertyName("file"u8);
writer.WriteObjectValue(instance._fileFile, options);
}
writer.WriteSerializedAdditionalRawData(instance._additionalBinaryDataProperties, options);
writer.WriteEndObject();
}
Expand All @@ -56,6 +61,7 @@ internal static ChatMessageContentPart DeserializeChatMessageContentPart(JsonEle
string refusal = default;
InternalChatCompletionRequestMessageContentPartImageImageUrl imageUri = default;
InternalChatCompletionRequestMessageContentPartAudioInputAudio inputAudio = default;
InternalChatCompletionRequestMessageContentPartFileFile fileFile = default;
IDictionary<string, BinaryData> serializedAdditionalRawData = default;
Dictionary<string, BinaryData> rawDataDictionary = new Dictionary<string, BinaryData>();
foreach (var property in element.EnumerateObject())
Expand Down Expand Up @@ -86,12 +92,18 @@ internal static ChatMessageContentPart DeserializeChatMessageContentPart(JsonEle
.DeserializeInternalChatCompletionRequestMessageContentPartAudioInputAudio(property.Value, options);
continue;
}
if (property.NameEquals("file"u8))
{
fileFile = InternalChatCompletionRequestMessageContentPartFileFile
.DeserializeInternalChatCompletionRequestMessageContentPartFileFile(property.Value, options);
continue;
}
if (true)
{
rawDataDictionary.Add(property.Name, BinaryData.FromString(property.Value.GetRawText()));
}
}
serializedAdditionalRawData = rawDataDictionary;
return new ChatMessageContentPart(kind, text, imageUri, refusal, inputAudio, serializedAdditionalRawData);
return new ChatMessageContentPart(kind, text, imageUri, refusal, inputAudio, fileFile, serializedAdditionalRawData);
}
}
57 changes: 57 additions & 0 deletions src/Custom/Chat/ChatMessageContentPart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public partial class ChatMessageContentPart
private readonly string _text;
private readonly InternalChatCompletionRequestMessageContentPartImageImageUrl _imageUri;
private readonly InternalChatCompletionRequestMessageContentPartAudioInputAudio _inputAudio;
private readonly InternalChatCompletionRequestMessageContentPartFileFile _fileFile;
private readonly string _refusal;

// CUSTOM: Made internal.
Expand All @@ -47,13 +48,15 @@ internal ChatMessageContentPart(
InternalChatCompletionRequestMessageContentPartImageImageUrl imageUri = default,
string refusal = default,
InternalChatCompletionRequestMessageContentPartAudioInputAudio inputAudio = default,
InternalChatCompletionRequestMessageContentPartFileFile fileFile = default,
IDictionary<string, BinaryData> serializedAdditionalRawData = default)
{
_kind = kind;
_text = text;
_imageUri = imageUri;
_refusal = refusal;
_inputAudio = inputAudio;
_fileFile = fileFile;
_additionalBinaryDataProperties = serializedAdditionalRawData;
}

Expand Down Expand Up @@ -98,6 +101,26 @@ internal ChatMessageContentPart(
/// </remarks>
public ChatInputAudioFormat? InputAudioFormat => _inputAudio?.Format;

// CUSTOM: Spread.
/// <summary> The ID of the previously uploaded file that the content part represents. </summary>
/// <remarks> Present when <see cref="Kind"/> is <see cref="ChatMessageContentPartKind.File"/> and the content part refers to a previously uploaded file. </remarks>
public string FileId => _fileFile?.FileId;

// CUSTOM: Spread.
/// <summary> The binary file content of the file content part. </summary>
/// <remarks> Present when <see cref="Kind"/> is <see cref="ChatMessageContentPartKind.File"/> and the content refers to data for a new file. </remarks>
public BinaryData FileBytes => _fileFile?.FileBytes;

// CUSTOM: Spread.
/// <summary> The MIME type of the file, e.g., <c>application/pdf</c>. </summary>
/// <remarks> Present when <see cref="Kind"/> is <see cref="ChatMessageContentPartKind.File"/> and the content refers to data for a new file. </remarks>
public string FileBytesMediaType => _fileFile?.FileBytesMediaType;

// CUSTOM: Spread.
/// <summary> The filename for the new file content creation that the content part encapsulates. </summary>
/// <remarks> Present when <see cref="Kind"/> is <see cref="ChatMessageContentPartKind.File"/> and the content refers to data for a new file. </remarks>
public string Filename => _fileFile?.Filename;

// CUSTOM: Spread.
/// <summary>
/// The level of detail with which the model should process the image and generate its textual understanding of
Expand Down Expand Up @@ -184,6 +207,40 @@ public static ChatMessageContentPart CreateInputAudioPart(BinaryData inputAudioB
inputAudio: new(inputAudioBytes, inputAudioFormat));
}

/// <summary> Creates a new <see cref="ChatMessageContentPart"/> that represents a previously uploaded file. </summary>
/// <exception cref="ArgumentException"> <paramref name="fileId"/> is null or empty. </exception>
public static ChatMessageContentPart CreateFilePart(string fileId)
{
Argument.AssertNotNullOrEmpty(fileId, nameof(fileId));

return new ChatMessageContentPart(
kind: ChatMessageContentPartKind.File,
fileFile: new()
{
FileId = fileId,
});
}

/// <summary> Creates a new <see cref="ChatMessageContentPart"/> that encapsulates new file data to upload. </summary>
/// <param name="fileBytes"> The binary content of the file. </param>
/// <param name="fileBytesMediaType"> The MIME type of the file, e.g., <c>application/pdf</c>. </param>
/// <param name="filename"> The filename to use for the file that will be created. </param>
/// <exception cref="ArgumentNullException"> <paramref name="fileBytes"/> or <paramref name="fileBytesMediaType"/> is null. </exception>
/// <exception cref="ArgumentException"> <paramref name="fileBytesMediaType"/> or <paramref name="filename"/>> is an empty string, and was expected to be non-empty. </exception>
public static ChatMessageContentPart CreateFilePart(BinaryData fileBytes, string fileBytesMediaType, string filename)
{
Argument.AssertNotNull(fileBytes, nameof(fileBytes));
Argument.AssertNotNullOrEmpty(fileBytesMediaType, nameof(fileBytesMediaType));
Argument.AssertNotNullOrEmpty(filename, nameof(filename));

return new ChatMessageContentPart(
kind: ChatMessageContentPartKind.File,
fileFile: new(fileBytes, fileBytesMediaType)
{
Filename = filename,
});
}

/// <summary>
/// Implicitly instantiates a new <see cref="ChatMessageContentPart"/> from a <see cref="string"/>. As such,
/// using a <see cref="string"/> in place of a <see cref="ChatMessageContentPart"/> is equivalent to calling the
Expand Down
2 changes: 2 additions & 0 deletions src/Custom/Chat/ChatMessageContentPartKind.Serialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal static partial class ChatMessageContentPartKindExtensions
ChatMessageContentPartKind.Refusal => "refusal",
ChatMessageContentPartKind.Image => "image_url",
ChatMessageContentPartKind.InputAudio => "input_audio",
ChatMessageContentPartKind.File => "file",
_ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown ChatMessageContentPartKind value.")
};

Expand All @@ -23,6 +24,7 @@ public static ChatMessageContentPartKind ToChatMessageContentPartKind(this strin
if (StringComparer.OrdinalIgnoreCase.Equals(value, "refusal")) return ChatMessageContentPartKind.Refusal;
if (StringComparer.OrdinalIgnoreCase.Equals(value, "image_url")) return ChatMessageContentPartKind.Image;
if (StringComparer.OrdinalIgnoreCase.Equals(value, "input_audio")) return ChatMessageContentPartKind.InputAudio;
if (StringComparer.OrdinalIgnoreCase.Equals(value, "file")) return ChatMessageContentPartKind.File;
throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown ChatMessageContentPartKind value.");
}
}
2 changes: 2 additions & 0 deletions src/Custom/Chat/ChatMessageContentPartKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public enum ChatMessageContentPartKind
Image,

InputAudio,

File,
}
7 changes: 1 addition & 6 deletions src/Custom/Chat/Internal/GeneratorStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ internal readonly partial struct InternalCreateChatCompletionStreamResponseObjec
[CodeGenType("CreateChatCompletionStreamResponseServiceTier")]
internal readonly partial struct InternalCreateChatCompletionStreamResponseServiceTier { }

[CodeGenType("CreateChatCompletionStreamResponseUsage1")]
[CodeGenType("CreateChatCompletionStreamResponseUsage")]
internal partial class InternalCreateChatCompletionStreamResponseUsage { }

[CodeGenType("CreateChatCompletionRequestModality")]
Expand Down Expand Up @@ -122,9 +122,6 @@ internal readonly partial struct InternalChatCompletionResponseMessageAnnotation
[CodeGenType("ChatCompletionRequestMessageContentPartFile")]
internal partial class InternalChatCompletionRequestMessageContentPartFile { }

[CodeGenType("ChatCompletionRequestMessageContentPartFileFile")]
internal partial class InternalChatCompletionRequestMessageContentPartFileFile { }

[CodeGenType("CreateChatCompletionRequestWebSearchOptionsUserLocation1")]
internal partial class InternalCreateChatCompletionRequestWebSearchOptionsUserLocation1 { }

Expand All @@ -133,5 +130,3 @@ internal partial class InternalChatCompletionResponseMessageAnnotationUrlCitatio

[CodeGenType("CreateChatCompletionStreamResponseChoiceFinishReason1")]
internal readonly partial struct InternalCreateChatCompletionStreamResponseChoiceFinishReason1 { }

[CodeGenType("CreateChatCompletionStreamResponseUsage")] internal partial class InternalCreateChatCompletionStreamResponseUsage { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Text.RegularExpressions;

namespace OpenAI.Chat;

[CodeGenType("ChatCompletionRequestMessageContentPartFileFile")]
internal partial class InternalChatCompletionRequestMessageContentPartFileFile
{
private readonly BinaryData _fileBytes;
private readonly string _fileBytesMediaType;

// CUSTOM: Changed type from Uri to string to be able to support data URIs properly.
/// <summary> Either a URL of the image or the base64 encoded image data. </summary>
[CodeGenMember("FileData")]
internal string FileData { get; }

public InternalChatCompletionRequestMessageContentPartFileFile(BinaryData fileBytes, string fileBytesMediaType)
{
Argument.AssertNotNull(fileBytes, nameof(fileBytes));
Argument.AssertNotNull(fileBytesMediaType, nameof(fileBytesMediaType));

_fileBytes = fileBytes;
_fileBytesMediaType = fileBytesMediaType;

string base64EncodedData = Convert.ToBase64String(_fileBytes.ToArray());
FileData = $"data:{_fileBytesMediaType};base64,{base64EncodedData}";
}

public BinaryData FileBytes => _fileBytes;

public string FileBytesMediaType => _fileBytesMediaType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
writer.WritePropertyName("filename"u8);
writer.WriteStringValue(Filename);
}
if (Optional.IsDefined(FileData) && _additionalBinaryDataProperties?.ContainsKey("file_data") != true)
{
writer.WritePropertyName("file_data"u8);
writer.WriteStringValue(FileData);
}
if (Optional.IsDefined(FileId) && _additionalBinaryDataProperties?.ContainsKey("file_id") != true)
{
writer.WritePropertyName("file_id"u8);
writer.WriteStringValue(FileId);
}
if (Optional.IsDefined(FileData) && _additionalBinaryDataProperties?.ContainsKey("file_data") != true)
{
writer.WritePropertyName("file_data"u8);
writer.WriteStringValue(FileData);
}
if (_additionalBinaryDataProperties != null)
{
foreach (var item in _additionalBinaryDataProperties)
Expand Down Expand Up @@ -83,8 +83,8 @@ internal static InternalChatCompletionRequestMessageContentPartFileFile Deserial
return null;
}
string filename = default;
string fileData = default;
string fileId = default;
string fileData = default;
IDictionary<string, BinaryData> additionalBinaryDataProperties = new ChangeTrackingDictionary<string, BinaryData>();
foreach (var prop in element.EnumerateObject())
{
Expand All @@ -93,19 +93,19 @@ internal static InternalChatCompletionRequestMessageContentPartFileFile Deserial
filename = prop.Value.GetString();
continue;
}
if (prop.NameEquals("file_data"u8))
if (prop.NameEquals("file_id"u8))
{
fileData = prop.Value.GetString();
fileId = prop.Value.GetString();
continue;
}
if (prop.NameEquals("file_id"u8))
if (prop.NameEquals("file_data"u8))
{
fileId = prop.Value.GetString();
fileData = prop.Value.GetString();
continue;
}
additionalBinaryDataProperties.Add(prop.Name, BinaryData.FromString(prop.Value.GetRawText()));
}
return new InternalChatCompletionRequestMessageContentPartFileFile(filename, fileData, fileId, additionalBinaryDataProperties);
return new InternalChatCompletionRequestMessageContentPartFileFile(filename, fileId, fileData, additionalBinaryDataProperties);
}

BinaryData IPersistableModel<InternalChatCompletionRequestMessageContentPartFileFile>.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,16 @@ public InternalChatCompletionRequestMessageContentPartFileFile()
{
}

internal InternalChatCompletionRequestMessageContentPartFileFile(string filename, string fileData, string fileId, IDictionary<string, BinaryData> additionalBinaryDataProperties)
internal InternalChatCompletionRequestMessageContentPartFileFile(string filename, string fileId, string fileData, IDictionary<string, BinaryData> additionalBinaryDataProperties)
{
Filename = filename;
FileData = fileData;
FileId = fileId;
FileData = fileData;
_additionalBinaryDataProperties = additionalBinaryDataProperties;
}

public string Filename { get; set; }

public string FileData { get; set; }

public string FileId { get; set; }

internal IDictionary<string, BinaryData> SerializedAdditionalRawData
Expand Down
3 changes: 2 additions & 1 deletion src/Generated/OpenAIModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ public static ChatMessageContent ChatMessageContent()
return new ChatMessageContent(additionalBinaryDataProperties: null);
}

public static ChatMessageContentPart ChatMessageContentPart(ChatMessageContentPartKind kind = default, string text = default, InternalChatCompletionRequestMessageContentPartImageImageUrl imageUri = default, string refusal = default, InternalChatCompletionRequestMessageContentPartAudioInputAudio inputAudio = default)
public static ChatMessageContentPart ChatMessageContentPart(ChatMessageContentPartKind kind = default, string text = default, InternalChatCompletionRequestMessageContentPartImageImageUrl imageUri = default, string refusal = default, InternalChatCompletionRequestMessageContentPartAudioInputAudio inputAudio = default, InternalChatCompletionRequestMessageContentPartFileFile fileFile = default)
{

return new ChatMessageContentPart(
Expand All @@ -817,6 +817,7 @@ public static ChatMessageContentPart ChatMessageContentPart(ChatMessageContentPa
imageUri,
refusal,
inputAudio,
fileFile,
serializedAdditionalRawData: null);
}

Expand Down
Binary file added tests/Assets/files_travis_favorite_food.pdf
Binary file not shown.
Loading