Skip to content

Commit d80f32c

Browse files
committed
ChatMessageTypes.FileLink now supports openai provider
1 parent 279b0ed commit d80f32c

File tree

7 files changed

+163
-11
lines changed

7 files changed

+163
-11
lines changed

src/LlmTornado.Demo/ChatDemo2.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,28 @@ public static async Task PerplexitySecSearchTest()
18121812
Console.WriteLine(response.Result?.Usage?.TotalTokens);
18131813
}
18141814

1815+
[TornadoTest]
1816+
public static async Task OpenAiFileInput()
1817+
{
1818+
TornadoApi api = Program.Connect();
1819+
HttpCallResult<TornadoFile> uploadedFile = await api.Files.Upload("Static/Files/prezSample.pdf", mimeType: "application/pdf", expiration: new FileUploadExpiration(TimeSpan.FromHours(1)), provider: LLmProviders.OpenAi);
1820+
1821+
Conversation chat = Program.Connect().Chat.CreateConversation(new ChatRequest
1822+
{
1823+
Model = ChatModel.OpenAi.Gpt51.V51
1824+
});
1825+
1826+
chat.AppendUserInput([
1827+
new ChatMessagePart("What is this file about?"),
1828+
new ChatMessagePart(new ChatMessagePartFileLinkData(uploadedFile.Data.Reference))
1829+
]);
1830+
1831+
ChatRichResponse response = await chat.GetResponseRich();
1832+
1833+
Console.WriteLine(response);
1834+
Console.WriteLine(response.Result?.Usage?.TotalTokens);
1835+
}
1836+
18151837
[TornadoTest]
18161838
public static async Task AnthropicFileInput()
18171839
{

src/LlmTornado/Chat/ChatMessageTypes.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ public enum ChatMessageTypes
2424
Audio,
2525

2626
/// <summary>
27-
/// Message part is URI-based file.<br/>
28-
/// <b>Supported only by Google.</b>
27+
/// Message part is URI-based file.
2928
/// </summary>
3029
FileLink,
3130

src/LlmTornado/Chat/ChatRequest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,7 @@ public override void WriteJson(JsonWriter writer, IList<ChatMessage>? value, Jso
10991099
ChatMessageTypes.Image => "image_url",
11001100
ChatMessageTypes.Audio => part.Audio?.Url is not null ? "audio_url" : "input_audio",
11011101
ChatMessageTypes.Video => "video_url",
1102+
ChatMessageTypes.FileLink => "file",
11021103
_ => "text"
11031104
};
11041105

@@ -1180,6 +1181,26 @@ public override void WriteJson(JsonWriter writer, IList<ChatMessage>? value, Jso
11801181
writer.WriteEndObject();
11811182
break;
11821183
}
1184+
case ChatMessageTypes.FileLink:
1185+
{
1186+
writer.WritePropertyName("file");
1187+
writer.WriteStartObject();
1188+
1189+
// writer.WritePropertyName("file_data");
1190+
// writer.WriteValue(part.Video?.Url);
1191+
1192+
writer.WritePropertyName("file_id");
1193+
writer.WriteValue(part.FileLinkData?.File?.Uri ?? part.FileLinkData?.FileUri);
1194+
1195+
if (part.FileLinkData?.File?.Name is not null)
1196+
{
1197+
writer.WritePropertyName("filename");
1198+
writer.WriteValue(part.FileLinkData?.File?.Name);
1199+
}
1200+
1201+
writer.WriteEndObject();
1202+
break;
1203+
}
11831204
}
11841205

11851206
writer.WriteEndObject();

src/LlmTornado/Files/FilePurpose.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,29 @@ public enum FilePurpose
2727
/// Agent purpose for ZAI file uploads.
2828
/// </summary>
2929
[EnumMember(Value = "agent")]
30-
Agent
30+
Agent,
31+
32+
/// <summary>
33+
/// Used in the Batch API.
34+
/// </summary>
35+
[EnumMember(Value = "batch")]
36+
Batch,
37+
38+
/// <summary>
39+
/// Images used for vision fine-tuning.
40+
/// </summary>
41+
[EnumMember(Value = "vision")]
42+
Vision,
43+
44+
/// <summary>
45+
/// Flexible file type for any purpose.
46+
/// </summary>
47+
[EnumMember(Value = "user_data")]
48+
UserData,
49+
50+
/// <summary>
51+
/// Flexible file type for any purpose.
52+
/// </summary>
53+
[EnumMember(Value = "evals")]
54+
Evals
3155
}

src/LlmTornado/Files/FileUploadRequest.cs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,53 @@ internal enum FileUploadRequestStates
2020
PayloadUrlObtained
2121
}
2222

23+
/// <summary>
24+
/// Anchor timestamp after which the expiration policy applies.
25+
/// </summary>
26+
public enum FileUploadExpirationAnchor
27+
{
28+
/// <summary>
29+
/// Anchor to the file creation timestamp.
30+
/// </summary>
31+
CreatedAt
32+
}
33+
34+
/// <summary>
35+
/// Expiration policy for a file. By default, files with purpose=batch expire after 30 days and all other files are persisted until they are manually deleted.
36+
/// </summary>
37+
public class FileUploadExpiration
38+
{
39+
/// <summary>
40+
/// Anchor timestamp after which the expiration policy applies. Supported anchors: created_at.
41+
/// </summary>
42+
[JsonProperty("anchor")]
43+
public FileUploadExpirationAnchor Anchor { get; set; } = FileUploadExpirationAnchor.CreatedAt;
44+
45+
/// <summary>
46+
/// The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days).
47+
/// </summary>
48+
[JsonProperty("seconds")]
49+
public int Seconds { get; set; }
50+
51+
/// <summary>
52+
/// Creates an empty expiration
53+
/// </summary>
54+
public FileUploadExpiration()
55+
{
56+
57+
}
58+
59+
/// <summary>
60+
/// Creates an expiration from a timespan.
61+
/// </summary>
62+
/// <param name="ttl"></param>
63+
public FileUploadExpiration(TimeSpan ttl)
64+
{
65+
Seconds = (int)ttl.TotalSeconds;
66+
}
67+
}
68+
69+
2370
/// <summary>
2471
/// Request to upload a file.
2572
/// </summary>
@@ -50,6 +97,11 @@ public class FileUploadRequest
5097
/// </summary>
5198
public string? DisplayName { get; set; }
5299

100+
/// <summary>
101+
/// Optional expiration policy for the file. Supported only by OpenAI.
102+
/// </summary>
103+
public FileUploadExpiration? Expiration { get; set; }
104+
53105
internal FileUploadRequestStates? InternalState { get; set; }
54106

55107
private static string GetPurpose(FilePurpose purpose)
@@ -59,10 +111,23 @@ private static string GetPurpose(FilePurpose purpose)
59111
FilePurpose.Finetune => "fine-tune",
60112
FilePurpose.Assistants => "assistants",
61113
FilePurpose.Agent => "agent",
114+
FilePurpose.Batch => "batch",
115+
FilePurpose.Vision => "vision",
116+
FilePurpose.UserData => "user_data",
117+
FilePurpose.Evals => "evals",
62118
_ => string.Empty
63119
};
64120
}
65121

122+
private static string GetExpirationAnchor(FileUploadExpirationAnchor anchor)
123+
{
124+
return anchor switch
125+
{
126+
FileUploadExpirationAnchor.CreatedAt => "created_at",
127+
_ => "created_at"
128+
};
129+
}
130+
66131
internal static TornadoFile? Deserialize(LLmProviders provider, string jsonData, string? postData)
67132
{
68133
return provider switch
@@ -79,11 +144,23 @@ private static string GetPurpose(FilePurpose purpose)
79144
LLmProviders.OpenAi, (x, y) =>
80145
{
81146
ByteArrayContent bc = new ByteArrayContent(x.Bytes);
82-
StringContent sc = new StringContent(x.Purpose is null ? "assistants" : GetPurpose(x.Purpose.Value));
147+
StringContent sc = new StringContent(x.Purpose is null ? "user_data" : GetPurpose(x.Purpose.Value));
83148

84149
MultipartFormDataContent content = new MultipartFormDataContent();
85150
content.Add(sc, "purpose");
86151
content.Add(bc, "file", x.Name);
152+
153+
if (x.Expiration is not null)
154+
{
155+
string anchorValue = GetExpirationAnchor(x.Expiration.Anchor);
156+
string secondsValue = x.Expiration.Seconds.ToString();
157+
158+
StringContent anchorContent = new StringContent(anchorValue);
159+
content.Add(anchorContent, "expires_after[anchor]");
160+
161+
StringContent secondsContent = new StringContent(secondsValue);
162+
content.Add(secondsContent, "expires_after[seconds]");
163+
}
87164

88165
return content;
89166
}

src/LlmTornado/Files/FilesEndpoint.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,9 @@ public async Task<HttpCallResult<DeletedTornadoFile>> Delete(string fileId, LLmP
210210
/// </param>
211211
/// <param name="fileName">Determined from path if not set</param>
212212
/// <param name="mimeType">MIME type of the file</param>
213+
/// <param name="expiration">Optional expiration policy for the file. Supported only by OpenAI.</param>
213214
/// <param name="provider">Which provider will be used</param>
214-
public async Task<HttpCallResult<TornadoFile>> Upload(string filePath, FilePurpose purpose = FilePurpose.Finetune, string? fileName = null, string? mimeType = null, LLmProviders? provider = null)
215+
public async Task<HttpCallResult<TornadoFile>> Upload(string filePath, FilePurpose purpose = FilePurpose.UserData, string? fileName = null, string? mimeType = null, FileUploadExpiration? expiration = null, LLmProviders? provider = null)
215216
{
216217
if (!File.Exists(filePath))
217218
{
@@ -227,7 +228,7 @@ public async Task<HttpCallResult<TornadoFile>> Upload(string filePath, FilePurpo
227228
#endif
228229
string finalFileName = fileName ?? (resolvedProvider.Provider is LLmProviders.Google ? Guid.NewGuid().ToString() : Path.GetFileName(filePath)); // google requires alphanum + dashes, up to 40 chars
229230

230-
return await Upload(bytes, finalFileName, purpose, mimeType, resolvedProvider.Provider).ConfigureAwait(false);
231+
return await Upload(bytes, finalFileName, purpose, mimeType, expiration, resolvedProvider.Provider).ConfigureAwait(false);
231232
}
232233

233234
/// <summary>
@@ -242,11 +243,12 @@ public async Task<HttpCallResult<TornadoFile>> Upload(string filePath, FilePurpo
242243
/// </param>
243244
/// <param name="fileName">Determined from path if not set</param>
244245
/// <param name="mimeType">MIME type of the file</param>
246+
/// <param name="expiration">Optional expiration policy for the file. Supported only by OpenAI.</param>
245247
/// <param name="provider">Which provider will be used</param>
246-
public async Task<HttpCallResult<TornadoFile>> Upload(Stream stream, string fileName, FilePurpose purpose = FilePurpose.Finetune, string? mimeType = null, LLmProviders? provider = null)
248+
public async Task<HttpCallResult<TornadoFile>> Upload(Stream stream, string fileName, FilePurpose purpose = FilePurpose.UserData, string? mimeType = null, FileUploadExpiration? expiration = null, LLmProviders? provider = null)
247249
{
248250
byte[] bytes = await stream.ToArrayAsync().ConfigureAwait(false);
249-
return await Upload(bytes, fileName, purpose, mimeType, provider).ConfigureAwait(false);
251+
return await Upload(bytes, fileName, purpose, mimeType, expiration, provider).ConfigureAwait(false);
250252
}
251253

252254
/// <summary>
@@ -261,15 +263,17 @@ public async Task<HttpCallResult<TornadoFile>> Upload(Stream stream, string file
261263
/// </param>
262264
/// <param name="fileName">Determined from path if not set</param>
263265
/// <param name="mimeType">MIME type of the file</param>
266+
/// <param name="expiration">Optional expiration policy for the file. Supported only by OpenAI.</param>
264267
/// <param name="provider">Which provider will be used</param>
265-
public async Task<HttpCallResult<TornadoFile>> Upload(byte[] fileBytes, string fileName, FilePurpose purpose = FilePurpose.Finetune, string? mimeType = null, LLmProviders? provider = null)
268+
public async Task<HttpCallResult<TornadoFile>> Upload(byte[] fileBytes, string fileName, FilePurpose purpose = FilePurpose.UserData, string? mimeType = null, FileUploadExpiration? expiration = null, LLmProviders? provider = null)
266269
{
267270
return await Upload(new FileUploadRequest
268271
{
269272
Bytes = fileBytes,
270273
Name = fileName,
271274
Purpose = purpose,
272-
MimeType = mimeType
275+
MimeType = mimeType,
276+
Expiration = expiration
273277
}, provider).ConfigureAwait(false);
274278
}
275279

src/LlmTornado/Files/TornadoFile.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class TornadoFile
3232
/// The name of the file
3333
/// </summary>
3434
[JsonProperty("filename")]
35-
public string Name { get; set; }
35+
public string? Name { get; set; }
3636

3737
/// <summary>
3838
/// The size of the file in bytes
@@ -93,4 +93,9 @@ public class TornadoFile
9393
/// </summary>
9494
[JsonIgnore]
9595
public FileLinkStates? State { get; set; }
96+
97+
/// <summary>
98+
/// Returns <see cref="Uri"/> or <see cref="Id"/> - this property should be used for cross-provider referencing of the file.
99+
/// </summary>
100+
public string Reference => Uri ?? Id;
96101
}

0 commit comments

Comments
 (0)