Skip to content

Commit ea18d63

Browse files
committed
feat: use custom form content for OSS api
1 parent 3bc04a5 commit ea18d63

File tree

12 files changed

+244
-43
lines changed

12 files changed

+244
-43
lines changed

sample/Cnblogs.DashScope.Sample/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ async Task ChatStreamAsync()
190190

191191
async Task ChatWithImageAsync()
192192
{
193-
var image = await File.ReadAllBytesAsync("Lenna.jpg");
193+
var image = File.OpenRead("Lenna.jpg");
194+
var ossLink = await dashScopeClient.UploadTemporaryFileAsync("qvq-plus", image, "Lenna.jpg");
195+
Console.WriteLine($"Successfully uploaded temp file: {ossLink}");
194196
var response = dashScopeClient.GetMultimodalGenerationStreamAsync(
195197
new ModelRequest<MultimodalInput, IMultimodalParameters>()
196198
{
@@ -201,7 +203,7 @@ async Task ChatWithImageAsync()
201203
[
202204
MultimodalMessage.User(
203205
[
204-
MultimodalMessageContent.ImageContent(image, "image/jpeg"),
206+
MultimodalMessageContent.ImageContent(ossLink),
205207
MultimodalMessageContent.TextContent("她是谁?")
206208
])
207209
]

src/Cnblogs.DashScope.Core/DashScopeClientCore.cs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -322,19 +322,19 @@ public async Task<string> UploadTemporaryFileAsync(
322322
string filename,
323323
DashScopeTemporaryUploadPolicy policy)
324324
{
325-
var filenameStartsWithSlash = filename.StartsWith('/');
326-
var key = filenameStartsWithSlash
327-
? $"{policy.Data.UploadDir}{filename}"
328-
: $"{policy.Data.UploadDir}/{filename}";
329-
330-
var form = new MultipartFormDataContent();
331-
form.Add(new StringContent(policy.Data.OssAccessKeyId), "OSSAccessKeyId");
332-
form.Add(new StringContent(policy.Data.Policy), "policy");
333-
form.Add(new StringContent(policy.Data.Signature), "Signature");
334-
form.Add(new StringContent(key), "key");
335-
form.Add(new StringContent(policy.Data.XOssObjectAcl), "x-oss-object-acl");
336-
form.Add(new StringContent(policy.Data.XOssForbidOverwrite), "x-oss-forbid-overwrite");
337-
form.Add(new StreamContent(fileStream), "file");
325+
var key = $"{policy.Data.UploadDir}/{filename}";
326+
var form = DashScopeMultipartContent.Create();
327+
form.Add(GetFormDataStringContent(policy.Data.OssAccessKeyId, "OSSAccessKeyId"));
328+
form.Add(GetFormDataStringContent(policy.Data.Policy, "policy"));
329+
form.Add(GetFormDataStringContent(policy.Data.Signature, "Signature"));
330+
form.Add(GetFormDataStringContent(key, "key"));
331+
form.Add(GetFormDataStringContent(policy.Data.XOssObjectAcl, "x-oss-object-acl"));
332+
form.Add(GetFormDataStringContent(policy.Data.XOssForbidOverwrite, "x-oss-forbid-overwrite"));
333+
var file = new StreamContent(fileStream);
334+
file.Headers.ContentType = null;
335+
file.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"file\"; filename=\"{filename}\"");
336+
file.Headers.TryAddWithoutValidation("Content-Type", "application/octet-stream");
337+
form.Add(file);
338338
var response = await _httpClient.PostAsync(policy.Data.UploadHost, form);
339339
if (response.IsSuccessStatusCode)
340340
{
@@ -348,6 +348,14 @@ public async Task<string> UploadTemporaryFileAsync(
348348
await response.Content.ReadAsStringAsync());
349349
}
350350

351+
private static StringContent GetFormDataStringContent(string value, string key)
352+
{
353+
var content = new StringContent(value);
354+
content.Headers.ContentType = null;
355+
content.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"{key}\"");
356+
return content;
357+
}
358+
351359
private static HttpRequestMessage BuildSseRequest<TPayload>(HttpMethod method, string url, TPayload payload)
352360
where TPayload : class
353361
{
@@ -389,6 +397,11 @@ private static HttpRequestMessage BuildRequest<TPayload>(
389397
message.Headers.Add("X-DashScope-WorkSpace", config.WorkspaceId);
390398
}
391399

400+
if (payload is IDashScopeOssUploadConfig ossConfig && ossConfig.EnableOssResolve())
401+
{
402+
message.Headers.Add("X-DashScope-OssResourceResolve", "enable");
403+
}
404+
392405
return message;
393406
}
394407

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System.Buffers;
2+
using System.Net;
3+
using System.Text;
4+
5+
namespace Cnblogs.DashScope.Core;
6+
7+
internal class DashScopeMultipartContent : MultipartContent
8+
{
9+
private const string CrLf = "\r\n";
10+
private readonly string _boundary;
11+
12+
private DashScopeMultipartContent(string boundary)
13+
: base("form-data", boundary)
14+
{
15+
_boundary = boundary;
16+
}
17+
18+
/// <inheritdoc />
19+
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
20+
{
21+
// Write start boundary.
22+
await EncodeStringToStreamAsync(stream, "--" + _boundary + CrLf);
23+
24+
// Write each nested content.
25+
var output = new MemoryStream();
26+
var contentIndex = 0;
27+
foreach (var content in this)
28+
{
29+
output.SetLength(0);
30+
SerializeHeadersToStream(output, content, writeDivider: contentIndex != 0);
31+
output.Position = 0;
32+
await output.CopyToAsync(stream);
33+
await content.CopyToAsync(stream, context);
34+
contentIndex++;
35+
}
36+
37+
// Write footer boundary.
38+
await EncodeStringToStreamAsync(stream, CrLf + "--" + _boundary + "--" + CrLf);
39+
}
40+
41+
/// <inheritdoc />
42+
protected override bool TryComputeLength(out long length)
43+
{
44+
var success = base.TryComputeLength(out length);
45+
return success;
46+
}
47+
48+
private void SerializeHeadersToStream(Stream stream, HttpContent content, bool writeDivider)
49+
{
50+
// Add divider.
51+
if (writeDivider)
52+
{
53+
WriteToStream(stream, CrLf + "--");
54+
WriteToStream(stream, _boundary);
55+
WriteToStream(stream, CrLf);
56+
}
57+
58+
// Add headers.
59+
foreach (var headerPair in content.Headers.NonValidated)
60+
{
61+
var headerValueEncoding = HeaderEncodingSelector?.Invoke(headerPair.Key, content)
62+
?? Encoding.UTF8;
63+
64+
WriteToStream(stream, headerPair.Key);
65+
WriteToStream(stream, ": ");
66+
var delim = string.Empty;
67+
foreach (var value in headerPair.Value)
68+
{
69+
WriteToStream(stream, delim);
70+
WriteToStream(stream, value, headerValueEncoding);
71+
delim = ", ";
72+
}
73+
74+
WriteToStream(stream, CrLf);
75+
}
76+
77+
WriteToStream(stream, CrLf);
78+
}
79+
80+
private static void WriteToStream(Stream stream, string content) => WriteToStream(stream, content, Encoding.UTF8);
81+
82+
private static void WriteToStream(Stream stream, string content, Encoding encoding)
83+
{
84+
const int stackallocThreshold = 1024;
85+
86+
var maxLength = encoding.GetMaxByteCount(content.Length);
87+
88+
byte[]? rentedBuffer = null;
89+
var buffer = maxLength <= stackallocThreshold
90+
? stackalloc byte[stackallocThreshold]
91+
: (rentedBuffer = ArrayPool<byte>.Shared.Rent(maxLength));
92+
93+
try
94+
{
95+
var written = encoding.GetBytes(content, buffer);
96+
stream.Write(buffer.Slice(0, written));
97+
}
98+
finally
99+
{
100+
if (rentedBuffer != null)
101+
{
102+
ArrayPool<byte>.Shared.Return(rentedBuffer);
103+
}
104+
}
105+
}
106+
107+
private static ValueTask EncodeStringToStreamAsync(Stream stream, string input)
108+
{
109+
var buffer = Encoding.UTF8.GetBytes(input);
110+
return stream.WriteAsync(new ReadOnlyMemory<byte>(buffer));
111+
}
112+
113+
public static DashScopeMultipartContent Create()
114+
{
115+
return Create(Guid.NewGuid().ToString());
116+
}
117+
118+
internal static DashScopeMultipartContent Create(string boundary)
119+
{
120+
var content = new DashScopeMultipartContent(boundary);
121+
content.Headers.ContentType = null;
122+
content.Headers.TryAddWithoutValidation(
123+
"Content-Type",
124+
$"multipart/form-data; boundary={boundary}");
125+
return content;
126+
}
127+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Cnblogs.DashScope.Core.Internals;
4+
5+
/// <summary>
6+
/// Indicates the request have configuration for oss resource resolve.
7+
/// </summary>
8+
public interface IDashScopeOssUploadConfig
9+
{
10+
/// <summary>
11+
/// Needs resolve oss resource.
12+
/// </summary>
13+
public bool EnableOssResolve();
14+
}

src/Cnblogs.DashScope.Core/ModelRequest.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Cnblogs.DashScope.Core;
1+
using Cnblogs.DashScope.Core.Internals;
2+
3+
namespace Cnblogs.DashScope.Core;
24

35
/// <summary>
46
/// Represents a request for model generation.
@@ -23,12 +25,15 @@ public class ModelRequest<TInput>
2325
/// </summary>
2426
/// <typeparam name="TInput">The input type for this request.</typeparam>
2527
/// <typeparam name="TParameter">The option type for this request.</typeparam>
26-
public class ModelRequest<TInput, TParameter> : ModelRequest<TInput>
28+
public class ModelRequest<TInput, TParameter> : ModelRequest<TInput>, IDashScopeOssUploadConfig
2729
where TInput : class
2830
where TParameter : class
2931
{
3032
/// <summary>
3133
/// Optional configuration of this request.
3234
/// </summary>
3335
public TParameter? Parameters { get; set; }
36+
37+
/// <inheritdoc />
38+
public bool EnableOssResolve() => Input is IDashScopeOssUploadConfig config && config.EnableOssResolve();
3439
}
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
namespace Cnblogs.DashScope.Core;
1+
using Cnblogs.DashScope.Core.Internals;
2+
3+
namespace Cnblogs.DashScope.Core;
24

35
/// <summary>
46
/// Represents inputs of a multi-model generation request.
57
/// </summary>
6-
public class MultimodalInput
8+
public class MultimodalInput : IDashScopeOssUploadConfig
79
{
810
/// <summary>
911
/// The messages of context, model will generate from last user message.
1012
/// </summary>
1113
public IEnumerable<MultimodalMessage> Messages { get; set; } = Array.Empty<MultimodalMessage>();
14+
15+
/// <inheritdoc />
16+
public bool EnableOssResolve() => Messages.Any(m => m.IsOss());
1217
}

src/Cnblogs.DashScope.Core/MultimodalMessage.cs

Lines changed: 5 additions & 1 deletion
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

@@ -14,6 +15,7 @@ public record MultimodalMessage(
1415
string? ReasoningContent = null)
1516
: IMessage<IReadOnlyList<MultimodalMessageContent>>
1617
{
18+
1719
/// <summary>
1820
/// Create a user message.
1921
/// </summary>
@@ -46,4 +48,6 @@ public static MultimodalMessage Assistant(
4648
{
4749
return new MultimodalMessage(DashScopeRoleNames.Assistant, contents, reasoningContent);
4850
}
51+
52+
internal bool IsOss() => Content.Any(c => c.IsOss());
4953
}

src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Cnblogs.DashScope.Core;
1+
using System.Text.Json.Serialization;
2+
3+
namespace Cnblogs.DashScope.Core;
24

35
/// <summary>
46
/// Represents one content of a <see cref="MultimodalMessage"/>.
@@ -17,6 +19,8 @@ public record MultimodalMessageContent(
1719
int? MinPixels = null,
1820
int? MaxPixels = null)
1921
{
22+
private const string OssSchema = "oss://";
23+
2024
/// <summary>
2125
/// Represents an image content.
2226
/// </summary>
@@ -78,4 +82,9 @@ public static MultimodalMessageContent VideoContent(IEnumerable<string> videoUrl
7882
{
7983
return new MultimodalMessageContent(Video: videoUrls);
8084
}
85+
86+
internal bool IsOss()
87+
=> Image?.StartsWith(OssSchema) == true
88+
|| Audio?.StartsWith(OssSchema) == true
89+
|| Video?.Any(v => v.StartsWith(OssSchema)) == true;
8190
}

test/Cnblogs.DashScope.Sdk.UnitTests/UploadSerializationTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ public async Task Upload_SubmitFileForm_SuccessAsync()
3535
var (client, handler) = await Sut.GetTestClientAsync(new HttpResponseMessage(HttpStatusCode.NoContent));
3636

3737
// Act
38-
var uri = await client.UploadTemporaryFileAsync(file.OpenRead(), file.Name, policy);
39-
var expectedRequestForm = await testCase.GetRequestFormAsync(false);
38+
var ossUri = await client.UploadTemporaryFileAsync(file.OpenRead(), file.Name, policy);
39+
var expectedRequestForm = testCase.GetRequestForm(false);
4040

4141
// Assert
4242
handler.Received().MockSend(
4343
Arg.Is<HttpRequestMessage>(r
4444
=> r.RequestUri == new Uri(policy.Data.UploadHost)
4545
&& Checkers.CheckFormContent(r, expectedRequestForm)),
4646
Arg.Any<CancellationToken>());
47-
Assert.Equal($"oss://{policy.Data.UploadDir}/{file.Name}", uri);
47+
Assert.Equal($"oss://{policy.Data.UploadDir}/{file.Name}", ossUri);
4848
}
4949

5050
[Fact]
@@ -59,7 +59,7 @@ public async Task Upload_GetOssLinkDirectly_SuccessAsync()
5959

6060
// Act
6161
var uri = await client.UploadTemporaryFileAsync("qwen-vl-plus", file.OpenRead(), file.Name);
62-
var expectedRequestForm = await testCase.GetRequestFormAsync(false);
62+
var expectedRequestForm = testCase.GetRequestForm(false);
6363

6464
// Assert
6565
handler.Received().MockSend(
Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,28 @@
11
--5aa22a67-eae4-4c54-8f62-c486fefd11a5
22
Content-Type: text/plain; charset=utf-8
33
Content-Disposition: form-data; name=OSSAccessKeyId
4-
54
LTAI5tG7vL6zZFFbuNrkCjdo
65
--5aa22a67-eae4-4c54-8f62-c486fefd11a5
76
Content-Type: text/plain; charset=utf-8
87
Content-Disposition: form-data; name=policy
9-
108
eyJleHBpcmF0aW9uIjoiMjAyNS0wNy0xMlQxMjoxMDoyNC40ODhaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA3Mzc0MTgyNF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJkYXNoc2NvcGUtaW5zdGFudFwvNTJhZmUwNzdmYjQ4MjVjNmQ3NDQxMTc1OGNiMWFiOThcLzIwMjUtMDctMTJcL2I3NDRmNGY4LTFhOWMtOWM2Yi05NTBkLTBkMzI3ZTMzMWYyZiJdLHsiYnVja2V0IjoiZGFzaHNjb3BlLWZpbGUtbWdyIn0seyJ4LW9zcy1vYmplY3QtYWNsIjoicHJpdmF0ZSJ9LHsieC1vc3MtZm9yYmlkLW92ZXJ3cml0ZSI6InRydWUifV19
119
--5aa22a67-eae4-4c54-8f62-c486fefd11a5
1210
Content-Type: text/plain; charset=utf-8
1311
Content-Disposition: form-data; name=Signature
14-
1512
n3dNX/aD3+WAly0QgzsURfiIk00=
1613
--5aa22a67-eae4-4c54-8f62-c486fefd11a5
1714
Content-Type: text/plain; charset=utf-8
1815
Content-Disposition: form-data; name=key
19-
2016
dashscope-instant/52afe077fb4825c6d74411758cb1ab98/2025-07-12/b744f4f8-1a9c-9c6b-950d-0d327e331f2f/Lenna.jpg
2117
--5aa22a67-eae4-4c54-8f62-c486fefd11a5
2218
Content-Type: text/plain; charset=utf-8
2319
Content-Disposition: form-data; name=x-oss-object-acl
24-
2520
private
2621
--5aa22a67-eae4-4c54-8f62-c486fefd11a5
2722
Content-Type: text/plain; charset=utf-8
2823
Content-Disposition: form-data; name=x-oss-forbid-overwrite
29-
3024
true
3125
--5aa22a67-eae4-4c54-8f62-c486fefd11a5
3226
Content-Disposition: form-data; name=file
33-
3427
1
3528
--5aa22a67-eae4-4c54-8f62-c486fefd11a5--

0 commit comments

Comments
 (0)