Skip to content

Commit e119dbc

Browse files
committed
feat: support file upload and qwen-long
1 parent 2388620 commit e119dbc

33 files changed

+697
-19
lines changed

src/Cnblogs.DashScope.Core/ChatMessage.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Cnblogs.DashScope.Core.Internals;
1+
using System.Text.Json.Serialization;
2+
using Cnblogs.DashScope.Core.Internals;
23

34
namespace Cnblogs.DashScope.Core;
45

@@ -9,4 +10,28 @@ namespace Cnblogs.DashScope.Core;
910
/// <param name="Content">The content of this message.</param>
1011
/// <param name="Name">Used when role is tool, represents the function name of this message generated by.</param>
1112
/// <param name="ToolCalls">Calls to the function.</param>
12-
public record ChatMessage(string Role, string Content, string? Name = null, List<ToolCall>? ToolCalls = null) : IMessage<string>;
13+
[method: JsonConstructor]
14+
public record ChatMessage(
15+
string Role,
16+
string Content,
17+
string? Name = null,
18+
List<ToolCall>? ToolCalls = null) : IMessage<string>
19+
{
20+
/// <summary>
21+
/// Create chat message from an uploaded DashScope file.
22+
/// </summary>
23+
/// <param name="fileId">The id of the file.</param>
24+
public ChatMessage(DashScopeFileId fileId)
25+
: this("system", fileId.ToUrl())
26+
{
27+
}
28+
29+
/// <summary>
30+
/// Create chat message from multiple DashScope file.
31+
/// </summary>
32+
/// <param name="fileIds">Ids of the files.</param>
33+
public ChatMessage(IEnumerable<DashScopeFileId> fileIds)
34+
: this("system", string.Join(',', fileIds.Select(f => f.ToUrl())))
35+
{
36+
}
37+
}

src/Cnblogs.DashScope.Core/DashScopeClientCore.cs

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Net.Http.Headers;
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Net.Http.Headers;
23
using System.Net.Http.Json;
34
using System.Runtime.CompilerServices;
45
using System.Text;
@@ -130,32 +131,32 @@ public async Task<DashScopeTaskList> ListTasksAsync(
130131

131132
if (startTime.HasValue)
132133
{
133-
queryString.Append($"start_time={startTime:YYYYMMDDhhmmss}");
134+
queryString.Append($"&start_time={startTime:YYYYMMDDhhmmss}");
134135
}
135136

136137
if (endTime.HasValue)
137138
{
138-
queryString.Append($"end_time={endTime:YYYYMMDDhhmmss}");
139+
queryString.Append($"&end_time={endTime:YYYYMMDDhhmmss}");
139140
}
140141

141142
if (string.IsNullOrEmpty(modelName) == false)
142143
{
143-
queryString.Append($"model_name={modelName}");
144+
queryString.Append($"&model_name={modelName}");
144145
}
145146

146147
if (status.HasValue)
147148
{
148-
queryString.Append($"status={status}");
149+
queryString.Append($"&status={status}");
149150
}
150151

151152
if (pageNo.HasValue)
152153
{
153-
queryString.Append($"page_no={pageNo}");
154+
queryString.Append($"&page_no={pageNo}");
154155
}
155156

156157
if (pageSize.HasValue)
157158
{
158-
queryString.Append($"page_size={pageSize}");
159+
queryString.Append($"&page_size={pageSize}");
159160
}
160161

161162
var request = BuildRequest(HttpMethod.Get, $"{ApiLinks.Tasks}?{queryString}");
@@ -202,6 +203,41 @@ public async Task<ModelResponse<BackgroundGenerationOutput, BackgroundGeneration
202203
cancellationToken))!;
203204
}
204205

206+
/// <inheritdoc />
207+
public async Task<DashScopeFile> UploadFileAsync(
208+
Stream file,
209+
string filename,
210+
string purpose = "file-extract",
211+
CancellationToken cancellationToken = default)
212+
{
213+
var form = new MultipartFormDataContent();
214+
form.Add(new StreamContent(file), "file", filename);
215+
form.Add(new StringContent(purpose), nameof(purpose));
216+
var request = new HttpRequestMessage(HttpMethod.Post, ApiLinks.Files) { Content = form };
217+
return (await SendCompatibleAsync<DashScopeFile>(request, cancellationToken))!;
218+
}
219+
220+
/// <inheritdoc />
221+
public async Task<DashScopeFile> GetFileAsync(DashScopeFileId id, CancellationToken cancellationToken = default)
222+
{
223+
var request = BuildRequest(HttpMethod.Get, ApiLinks.Files + $"/{id}");
224+
return (await SendCompatibleAsync<DashScopeFile>(request, cancellationToken))!;
225+
}
226+
227+
/// <inheritdoc />
228+
public async Task<DashScopeFileList> ListFilesAsync(CancellationToken cancellationToken = default)
229+
{
230+
var request = BuildRequest(HttpMethod.Get, ApiLinks.Files);
231+
return (await SendCompatibleAsync<DashScopeFileList>(request, cancellationToken))!;
232+
}
233+
234+
/// <inheritdoc />
235+
public async Task<DashScopeDeleteFileResult> DeleteFileAsync(DashScopeFileId id, CancellationToken cancellationToken = default)
236+
{
237+
var request = BuildRequest(HttpMethod.Delete, ApiLinks.Files + $"/{id}");
238+
return (await SendCompatibleAsync<DashScopeDeleteFileResult>(request, cancellationToken))!;
239+
}
240+
205241
private static HttpRequestMessage BuildSseRequest<TPayload>(HttpMethod method, string url, TPayload payload)
206242
where TPayload : class
207243
{
@@ -239,6 +275,24 @@ private static HttpRequestMessage BuildRequest<TPayload>(
239275
return message;
240276
}
241277

278+
private async Task<TResponse?> SendCompatibleAsync<TResponse>(
279+
HttpRequestMessage message,
280+
CancellationToken cancellationToken)
281+
where TResponse : class
282+
{
283+
var response = await GetSuccessResponseAsync<OpenAiErrorResponse>(
284+
message,
285+
r => new DashScopeError()
286+
{
287+
Code = r.Error.Type,
288+
Message = r.Error.Message,
289+
RequestId = string.Empty
290+
},
291+
HttpCompletionOption.ResponseContentRead,
292+
cancellationToken);
293+
return await response.Content.ReadFromJsonAsync<TResponse>(SerializationOptions, cancellationToken);
294+
}
295+
242296
private async Task<TResponse?> SendAsync<TResponse>(HttpRequestMessage message, CancellationToken cancellationToken)
243297
where TResponse : class
244298
{
@@ -286,6 +340,15 @@ private async Task<HttpResponseMessage> GetSuccessResponseAsync(
286340
HttpRequestMessage message,
287341
HttpCompletionOption completeOption = HttpCompletionOption.ResponseContentRead,
288342
CancellationToken cancellationToken = default)
343+
{
344+
return await GetSuccessResponseAsync<DashScopeError>(message, f => f, completeOption, cancellationToken);
345+
}
346+
347+
private async Task<HttpResponseMessage> GetSuccessResponseAsync<TError>(
348+
HttpRequestMessage message,
349+
Func<TError, DashScopeError> errorMapper,
350+
HttpCompletionOption completeOption = HttpCompletionOption.ResponseContentRead,
351+
CancellationToken cancellationToken = default)
289352
{
290353
HttpResponseMessage response;
291354
try
@@ -305,14 +368,31 @@ private async Task<HttpResponseMessage> GetSuccessResponseAsync(
305368
DashScopeError? error = null;
306369
try
307370
{
308-
error = await response.Content.ReadFromJsonAsync<DashScopeError>(SerializationOptions, cancellationToken);
371+
var r = await response.Content.ReadFromJsonAsync<TError>(SerializationOptions, cancellationToken);
372+
error = r == null ? null : errorMapper.Invoke(r);
309373
}
310374
catch (Exception)
311375
{
312376
// ignore
313377
}
314378

379+
await ThrowDashScopeExceptionAsync(error, message, response, cancellationToken);
380+
// will never reach here
381+
return response;
382+
}
383+
384+
[DoesNotReturn]
385+
private static async Task ThrowDashScopeExceptionAsync(
386+
DashScopeError? error,
387+
HttpRequestMessage message,
388+
HttpResponseMessage response,
389+
CancellationToken cancellationToken)
390+
{
315391
var errorMessage = error?.Message ?? await response.Content.ReadAsStringAsync(cancellationToken);
316-
throw new DashScopeException(message.RequestUri?.ToString(), (int)response.StatusCode, error, errorMessage);
392+
throw new DashScopeException(
393+
message.RequestUri?.ToString(),
394+
(int)response.StatusCode,
395+
error,
396+
errorMessage);
317397
}
318398
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Cnblogs.DashScope.Core;
2+
3+
/// <summary>
4+
/// Result of a delete file action.
5+
/// </summary>
6+
/// <param name="Object">Always be "file".</param>
7+
/// <param name="Deleted">Deletion result.</param>
8+
/// <param name="Id">Deleting file's id.</param>
9+
public record DashScopeDeleteFileResult(string Object, bool Deleted, DashScopeFileId Id);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Cnblogs.DashScope.Core;
2+
3+
/// <summary>
4+
/// Represents a DashScope file.
5+
/// </summary>
6+
/// <param name="Id">Id of the file.</param>
7+
/// <param name="Object">Always be "file".</param>
8+
/// <param name="Bytes">Total bytes of the file.</param>
9+
/// <param name="CreatedAt">Unix timestamp(in seconds) of file create time.</param>
10+
/// <param name="Filename">Name of the file.</param>
11+
/// <param name="Purpose">Purpose of the file.</param>
12+
public record DashScopeFile(
13+
DashScopeFileId Id,
14+
string Object,
15+
int Bytes,
16+
int CreatedAt,
17+
string Filename,
18+
string? Purpose);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Text.Json.Serialization;
2+
using Cnblogs.DashScope.Core.Internals;
3+
4+
namespace Cnblogs.DashScope.Core;
5+
6+
/// <summary>
7+
/// Represents file id of the DashScope file.
8+
/// </summary>
9+
[JsonConverter(typeof(DashScopeFileIdConvertor))]
10+
public readonly struct DashScopeFileId
11+
{
12+
/// <summary>
13+
/// Check if two DashScopeFileId equals.
14+
/// </summary>
15+
/// <param name="other"></param>
16+
/// <returns></returns>
17+
public bool Equals(DashScopeFileId other)
18+
{
19+
return Value == other.Value;
20+
}
21+
22+
/// <inheritdoc />
23+
public override bool Equals(object? obj)
24+
{
25+
return obj is DashScopeFileId other && Equals(other);
26+
}
27+
28+
/// <inheritdoc />
29+
public override int GetHashCode()
30+
{
31+
return Value.GetHashCode();
32+
}
33+
34+
/// <summary>
35+
/// Initialize a DashScopeFileId.
36+
/// </summary>
37+
/// <param name="fileId">The id of the file.</param>
38+
public DashScopeFileId(string fileId)
39+
{
40+
Value = fileId;
41+
}
42+
43+
/// <summary>
44+
/// The value of the file id.
45+
/// </summary>
46+
public string Value { get; }
47+
48+
/// <summary>
49+
/// Get url for chat messages.
50+
/// </summary>
51+
/// <returns>Url like <c>fileid://xxxxxxx</c></returns>
52+
public string ToUrl() => "fileid://" + Value;
53+
54+
/// <inheritdoc />
55+
public override string ToString()
56+
{
57+
return Value;
58+
}
59+
60+
/// <summary>
61+
/// Convert string to DashScopeFileId implicitly.
62+
/// </summary>
63+
/// <param name="value">The string value to convert.</param>
64+
/// <returns></returns>
65+
public static implicit operator DashScopeFileId(string value) => new(value);
66+
67+
/// <summary>
68+
/// Check if two file id is same.
69+
/// </summary>
70+
/// <param name="left"></param>
71+
/// <param name="right"></param>
72+
/// <returns></returns>
73+
public static bool operator ==(DashScopeFileId left, DashScopeFileId right) => left.Value == right.Value;
74+
75+
/// <summary>
76+
/// Check if two file id is not same.
77+
/// </summary>
78+
/// <param name="left"></param>
79+
/// <param name="right"></param>
80+
/// <returns></returns>
81+
public static bool operator !=(DashScopeFileId left, DashScopeFileId right) => !(left == right);
82+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Cnblogs.DashScope.Core;
2+
3+
/// <summary>
4+
/// Represents a list of DashScope files.
5+
/// </summary>
6+
/// <param name="Object">Always be "list".</param>
7+
/// <param name="HasMore">True if not reached last page.</param>
8+
/// <param name="Data">Items of current page.</param>
9+
public record DashScopeFileList(string Object, bool HasMore, List<DashScopeFile> Data);

src/Cnblogs.DashScope.Core/IDashScopeClient.cs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Task<ModelResponse<TextEmbeddingOutput, TextEmbeddingTokenUsage>> GetEmbeddingsA
6666
CancellationToken cancellationToken = default);
6767

6868
/// <summary>
69-
/// Create a image synthesis task.
69+
/// Create an image synthesis task.
7070
/// </summary>
7171
/// <param name="input">The input of image synthesis task.</param>
7272
/// <param name="cancellationToken">The cancellation token to use.</param>
@@ -130,7 +130,7 @@ Task<ModelResponse<TokenizationOutput, TokenizationUsage>> TokenizeAsync(
130130
CancellationToken cancellationToken = default);
131131

132132
/// <summary>
133-
/// Create a image generation task.
133+
/// Create an image generation task.
134134
/// </summary>
135135
/// <param name="input">The input of task.</param>
136136
/// <param name="cancellationToken">The cancellation token to use.</param>
@@ -149,4 +149,45 @@ public Task<ModelResponse<BackgroundGenerationOutput, BackgroundGenerationUsage>
149149
CreateBackgroundGenerationTaskAsync(
150150
ModelRequest<BackgroundGenerationInput, IBackgroundGenerationParameters> input,
151151
CancellationToken cancellationToken = default);
152+
153+
/// <summary>
154+
/// Upload file for model to reference.
155+
/// </summary>
156+
/// <param name="file">File data.</param>
157+
/// <param name="filename">Name of the file.</param>
158+
/// <param name="purpose">Purpose of the file, use "file-extract" to allow model access the file.</param>
159+
/// <param name="cancellationToken">The cancellation token to use.</param>
160+
/// <returns></returns>
161+
public Task<DashScopeFile> UploadFileAsync(
162+
Stream file,
163+
string filename,
164+
string purpose = "file-extract",
165+
CancellationToken cancellationToken = default);
166+
167+
/// <summary>
168+
/// Get DashScope file by id.
169+
/// </summary>
170+
/// <param name="id">Id of the file.</param>
171+
/// <param name="cancellationToken">The cancellation token to use.</param>
172+
/// <returns></returns>
173+
/// <exception cref="DashScopeException">Throws when file not exists, Status will be 404 in this case.</exception>
174+
public Task<DashScopeFile> GetFileAsync(DashScopeFileId id, CancellationToken cancellationToken = default);
175+
176+
/// <summary>
177+
/// List DashScope files.
178+
/// </summary>
179+
/// <param name="cancellationToken">The cancellation token to use.</param>
180+
/// <returns></returns>
181+
public Task<DashScopeFileList> ListFilesAsync(CancellationToken cancellationToken = default);
182+
183+
/// <summary>
184+
/// Delete DashScope file.
185+
/// </summary>
186+
/// <param name="id">The id of the file to delete.</param>
187+
/// <param name="cancellationToken">The cancellation token to use.</param>
188+
/// <returns></returns>
189+
/// <exception cref="DashScopeException">Throws when file not exists, Status would be 404.</exception>
190+
public Task<DashScopeDeleteFileResult> DeleteFileAsync(
191+
DashScopeFileId id,
192+
CancellationToken cancellationToken = default);
152193
}

src/Cnblogs.DashScope.Core/Internals/ApiLinks.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ internal static class ApiLinks
1010
public const string BackgroundGeneration = "services/aigc/background-generation/generation/";
1111
public const string Tasks = "tasks/";
1212
public const string Tokenizer = "tokenizer";
13+
public const string Files = "/compatible-mode/v1/files";
1314
}

0 commit comments

Comments
 (0)