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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" Version="9.9.0" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.9.1" />
</ItemGroup>

</Project>
6 changes: 4 additions & 2 deletions sample/Cnblogs.DashScope.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ async Task ChatStreamAsync()

async Task ChatWithImageAsync()
{
var image = await File.ReadAllBytesAsync("Lenna.jpg");
var image = File.OpenRead("Lenna.jpg");
var ossLink = await dashScopeClient.UploadTemporaryFileAsync("qvq-plus", image, "Lenna.jpg");
Console.WriteLine($"Successfully uploaded temp file: {ossLink}");
var response = dashScopeClient.GetMultimodalGenerationStreamAsync(
new ModelRequest<MultimodalInput, IMultimodalParameters>()
{
Expand All @@ -201,7 +203,7 @@ async Task ChatWithImageAsync()
[
MultimodalMessage.User(
[
MultimodalMessageContent.ImageContent(image, "image/jpeg"),
MultimodalMessageContent.ImageContent(ossLink),
MultimodalMessageContent.TextContent("她是谁?")
])
]
Expand Down
4 changes: 2 additions & 2 deletions src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="JsonSchema.Net.Generation" Version="5.0.4" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.0" />
<PackageReference Include="JsonSchema.Net.Generation" Version="4.6.0" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.1" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Cnblogs.DashScope.Core/Cnblogs.DashScope.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.DeepDev.TokenizerLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" Version="8.0.6" />
</ItemGroup>

</Project>
74 changes: 74 additions & 0 deletions src/Cnblogs.DashScope.Core/DashScopeClientCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,75 @@ public async Task<SpeechSynthesizerSocketSession> CreateSpeechSynthesizerSocketS
return new SpeechSynthesizerSocketSession(socket, modelId);
}

/// <inheritdoc />
public Task<DashScopeTemporaryUploadPolicy?> GetTemporaryUploadPolicyAsync(
string modelId,
CancellationToken cancellationToken = default)
{
var request = BuildRequest(HttpMethod.Get, ApiLinks.Uploads + $"?action=getPolicy&model={modelId}");
return SendAsync<DashScopeTemporaryUploadPolicy>(request, cancellationToken);
}

/// <inheritdoc />
public async Task<string> UploadTemporaryFileAsync(
string modelId,
Stream fileStream,
string filename,
CancellationToken cancellationToken = default)
{
var policy = await GetTemporaryUploadPolicyAsync(modelId, cancellationToken);
if (policy is null)
{
throw new DashScopeException(
"/api/v1/upload",
200,
null,
"GET /api/v1/upload returns empty response, check your connection");
}

return await UploadTemporaryFileAsync(fileStream, filename, policy);
}

/// <inheritdoc />
public async Task<string> UploadTemporaryFileAsync(
Stream fileStream,
string filename,
DashScopeTemporaryUploadPolicy policy)
{
var key = $"{policy.Data.UploadDir}/{filename}";
var form = DashScopeMultipartContent.Create();
form.Add(GetFormDataStringContent(policy.Data.OssAccessKeyId, "OSSAccessKeyId"));
form.Add(GetFormDataStringContent(policy.Data.Policy, "policy"));
form.Add(GetFormDataStringContent(policy.Data.Signature, "Signature"));
form.Add(GetFormDataStringContent(key, "key"));
form.Add(GetFormDataStringContent(policy.Data.XOssObjectAcl, "x-oss-object-acl"));
form.Add(GetFormDataStringContent(policy.Data.XOssForbidOverwrite, "x-oss-forbid-overwrite"));
var file = new StreamContent(fileStream);
file.Headers.ContentType = null;
file.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"file\"; filename=\"{filename}\"");
file.Headers.TryAddWithoutValidation("Content-Type", "application/octet-stream");
form.Add(file);
var response = await _httpClient.PostAsync(policy.Data.UploadHost, form);
if (response.IsSuccessStatusCode)
{
return $"oss://{key}";
}

throw new DashScopeException(
policy.Data.UploadHost,
(int)response.StatusCode,
null,
await response.Content.ReadAsStringAsync());
}

private static StringContent GetFormDataStringContent(string value, string key)
{
var content = new StringContent(value);
content.Headers.ContentType = null;
content.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"{key}\"");
return content;
}

private static HttpRequestMessage BuildSseRequest<TPayload>(HttpMethod method, string url, TPayload payload)
where TPayload : class
{
Expand Down Expand Up @@ -328,6 +397,11 @@ private static HttpRequestMessage BuildRequest<TPayload>(
message.Headers.Add("X-DashScope-WorkSpace", config.WorkspaceId);
}

if (payload is IDashScopeOssUploadConfig ossConfig && ossConfig.EnableOssResolve())
{
message.Headers.Add("X-DashScope-OssResourceResolve", "enable");
}

return message;
}

Expand Down
1 change: 1 addition & 0 deletions src/Cnblogs.DashScope.Core/DashScopeClientWebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public sealed class DashScopeClientWebSocket : IDisposable
};

private readonly IClientWebSocket _socket;
// ReSharper disable once NotAccessedField.Local
private Task? _receiveTask;
private TaskCompletionSource<bool> _taskStartedSignal = new();
private Channel<byte>? _binaryOutput;
Expand Down
127 changes: 127 additions & 0 deletions src/Cnblogs.DashScope.Core/DashScopeMultipartContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Buffers;
using System.Net;
using System.Text;

namespace Cnblogs.DashScope.Core;

internal class DashScopeMultipartContent : MultipartContent
{
private const string CrLf = "\r\n";
private readonly string _boundary;

private DashScopeMultipartContent(string boundary)
: base("form-data", boundary)
{
_boundary = boundary;
}

/// <inheritdoc />
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
// Write start boundary.
await EncodeStringToStreamAsync(stream, "--" + _boundary + CrLf);

// Write each nested content.
var output = new MemoryStream();
var contentIndex = 0;
foreach (var content in this)
{
output.SetLength(0);
SerializeHeadersToStream(output, content, writeDivider: contentIndex != 0);
output.Position = 0;
await output.CopyToAsync(stream);
await content.CopyToAsync(stream, context);
contentIndex++;
}

// Write footer boundary.
await EncodeStringToStreamAsync(stream, CrLf + "--" + _boundary + "--" + CrLf);
}

/// <inheritdoc />
protected override bool TryComputeLength(out long length)
{
var success = base.TryComputeLength(out length);
return success;
}

private void SerializeHeadersToStream(Stream stream, HttpContent content, bool writeDivider)
{
// Add divider.
if (writeDivider)
{
WriteToStream(stream, CrLf + "--");
WriteToStream(stream, _boundary);
WriteToStream(stream, CrLf);
}

// Add headers.
foreach (var headerPair in content.Headers.NonValidated)
{
var headerValueEncoding = HeaderEncodingSelector?.Invoke(headerPair.Key, content)
?? Encoding.UTF8;

WriteToStream(stream, headerPair.Key);
WriteToStream(stream, ": ");
var delim = string.Empty;
foreach (var value in headerPair.Value)
{
WriteToStream(stream, delim);
WriteToStream(stream, value, headerValueEncoding);
delim = ", ";
}

WriteToStream(stream, CrLf);
}

WriteToStream(stream, CrLf);
}

private static void WriteToStream(Stream stream, string content) => WriteToStream(stream, content, Encoding.UTF8);

private static void WriteToStream(Stream stream, string content, Encoding encoding)
{
const int stackallocThreshold = 1024;

var maxLength = encoding.GetMaxByteCount(content.Length);

byte[]? rentedBuffer = null;
var buffer = maxLength <= stackallocThreshold
? stackalloc byte[stackallocThreshold]
: (rentedBuffer = ArrayPool<byte>.Shared.Rent(maxLength));

try
{
var written = encoding.GetBytes(content, buffer);
stream.Write(buffer.Slice(0, written));
}
finally
{
if (rentedBuffer != null)
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}
}

private static ValueTask EncodeStringToStreamAsync(Stream stream, string input)
{
var buffer = Encoding.UTF8.GetBytes(input);
return stream.WriteAsync(new ReadOnlyMemory<byte>(buffer));
}

public static DashScopeMultipartContent Create()
{
return Create(Guid.NewGuid().ToString());
}

internal static DashScopeMultipartContent Create(string boundary)
{
var content = new DashScopeMultipartContent(boundary);
content.Headers.ContentType = null;
content.Headers.TryAddWithoutValidation(
"Content-Type",
$"multipart/form-data; boundary={boundary}");
return content;
}
}
8 changes: 8 additions & 0 deletions src/Cnblogs.DashScope.Core/DashScopeTemporaryUploadPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Cnblogs.DashScope.Core;

/// <summary>
/// Represents one response of get upload policy api call.
/// </summary>
/// <param name="RequestId">Unique id for current request.</param>
/// <param name="Data">The grant data.</param>
public record DashScopeTemporaryUploadPolicy(string RequestId, DashScopeTemporaryUploadPolicyData Data);
26 changes: 26 additions & 0 deletions src/Cnblogs.DashScope.Core/DashScopeTemporaryUploadPolicyData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Cnblogs.DashScope.Core;

/// <summary>
/// Represent data of oss temp file upload grant.
/// </summary>
/// <param name="Policy">Upload policy.</param>
/// <param name="Signature">Upload signature.</param>
/// <param name="UploadDir">Directory that granted to upload.</param>
/// <param name="UploadHost">Hostname that upload to.</param>
/// <param name="ExpireInSeconds">Grant's expiration.</param>
/// <param name="MaxFileSizeMb">Maximum size of file.</param>
/// <param name="CapacityLimitMb">Total upload limit of account.</param>
/// <param name="OssAccessKeyId">Key used to upload.</param>
/// <param name="XOssObjectAcl">Access of the uploaded file.</param>
/// <param name="XOssForbidOverwrite">Can file be overwritten by another file with same name.</param>
public record DashScopeTemporaryUploadPolicyData(
string Policy,
string Signature,
string UploadDir,
string UploadHost,
int ExpireInSeconds,
int MaxFileSizeMb,
int CapacityLimitMb,
string OssAccessKeyId,
string XOssObjectAcl,
string XOssForbidOverwrite);
38 changes: 38 additions & 0 deletions src/Cnblogs.DashScope.Core/IDashScopeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,42 @@ public Task<DashScopeDeleteFileResult> DeleteFileAsync(
public Task<SpeechSynthesizerSocketSession> CreateSpeechSynthesizerSocketSessionAsync(
string modelId,
CancellationToken cancellationToken = default);

/// <summary>
/// Get a temporary upload grant for <see cref="modelId"/> to access.
/// </summary>
/// <param name="modelId">The name of the model.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<DashScopeTemporaryUploadPolicy?> GetTemporaryUploadPolicyAsync(
string modelId,
CancellationToken cancellationToken = default);

/// <summary>
/// Upload file that granted.
/// </summary>
/// <param name="modelId">The model's id that can access the file.</param>
/// <param name="fileStream">The file data.</param>
/// <param name="filename">The name of the file.</param>
/// <param name="cancellationToken"></param>
/// <returns>Oss url of the file.</returns>
/// <exception cref="DashScopeException">Throws if response code is not 200.</exception>
public Task<string> UploadTemporaryFileAsync(
string modelId,
Stream fileStream,
string filename,
CancellationToken cancellationToken = default);

/// <summary>
/// Upload file that granted.
/// </summary>
/// <param name="fileStream">The file data.</param>
/// <param name="filename"></param>
/// <param name="policy">The grant info.</param>
/// <returns></returns>
/// <exception cref="DashScopeException">Throws if response code is not 200.</exception>
public Task<string> UploadTemporaryFileAsync(
Stream fileStream,
string filename,
DashScopeTemporaryUploadPolicy policy);
}
1 change: 1 addition & 0 deletions src/Cnblogs.DashScope.Core/Internals/ApiLinks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal static class ApiLinks
public const string ImageGeneration = "services/aigc/image-generation/generation";
public const string BackgroundGeneration = "services/aigc/background-generation/generation/";
public const string Tasks = "tasks/";
public const string Uploads = "uploads/";
public const string Tokenizer = "tokenizer";
public const string Files = "/compatible-mode/v1/files";
public static string Application(string applicationId) => $"apps/{applicationId}/completion";
Expand Down
12 changes: 12 additions & 0 deletions src/Cnblogs.DashScope.Core/Internals/IDashScopeOssUploadConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Cnblogs.DashScope.Core.Internals;

/// <summary>
/// Indicates the request have configuration for oss resource resolve.
/// </summary>
public interface IDashScopeOssUploadConfig
{
/// <summary>
/// Needs resolve oss resource.
/// </summary>
public bool EnableOssResolve();
}
9 changes: 7 additions & 2 deletions src/Cnblogs.DashScope.Core/ModelRequest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Cnblogs.DashScope.Core;
using Cnblogs.DashScope.Core.Internals;

namespace Cnblogs.DashScope.Core;

/// <summary>
/// Represents a request for model generation.
Expand All @@ -23,12 +25,15 @@ public class ModelRequest<TInput>
/// </summary>
/// <typeparam name="TInput">The input type for this request.</typeparam>
/// <typeparam name="TParameter">The option type for this request.</typeparam>
public class ModelRequest<TInput, TParameter> : ModelRequest<TInput>
public class ModelRequest<TInput, TParameter> : ModelRequest<TInput>, IDashScopeOssUploadConfig
where TInput : class
where TParameter : class
{
/// <summary>
/// Optional configuration of this request.
/// </summary>
public TParameter? Parameters { get; set; }

/// <inheritdoc />
public bool EnableOssResolve() => Input is IDashScopeOssUploadConfig config && config.EnableOssResolve();
}
Loading