diff --git a/.editorconfig b/.editorconfig
index 6b17d25..644e447 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -384,5 +384,5 @@ dotnet_naming_style.s_camelcase.capitalization = camel_case
indent_size = 2
tab_width = 2
-[*.{props,targets,config,nuspec,json}]
+[*.{props,targets,config,nuspec,json,yml}]
indent_size = 2
diff --git "a/.github/ISSUE_TEMPLATE/api-\347\274\272\345\244\261.md" "b/.github/ISSUE_TEMPLATE/api-\347\274\272\345\244\261.md"
new file mode 100644
index 0000000..9cadf76
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/api-\347\274\272\345\244\261.md"
@@ -0,0 +1,16 @@
+---
+name: API 缺失
+about: 百炼文档中提到了某个 API/参数/返回值,但 SDK 中没有
+title: "[API Sync]"
+labels: bug, untriaged
+assignees: ikesnowy
+
+---
+
+**百炼文档链接**
+提供百炼对应的文档链接,以及希望添加的内容
+
+**SDK 对应的方法(如果有)**
+
+**其他信息**
+例如,希望 SDK 通过 Enum 提供代码提示友好的接入
diff --git "a/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md" "b/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md"
new file mode 100644
index 0000000..c9d377f
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md"
@@ -0,0 +1,30 @@
+---
+name: Bug 反馈
+about: 代码不起作用/抛出异常/结果不正确
+title: "[BUG]"
+labels: bug, untriaged
+assignees: ikesnowy
+
+---
+
+**环境信息**
+安装的包及版本(例如 `Cnblogs.DashScope.Sdk/v0.7.6`):
+`.NET` 版本(例如 `net8.0)`:
+使用的模型名称(例如 `qwen-max`):
+
+**复现流程**
+
+进行如下调用时触发了 bug:
+
+```csharp
+// 输入参数
+
+// 调用的方法
+
+```
+
+**预期结果**
+按预期,以上调用应当返回 xxx 结果
+
+**其他相关信息**
+例如初步 debug 截图/部署方法(IIS, docker,k8s,控制台等)/日志信息/使用场景/访问规模等有关 BUG 出现场景的信息。
diff --git "a/.github/ISSUE_TEMPLATE/sdk-\344\275\277\347\224\250\344\275\223\351\252\214.md" "b/.github/ISSUE_TEMPLATE/sdk-\344\275\277\347\224\250\344\275\223\351\252\214.md"
new file mode 100644
index 0000000..1fb6156
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/sdk-\344\275\277\347\224\250\344\275\223\351\252\214.md"
@@ -0,0 +1,24 @@
+---
+name: SDK 相关
+about: SDK 本身的改进,例如升级依赖包版本/添加方法重载/提供额外能力等
+title: "[SDK]"
+labels: untriaged
+assignees: ikesnowy
+
+---
+
+**希望更改的 SDK 内容**
+
+受影响的 SDK API
+```csharp
+// 提供 SDK 的方法名或者调用方法
+```
+
+希望做出的改进
+```csharp
+// 希望以这种方式调用
+```
+
+**其他信息**
+
+使用场景/项目背景/限制条件(例如:无法依赖 xx 包),以及可接受的其他方式
diff --git "a/.github/ISSUE_TEMPLATE/\346\226\207\346\241\243\347\233\270\345\205\263.md" "b/.github/ISSUE_TEMPLATE/\346\226\207\346\241\243\347\233\270\345\205\263.md"
new file mode 100644
index 0000000..90e2b60
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/\346\226\207\346\241\243\347\233\270\345\205\263.md"
@@ -0,0 +1,12 @@
+---
+name: 文档相关
+about: SDK 在文档上需要做出的改进
+title: "[DOC]"
+labels: documentation, untriaged
+assignees: ikesnowy
+
+---
+
+**文档文件位置(如果有)**
+
+**希望做出的改进**
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1ab7c15..0af6bf5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,15 +7,25 @@ on:
branches: [ "main" ]
jobs:
- test:
+ test-net6:
runs-on: ubuntu-latest
- container: mcr.microsoft.com/dotnet/sdk:8.0
+ container: mcr.microsoft.com/dotnet/sdk:6.0
steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Build
- run: dotnet build -c Release
- - name: Test
- run: dotnet test -c Release
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Build
+ run: dotnet build src/Cnblogs.DashScope.AspNetCore -c Release
+ - name: Test
+ run: dotnet test test/Cnblogs.DashScope.Sdk.UnitTests -c Release
+ test-net8:
+ runs-on: ubuntu-latest
+ container: mcr.microsoft.com/dotnet/sdk:8.0
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Build
+ run: dotnet build src/Cnblogs.DashScope.AI -c Release
+ - name: Test
+ run: dotnet test test/Cnblogs.DashScope.AI.UnitTests -c Release
diff --git a/Cnblogs.DashScope.Sdk.sln b/Cnblogs.DashScope.Sdk.sln
index 26d0161..2cb77fe 100644
--- a/Cnblogs.DashScope.Sdk.sln
+++ b/Cnblogs.DashScope.Sdk.sln
@@ -16,10 +16,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cnblogs.DashScope.AspNetCor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cnblogs.DashScope.Core", "src\Cnblogs.DashScope.Core\Cnblogs.DashScope.Core.csproj", "{CC389455-A3EA-4F09-B524-4DC351A1E1AA}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cnblogs.DashScope.Sdk.SnapshotGenerator", "test\Cnblogs.DashScope.Sdk.SnapshotGenerator\Cnblogs.DashScope.Sdk.SnapshotGenerator.csproj", "{5088DE77-1CE3-46FB-B9D0-27A6C9A5EED1}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cnblogs.DashScope.AI", "src\Cnblogs.DashScope.AI\Cnblogs.DashScope.AI.csproj", "{5D5AD75A-8084-4738-AC56-B8A23E649452}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cnblogs.DashScope.AI.UnitTests", "test\Cnblogs.DashScope.AI.UnitTests\Cnblogs.DashScope.AI.UnitTests.csproj", "{25EE79E1-147B-42FD-AFEA-E1550EDD1D36}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cnblogs.DashScope.Tests.Shared", "test\Cnblogs.DashScope.Tests.Shared\Cnblogs.DashScope.Tests.Shared.csproj", "{06F0AF23-445B-4C6F-9E19-570DA9B7435D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -31,8 +33,9 @@ Global
{8885149A-78F0-4C8E-B9AA-87A46EA69219} = {2E15D1EC-4A07-416E-8BE6-D907F509FD35}
{C910495B-87AB-4AC1-989C-B6720695A139} = {008988ED-0A3B-4272-BCC3-7B4110699345}
{CC389455-A3EA-4F09-B524-4DC351A1E1AA} = {008988ED-0A3B-4272-BCC3-7B4110699345}
- {5088DE77-1CE3-46FB-B9D0-27A6C9A5EED1} = {CFC8ECB3-5248-46CD-A56C-EC088F2A3804}
{5D5AD75A-8084-4738-AC56-B8A23E649452} = {008988ED-0A3B-4272-BCC3-7B4110699345}
+ {25EE79E1-147B-42FD-AFEA-E1550EDD1D36} = {CFC8ECB3-5248-46CD-A56C-EC088F2A3804}
+ {06F0AF23-445B-4C6F-9E19-570DA9B7435D} = {CFC8ECB3-5248-46CD-A56C-EC088F2A3804}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FA6A118A-8D26-4B7A-9952-8504B8A0025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -55,13 +58,17 @@ Global
{CC389455-A3EA-4F09-B524-4DC351A1E1AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC389455-A3EA-4F09-B524-4DC351A1E1AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC389455-A3EA-4F09-B524-4DC351A1E1AA}.Release|Any CPU.Build.0 = Release|Any CPU
- {5088DE77-1CE3-46FB-B9D0-27A6C9A5EED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5088DE77-1CE3-46FB-B9D0-27A6C9A5EED1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5088DE77-1CE3-46FB-B9D0-27A6C9A5EED1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5088DE77-1CE3-46FB-B9D0-27A6C9A5EED1}.Release|Any CPU.Build.0 = Release|Any CPU
{5D5AD75A-8084-4738-AC56-B8A23E649452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D5AD75A-8084-4738-AC56-B8A23E649452}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D5AD75A-8084-4738-AC56-B8A23E649452}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D5AD75A-8084-4738-AC56-B8A23E649452}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25EE79E1-147B-42FD-AFEA-E1550EDD1D36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25EE79E1-147B-42FD-AFEA-E1550EDD1D36}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25EE79E1-147B-42FD-AFEA-E1550EDD1D36}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25EE79E1-147B-42FD-AFEA-E1550EDD1D36}.Release|Any CPU.Build.0 = Release|Any CPU
+ {06F0AF23-445B-4C6F-9E19-570DA9B7435D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {06F0AF23-445B-4C6F-9E19-570DA9B7435D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {06F0AF23-445B-4C6F-9E19-570DA9B7435D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {06F0AF23-445B-4C6F-9E19-570DA9B7435D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/Cnblogs.DashScope.Sdk.sln.DotSettings b/Cnblogs.DashScope.Sdk.sln.DotSettings
index 3ad7f9e..9866f59 100644
--- a/Cnblogs.DashScope.Sdk.sln.DotSettings
+++ b/Cnblogs.DashScope.Sdk.sln.DotSettings
@@ -1,4 +1,8 @@
+ True
True
True
- True
\ No newline at end of file
+ True
+ True
+ True
+ True
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index f39a7b6..3eaabd9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- net8.0
+ net6.0
enable
enable
Cnblogs
diff --git a/README.md b/README.md
index efd74b1..18049e2 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,40 @@
English | [简体中文](https://github.com/cnblogs/dashscope-sdk/blob/main/README.zh-Hans.md)
+# Cnblogs.DashScopeSDK
+
[](https://www.nuget.org/packages/Cnblogs.DashScope.AI)
[](https://www.nuget.org/packages/Cnblogs.DashScope.Sdk)
[](https://www.nuget.org/packages/Cnblogs.DashScope.AspNetCore)
-# DashScope SDK for .NET
-
-An unofficial DashScope SDK maintained by Cnblogs.
-
-**Warning**: this project is under active development, **Breaking Changes** may introduced without notice or major version change. Make sure you read the Release Notes before upgrading.
+A non-official DashScope (Bailian) service SDK maintained by Cnblogs.
-# Quick Start
+**Note:** This project is actively under development. Breaking changes may occur even in minor versions. Please review the Release Notes before upgrading.
-## Using `Microsoft.Extensions.AI`
+## Quick Start
-Install `Cnblogs.DashScope.AI` Package
+### Using `Microsoft.Extensions.AI` Interface
+Install NuGet package `Cnblogs.DashScope.AI`
```csharp
var client = new DashScopeClient("your-api-key").AsChatClient("qwen-max");
var completion = await client.CompleteAsync("hello");
Console.WriteLine(completion)
```
-## Console App
-
-Install `Cnblogs.DashScope.Sdk` package.
+### Console Application
+Install NuGet package `Cnblogs.DashScope.Sdk`
```csharp
var client = new DashScopeClient("your-api-key");
var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt);
-// or pass the model name string directly.
+// Or use model name string
// var completion = await client.GetQWenCompletionAsync("qwen-max", prompt);
Console.WriteLine(completion.Output.Text);
```
-## ASP.NET Core
+### ASP.NET Core Application
-Install the Cnblogs.DashScope.AspNetCore package.
+Install NuGet package `Cnblogs.DashScope.AspNetCore`
`Program.cs`
```csharp
@@ -44,6 +42,7 @@ builder.AddDashScopeClient(builder.Configuration);
```
`appsettings.json`
+
```json
{
"DashScope": {
@@ -52,44 +51,33 @@ builder.AddDashScopeClient(builder.Configuration);
}
```
-`Usage`
+Application class:
+
```csharp
public class YourService(IDashScopeClient client)
{
public async Task CompletePromptAsync(string prompt)
{
- var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt);
- return completion.Output.Text;
+ var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt);
+ return completion.Output.Text;
}
}
```
+## Supported APIs
+- [Chat](#Chat) - QWen3, DeepSeek, etc. Supports reasoning, tool calling, web search, translation
+- [Multimodal](#multimodal) - QWen-VL, QVQ, etc. Supports reasoning, visual understanding, OCR, audio understanding
+- [Text-to-Speech (TTS)](#Text-to-Speech) - CosyVoice, Sambert
+- [Image Generation](#image-generation) - Wanx2.1 (text-to-image, portrait style transfer)
+- [Application Call](#application-call)
+- [Text Vectorization](#text-vectorization)
-# Supported APIs
-- Text Embedding API - `dashScopeClient.GetTextEmbeddingsAsync()`
-- Text Generation API(qwen-turbo, qwen-max, etc.) - `dashScopeClient.GetQwenCompletionAsync()` and `dashScopeClient.GetQWenCompletionStreamAsync()`
-- BaiChuan Models - Use `dashScopeClient.GetBaiChuanTextCompletionAsync()`
-- LLaMa2 Models - `dashScopeClient.GetLlama2TextCompletionAsync()`
-- Multimodal Generation API(qwen-vl-max, etc.) - `dashScopeClient.GetQWenMultimodalCompletionAsync()` and `dashScopeClient.GetQWenMultimodalCompletionStreamAsync()`
-- Wanx Models(Image generation, background generation, etc)
- - Image Synthesis - `CreateWanxImageSynthesisTaskAsync()` and `GetWanxImageSynthesisTaskAsync()`
- - Image Generation - `CreateWanxImageGenerationTaskAsync()` and `GetWanxImageGenerationTaskAsync()`
- - Background Image Generation - `CreateWanxBackgroundGenerationTaskAsync()` and `GetWanxBackgroundGenerationTaskAsync()`
-- File API that used by Qwen-Long - `dashScopeClient.UploadFileAsync()` and `dashScopeClient.DeleteFileAsync`
+### Chat
-# Examples
+Use `GetTextCompletionAsync`/`GetTextCompletionStreamAsync` for direct text generation.
+For QWen and DeepSeek, use shortcuts: `GetQWenChatCompletionAsync`/`GetDeepSeekChatCompletionAsync`
-Visit [tests](./test) for more usage of each api.
-
-## Single Text Completion
-
-```csharp
-var prompt = "hello"
-var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt);
-Console.WriteLine(completion.Output.Text);
-```
-
-## Multi-round chat
+[Official Documentation](https://help.aliyun.com/zh/model-studio/user-guide/text-generation/)
```csharp
var history = new List
@@ -106,105 +94,262 @@ var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, histor
Console.WriteLine(completion.Output.Choices[0].Message.Content); // The number is 42
```
-## Function Call
+#### Reasoning
-Creates a function with parameters
+Access model thoughts via `ReasoningContent` property
+```csharp
+var history = new List
+{
+ TextChatMessage.User("Calculate 1+1")
+};
+var completion = await client.GetDeepSeekChatCompletionAsync(DeepSeekLlm.DeepSeekR1, history);
+Console.WriteLine(completion.Output.Choices[0]!.Message.ReasoningContent);
+```
+For QWen3 models, enable reasoning with `TextGenerationParameters.EnableThinking`
+```csharp
+var stream = dashScopeClient
+ .GetQWenChatStreamAsync(
+ QWenLlm.QWenPlusLatest,
+ history,
+ new TextGenerationParameters
+ {
+ IncrementalOutput = true,
+ ResultFormat = ResultFormats.Message,
+ EnableThinking = true
+ });
+```
+#### Tool Calling
+Define a function for model to use:
```csharp
string GetCurrentWeather(GetCurrentWeatherParameters parameters)
{
- // actual implementation should be different.
- return "Sunny, 14" + parameters.Unit switch
- {
- TemperatureUnit.Celsius => "℃",
- TemperatureUnit.Fahrenheit => "℉"
- };
+ return "Sunny";
}
-
public record GetCurrentWeatherParameters(
[property: Required]
- [property: Description("The city and state, e.g. San Francisco, CA")]
+ [property: Description("City and state, e.g. San Francisco, CA")]
string Location,
[property: JsonConverter(typeof(EnumStringConverter))]
TemperatureUnit Unit = TemperatureUnit.Celsius);
-
-public enum TemperatureUnit
-{
- Celsius,
- Fahrenheit
-}
+public enum TemperatureUnit { Celsius, Fahrenheit }
```
-
-Append tool information to chat messages.
-
+Invoke with tool definitions. We using `JsonSchema.Net` for example, you could use any other library to generate JSON schema)
```csharp
-var tools = new List()
+var tools = new List
{
new(
ToolTypes.Function,
new FunctionDefinition(
nameof(GetCurrentWeather),
- "Get the weather abount given location",
+ "Get current weather",
new JsonSchemaBuilder().FromType().Build()))
};
+var history = new List { ChatMessage.User("What's the weather in CA?") };
+var parameters = new TextGenerationParameters { ResultFormat = ResultFormats.Message, Tools = tools };
-var history = new List
-{
- ChatMessage.User("What is the weather today in C.A?")
-};
-
-var parameters = new TextGenerationParamters()
-{
- ResultFormat = ResultFormats.Message,
- Tools = tools
-};
-
-// send question with available tools.
+// request model
var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, parameters);
-history.Add(completion.Output.Choice[0].Message);
-
-// model responding with tool calls.
Console.WriteLine(completion.Output.Choice[0].Message.ToolCalls[0].Function.Name); // GetCurrentWeather
+history.Add(completion.Output.Choice[0].Message);
-// calling tool that model requests and append result into history.
-var result = GetCurrentWeather(JsonSerializer.Deserialize(completion.Output.Choice[0].Message.ToolCalls[0].Function.Arguments));
-history.Add(ChatMessage.Tool(result, nameof(GetCurrentWeather)));
+// calls tool
+var result = GetCurrentWeather(new() { Location = "CA" });
+history.Add(new("tool", result, nameof(GetCurrentWeather)));
-// get back answers.
+// Get final answer
completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, parameters);
-Console.WriteLine(completion.Output.Choice[0].Message.Content);
+Console.WriteLine(completion.Output.Choices[0].Message.Content); // "Current weather in California: Sunny"
```
-
-Append the tool calling result with `tool` role, then model will generate answers based on tool calling result.
-
-
-## QWen-Long with files
-
-Upload file first.
-
+#### File Upload (Long Context Models)
+For Qwen-Long models:
```csharp
var file = new FileInfo("test.txt");
var uploadedFile = await dashScopeClient.UploadFileAsync(file.OpenRead(), file.Name);
+var history = new List { ChatMessage.File(uploadedFile.Id) };
+var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenLong, history);
+Console.WriteLine(completion.Output.Choices[0].Message.Content);
+// Cleanup
+await dashScopeClient.DeleteFileAsync(uploadedFile.Id);
+```
+### Multimodal
+Use `GetMultimodalGenerationAsync`/`GetMultimodalGenerationStreamAsync`
+[Official Documentation](https://help.aliyun.com/zh/model-studio/multimodal)
+
+```csharp
+var image = await File.ReadAllBytesAsync("Lenna.jpg");
+var response = dashScopeClient.GetMultimodalGenerationStreamAsync(
+ new ModelRequest()
+ {
+ Model = "qvq-plus",
+ Input = new MultimodalInput()
+ {
+ Messages =
+ [
+ MultimodalMessage.User(
+ [
+ MultimodalMessageContent.ImageContent(image, "image/jpeg"),
+ MultimodalMessageContent.TextContent("她是谁?")
+ ])
+ ]
+ },
+ Parameters = new MultimodalParameters { IncrementalOutput = true, VlHighResolutionImages = false }
+ });
+
+// output
+var reasoning = false;
+await foreach (var modelResponse in response)
+{
+ var choice = modelResponse.Output.Choices.FirstOrDefault();
+ if (choice != null)
+ {
+ if (choice.FinishReason != "null")
+ {
+ break;
+ }
+
+ if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false)
+ {
+ if (reasoning == false)
+ {
+ reasoning = true;
+ Console.WriteLine("");
+ }
+
+ Console.Write(choice.Message.ReasoningContent);
+ continue;
+ }
+
+ if (reasoning)
+ {
+ reasoning = false;
+ Console.WriteLine("");
+ }
+
+ Console.Write(choice.Message.Content[0].Text);
+ }
+}
```
+### Text-to-Speech
-Using uploaded file id in messages.
+Create a speech synthesis session using `dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync()`.
+Note: Use the using statement to automatically dispose the session, or manually call Dispose() to release resources. Avoid reusing sessions.
+
+Create a synthesis session:
```csharp
-var history = new List
+using var tts = await dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync("cosyvoice-v2");
+var taskId = await tts.RunTaskAsync(new SpeechSynthesizerParameters { Voice = "longxiaochun_v2", Format = "mp3" });
+await tts.ContinueTaskAsync(taskId, "Cnblogs");
+await tts.ContinueTaskAsync(taskId, "Code changes the world");
+await tts.FinishTaskAsync(taskId);
+var file = new FileInfo("tts.mp3");
+using var stream = file.OpenWrite();
+await foreach (var b in tts.GetAudioAsync())
{
- ChatMessage.File(uploadedFile.Id), // use array for multiple files, e.g. [file1.Id, file2.Id]
- ChatMessage.User("Summarize the content of file.")
+ stream.WriteByte(b);
}
-var parameters = new TextGenerationParameters()
+Console.WriteLine($"Audio saved to {file.FullName}");
+```
+### Image Generation
+#### Text-to-Image
+Use shortcuts for Wanx models:
+```csharp
+var task = await dashScopeClient.CreateWanxImageSynthesisTaskAsync(
+ WanxModel.WanxV21Turbo,
+ "A futuristic cityscape at sunset",
+ new ImageSynthesisParameters { Style = ImageStyles.OilPainting });
+// Pull status
+while (true)
{
- ResultFormat = ResultFormats.Message
-};
-var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenLong, history, parameters);
-Console.WriteLine(completion.Output.Choices[0].Message.Content);
+ var result = await dashScopeClient.GetWanxImageSynthesisTaskAsync(task.TaskId);
+ if (result.Output.TaskStatus == DashScopeTaskStatus.Succeeded)
+ {
+ Console.WriteLine($"Image URL: {result.Output.Results[0].Url}");
+ break;
+ }
+ await Task.Delay(500);
+}
+```
+#### Portrait Style Transfer
+Use `CreateWanxImageGenerationTaskAsync` and `GetWanxImageGenerationTaskAsync`
+
+#### Background Generation
+
+Use `CreateWanxBackgroundGenerationTaskAsync` and `GetWanxBackgroundGenerationTaskAsync`
+
+### Application Call
+```csharp
+var request =
+ new ApplicationRequest()
+ {
+ Input = new ApplicationInput() { Prompt = "Summarize this file." },
+ Parameters = new ApplicationParameters()
+ {
+ TopK = 100,
+ TopP = 0.8f,
+ Seed = 1234,
+ Temperature = 0.85f,
+ RagOptions = new ApplicationRagOptions()
+ {
+ PipelineIds = ["thie5bysoj"],
+ FileIds = ["file_d129d632800c45aa9e7421b30561f447_10207234"]
+ }
+ }
+ };
+var response = await client.GetApplicationResponseAsync("your-application-id", request);
+Console.WriteLine(response.Output.Text);
+```
+`ApplicationRequest` uses `Dictionary` as the default type for `BizParams`.
+
+```csharp
+var request =
+ new ApplicationRequest()
+ {
+ Input = new ApplicationInput()
+ {
+ Prompt = "Summarize this file.",
+ BizParams = new Dictionary()
+ {
+ { "customKey1", "custom-value" }
+ }
+ }
+ };
+var response = await client.GetApplicationResponseAsync("your-application-id", request);
+Console.WriteLine(response.Output.Text);
+```
+For strong typing support, you can use the generic class `ApplicationRequest`.
+Note that the SDK uses `snake_case` for JSON serialization. If your application uses different naming conventions, manually specify the serialized property names using `[JsonPropertyName("camelCase")]`.
+
+```csharp
+public record TestApplicationBizParam(
+ [property: JsonPropertyName("sourceCode")]
+ string SourceCode);
+var request =
+ new ApplicationRequest()
+ {
+ Input = new ApplicationInput()
+ {
+ Prompt = "Summarize this file.",
+ BizParams = new TestApplicationBizParam("test")
+ }
+ };
+var response = await client.GetApplicationResponseAsync("your-application-id", request);
+Console.WriteLine(response.Output.Text);
```
-Delete file if needed
+### Text Vectorization
```csharp
-var deletionResult = await dashScopeClient.DeleteFileAsync(uploadedFile.Id);
+var text = "Sample text for embedding";
+var response = await dashScopeClient.GetTextEmbeddingsAsync(
+ TextEmbeddingModel.TextEmbeddingV4,
+ [text],
+ new TextEmbeddingParameters { Dimension = 512 });
+var embedding = response.Output.Embeddings.First().Embedding;
+Console.WriteLine($"Embedding vector length: {embedding.Length}");
```
+
+See [Snapshot Files](./test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.cs) for API parameter examples.
+
+Review [Tests](./test) for comprehensive usage examples.
diff --git a/README.zh-Hans.md b/README.zh-Hans.md
index 9f81d6b..aea5642 100644
--- a/README.zh-Hans.md
+++ b/README.zh-Hans.md
@@ -1,18 +1,18 @@
[English](https://github.com/cnblogs/dashscope-sdk/blob/main/README.md) | 简体中文
+# Cnblogs.DashScopeSDK
+
[](https://www.nuget.org/packages/Cnblogs.DashScope.AI)
[](https://www.nuget.org/packages/Cnblogs.DashScope.Sdk)
[](https://www.nuget.org/packages/Cnblogs.DashScope.AspNetCore)
-# Cnblogs.DashScopeSDK
-
由博客园维护并使用的非官方灵积(百炼)服务 SDK。
使用前注意:当前项目正在积极开发中,小版本也可能包含破坏性更改,升级前请查看对应版本 Release Note 进行迁移。
-# 快速开始
+## 快速开始
-## 使用 `Microsoft.Extensions.AI` 接口
+### 使用 `Microsoft.Extensions.AI` 接口
安装 NuGet 包 `Cnblogs.DashScope.AI`
@@ -22,7 +22,7 @@ var completion = await client.CompleteAsync("hello");
Console.WriteLine(completion)
```
-## 控制台应用
+### 控制台应用
安装 NuGet 包 `Cnblogs.DashScope.Sdk`。
@@ -34,7 +34,7 @@ var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt);
Console.WriteLine(completion.Output.Text);
```
-## ASP.NET Core 应用
+### ASP.NET Core 应用
安装 NuGet 包 `Cnblogs.DashScope.AspNetCore`。
@@ -64,33 +64,22 @@ public class YourService(IDashScopeClient client)
}
```
-# 支持的 API
+## 支持的 API
-- 通用文本向量 - `dashScopeClient.GetTextEmbeddingsAsync()`
-- 通义千问(`qwen-turbo`, `qwen-max` 等) - `dashScopeClient.GetQwenCompletionAsync()` and `dashScopeClient.GetQWenCompletionStreamAsync()`
-- 百川开源大模型 - Use `dashScopeClient.GetBaiChuanTextCompletionAsync()`
-- LLaMa2 大语言模型 - `dashScopeClient.GetLlama2TextCompletionAsync()`
-- 通义千问 VL 和通义千问 Audio(`qwen-vl-max`, `qwen-audio`) - `dashScopeClient.GetQWenMultimodalCompletionAsync()` and `dashScopeClient.GetQWenMultimodalCompletionStreamAsync()`
-- 通义万相系列
- - 文生图 - `CreateWanxImageSynthesisTaskAsync()` and `GetWanxImageSynthesisTaskAsync()`
- - 人像风格重绘 - `CreateWanxImageGenerationTaskAsync()` and `GetWanxImageGenerationTaskAsync()`
- - 图像背景生成 - `CreateWanxBackgroundGenerationTaskAsync()` and `GetWanxBackgroundGenerationTaskAsync()`
-- 适用于 QWen-Long 的文件 API `dashScopeClient.UploadFileAsync()` and `dashScopeClient.DeleteFileAsync`
-- 其他使用相同 Endpoint 的模型
+- [对话](#对话) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景
+- [多模态](#多模态) - QWen-VL,QVQ 等,支持推理/视觉理解/OCR/音频理解等场景
+- [语音合成](#语音合成) - CosyVoice,Sambert 等,支持 TTS 等应用场景
+- [图像生成](#图像生成) - wanx2.1 等,支持文生图,人像风格重绘等应用场景
+- [应用调用](#应用调用)
+- [文本向量](#文本向量)
-# 示例
+### 对话
-查看 [测试](./test) 获得更多 API 使用示例。
+使用 `dashScopeClient.GetTextCompletionAsync` 和 `dashScopeClient.GetTextCompletionStreamAsync` 来直接访问文本生成接口。
-## 单轮对话
+针对通义千问和 DeekSeek,我们提供了快捷方法进行调用: `GetQWenChatCompletionAsync` /`GetDeepSeekChatCompletionAsync`
-```csharp
-var prompt = "你好"
-var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt);
-Console.WriteLine(completion.Output.Text);
-```
-
-## 多轮对话
+相关文档:https://help.aliyun.com/zh/model-studio/user-guide/text-generation/
```csharp
var history = new List
@@ -107,7 +96,35 @@ var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, histor
Console.WriteLine(completion.Output.Choices[0].Message.Content); // The number is 42
```
-## 工具调用
+#### 推理
+
+使用推理模型时,模型的思考过程可以通过 `ReasoningContent` 属性获取。
+
+```csharp
+var history = new List
+{
+ TextChatMessage.User("Calculate 1+1")
+};
+var completion = await client.GetDeepSeekChatCompletionAsync(DeepSeekLlm.DeepSeekR1, history);
+Console.WriteLine(completion.Output.Choices[0]!.Message.ReasoningContent);
+```
+
+对于支持的模型(例如 qwen3),可以使用 `TextGenerationParameters.EnableThinking` 决定是否使用模型的推理能力。
+
+```csharp
+var stream = dashScopeClient
+ .GetQWenChatStreamAsync(
+ QWenLlm.QWenPlusLatest,
+ history,
+ new TextGenerationParameters
+ {
+ IncrementalOutput = true,
+ ResultFormat = ResultFormats.Message,
+ EnableThinking = true
+ });
+```
+
+#### 工具调用
创建一个可供模型使用的方法。
@@ -132,7 +149,7 @@ public enum TemperatureUnit
}
```
-对话时带上方法的名称、描述和参数列表,参数列表以 JSON Schema 的形式提供。
+对话时带上方法的名称、描述和参数列表,参数列表以 JSON Schema 的形式提供(这里使用 `JsonSchema.Net` 库,您也可以使用其它具有类似功能的库)。
```csharp
var tools = new List()
@@ -174,9 +191,9 @@ Console.WriteLine(completion.Output.Choice[0].Message.Content) // 现在浙江
当模型认为应当调用工具时,返回消息中 `ToolCalls` 会提供调用的详情,本地在调用完成后可以把结果以 `tool` 角色返回。
-## 上传文件(QWen-Long)
+#### 上传文件(qwen-long)
-需要先提前将文件上传到 DashScope 来获得 Id。
+使用长上下文模型时,需要先提前将文件上传到 DashScope 来获得 Id。
```csharp
var file = new FileInfo("test.txt");
@@ -204,3 +221,248 @@ Console.WriteLine(completion.Output.Choices[0].Message.Content);
```csharp
var deletionResult = await dashScopeClient.DeleteFileAsync(uploadedFile.Id);
```
+
+### 多模态
+
+使用 `dashScopeClient.GetMultimodalGenerationAsync` 和 `dashScopeClient.GetMultimodalGenerationStreamAsync` 来访问多模态文本生成接口。
+
+相关文档:[多模态_大模型服务平台百炼(Model Studio)-阿里云帮助中心](https://help.aliyun.com/zh/model-studio/multimodal)
+
+#### 视觉理解/推理
+
+使用 `MultimodalMessage.User()` 可以快速创建对应角色的消息。
+
+媒体内容可以通过公网 URL 或者 `byte[]` 传入。
+
+```csharp
+var image = await File.ReadAllBytesAsync("Lenna.jpg");
+var response = dashScopeClient.GetMultimodalGenerationStreamAsync(
+ new ModelRequest()
+ {
+ Model = "qvq-plus",
+ Input = new MultimodalInput()
+ {
+ Messages =
+ [
+ MultimodalMessage.User(
+ [
+ MultimodalMessageContent.ImageContent(image, "image/jpeg"),
+ MultimodalMessageContent.TextContent("她是谁?")
+ ])
+ ]
+ },
+ Parameters = new MultimodalParameters { IncrementalOutput = true, VlHighResolutionImages = false }
+ });
+
+// output
+var reasoning = false;
+await foreach (var modelResponse in response)
+{
+ var choice = modelResponse.Output.Choices.FirstOrDefault();
+ if (choice != null)
+ {
+ if (choice.FinishReason != "null")
+ {
+ break;
+ }
+
+ if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false)
+ {
+ if (reasoning == false)
+ {
+ reasoning = true;
+ Console.WriteLine("");
+ }
+
+ Console.Write(choice.Message.ReasoningContent);
+ continue;
+ }
+
+ if (reasoning)
+ {
+ reasoning = false;
+ Console.WriteLine("");
+ }
+
+ Console.Write(choice.Message.Content[0].Text);
+ }
+}
+```
+
+### 语音合成
+
+通过 `dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync()` 来创建一个语音合成会话。
+
+**注意:使用 using 语句来自动释放会话,或者手动 Dispose 会话,尽量不要重用会话。**
+
+相关文档:[语音合成-CosyVoice_大模型服务平台百炼(Model Studio)-阿里云帮助中心](https://help.aliyun.com/zh/model-studio/cosyvoice-large-model-for-speech-synthesis)
+
+```csharp
+using var tts = await dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync("cosyvoice-v2");
+var taskId = await tts.RunTaskAsync(
+ new SpeechSynthesizerParameters { Voice = "longxiaochun_v2", Format = "mp3" });
+await tts.ContinueTaskAsync(taskId, "博客园");
+await tts.ContinueTaskAsync(taskId, "代码改变世界");
+await tts.FinishTaskAsync(taskId);
+var file = new FileInfo("tts.mp3");
+using var stream = file.OpenWrite();
+await foreach (var b in tts.GetAudioAsync())
+{
+ stream.WriteByte(b);
+}
+
+stream.Close();
+
+var tokenUsage = 0;
+await foreach (var message in tts.GetMessagesAsync())
+{
+ if (message.Payload.Usage?.Characters > tokenUsage)
+ {
+ tokenUsage = message.Payload.Usage.Characters;
+ }
+}
+
+Console.WriteLine($"audio saved to {file.FullName}, token usage: {tokenUsage}");
+break;
+```
+
+### 图像生成
+
+#### 文生图
+
+我们针对通义万相提供了快捷 API `dashScopeClient.CreateWanxImageSynthesisTaskAsync()` 和 `GetWanxImageSynthesisTaskAsync()`。
+
+图片生成需要数秒到数十秒不等,对于 HTTP 请求来说太长,需要通过任务方式生成。
+
+先使用 `CreateWanxImageSynthesisTaskAsync()` 创建任务,再轮询 `GetWanxImageSynthesisTaskAsync()` 检查任务完成状态。
+
+相关文档:[通义万相2.1文生图V2版API参考_大模型服务平台百炼(Model Studio)-阿里云帮助中心](https://help.aliyun.com/zh/model-studio/text-to-image-v2-api-reference)
+
+```csharp
+var prompt = Console.ReadLine();
+var task = await dashScopeClient.CreateWanxImageSynthesisTaskAsync(
+ WanxModel.WanxV21Turbo,
+ prompt,
+ null,
+ new ImageSynthesisParameters { Style = ImageStyles.OilPainting });
+Console.WriteLine($"Task({task.TaskId}) submitted, checking status...");
+var watch = Stopwatch.StartNew();
+while (watch.Elapsed.TotalSeconds < 120)
+{
+ var result = await dashScopeClient.GetWanxImageSynthesisTaskAsync(task.TaskId);
+ Console.WriteLine($"{watch.ElapsedMilliseconds}ms - Status: {result.Output.TaskStatus}");
+ if (result.Output.TaskStatus == DashScopeTaskStatus.Succeeded)
+ {
+ Console.WriteLine($"Image generation finished, URL: {result.Output.Results![0].Url}");
+ return;
+ }
+
+ if (result.Output.TaskStatus == DashScopeTaskStatus.Failed)
+ {
+ Console.WriteLine($"Image generation failed, error message: {result.Output.Message}");
+ return;
+ }
+
+ await Task.Delay(500);
+}
+
+Console.WriteLine($"Task timout, taskId: {task.TaskId}");
+```
+
+#### 人像风格重绘和图像背景生成
+
+与文生图类似,先创建任务,再轮询状态。
+
+人像风格重绘 - `CreateWanxImageGenerationTaskAsync` 和 `GetWanxImageGenerationTaskAsync`
+
+图像背景生成 - `CreateWanxBackgroundGenerationTaskAsync` 和 `GetWanxBackgroundGenerationTaskAsync`
+
+### 应用调用
+
+`GetApplicationResponseAsync` 用于进行应用调用。
+
+`GetApplicationResponseStreamAsync` 用于流式调用。
+
+```csharp
+var request =
+ new ApplicationRequest()
+ {
+ Input = new ApplicationInput() { Prompt = "Summarize this file." },
+ Parameters = new ApplicationParameters()
+ {
+ TopK = 100,
+ TopP = 0.8f,
+ Seed = 1234,
+ Temperature = 0.85f,
+ RagOptions = new ApplicationRagOptions()
+ {
+ PipelineIds = ["thie5bysoj"],
+ FileIds = ["file_d129d632800c45aa9e7421b30561f447_10207234"]
+ }
+ }
+ };
+var response = await client.GetApplicationResponseAsync("your-application-id", request);
+Console.WriteLine(response.Output.Text);
+```
+
+`ApplicationRequest` 默认使用 `Dictionary` 作为 `BizParams` 的类型。
+
+```csharp
+var request =
+ new ApplicationRequest()
+ {
+ Input = new ApplicationInput()
+ {
+ Prompt = "Summarize this file.",
+ BizParams = new Dictionary()
+ {
+ { "customKey1", "custom-value" }
+ }
+ }
+ };
+var response = await client.GetApplicationResponseAsync("your-application-id", request);
+Console.WriteLine(response.Output.Text);
+```
+
+如需强类型支持,可以使用泛型类 `ApplicationRequest`。
+注意 SDK 在 JSON 序列化时使用 `snake_case`。如果你的应用采用其他的命名规则,请使用 `[JsonPropertyName("camelCase")]` 来手动指定序列化时的属性名称。
+
+```csharp
+public record TestApplicationBizParam(
+ [property: JsonPropertyName("sourceCode")]
+ string SourceCode);
+
+var request =
+ new ApplicationRequest()
+ {
+ Input = new ApplicationInput()
+ {
+ Prompt = "Summarize this file.",
+ BizParams = new TestApplicationBizParam("test")
+ }
+ };
+var response = await client.GetApplicationResponseAsync("your-application-id", request);
+Console.WriteLine(response.Output.Text);
+```
+
+### 文本向量
+
+使用 `GetTextEmbeddingsAsync` 来调用文本向量接口。
+
+相关文档:[通用文本向量同步接口API详情_大模型服务平台百炼(Model Studio)-阿里云帮助中心](https://help.aliyun.com/zh/model-studio/text-embedding-synchronous-api)
+
+```csharp
+var text = Console.ReadLine();
+var response = await dashScopeClient.GetTextEmbeddingsAsync(
+ TextEmbeddingModel.TextEmbeddingV4,
+ [text],
+ new TextEmbeddingParameters() { Dimension = 512, });
+var array = response.Output.Embeddings.First().Embedding;
+Console.WriteLine("Embedding");
+Console.WriteLine(string.Join('\n', array));
+Console.WriteLine($"Token usage: {response.Usage?.TotalTokens}");
+```
+
+查看 [快照文件](./test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.cs) 获得 API 调用参数示例.
+
+查看 [测试](./test) 获得更多 API 使用示例。
diff --git a/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj b/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj
index a7ba2f0..fcff303 100644
--- a/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj
+++ b/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj
@@ -1,26 +1,29 @@
-
- Exe
- net8.0
- enable
- enable
- false
-
+
+ Exe
+ net8.0
+ enable
+ enable
+ false
+
-
-
-
-
+
+
+
+
-
-
- Always
-
-
+
+
+ Always
+
+
+ PreserveNewest
+
+
-
-
-
+
+
+
diff --git a/sample/Cnblogs.DashScope.Sample/Lenna.jpg b/sample/Cnblogs.DashScope.Sample/Lenna.jpg
new file mode 100644
index 0000000..4030eed
Binary files /dev/null and b/sample/Cnblogs.DashScope.Sample/Lenna.jpg differ
diff --git a/sample/Cnblogs.DashScope.Sample/Program.cs b/sample/Cnblogs.DashScope.Sample/Program.cs
index c5958f3..a0aa669 100644
--- a/sample/Cnblogs.DashScope.Sample/Program.cs
+++ b/sample/Cnblogs.DashScope.Sample/Program.cs
@@ -1,15 +1,19 @@
-using System.Text;
+using System.Diagnostics;
+using System.Text;
using System.Text.Json;
using Cnblogs.DashScope.Core;
using Cnblogs.DashScope.Sample;
using Cnblogs.DashScope.Sdk;
using Cnblogs.DashScope.Sdk.QWen;
+using Cnblogs.DashScope.Sdk.TextEmbedding;
+using Cnblogs.DashScope.Sdk.Wanx;
using Json.Schema;
using Json.Schema.Generation;
using Microsoft.Extensions.AI;
Console.WriteLine("Reading key from environment variable DASHSCOPE_KEY");
-var apiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY");
+var apiKey = Environment.GetEnvironmentVariable("DASHSCOPE_KEY", EnvironmentVariableTarget.Process)
+ ?? Environment.GetEnvironmentVariable("DASHSCOPE_KEY", EnvironmentVariableTarget.User);
if (string.IsNullOrEmpty(apiKey))
{
Console.Write("ApiKey > ");
@@ -32,12 +36,12 @@
switch (type)
{
case SampleType.TextCompletion:
- Console.WriteLine("Prompt > ");
+ Console.Write("Prompt > ");
userInput = Console.ReadLine()!;
await TextCompletionAsync(userInput);
break;
case SampleType.TextCompletionSse:
- Console.WriteLine("Prompt > ");
+ Console.Write("Prompt > ");
userInput = Console.ReadLine()!;
await TextCompletionStreamAsync(userInput);
break;
@@ -47,15 +51,76 @@
case SampleType.ChatCompletionWithTool:
await ChatWithToolsAsync();
break;
+ case SampleType.MultimodalCompletion:
+ await ChatWithImageAsync();
+ break;
case SampleType.ChatCompletionWithFiles:
await ChatWithFilesAsync();
break;
+ case SampleType.Text2Image:
+ await Text2ImageAsync();
+ break;
case SampleType.MicrosoftExtensionsAi:
await ChatWithMicrosoftExtensions();
break;
case SampleType.MicrosoftExtensionsAiToolCall:
await dashScopeClient.ToolCallWithExtensionAsync();
break;
+ case SampleType.ApplicationCall:
+ Console.Write("Application Id > ");
+ var applicationId = Console.ReadLine()!;
+ Console.Write("Prompt > ");
+ userInput = Console.ReadLine()!;
+ await ApplicationCallAsync(applicationId, userInput);
+ break;
+ case SampleType.TextToSpeech:
+ {
+ using var tts = await dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync("cosyvoice-v2");
+ var taskId = await tts.RunTaskAsync(
+ new SpeechSynthesizerParameters { Voice = "longxiaochun_v2", Format = "mp3" });
+ await tts.ContinueTaskAsync(taskId, "博客园");
+ await tts.ContinueTaskAsync(taskId, "代码改变世界");
+ await tts.FinishTaskAsync(taskId);
+ var file = new FileInfo("tts.mp3");
+ await using var stream = file.OpenWrite();
+ await foreach (var b in tts.GetAudioAsync())
+ {
+ stream.WriteByte(b);
+ }
+
+ stream.Close();
+
+ var tokenUsage = 0;
+ await foreach (var message in tts.GetMessagesAsync())
+ {
+ if (message.Payload.Usage?.Characters > tokenUsage)
+ {
+ tokenUsage = message.Payload.Usage.Characters;
+ }
+ }
+
+ Console.WriteLine($"audio saved to {file.FullName}, token usage: {tokenUsage}");
+ break;
+ }
+
+ case SampleType.TextEmbedding:
+ Console.Write("text> ");
+ var text = Console.ReadLine();
+ if (string.IsNullOrEmpty(text))
+ {
+ text = "Coding changes world";
+ Console.WriteLine($"using default text: {text}");
+ }
+
+ var response = await dashScopeClient.GetTextEmbeddingsAsync(
+ TextEmbeddingModel.TextEmbeddingV3,
+ [text],
+ new TextEmbeddingParameters() { Dimension = 512, });
+ var array = response.Output.Embeddings.First().Embedding;
+ Console.WriteLine("Embedding");
+ Console.WriteLine(string.Join('\n', array));
+ Console.WriteLine($"Token usage: {response.Usage?.TotalTokens}");
+ break;
}
return;
@@ -90,9 +155,14 @@ async Task ChatStreamAsync()
history.Add(TextChatMessage.User(input));
var stream = dashScopeClient
.GetQWenChatStreamAsync(
- QWenLlm.QWenMax,
+ QWenLlm.QWenPlusLatest,
history,
- new TextGenerationParameters { IncrementalOutput = true, ResultFormat = ResultFormats.Message });
+ new TextGenerationParameters
+ {
+ IncrementalOutput = true,
+ ResultFormat = ResultFormats.Message,
+ EnableThinking = true
+ });
var role = string.Empty;
var message = new StringBuilder();
await foreach (var modelResponse in stream)
@@ -105,7 +175,10 @@ async Task ChatStreamAsync()
}
message.Append(chunk.Message.Content);
- Console.Write(chunk.Message.Content);
+ var write = string.IsNullOrEmpty(chunk.Message.ReasoningContent)
+ ? chunk.Message.Content
+ : chunk.Message.ReasoningContent;
+ Console.Write(write);
}
Console.WriteLine();
@@ -115,6 +188,60 @@ async Task ChatStreamAsync()
// ReSharper disable once FunctionNeverReturns
}
+async Task ChatWithImageAsync()
+{
+ var image = await File.ReadAllBytesAsync("Lenna.jpg");
+ var response = dashScopeClient.GetMultimodalGenerationStreamAsync(
+ new ModelRequest()
+ {
+ Model = "qvq-plus",
+ Input = new MultimodalInput()
+ {
+ Messages =
+ [
+ MultimodalMessage.User(
+ [
+ MultimodalMessageContent.ImageContent(image, "image/jpeg"),
+ MultimodalMessageContent.TextContent("她是谁?")
+ ])
+ ]
+ },
+ Parameters = new MultimodalParameters { IncrementalOutput = true, VlHighResolutionImages = false }
+ });
+ var reasoning = false;
+ await foreach (var modelResponse in response)
+ {
+ var choice = modelResponse.Output.Choices.FirstOrDefault();
+ if (choice != null)
+ {
+ if (choice.FinishReason != "null")
+ {
+ break;
+ }
+
+ if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false)
+ {
+ if (reasoning == false)
+ {
+ reasoning = true;
+ Console.WriteLine("");
+ }
+
+ Console.Write(choice.Message.ReasoningContent);
+ continue;
+ }
+
+ if (reasoning)
+ {
+ reasoning = false;
+ Console.WriteLine("");
+ }
+
+ Console.Write(choice.Message.Content[0].Text);
+ }
+ }
+}
+
async Task ChatWithFilesAsync()
{
var history = new List();
@@ -170,7 +297,7 @@ async Task ChatWithToolsAsync()
"获得当前天气",
new JsonSchemaBuilder().FromType().Build()))
};
- var chatParameters = new TextGenerationParameters() { ResultFormat = ResultFormats.Message, Tools = tools };
+ var chatParameters = new TextGenerationParameters { ResultFormat = ResultFormats.Message, Tools = tools };
var question = TextChatMessage.User("请问现在杭州的天气如何?");
history.Add(question);
Console.WriteLine($"{question.Role} > {question.Content}");
@@ -207,11 +334,54 @@ async Task ChatWithMicrosoftExtensions()
Console.WriteLine("Requesting model...");
var chatClient = dashScopeClient.AsChatClient("qwen-max");
List conversation =
- [
- new(ChatRole.System, "You are a helpful AI assistant"),
- new(ChatRole.User, "What is AI?")
- ];
+ new() { new(ChatRole.System, "You are a helpful AI assistant"), new(ChatRole.User, "What is AI?") };
var response = await chatClient.GetResponseAsync(conversation);
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true };
Console.WriteLine(JsonSerializer.Serialize(response, serializerOptions));
}
+
+async Task Text2ImageAsync()
+{
+ Console.Write("Prompt> ");
+ var prompt = Console.ReadLine();
+ if (string.IsNullOrEmpty(prompt))
+ {
+ Console.WriteLine("Using sample prompt");
+ prompt = "A fluffy cat";
+ }
+
+ var task = await dashScopeClient.CreateWanxImageSynthesisTaskAsync(
+ WanxModel.WanxV21Turbo,
+ prompt,
+ null,
+ new ImageSynthesisParameters { Style = ImageStyles.OilPainting });
+ Console.WriteLine($"Task({task.TaskId}) submitted, checking status...");
+ var watch = Stopwatch.StartNew();
+ while (watch.Elapsed.TotalSeconds < 120)
+ {
+ var result = await dashScopeClient.GetWanxImageSynthesisTaskAsync(task.TaskId);
+ Console.WriteLine($"{watch.ElapsedMilliseconds}ms - Status: {result.Output.TaskStatus}");
+ if (result.Output.TaskStatus == DashScopeTaskStatus.Succeeded)
+ {
+ Console.WriteLine($"Image generation finished, URL: {result.Output.Results![0].Url}");
+ return;
+ }
+
+ if (result.Output.TaskStatus == DashScopeTaskStatus.Failed)
+ {
+ Console.WriteLine($"Image generation failed, error message: {result.Output.Message}");
+ return;
+ }
+
+ await Task.Delay(500);
+ }
+
+ Console.WriteLine($"Task timout, taskId: {task.TaskId}");
+}
+
+async Task ApplicationCallAsync(string applicationId, string prompt)
+{
+ var request = new ApplicationRequest { Input = new ApplicationInput { Prompt = prompt } };
+ var response = await dashScopeClient.GetApplicationResponseAsync(applicationId, request);
+ Console.WriteLine(response.Output.Text);
+}
diff --git a/sample/Cnblogs.DashScope.Sample/SampleType.cs b/sample/Cnblogs.DashScope.Sample/SampleType.cs
index 94d119d..c78d94a 100644
--- a/sample/Cnblogs.DashScope.Sample/SampleType.cs
+++ b/sample/Cnblogs.DashScope.Sample/SampleType.cs
@@ -12,7 +12,17 @@ public enum SampleType
ChatCompletionWithFiles,
+ MultimodalCompletion,
+
+ Text2Image,
+
MicrosoftExtensionsAi,
- MicrosoftExtensionsAiToolCall
+ MicrosoftExtensionsAiToolCall,
+
+ ApplicationCall,
+
+ TextToSpeech,
+
+ TextEmbedding
}
diff --git a/sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs b/sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs
index e39f284..2cd398a 100644
--- a/sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs
+++ b/sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs
@@ -11,8 +11,13 @@ public static string GetDescription(this SampleType sampleType)
SampleType.ChatCompletion => "Conversation between user and assistant",
SampleType.ChatCompletionWithTool => "Function call sample",
SampleType.ChatCompletionWithFiles => "File upload sample using qwen-long",
+ SampleType.MultimodalCompletion => "Multimodal completion",
+ SampleType.Text2Image => "Text to Image generation",
SampleType.MicrosoftExtensionsAi => "Use with Microsoft.Extensions.AI",
SampleType.MicrosoftExtensionsAiToolCall => "Use tool call with Microsoft.Extensions.AI interfaces",
+ SampleType.ApplicationCall => "Call pre-defined application",
+ SampleType.TextToSpeech => "TTS task",
+ SampleType.TextEmbedding => "Get text embedding",
_ => throw new ArgumentOutOfRangeException(nameof(sampleType), sampleType, "Unsupported sample option")
};
}
diff --git a/sample/Cnblogs.DashScope.Sample/ToolCallWithExtensions.cs b/sample/Cnblogs.DashScope.Sample/ToolCallWithExtensions.cs
index 0c30cd4..72ccc1e 100644
--- a/sample/Cnblogs.DashScope.Sample/ToolCallWithExtensions.cs
+++ b/sample/Cnblogs.DashScope.Sample/ToolCallWithExtensions.cs
@@ -12,7 +12,7 @@ public static async Task ToolCallWithExtensionAsync(this IDashScopeClient dashSc
[Description("Gets the weather")]
string GetWeather(string location) => Random.Shared.NextDouble() > 0.5 ? "It's sunny" : "It's raining";
- var chatOptions = new ChatOptions { Tools = [AIFunctionFactory.Create(GetWeather)] };
+ var chatOptions = new ChatOptions { Tools = new List { AIFunctionFactory.Create(GetWeather) } };
var client = dashScopeClient.AsChatClient("qwen-max").AsBuilder().UseFunctionInvocation().Build();
await foreach (var message in client.GetStreamingResponseAsync("What is weather of LA today?", chatOptions))
diff --git a/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj b/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj
index fe758a6..a5d8117 100644
--- a/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj
+++ b/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj
@@ -1,5 +1,6 @@
+ net8.0
Cnblogs.DashScope.AI
true
Cnblogs;Dashscope;Microsoft.Extensions.AI;Sdk;Embedding;
@@ -10,7 +11,8 @@
-
+
+
diff --git a/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs b/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs
index 8b0012f..c8a0d0f 100644
--- a/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs
+++ b/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs
@@ -1,4 +1,4 @@
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
using System.Text.Json;
using Cnblogs.DashScope.Core;
using Cnblogs.DashScope.Sdk;
@@ -17,7 +17,7 @@ public sealed class DashScopeChatClient : IChatClient
private readonly string _modelId;
private static readonly JsonSchema EmptyObjectSchema =
- JsonSchema.FromText("""{"type":"object","required":[],"properties":{}}""");
+ JsonSchema.FromText("{\"type\":\"object\",\"required\":[],\"properties\":{}}");
private static readonly TextGenerationParameters
DefaultTextGenerationParameter = new() { ResultFormat = "message" };
@@ -43,7 +43,7 @@ public DashScopeChatClient(IDashScopeClient dashScopeClient, string modelId)
///
public async Task GetResponseAsync(
- IList chatMessages,
+ IEnumerable chatMessages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
@@ -55,7 +55,7 @@ public async Task GetResponseAsync(
if (useVl)
{
var response = await _dashScopeClient.GetMultimodalGenerationAsync(
- new ModelRequest()
+ new ModelRequest
{
Input = new MultimodalInput { Messages = ToMultimodalMessages(chatMessages) },
Parameters = ToMultimodalParameters(options),
@@ -63,7 +63,7 @@ public async Task GetResponseAsync(
},
cancellationToken);
- var returnMessage = new ChatMessage()
+ var returnMessage = new ChatMessage
{
RawRepresentation = response, Role = ToChatRole(response.Output.Choices[0].Message.Role),
};
@@ -80,7 +80,7 @@ public async Task GetResponseAsync(
if (response.Usage != null)
{
- completion.Usage = new UsageDetails()
+ completion.Usage = new UsageDetails
{
InputTokenCount = response.Usage.InputTokens, OutputTokenCount = response.Usage.OutputTokens,
};
@@ -92,7 +92,7 @@ public async Task GetResponseAsync(
{
var parameters = ToTextGenerationParameters(options) ?? DefaultTextGenerationParameter;
var response = await _dashScopeClient.GetTextCompletionAsync(
- new ModelRequest()
+ new ModelRequest
{
Input = new TextGenerationInput
{
@@ -116,7 +116,7 @@ public async Task GetResponseAsync(
if (response.Usage != null)
{
- completion.Usage = new UsageDetails()
+ completion.Usage = new UsageDetails
{
InputTokenCount = response.Usage.InputTokens,
OutputTokenCount = response.Usage.OutputTokens,
@@ -130,7 +130,7 @@ public async Task GetResponseAsync(
///
public async IAsyncEnumerable GetStreamingResponseAsync(
- IList chatMessages,
+ IEnumerable chatMessages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
@@ -147,7 +147,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync(
var parameter = ToMultimodalParameters(options);
parameter.IncrementalOutput = true;
var stream = _dashScopeClient.GetMultimodalGenerationStreamAsync(
- new ModelRequest()
+ new ModelRequest
{
Input = new MultimodalInput { Messages = ToMultimodalMessages(chatMessages) },
Parameters = parameter,
@@ -164,7 +164,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync(
: ToFinishReason(response.Output.Choices[0].FinishReason);
completionId ??= response.RequestId;
- var update = new ChatResponseUpdate()
+ var update = new ChatResponseUpdate
{
ResponseId = completionId,
CreatedAt = DateTimeOffset.Now,
@@ -183,7 +183,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync(
{
update.Contents.Add(
new UsageContent(
- new UsageDetails()
+ new UsageDetails
{
InputTokenCount = response.Usage.InputTokens,
OutputTokenCount = response.Usage.OutputTokens,
@@ -199,16 +199,16 @@ public async IAsyncEnumerable GetStreamingResponseAsync(
{
// qwen does not support streaming with function call, fallback to non-streaming
var completion = await GetResponseAsync(chatMessages, options, cancellationToken);
- yield return new ChatResponseUpdate()
+ yield return new ChatResponseUpdate
{
ResponseId = completion.ResponseId,
- Role = completion.Message.Role,
+ Role = completion.Messages[0].Role,
AdditionalProperties = completion.AdditionalProperties,
- Contents = completion.Message.Contents,
- RawRepresentation = completion.Message.RawRepresentation,
+ Contents = completion.Messages[0].Contents,
+ RawRepresentation = completion.Messages[0].RawRepresentation,
CreatedAt = completion.CreatedAt,
FinishReason = completion.FinishReason,
- ModelId = completion.ModelId,
+ ModelId = completion.ModelId
};
}
else
@@ -216,7 +216,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync(
var parameters = ToTextGenerationParameters(options) ?? DefaultTextGenerationParameter;
parameters.IncrementalOutput = true;
var stream = _dashScopeClient.GetTextCompletionStreamAsync(
- new ModelRequest()
+ new ModelRequest
{
Input = new TextGenerationInput
{
@@ -238,7 +238,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync(
: ToFinishReason(response.Output.Choices[0].FinishReason);
completionId ??= response.RequestId;
- var update = new ChatResponseUpdate()
+ var update = new ChatResponseUpdate
{
ResponseId = completionId,
CreatedAt = DateTimeOffset.Now,
@@ -257,7 +257,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync(
{
update.Contents.Add(
new UsageContent(
- new UsageDetails()
+ new UsageDetails
{
InputTokenCount = response.Usage.InputTokens,
OutputTokenCount = response.Usage.OutputTokens,
@@ -299,7 +299,7 @@ public void Dispose()
private static ChatMessage ToChatMessage(TextChatMessage message)
{
- var returnMessage = new ChatMessage()
+ var returnMessage = new ChatMessage
{
RawRepresentation = message, Role = ToChatRole(message.Role),
};
@@ -392,11 +392,11 @@ private List ToMultimodalMessageContents(IList MultimodalMessageContent.TextContent(text.Text),
- DataContent { Data.Length: > 0 } data when data.MediaTypeStartsWith("image") =>
+ DataContent { Data.Length: > 0 } data when data.HasTopLevelMediaType("image") =>
MultimodalMessageContent.ImageContent(
- data.Data.Value.Span,
+ data.Data.Span,
data.MediaType ?? throw new InvalidOperationException("image media type should not be null")),
- DataContent { Uri: { } uri } data when data.MediaTypeStartsWith("image") =>
+ DataContent { Uri: { } uri } data when data.HasTopLevelMediaType("image") =>
MultimodalMessageContent.ImageContent(uri),
_ => null
};
@@ -422,7 +422,7 @@ private IEnumerable ToTextChatMessages(
{
yield return new TextChatMessage(
from.Role.Value,
- from.Text ?? string.Empty,
+ from.Text,
from.AuthorName);
}
else if (from.Role == ChatRole.Tool)
@@ -459,12 +459,16 @@ private IEnumerable ToTextChatMessages(
tools?.FindIndex(f => f.Function?.Name == c.Name) ?? -1,
new FunctionCall(c.Name, JsonSerializer.Serialize(c.Arguments, ToolCallJsonSerializerOptions))))
.ToList();
+
+ // function all array must be null when empty
+ // <400> InternalError.Algo.InvalidParameter: Empty tool_calls is not supported in message
yield return new TextChatMessage(
from.Role.Value,
- from.Text ?? string.Empty,
+ from.Text,
from.AuthorName,
null,
- functionCall);
+ null,
+ functionCall.Count > 0 ? functionCall : null);
}
}
@@ -481,7 +485,7 @@ private IEnumerable ToTextChatMessages(
format = "json_object";
}
- return new TextGenerationParameters()
+ return new TextGenerationParameters
{
ResultFormat = format,
Temperature = options.Temperature,
@@ -499,7 +503,8 @@ private IEnumerable ToTextChatMessages(
RequiredChatToolMode required when string.IsNullOrEmpty(required.RequiredFunctionName) == false =>
ToolChoice.FunctionChoice(required.RequiredFunctionName),
_ => ToolChoice.AutoChoice
- }
+ },
+ ParallelToolCalls = options.AllowMultipleToolCalls,
};
}
diff --git a/src/Cnblogs.DashScope.AI/DashScopeTextEmbeddingGenerator.cs b/src/Cnblogs.DashScope.AI/DashScopeTextEmbeddingGenerator.cs
index fba5e02..f735844 100644
--- a/src/Cnblogs.DashScope.AI/DashScopeTextEmbeddingGenerator.cs
+++ b/src/Cnblogs.DashScope.AI/DashScopeTextEmbeddingGenerator.cs
@@ -29,7 +29,6 @@ public DashScopeTextEmbeddingGenerator(IDashScopeClient dashScopeClient, string
_dashScopeClient = dashScopeClient;
_modelId = modelId;
_parameters = new TextEmbeddingParameters { Dimension = dimensions };
- Metadata = new EmbeddingGeneratorMetadata("dashscope", _dashScopeClient.BaseAddress, modelId, dimensions);
}
///
@@ -45,7 +44,7 @@ public async Task>> GenerateAsync(
e => new Embedding(e.Embedding) { ModelId = _modelId, CreatedAt = DateTimeOffset.Now });
var rawUsage = rawResponse.Usage;
var usage = rawUsage != null
- ? new UsageDetails() { InputTokenCount = rawUsage.TotalTokens, TotalTokenCount = rawUsage.TotalTokens }
+ ? new UsageDetails { InputTokenCount = rawUsage.TotalTokens, TotalTokenCount = rawUsage.TotalTokens }
: null;
return new GeneratedEmbeddings>(embeddings)
{
@@ -88,7 +87,4 @@ public void Dispose()
options.AdditionalProperties?.GetValueOrDefault(nameof(TextEmbeddingParameters.TextType)) as string,
};
}
-
- ///
- public EmbeddingGeneratorMetadata Metadata { get; }
}
diff --git a/src/Cnblogs.DashScope.AspNetCore/Assembly.cs b/src/Cnblogs.DashScope.AspNetCore/Assembly.cs
new file mode 100644
index 0000000..b79c4ed
--- /dev/null
+++ b/src/Cnblogs.DashScope.AspNetCore/Assembly.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Cnblogs.DashScope.Sdk.UnitTests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/Cnblogs.DashScope.AspNetCore/Cnblogs.DashScope.AspNetCore.csproj b/src/Cnblogs.DashScope.AspNetCore/Cnblogs.DashScope.AspNetCore.csproj
index 1ab06a5..ba30d5f 100644
--- a/src/Cnblogs.DashScope.AspNetCore/Cnblogs.DashScope.AspNetCore.csproj
+++ b/src/Cnblogs.DashScope.AspNetCore/Cnblogs.DashScope.AspNetCore.csproj
@@ -3,7 +3,8 @@
Cnblogs.DashScopeSDK
true
- Cnblogs;Dashscope;AI;Sdk;Embedding;AspNetCore
+ Cnblogs;Dashscope;AI;Sdk;Embedding;AspNetCore;Bailian
+ Cnblogs.DashScope.AspNetCore
diff --git a/src/Cnblogs.DashScope.AspNetCore/DashScopeAspNetCoreDefaults.cs b/src/Cnblogs.DashScope.AspNetCore/DashScopeAspNetCoreDefaults.cs
new file mode 100644
index 0000000..1cdbb63
--- /dev/null
+++ b/src/Cnblogs.DashScope.AspNetCore/DashScopeAspNetCoreDefaults.cs
@@ -0,0 +1,6 @@
+namespace Cnblogs.DashScope.AspNetCore;
+
+internal static class DashScopeAspNetCoreDefaults
+{
+ public const string DefaultHttpClientName = "Cnblogs.DashScope.Http";
+}
diff --git a/src/Cnblogs.DashScope.AspNetCore/DashScopeClientAspNetCore.cs b/src/Cnblogs.DashScope.AspNetCore/DashScopeClientAspNetCore.cs
new file mode 100644
index 0000000..be4a685
--- /dev/null
+++ b/src/Cnblogs.DashScope.AspNetCore/DashScopeClientAspNetCore.cs
@@ -0,0 +1,20 @@
+using Cnblogs.DashScope.Core;
+
+namespace Cnblogs.DashScope.AspNetCore;
+
+///
+/// The with DI and options pattern support.
+///
+public class DashScopeClientAspNetCore
+ : DashScopeClientCore
+{
+ ///
+ /// The with DI and options pattern support.
+ ///
+ /// The factory to create .
+ /// The socket pool for WebSocket API calls.
+ public DashScopeClientAspNetCore(IHttpClientFactory factory, DashScopeClientWebSocketPool pool)
+ : base(factory.CreateClient(DashScopeAspNetCoreDefaults.DefaultHttpClientName), pool)
+ {
+ }
+}
diff --git a/src/Cnblogs.DashScope.AspNetCore/ServiceCollectionInjector.cs b/src/Cnblogs.DashScope.AspNetCore/ServiceCollectionInjector.cs
index 6d91b83..8b521f0 100644
--- a/src/Cnblogs.DashScope.AspNetCore/ServiceCollectionInjector.cs
+++ b/src/Cnblogs.DashScope.AspNetCore/ServiceCollectionInjector.cs
@@ -1,6 +1,9 @@
using System.Net.Http.Headers;
+using Cnblogs.DashScope.AspNetCore;
using Cnblogs.DashScope.Core;
+using Cnblogs.DashScope.Core.Internals;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;
@@ -37,10 +40,10 @@ public static IHttpClientBuilder AddDashScopeClient(this IServiceCollection serv
{
var apiKey = section["apiKey"]
?? throw new InvalidOperationException("There is no apiKey provided in given section");
- var baseAddress = section["baseAddress"];
- return string.IsNullOrEmpty(baseAddress)
- ? services.AddDashScopeClient(apiKey)
- : services.AddDashScopeClient(apiKey, baseAddress);
+ var baseAddress = section["baseAddress"] ?? DashScopeDefaults.HttpApiBaseAddress;
+ var workspaceId = section["workspaceId"];
+ services.Configure(section);
+ return services.AddDashScopeHttpClient(apiKey, baseAddress, workspaceId);
}
///
@@ -49,17 +52,58 @@ public static IHttpClientBuilder AddDashScopeClient(this IServiceCollection serv
/// The service collection to add service to.
/// The DashScope api key.
/// The DashScope api base address, you may change this value if you are using proxy.
+ /// The DashScope websocket base address, you may want to change this value if use are using proxy.
+ /// Default workspace id to use.
///
public static IHttpClientBuilder AddDashScopeClient(
this IServiceCollection services,
string apiKey,
- string baseAddress = "https://dashscope.aliyuncs.com/api/v1/")
+ string? baseAddress = null,
+ string? baseWebsocketAddress = null,
+ string? workspaceId = null)
{
- return services.AddHttpClient(
+ services.Configure(o =>
+ {
+ o.ApiKey = apiKey;
+ if (baseAddress != null)
+ {
+ o.BaseAddress = baseAddress;
+ }
+
+ if (baseWebsocketAddress != null)
+ {
+ o.WebsocketBaseAddress = baseWebsocketAddress;
+ }
+
+ o.WorkspaceId = workspaceId;
+ });
+
+ return services.AddDashScopeHttpClient(apiKey, baseAddress, workspaceId);
+ }
+
+ private static IHttpClientBuilder AddDashScopeHttpClient(
+ this IServiceCollection services,
+ string apiKey,
+ string? baseAddress,
+ string? workspaceId)
+ {
+ services.AddSingleton();
+ services.AddSingleton(sp
+ => new DashScopeClientWebSocketPool(
+ sp.GetRequiredService(),
+ sp.GetRequiredService>().Value));
+ services.AddScoped();
+ return services.AddHttpClient(
+ DashScopeAspNetCoreDefaults.DefaultHttpClientName,
h =>
{
h.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
- h.BaseAddress = new Uri(baseAddress);
+ if (string.IsNullOrWhiteSpace(workspaceId) == false)
+ {
+ h.DefaultRequestHeaders.Add("X-DashScope-WorkSpace", workspaceId);
+ }
+
+ h.BaseAddress = new Uri(baseAddress ?? DashScopeDefaults.HttpApiBaseAddress);
});
}
}
diff --git a/src/Cnblogs.DashScope.Core/ApplicationDocReference.cs b/src/Cnblogs.DashScope.Core/ApplicationDocReference.cs
new file mode 100644
index 0000000..83c48af
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationDocReference.cs
@@ -0,0 +1,20 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// One reference for application output.
+///
+/// The index id of the doc.
+/// Text slice title.
+/// Unique id of the doc been referenced.
+/// Name of the doc been referenced.
+/// Referenced content.
+/// Image URLs beed referenced.
+/// Page numbers of referenced content belongs to.
+public record ApplicationDocReference(
+ string IndexId,
+ string Title,
+ string DocId,
+ string DocName,
+ string Text,
+ List? Images,
+ List? PageNumber);
diff --git a/src/Cnblogs.DashScope.Core/ApplicationInput.cs b/src/Cnblogs.DashScope.Core/ApplicationInput.cs
new file mode 100644
index 0000000..fc537e6
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationInput.cs
@@ -0,0 +1,49 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Inputs for application call.
+///
+/// Type of the BizContent.
+public class ApplicationInput
+ where TBizParams : class
+{
+ ///
+ /// The prompt for model to generate response upon. Optional when has been set.
+ ///
+ ///
+ /// Prompt will be appended to when both set.
+ ///
+ public string? Prompt { get; set; }
+
+ ///
+ /// The session id for conversation history. This will be ignored if has been set.
+ ///
+ public string? SessionId { get; set; }
+
+ ///
+ /// The conversation history.
+ ///
+ public IEnumerable? Messages { get; set; }
+
+ ///
+ /// The id of memory when enabled.
+ ///
+ public string? MemoryId { get; set; }
+
+ ///
+ /// List of image urls for inputs.
+ ///
+ public IEnumerable? ImageList { get; set; }
+
+ ///
+ /// User defined content.
+ ///
+ public TBizParams? BizParams { get; set; } = null;
+}
+
+///
+/// Inputs for application call.
+///
+public class ApplicationInput : ApplicationInput>
+{
+}
diff --git a/src/Cnblogs.DashScope.Core/ApplicationMessage.cs b/src/Cnblogs.DashScope.Core/ApplicationMessage.cs
new file mode 100644
index 0000000..9edfdf5
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationMessage.cs
@@ -0,0 +1,30 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// A single message for application call.
+///
+/// The role of this message belongs to.
+/// The content of the message.
+public record ApplicationMessage(string Role, string Content)
+{
+ ///
+ /// Creates a user message.
+ ///
+ /// Content of the message.
+ ///
+ public static ApplicationMessage User(string content) => new("user", content);
+
+ ///
+ /// Creates a system message.
+ ///
+ /// Content of the message.
+ ///
+ public static ApplicationMessage System(string content) => new("system", content);
+
+ ///
+ /// Creates a assistant message.
+ ///
+ /// Content of the message.
+ ///
+ public static ApplicationMessage Assistant(string content) => new("assistant", content);
+}
diff --git a/src/Cnblogs.DashScope.Core/ApplicationModelUsage.cs b/src/Cnblogs.DashScope.Core/ApplicationModelUsage.cs
new file mode 100644
index 0000000..e0e838e
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationModelUsage.cs
@@ -0,0 +1,9 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Token usages for one model.
+///
+/// The id of the model.
+/// Total input tokens of this model.
+/// Total output tokens from this model.
+public record ApplicationModelUsage(string ModelId, int InputTokens, int OutputTokens);
diff --git a/src/Cnblogs.DashScope.Core/ApplicationOutput.cs b/src/Cnblogs.DashScope.Core/ApplicationOutput.cs
new file mode 100644
index 0000000..258bd60
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationOutput.cs
@@ -0,0 +1,16 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// The output of application call.
+///
+/// Output text from application.
+/// Finish reason from application.
+/// Unique id of current session.
+/// Thoughts from application.
+/// Doc references from application output.
+public record ApplicationOutput(
+ string Text,
+ string FinishReason,
+ string SessionId,
+ List? Thoughts,
+ List? DocReferences);
diff --git a/src/Cnblogs.DashScope.Core/ApplicationOutputThought.cs b/src/Cnblogs.DashScope.Core/ApplicationOutputThought.cs
new file mode 100644
index 0000000..b4bf55f
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationOutputThought.cs
@@ -0,0 +1,24 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// The model thought output.
+///
+/// The thought content of the model.
+/// Type of the action. e.g. agentRag, reasoning.
+/// The name of the action.
+/// The action been executed.
+/// The streaming result of action input.
+/// The input of the action.
+/// Lookup or plugin output.
+/// Reasoning output when using DeepSeek-R1.
+/// Arguments of the action.
+public record ApplicationOutputThought(
+ string? Thought,
+ string? ActionType,
+ string? ActionName,
+ string? Action,
+ string? ActionInputStream,
+ string? ActionInput,
+ string? Observation,
+ string? Response,
+ string? Arguments);
diff --git a/src/Cnblogs.DashScope.Core/ApplicationParameters.cs b/src/Cnblogs.DashScope.Core/ApplicationParameters.cs
new file mode 100644
index 0000000..86303b0
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationParameters.cs
@@ -0,0 +1,37 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Parameters for application call.
+///
+public class ApplicationParameters : IIncrementalOutputParameter, ISeedParameter, IProbabilityParameter
+{
+ ///
+ public bool? IncrementalOutput { get; set; }
+
+ ///
+ /// Output format for flow application. Can be full_thoughts or agent_format. Defaults to full_thoughts.
+ ///
+ public string? FlowStreamMode { get; set; }
+
+ ///
+ /// Options for RAG applications.
+ ///
+ public ApplicationRagOptions? RagOptions { get; set; }
+
+ ///
+ public ulong? Seed { get; set; }
+
+ ///
+ public float? TopP { get; set; }
+
+ ///
+ public int? TopK { get; set; }
+
+ ///
+ public float? Temperature { get; set; }
+
+ ///
+ /// Controls whether output contains think block.
+ ///
+ public bool? HasThoughts { get; set; }
+}
diff --git a/src/Cnblogs.DashScope.Core/ApplicationRagOptions.cs b/src/Cnblogs.DashScope.Core/ApplicationRagOptions.cs
new file mode 100644
index 0000000..38aff36
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationRagOptions.cs
@@ -0,0 +1,37 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Options for RAG application.
+///
+public class ApplicationRagOptions
+{
+ ///
+ /// The pipelines to search from.
+ ///
+ public IEnumerable? PipelineIds { get; set; }
+
+ ///
+ /// The ids of file to reference from.
+ ///
+ public IEnumerable? FileIds { get; set; }
+
+ ///
+ /// Metadata filter for non-structured files.
+ ///
+ public Dictionary? MetadataFilter { get; set; }
+
+ ///
+ /// Tag filter for non-structured files.
+ ///
+ public IEnumerable? Tags { get; set; }
+
+ ///
+ /// Filter for structured files.
+ ///
+ public Dictionary? StructuredFilter { get; set; }
+
+ ///
+ /// File ids for current session.
+ ///
+ public IEnumerable? SessionFileIds { get; set; }
+}
diff --git a/src/Cnblogs.DashScope.Core/ApplicationRequest.cs b/src/Cnblogs.DashScope.Core/ApplicationRequest.cs
new file mode 100644
index 0000000..c1980e7
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationRequest.cs
@@ -0,0 +1,35 @@
+using System.Text.Json.Serialization;
+using Cnblogs.DashScope.Core.Internals;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Request body for an application all.
+///
+/// Type of the biz_content
+public class ApplicationRequest : IDashScopeWorkspaceConfig
+ where TBizParams : class
+{
+ ///
+ /// Content of this call.
+ ///
+ public ApplicationInput Input { get; set; } = new();
+
+ ///
+ /// Optional configurations.
+ ///
+ public ApplicationParameters? Parameters { get; set; }
+
+ ///
+ /// Optional workspace id.
+ ///
+ [JsonIgnore]
+ public string? WorkspaceId { get; set; }
+}
+
+///
+/// Request body for an application call with dictionary biz_content.
+///
+public class ApplicationRequest : ApplicationRequest>
+{
+}
diff --git a/src/Cnblogs.DashScope.Core/ApplicationResponse.cs b/src/Cnblogs.DashScope.Core/ApplicationResponse.cs
new file mode 100644
index 0000000..2089940
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationResponse.cs
@@ -0,0 +1,12 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Response of application call.
+///
+/// Unique id of this request.
+/// The output of application call.
+/// Token usage of this application call.
+public record ApplicationResponse(
+ string RequestId,
+ ApplicationOutput Output,
+ ApplicationUsage Usage);
diff --git a/src/Cnblogs.DashScope.Core/ApplicationUsage.cs b/src/Cnblogs.DashScope.Core/ApplicationUsage.cs
new file mode 100644
index 0000000..8a4ef98
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/ApplicationUsage.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Total token usages of this application call.
+///
+/// All models been used and their token usages. Can be null when workflow application without using any model.
+public record ApplicationUsage(List? Models);
diff --git a/src/Cnblogs.DashScope.Core/BackgroundGenerationInput.cs b/src/Cnblogs.DashScope.Core/BackgroundGenerationInput.cs
index bd820dc..dd5b2b3 100644
--- a/src/Cnblogs.DashScope.Core/BackgroundGenerationInput.cs
+++ b/src/Cnblogs.DashScope.Core/BackgroundGenerationInput.cs
@@ -8,7 +8,7 @@ public class BackgroundGenerationInput
///
/// The image url to generation background on.
///
- public required string BaseImageUrl { get; set; }
+ public string BaseImageUrl { get; set; } = string.Empty;
///
/// The reference image url for.
diff --git a/src/Cnblogs.DashScope.Core/BatchGetEmbeddingsInput.cs b/src/Cnblogs.DashScope.Core/BatchGetEmbeddingsInput.cs
index b9ab6e0..36b04cd 100644
--- a/src/Cnblogs.DashScope.Core/BatchGetEmbeddingsInput.cs
+++ b/src/Cnblogs.DashScope.Core/BatchGetEmbeddingsInput.cs
@@ -8,5 +8,5 @@ public class BatchGetEmbeddingsInput
///
/// The url of text file to compute embeddings from.
///
- public required string Url { get; set; }
+ public string Url { get; set; } = string.Empty;
}
diff --git a/src/Cnblogs.DashScope.Core/Cnblogs.DashScope.Core.csproj b/src/Cnblogs.DashScope.Core/Cnblogs.DashScope.Core.csproj
index 791029d..f337ae6 100644
--- a/src/Cnblogs.DashScope.Core/Cnblogs.DashScope.Core.csproj
+++ b/src/Cnblogs.DashScope.Core/Cnblogs.DashScope.Core.csproj
@@ -3,16 +3,17 @@
Cnblogs.DashScopeSDK
true
- Cnblogs;Dashscope;AI;Sdk;Embedding;
+ Cnblogs;Dashscope;AI;Sdk;Embedding;Bailian;
Provide pure api access to DashScope without extra references. Cnblogs.DashScope.Sdk should be used for general purpose.
-
+
-
+
+
-
+
diff --git a/src/Cnblogs.DashScope.Core/DashScopeClient.cs b/src/Cnblogs.DashScope.Core/DashScopeClient.cs
index 26f716f..1d1b0ca 100644
--- a/src/Cnblogs.DashScope.Core/DashScopeClient.cs
+++ b/src/Cnblogs.DashScope.Core/DashScopeClient.cs
@@ -9,38 +9,84 @@ namespace Cnblogs.DashScope.Core;
public class DashScopeClient : DashScopeClientCore
{
private static readonly Dictionary ClientPools = new();
+ private static readonly Dictionary SocketPools = new();
///
/// Creates a DashScopeClient for further api call.
///
/// The DashScope api key.
/// The timeout for internal http client, defaults to 2 minute.
+ /// The base address for DashScope api call.
+ /// The base address for DashScope websocket api call.
+ /// The workspace id.
+ /// Maximum size of socket pool.
///
- /// The underlying httpclient is cached by apiKey and timeout.
- /// Client created with same apiKey and timeout value will share same underlying instance.
+ /// The underlying httpclient is cached by constructor parameter list.
+ /// Client created with same parameter value will share same underlying instance.
///
- public DashScopeClient(string apiKey, TimeSpan? timeout = null)
- : base(GetConfiguredClient(apiKey, timeout))
+ public DashScopeClient(
+ string apiKey,
+ TimeSpan? timeout = null,
+ string baseAddress = DashScopeDefaults.HttpApiBaseAddress,
+ string baseWebsocketAddress = DashScopeDefaults.WebsocketApiBaseAddress,
+ string? workspaceId = null,
+ int socketPoolSize = 32)
+ : base(
+ GetConfiguredClient(apiKey, timeout, baseAddress, workspaceId),
+ GetConfiguredSocketPool(apiKey, baseWebsocketAddress, socketPoolSize, workspaceId))
{
}
- private static HttpClient GetConfiguredClient(string apiKey, TimeSpan? timeout)
+ private static DashScopeClientWebSocketPool GetConfiguredSocketPool(
+ string apiKey,
+ string baseAddress,
+ int socketPoolSize,
+ string? workspaceId = null)
+ {
+ var key = GetCacheKey();
+
+ var pool = SocketPools.GetValueOrDefault(key);
+ if (pool is null)
+ {
+ pool = new DashScopeClientWebSocketPool(
+ new DashScopeClientWebSocketFactory(),
+ new DashScopeOptions
+ {
+ ApiKey = apiKey,
+ WebsocketBaseAddress = baseAddress,
+ SocketPoolSize = socketPoolSize,
+ WorkspaceId = workspaceId
+ });
+ SocketPools.Add(key, pool);
+ }
+
+ return pool;
+
+ string GetCacheKey() => $"{apiKey}-{socketPoolSize}-{baseAddress}-{workspaceId}";
+ }
+
+ private static HttpClient GetConfiguredClient(
+ string apiKey,
+ TimeSpan? timeout = null,
+ string? baseAddress = null,
+ string? workspaceId = null)
{
var client = ClientPools.GetValueOrDefault(GetCacheKey());
if (client is null)
{
client = new HttpClient
{
- BaseAddress = new Uri(DashScopeDefaults.DashScopeApiBaseAddress),
+ BaseAddress = new Uri(baseAddress ?? DashScopeDefaults.HttpApiBaseAddress),
Timeout = timeout ?? TimeSpan.FromMinutes(2)
};
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
+ client.DefaultRequestHeaders.Add("X-DashScope-WorkSpace", workspaceId);
ClientPools.Add(GetCacheKey(), client);
}
return client;
- string GetCacheKey() => $"{apiKey}-{timeout?.TotalMilliseconds}";
+ string GetCacheKey() => $"{apiKey}-{timeout?.TotalMilliseconds}-{baseAddress}-{workspaceId}";
}
}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeClientCore.cs b/src/Cnblogs.DashScope.Core/DashScopeClientCore.cs
index 9784516..997ef63 100644
--- a/src/Cnblogs.DashScope.Core/DashScopeClientCore.cs
+++ b/src/Cnblogs.DashScope.Core/DashScopeClientCore.cs
@@ -4,7 +4,6 @@
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
-using System.Text.Json.Serialization;
using Cnblogs.DashScope.Core.Internals;
namespace Cnblogs.DashScope.Core;
@@ -14,27 +13,63 @@ namespace Cnblogs.DashScope.Core;
///
public class DashScopeClientCore : IDashScopeClient
{
- private static readonly JsonSerializerOptions SerializationOptions =
- new()
- {
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
- };
-
private readonly HttpClient _httpClient;
+ private readonly DashScopeClientWebSocketPool _socketPool;
///
/// For DI container to inject pre-configured httpclient.
///
/// Pre-configured httpclient.
- public DashScopeClientCore(HttpClient httpClient)
+ /// Websocket pool.
+ public DashScopeClientCore(HttpClient httpClient, DashScopeClientWebSocketPool pool)
{
_httpClient = httpClient;
+ _socketPool = pool;
}
///
public Uri? BaseAddress => _httpClient.BaseAddress;
+ ///
+ public Task GetApplicationResponseAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default)
+ {
+ return GetApplicationResponseAsync>(applicationId, input, cancellationToken);
+ }
+
+ ///
+ public async Task GetApplicationResponseAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default)
+ where TBizContent : class
+ {
+ var request = BuildRequest(HttpMethod.Post, ApiLinks.Application(applicationId), input);
+ return (await SendAsync(request, cancellationToken))!;
+ }
+
+ ///
+ public IAsyncEnumerable GetApplicationResponseStreamAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default)
+ {
+ return GetApplicationResponseStreamAsync>(applicationId, input, cancellationToken);
+ }
+
+ ///
+ public IAsyncEnumerable GetApplicationResponseStreamAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default)
+ where TBizContent : class
+ {
+ var request = BuildSseRequest(HttpMethod.Post, ApiLinks.Application(applicationId), input);
+ return StreamAsync(request, cancellationToken);
+ }
+
///
public async Task> GetTextCompletionAsync(
ModelRequest input,
@@ -235,12 +270,23 @@ public async Task ListFilesAsync(CancellationToken cancellati
}
///
- public async Task DeleteFileAsync(DashScopeFileId id, CancellationToken cancellationToken = default)
+ public async Task DeleteFileAsync(
+ DashScopeFileId id,
+ CancellationToken cancellationToken = default)
{
var request = BuildRequest(HttpMethod.Delete, ApiLinks.Files + $"/{id}");
return (await SendCompatibleAsync(request, cancellationToken))!;
}
+ ///
+ public async Task CreateSpeechSynthesizerSocketSessionAsync(
+ string modelId,
+ CancellationToken cancellationToken = default)
+ {
+ var socket = await _socketPool.RentSocketAsync(cancellationToken);
+ return new SpeechSynthesizerSocketSession(socket, modelId);
+ }
+
private static HttpRequestMessage BuildSseRequest(HttpMethod method, string url, TPayload payload)
where TPayload : class
{
@@ -262,7 +308,9 @@ private static HttpRequestMessage BuildRequest(
{
var message = new HttpRequestMessage(method, url)
{
- Content = payload != null ? JsonContent.Create(payload, options: SerializationOptions) : null
+ Content = payload != null
+ ? JsonContent.Create(payload, options: DashScopeDefaults.SerializationOptions)
+ : null
};
if (sse)
@@ -275,6 +323,11 @@ private static HttpRequestMessage BuildRequest(
message.Headers.Add("X-DashScope-Async", "enable");
}
+ if (payload is IDashScopeWorkspaceConfig config && string.IsNullOrWhiteSpace(config.WorkspaceId) == false)
+ {
+ message.Headers.Add("X-DashScope-WorkSpace", config.WorkspaceId);
+ }
+
return message;
}
@@ -285,7 +338,7 @@ private static HttpRequestMessage BuildRequest(
{
var response = await GetSuccessResponseAsync(
message,
- r => new DashScopeError()
+ r => new DashScopeError
{
Code = r.Error.Type,
Message = r.Error.Message,
@@ -293,7 +346,9 @@ private static HttpRequestMessage BuildRequest(
},
HttpCompletionOption.ResponseContentRead,
cancellationToken);
- return await response.Content.ReadFromJsonAsync(SerializationOptions, cancellationToken);
+ return await response.Content.ReadFromJsonAsync(
+ DashScopeDefaults.SerializationOptions,
+ cancellationToken);
}
private async Task SendAsync(HttpRequestMessage message, CancellationToken cancellationToken)
@@ -303,7 +358,9 @@ private static HttpRequestMessage BuildRequest(
message,
HttpCompletionOption.ResponseContentRead,
cancellationToken);
- return await response.Content.ReadFromJsonAsync(SerializationOptions, cancellationToken);
+ return await response.Content.ReadFromJsonAsync(
+ DashScopeDefaults.SerializationOptions,
+ cancellationToken);
}
private async IAsyncEnumerable StreamAsync(
@@ -320,13 +377,14 @@ private async IAsyncEnumerable StreamAsync(
if (cancellationToken.IsCancellationRequested)
throw new TaskCanceledException();
- var line = await reader.ReadLineAsync(cancellationToken);
+ var line = await reader.ReadLineAsync();
if (line != null && line.StartsWith("data:"))
{
var data = line["data:".Length..];
if (data.StartsWith("{\"code\":"))
{
- var error = JsonSerializer.Deserialize(data, SerializationOptions)!;
+ var error =
+ JsonSerializer.Deserialize(data, DashScopeDefaults.SerializationOptions)!;
throw new DashScopeException(
message.RequestUri?.ToString(),
(int)response.StatusCode,
@@ -334,7 +392,7 @@ private async IAsyncEnumerable StreamAsync(
error.Message);
}
- yield return JsonSerializer.Deserialize(data, SerializationOptions)!;
+ yield return JsonSerializer.Deserialize(data, DashScopeDefaults.SerializationOptions)!;
}
}
}
@@ -371,7 +429,9 @@ private async Task GetSuccessResponseAsync(
DashScopeError? error = null;
try
{
- var r = await response.Content.ReadFromJsonAsync(SerializationOptions, cancellationToken);
+ var r = await response.Content.ReadFromJsonAsync(
+ DashScopeDefaults.SerializationOptions,
+ cancellationToken);
error = r == null ? null : errorMapper.Invoke(r);
}
catch (Exception)
diff --git a/src/Cnblogs.DashScope.Core/DashScopeClientWebSocket.cs b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocket.cs
new file mode 100644
index 0000000..464aa4c
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocket.cs
@@ -0,0 +1,262 @@
+using System.Net.WebSockets;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Channels;
+using Cnblogs.DashScope.Core.Internals;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// A websocket client for DashScope websocket API.
+///
+public sealed class DashScopeClientWebSocket : IDisposable
+{
+ private static readonly UnboundedChannelOptions UnboundedChannelOptions =
+ new()
+ {
+ SingleWriter = true,
+ SingleReader = true,
+ AllowSynchronousContinuations = true
+ };
+
+ private readonly IClientWebSocket _socket;
+ private Task? _receiveTask;
+ private TaskCompletionSource _taskStartedSignal = new();
+ private Channel? _binaryOutput;
+ private Channel>? _jsonOutput;
+
+ ///
+ /// Unique id of this socket.
+ ///
+ internal Guid Id { get; } = Guid.NewGuid();
+
+ ///
+ /// The binary output.
+ ///
+ public ChannelReader BinaryOutput
+ => _binaryOutput?.Reader
+ ?? throw new InvalidOperationException("Please call ResetOutput() before accessing output");
+
+ ///
+ /// The json output.
+ ///
+ /// Throws when ResetOutput is not called.
+ public ChannelReader> JsonOutput
+ => _jsonOutput?.Reader
+ ?? throw new InvalidOperationException("Please call ResetOutput() before accessing output");
+
+ ///
+ /// A task that completed when received task-started event.
+ ///
+ public Task TaskStarted => _taskStartedSignal.Task;
+
+ ///
+ /// Current state for this websocket.
+ ///
+ public DashScopeWebSocketState State { get; private set; }
+
+ ///
+ /// Initialize a configured web socket client.
+ ///
+ /// The api key to use.
+ /// Optional workspace id.
+ public DashScopeClientWebSocket(string apiKey, string? workspaceId = null)
+ {
+ _socket = new ClientWebSocketWrapper(new ClientWebSocket());
+ _socket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");
+ _socket.Options.SetRequestHeader("Authorization", $"bearer {apiKey}");
+ if (string.IsNullOrEmpty(workspaceId) == false)
+ {
+ _socket.Options.SetRequestHeader("X-DashScope-WorkspaceId", workspaceId);
+ }
+ }
+
+ ///
+ /// Initiate a with a pre-configured .
+ ///
+ /// Pre-configured .
+ internal DashScopeClientWebSocket(IClientWebSocket socket)
+ {
+ _socket = socket;
+ }
+
+ ///
+ /// Start a websocket connection.
+ ///
+ /// Websocket API uri.
+ /// The cancellation token to use.
+ ///
+ /// When was request.
+ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken = default)
+ {
+ await _socket.ConnectAsync(uri, cancellationToken);
+ _receiveTask = ReceiveMessagesAsync(cancellationToken);
+ State = DashScopeWebSocketState.Ready;
+ }
+
+ ///
+ /// Reset binary output.
+ ///
+ public void ResetOutput()
+ {
+ _binaryOutput?.Writer.TryComplete();
+ _binaryOutput = Channel.CreateUnbounded(UnboundedChannelOptions);
+ _jsonOutput?.Writer.TryComplete();
+ _jsonOutput = Channel.CreateUnbounded>(UnboundedChannelOptions);
+ _taskStartedSignal.TrySetResult(false);
+ _taskStartedSignal = new TaskCompletionSource();
+ }
+
+ ///
+ /// Send message to server.
+ ///
+ /// Request to send.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ /// Type of the input.
+ /// Type of the parameter.
+ /// The is requested.
+ /// Websocket is not connected or already closed.
+ /// The underlying websocket has already been closed.
+ public Task SendMessageAsync(
+ DashScopeWebSocketRequest request,
+ CancellationToken cancellationToken = default)
+ where TInput : class, new()
+ where TParameter : class
+ {
+ if (State == DashScopeWebSocketState.Closed)
+ {
+ throw new InvalidOperationException("Socket is already closed.");
+ }
+
+ var json = JsonSerializer.Serialize(request, DashScopeDefaults.SerializationOptions);
+ return _socket.SendAsync(
+ new ArraySegment(Encoding.UTF8.GetBytes(json)),
+ WebSocketMessageType.Text,
+ true,
+ cancellationToken);
+ }
+
+ private async Task?> ReceiveMessageAsync(
+ CancellationToken cancellationToken = default)
+ {
+ var buffer = new byte[1024 * 4];
+ var segment = new ArraySegment(buffer);
+
+ try
+ {
+ var result = await _socket.ReceiveAsync(segment, cancellationToken);
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ await CloseAsync(cancellationToken);
+ return null;
+ }
+
+ if (result.MessageType == WebSocketMessageType.Binary)
+ {
+ for (var i = 0; i < result.Count; i++)
+ {
+ await _binaryOutput!.Writer.WriteAsync(buffer[i], cancellationToken);
+ }
+
+ return null;
+ }
+
+ var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
+ var jsonResponse =
+ JsonSerializer.Deserialize>(
+ message,
+ DashScopeDefaults.SerializationOptions);
+ return jsonResponse;
+ }
+ catch
+ {
+ // close socket when exception happens.
+ await CloseAsync(cancellationToken);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Wait for server response.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ /// The task was failed.
+ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken = default)
+ {
+ while (State != DashScopeWebSocketState.Closed && _socket.CloseStatus == null)
+ {
+ var json = await ReceiveMessageAsync(cancellationToken);
+ if (json == null)
+ {
+ continue;
+ }
+
+ if (_jsonOutput is not null)
+ {
+ await _jsonOutput.Writer.WriteAsync(json, cancellationToken);
+ }
+
+ var eventStr = json.Header.Event;
+ switch (eventStr)
+ {
+ case "task-started":
+ State = DashScopeWebSocketState.RunningTask;
+ _taskStartedSignal.TrySetResult(true);
+ break;
+ case "task-finished":
+ State = DashScopeWebSocketState.Ready;
+ _binaryOutput?.Writer.Complete();
+ _jsonOutput?.Writer.Complete();
+ break;
+ case "task-failed":
+ await CloseAsync(cancellationToken);
+ _binaryOutput?.Writer.Complete();
+ _jsonOutput?.Writer.Complete();
+ throw new DashScopeException(
+ null,
+ 400,
+ new DashScopeError
+ {
+ Code = json.Header.ErrorCode ?? string.Empty,
+ Message = json.Header.ErrorMessage ?? string.Empty,
+ RequestId = json.Header.Attributes.RequestUuid ?? string.Empty
+ },
+ json.Header.ErrorMessage ?? "The task was failed");
+ }
+ }
+
+ await CloseAsync(cancellationToken);
+ }
+
+ ///
+ /// Close the underlying websocket.
+ ///
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ public async Task CloseAsync(CancellationToken cancellationToken = default)
+ {
+ await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
+ State = DashScopeWebSocketState.Closed;
+ _binaryOutput?.Writer.TryComplete();
+ _jsonOutput?.Writer.TryComplete();
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // Dispose managed resources.
+ _socket.Dispose();
+ _binaryOutput?.Writer.TryComplete();
+ _jsonOutput?.Writer.TryComplete();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketFactory.cs b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketFactory.cs
new file mode 100644
index 0000000..06edecb
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketFactory.cs
@@ -0,0 +1,13 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Default implementation for .
+///
+public class DashScopeClientWebSocketFactory : IDashScopeClientWebSocketFactory
+{
+ ///
+ public DashScopeClientWebSocket GetClientWebSocket(string apiKey, string? workspaceId = null)
+ {
+ return new DashScopeClientWebSocket(apiKey, workspaceId);
+ }
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketPool.cs b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketPool.cs
new file mode 100644
index 0000000..57cbed5
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketPool.cs
@@ -0,0 +1,140 @@
+using System.Collections.Concurrent;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Socket pool for DashScope API.
+///
+public sealed class DashScopeClientWebSocketPool : IDisposable
+{
+ private readonly ConcurrentBag _available = new();
+ private readonly ConcurrentDictionary _active = new();
+ private readonly DashScopeOptions _options;
+ private readonly IDashScopeClientWebSocketFactory _dashScopeClientWebSocketFactory;
+
+ ///
+ /// Socket pool for DashScope API.
+ ///
+ ///
+ /// Options for DashScope sdk.
+ public DashScopeClientWebSocketPool(
+ IDashScopeClientWebSocketFactory dashScopeClientWebSocketFactory,
+ DashScopeOptions options)
+ {
+ _dashScopeClientWebSocketFactory = dashScopeClientWebSocketFactory;
+ _options = options;
+ }
+
+ ///
+ /// Get available connection count.
+ ///
+ internal int AvailableSocketCount => _available.Count;
+
+ ///
+ /// Get active connection count.
+ ///
+ internal int ActiveSocketCount => _active.Count;
+
+ internal DashScopeClientWebSocketPool(
+ IEnumerable sockets,
+ IDashScopeClientWebSocketFactory dashScopeClientWebSocketFactory)
+ {
+ _options = new DashScopeOptions();
+ foreach (var socket in sockets)
+ {
+ _available.Add(socket);
+ }
+
+ _dashScopeClientWebSocketFactory = dashScopeClientWebSocketFactory;
+ }
+
+ internal void ReturnSocket(DashScopeClientWebSocket socket)
+ {
+ _active.Remove(socket.Id, out _);
+
+ if (socket.State != DashScopeWebSocketState.Ready)
+ {
+ // not returnable, disposing.
+ socket.Dispose();
+ return;
+ }
+
+ _available.Add(socket);
+ }
+
+ ///
+ /// Rent or create a socket connection from pool.
+ ///
+ ///
+ ///
+ public async Task RentSocketAsync(CancellationToken cancellationToken = default)
+ {
+ var found = false;
+ DashScopeClientWebSocket? socket = null;
+ while (found == false)
+ {
+ if (_available.IsEmpty == false)
+ {
+ found = _available.TryTake(out socket);
+ if (socket?.State != DashScopeWebSocketState.Ready)
+ {
+ // expired
+ found = false;
+ socket?.Dispose();
+ }
+ }
+ else
+ {
+ socket = await InitializeNewSocketAsync(_options.WebsocketBaseAddress, cancellationToken);
+ found = true;
+ }
+ }
+
+ return ActivateSocket(socket!);
+ }
+
+ private DashScopeClientWebSocketWrapper ActivateSocket(DashScopeClientWebSocket socket)
+ {
+ _active.TryAdd(socket.Id, socket);
+ return new DashScopeClientWebSocketWrapper(socket, this);
+ }
+
+ private async Task InitializeNewSocketAsync(
+ string url,
+ CancellationToken cancellationToken = default)
+ {
+ if (_available.Count + _active.Count >= _options.SocketPoolSize)
+ {
+ throw new InvalidOperationException("[DashScopeSDK] Socket pool is full");
+ }
+
+ var socket = _dashScopeClientWebSocketFactory.GetClientWebSocket(_options.ApiKey, _options.WorkspaceId);
+ await socket.ConnectAsync(new Uri(url), cancellationToken);
+ return socket;
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // Dispose managed resources.
+ while (_available.IsEmpty == false)
+ {
+ _available.TryTake(out var socket);
+ socket?.Dispose();
+ }
+
+ var activeSockets = _active.Values;
+ foreach (var activeSocket in activeSockets)
+ {
+ activeSocket.Dispose();
+ }
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketWrapper.cs b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketWrapper.cs
new file mode 100644
index 0000000..696f304
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeClientWebSocketWrapper.cs
@@ -0,0 +1,56 @@
+using System.Text.Json;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Represents a transient wrapper for rented websocket, should be transient.
+///
+/// The rented websocket
+/// The pool to return the socket to.
+public sealed record DashScopeClientWebSocketWrapper(DashScopeClientWebSocket Socket, DashScopeClientWebSocketPool Pool)
+ : IDisposable
+{
+ ///
+ /// The binary output.
+ ///
+ public IAsyncEnumerable BinaryOutput => Socket.BinaryOutput.ReadAllAsync();
+
+ ///
+ /// The json message output.
+ ///
+ public IAsyncEnumerable> JsonOutput => Socket.JsonOutput.ReadAllAsync();
+
+ ///
+ /// Reset task signal and output cannel.
+ ///
+ public void ResetTask() => Socket.ResetOutput();
+
+ ///
+ /// The task that completes when received task-started event from server.
+ ///
+ public Task TaskStarted => Socket.TaskStarted;
+
+ ///
+ /// Send message to server.
+ ///
+ /// Request to send.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ /// Type of the input.
+ /// Type of the parameter.
+ /// The is requested.
+ /// Websocket is not connected.
+ /// The underlying websocket has already been closed.
+ public Task SendMessageAsync(
+ DashScopeWebSocketRequest request,
+ CancellationToken cancellationToken = default)
+ where TInput : class, new()
+ where TParameter : class
+ => Socket.SendMessageAsync(request, cancellationToken);
+
+ ///
+ public void Dispose()
+ {
+ Pool.ReturnSocket(Socket);
+ }
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeException.cs b/src/Cnblogs.DashScope.Core/DashScopeException.cs
index d4aba49..3826f87 100644
--- a/src/Cnblogs.DashScope.Core/DashScopeException.cs
+++ b/src/Cnblogs.DashScope.Core/DashScopeException.cs
@@ -3,24 +3,35 @@
///
/// Represents error detail for DashScope API calls.
///
-/// The requested api url.
-/// The status code of response. Would be 0 if no response is received.
-/// The error detail returned by server.
-/// The error message.
-public class DashScopeException(string? apiUrl, int status, DashScopeError? error, string message) : Exception(message)
+public class DashScopeException : Exception
{
+ ///
+ /// Represents error detail for DashScope API calls.
+ ///
+ /// The requested api url.
+ /// The status code of response. Would be 0 if no response is received.
+ /// The error detail returned by server.
+ /// The error message.
+ public DashScopeException(string? apiUrl, int status, DashScopeError? error, string message)
+ : base(message)
+ {
+ ApiUrl = apiUrl;
+ Error = error;
+ Status = status;
+ }
+
///
/// The requested api url.
///
- public string? ApiUrl { get; } = apiUrl;
+ public string? ApiUrl { get; }
///
/// The error detail returned by server.
///
- public DashScopeError? Error { get; } = error;
+ public DashScopeError? Error { get; }
///
/// The status code of response. Would be 0 if no response is received.
///
- public int Status { get; } = status;
+ public int Status { get; }
}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeOptions.cs b/src/Cnblogs.DashScope.Core/DashScopeOptions.cs
new file mode 100644
index 0000000..d68d1bb
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeOptions.cs
@@ -0,0 +1,34 @@
+using Cnblogs.DashScope.Core.Internals;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Options for DashScope client.
+///
+public class DashScopeOptions
+{
+ ///
+ /// The api key used to access DashScope api
+ ///
+ public string ApiKey { get; set; } = string.Empty;
+
+ ///
+ /// Base address for DashScope HTTP API.
+ ///
+ public string BaseAddress { get; set; } = DashScopeDefaults.HttpApiBaseAddress;
+
+ ///
+ /// Base address for DashScope websocket API.
+ ///
+ public string WebsocketBaseAddress { get; set; } = DashScopeDefaults.WebsocketApiBaseAddress;
+
+ ///
+ /// Default workspace Id.
+ ///
+ public string? WorkspaceId { get; set; }
+
+ ///
+ /// Default socket pool size.
+ ///
+ public int SocketPoolSize { get; set; } = 32;
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeTaskOutput.cs b/src/Cnblogs.DashScope.Core/DashScopeTaskOutput.cs
index 8ca43f9..7a8bee7 100644
--- a/src/Cnblogs.DashScope.Core/DashScopeTaskOutput.cs
+++ b/src/Cnblogs.DashScope.Core/DashScopeTaskOutput.cs
@@ -11,7 +11,7 @@ public abstract record DashScopeTaskOutput
///
/// The unique id of this task.
///
- public required string TaskId { get; set; }
+ public string TaskId { get; set; } = string.Empty;
///
/// The status of this task.
diff --git a/src/Cnblogs.DashScope.Core/DashScopeTaskStatus.cs b/src/Cnblogs.DashScope.Core/DashScopeTaskStatus.cs
index 83a3509..3b87579 100644
--- a/src/Cnblogs.DashScope.Core/DashScopeTaskStatus.cs
+++ b/src/Cnblogs.DashScope.Core/DashScopeTaskStatus.cs
@@ -5,7 +5,7 @@ namespace Cnblogs.DashScope.Core;
///
/// Represents status of DashScope task.
///
-[JsonConverter(typeof(JsonStringEnumConverter))]
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum DashScopeTaskStatus
{
///
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequest.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequest.cs
new file mode 100644
index 0000000..b1845e0
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequest.cs
@@ -0,0 +1,21 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Represents a websocket request to DashScope.
+///
+/// Type of the input.
+/// Type of the parameter.
+public class DashScopeWebSocketRequest
+ where TInput : class, new()
+ where TParameter : class
+{
+ ///
+ /// Metadata of the request.
+ ///
+ public DashScopeWebSocketRequestHeader Header { get; set; } = new();
+
+ ///
+ /// Payload of the request.
+ ///
+ public DashScopeWebSocketRequestPayload Payload { get; set; } = new();
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequestHeader.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequestHeader.cs
new file mode 100644
index 0000000..a54c9c0
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequestHeader.cs
@@ -0,0 +1,22 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Metadata for websocket request.
+///
+public class DashScopeWebSocketRequestHeader
+{
+ ///
+ /// Action name.
+ ///
+ public string Action { get; set; } = string.Empty;
+
+ ///
+ /// UUID for task.
+ ///
+ public string TaskId { get; set; } = string.Empty;
+
+ ///
+ /// Streaming type.
+ ///
+ public string? Streaming { get; set; } = "duplex";
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequestPayload.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequestPayload.cs
new file mode 100644
index 0000000..e994239
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketRequestPayload.cs
@@ -0,0 +1,41 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Payload for websocket request.
+///
+/// Type of the input.
+/// Type of the parameter.
+public class DashScopeWebSocketRequestPayload
+ where TInput : class, new() // Input's default value must be empty object(not null or omitted).
+ where TParameter : class
+{
+ ///
+ /// Group name of task.
+ ///
+ public string? TaskGroup { get; set; }
+
+ ///
+ /// Requesting task name.
+ ///
+ public string? Task { get; set; }
+
+ ///
+ /// Requesting function name.
+ ///
+ public string? Function { get; set; }
+
+ ///
+ /// Model id.
+ ///
+ public string? Model { get; set; }
+
+ ///
+ /// Optional parameters.
+ ///
+ public TParameter? Parameters { get; set; }
+
+ ///
+ /// The input of the request.
+ ///
+ public TInput Input { get; set; } = new();
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponse.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponse.cs
new file mode 100644
index 0000000..bffc68b
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponse.cs
@@ -0,0 +1,11 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Response from websocket API.
+///
+/// Response metadatas.
+/// Response body.
+/// Output type of the response.
+public record DashScopeWebSocketResponse(
+ DashScopeWebSocketResponseHeader Header,
+ DashScopeWebSocketResponsePayload Payload);
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseHeader.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseHeader.cs
new file mode 100644
index 0000000..26bbb9f
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseHeader.cs
@@ -0,0 +1,16 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Metadata of the websocket response.
+///
+/// TaskId of the task.
+/// Event name.
+/// Error code when is task-failed.
+/// Error message when is task-failed.
+/// Optional attributes
+public record DashScopeWebSocketResponseHeader(
+ string TaskId,
+ string Event,
+ string? ErrorCode,
+ string? ErrorMessage,
+ DashScopeWebSocketResponseHeaderAttributes Attributes);
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseHeaderAttributes.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseHeaderAttributes.cs
new file mode 100644
index 0000000..1ee663e
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseHeaderAttributes.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Attributes field in websocket response header.
+///
+/// UUID for current request.
+public record DashScopeWebSocketResponseHeaderAttributes(string? RequestUuid);
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseMapper.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseMapper.cs
new file mode 100644
index 0000000..3dea7d0
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseMapper.cs
@@ -0,0 +1,27 @@
+using System.Text.Json;
+using Cnblogs.DashScope.Core.Internals;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Mapper class for
+///
+internal static class DashScopeWebSocketResponseMapper
+{
+ public static DashScopeWebSocketResponse DeserializeOutput(this DashScopeWebSocketResponse source)
+ where TOutput : class
+ {
+ var output = source.Payload.Output;
+ if (output.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
+ {
+ return new DashScopeWebSocketResponse(
+ source.Header,
+ new DashScopeWebSocketResponsePayload(null, source.Payload.Usage));
+ }
+
+ var mapped = output.Deserialize(DashScopeDefaults.SerializationOptions);
+ return new DashScopeWebSocketResponse(
+ source.Header,
+ new DashScopeWebSocketResponsePayload(mapped, source.Payload.Usage));
+ }
+}
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponsePayload.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponsePayload.cs
new file mode 100644
index 0000000..cce48aa
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponsePayload.cs
@@ -0,0 +1,9 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Payload field of websocket API response.
+///
+/// Content of the response.
+/// Task usage info.
+/// Type of the response content.
+public record DashScopeWebSocketResponsePayload(TOutput? Output, DashScopeWebSocketResponseUsage? Usage);
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseUsage.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseUsage.cs
new file mode 100644
index 0000000..7e7f734
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketResponseUsage.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Usage info of websocket task.
+///
+/// Current character usage count.
+public record DashScopeWebSocketResponseUsage(int Characters);
diff --git a/src/Cnblogs.DashScope.Core/DashScopeWebSocketState.cs b/src/Cnblogs.DashScope.Core/DashScopeWebSocketState.cs
new file mode 100644
index 0000000..2cb73b1
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/DashScopeWebSocketState.cs
@@ -0,0 +1,27 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// The state of .
+///
+public enum DashScopeWebSocketState
+{
+ ///
+ /// The socket has been created but not connected yet.
+ ///
+ Created,
+
+ ///
+ /// The socket has been connected and ready.
+ ///
+ Ready,
+
+ ///
+ /// The socket has a running task waiting to be finished.
+ ///
+ RunningTask,
+
+ ///
+ /// The socket has been closed.
+ ///
+ Closed
+}
diff --git a/src/Cnblogs.DashScope.Core/IBatchGetEmbeddingsParameters.cs b/src/Cnblogs.DashScope.Core/IBatchGetEmbeddingsParameters.cs
index d2640e0..04a28d3 100644
--- a/src/Cnblogs.DashScope.Core/IBatchGetEmbeddingsParameters.cs
+++ b/src/Cnblogs.DashScope.Core/IBatchGetEmbeddingsParameters.cs
@@ -3,4 +3,6 @@
///
/// Optional parameter of batch get embeddings request.
///
-public interface IBatchGetEmbeddingsParameters : ITextEmbeddingParameters;
+public interface IBatchGetEmbeddingsParameters : ITextEmbeddingParameters
+{
+}
diff --git a/src/Cnblogs.DashScope.Core/IDashScopeClient.cs b/src/Cnblogs.DashScope.Core/IDashScopeClient.cs
index 3c32291..cb61eb5 100644
--- a/src/Cnblogs.DashScope.Core/IDashScopeClient.cs
+++ b/src/Cnblogs.DashScope.Core/IDashScopeClient.cs
@@ -10,6 +10,58 @@ public interface IDashScopeClient
///
Uri? BaseAddress { get; }
+ ///
+ /// Make a call to custom application.
+ ///
+ /// Name of the application.
+ /// The request body.
+ /// The cancellation token to use.
+ ///
+ Task GetApplicationResponseAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Make a call to custom application.
+ ///
+ /// Name of the application.
+ /// The request body.
+ /// The cancellation token to use.
+ /// Type of the biz_content.
+ ///
+ Task GetApplicationResponseAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default)
+ where TBizParams : class;
+
+ ///
+ /// Make a call to custom application.
+ ///
+ /// Name of the application.
+ /// The request body.
+ /// The cancellation token to use.
+ ///
+ IAsyncEnumerable GetApplicationResponseStreamAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Make a call to custom application.
+ ///
+ /// Name of the application.
+ /// The request body.
+ /// The cancellation token to use.
+ /// Type of the biz_content.
+ ///
+ IAsyncEnumerable GetApplicationResponseStreamAsync(
+ string applicationId,
+ ApplicationRequest input,
+ CancellationToken cancellationToken = default)
+ where TBizContent : class;
+
///
/// Return textual completions as configured for a given prompt.
///
@@ -195,4 +247,14 @@ public Task UploadFileAsync(
public Task DeleteFileAsync(
DashScopeFileId id,
CancellationToken cancellationToken = default);
+
+ ///
+ /// Start a speech synthesizer session. Related model: cosyvoice
+ ///
+ /// The model to use.
+ /// Cancellation token.
+ ///
+ public Task CreateSpeechSynthesizerSocketSessionAsync(
+ string modelId,
+ CancellationToken cancellationToken = default);
}
diff --git a/src/Cnblogs.DashScope.Core/IDashScopeClientWebSocketFactory.cs b/src/Cnblogs.DashScope.Core/IDashScopeClientWebSocketFactory.cs
new file mode 100644
index 0000000..0cf7359
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/IDashScopeClientWebSocketFactory.cs
@@ -0,0 +1,15 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// A factory abstraction for a component that can create DashScopeClientWebSocket instance.
+///
+public interface IDashScopeClientWebSocketFactory
+{
+ ///
+ /// Creates a new .
+ ///
+ /// The api key.
+ /// Optional workspace id.
+ ///
+ DashScopeClientWebSocket GetClientWebSocket(string apiKey, string? workspaceId = null);
+}
diff --git a/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs b/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs
index 5e241c6..70a3b47 100644
--- a/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs
+++ b/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs
@@ -24,4 +24,14 @@ public interface IImageSynthesisParameters
/// Seed for randomizer, max at 4294967290. Once set, generated image will use seed, seed+1, seed+2, seed+3 depends on .
///
public uint? Seed { get; }
+
+ ///
+ /// Let LLM to rewrite your positive prompt, Defaults to true.
+ ///
+ public bool? PromptExtend { get; }
+
+ ///
+ /// Adds AI-Generated watermark on bottom right corner.
+ ///
+ public bool? Watermark { get; }
}
diff --git a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs
index c32bb72..9c3b763 100644
--- a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs
+++ b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs
@@ -1,10 +1,11 @@
-namespace Cnblogs.DashScope.Core;
+namespace Cnblogs.DashScope.Core;
///
/// The text generation options.
///
public interface ITextGenerationParameters
- : IIncrementalOutputParameter, ISeedParameter, IProbabilityParameter, IPenaltyParameter, IMaxTokenParameter, IStopTokenParameter
+ : IIncrementalOutputParameter, ISeedParameter, IProbabilityParameter, IPenaltyParameter, IMaxTokenParameter,
+ IStopTokenParameter
{
///
/// The format of the result, must be text or message.
@@ -40,6 +41,31 @@ public interface ITextGenerationParameters
///
public bool? EnableSearch { get; }
+ ///
+ /// Search options. should set to true.
+ ///
+ public TextGenerationSearchOptions? SearchOptions { get; set; }
+
+ ///
+ /// Thinking option. Valid for supported models.(e.g. qwen3)
+ ///
+ public bool? EnableThinking { get; }
+
+ ///
+ /// Maximum length of thinking content. Valid for supported models.(e.g. qwen3)
+ ///
+ public int? ThinkingBudget { get; set; }
+
+ ///
+ /// Include log possibilities in response.
+ ///
+ public bool? Logprobs { get; set; }
+
+ ///
+ /// How many choices should be returned. Range: [0, 5]
+ ///
+ public int? TopLogprobs { get; set; }
+
///
/// Available tools for model to call.
///
@@ -49,4 +75,14 @@ public interface ITextGenerationParameters
/// Behavior when choosing tools.
///
public ToolChoice? ToolChoice { get; }
+
+ ///
+ /// Whether to enable parallel tool calling
+ ///
+ public bool? ParallelToolCalls { get; }
+
+ ///
+ /// Options when using QWen-MT models.
+ ///
+ public TextGenerationTranslationOptions? TranslationOptions { get; set; }
}
diff --git a/src/Cnblogs.DashScope.Core/ImageGenerationInput.cs b/src/Cnblogs.DashScope.Core/ImageGenerationInput.cs
index d21dca1..4ea9876 100644
--- a/src/Cnblogs.DashScope.Core/ImageGenerationInput.cs
+++ b/src/Cnblogs.DashScope.Core/ImageGenerationInput.cs
@@ -8,10 +8,10 @@ public class ImageGenerationInput
///
/// The image url to generation new image from.
///
- public required string ImageUrl { get; set; }
+ public string ImageUrl { get; set; } = string.Empty;
///
/// The style the new image should use, checkout docs for sample image of different indexes: https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-wanxiang-style-repaint
///
- public required int StyleIndex { get; set; }
+ public int StyleIndex { get; set; }
}
diff --git a/src/Cnblogs.DashScope.Core/ImageSynthesisInput.cs b/src/Cnblogs.DashScope.Core/ImageSynthesisInput.cs
index 9c78118..21c0dcc 100644
--- a/src/Cnblogs.DashScope.Core/ImageSynthesisInput.cs
+++ b/src/Cnblogs.DashScope.Core/ImageSynthesisInput.cs
@@ -8,7 +8,7 @@ public class ImageSynthesisInput
///
/// The prompt to generate image from. This will be chopped at max length of 500 characters.
///
- public required string Prompt { get; set; }
+ public string Prompt { get; set; } = string.Empty;
///
/// The negative prompt to generate image from. This will be chopped at max length of 500 characters.
diff --git a/src/Cnblogs.DashScope.Core/ImageSynthesisParameters.cs b/src/Cnblogs.DashScope.Core/ImageSynthesisParameters.cs
index 58497fe..8ca30bd 100644
--- a/src/Cnblogs.DashScope.Core/ImageSynthesisParameters.cs
+++ b/src/Cnblogs.DashScope.Core/ImageSynthesisParameters.cs
@@ -16,4 +16,10 @@ public class ImageSynthesisParameters : IImageSynthesisParameters
///
public uint? Seed { get; set; }
+
+ ///
+ public bool? PromptExtend { get; set; }
+
+ ///
+ public bool? Watermark { get; set; }
}
diff --git a/src/Cnblogs.DashScope.Core/Internals/ApiLinks.cs b/src/Cnblogs.DashScope.Core/Internals/ApiLinks.cs
index 22cc689..d098719 100644
--- a/src/Cnblogs.DashScope.Core/Internals/ApiLinks.cs
+++ b/src/Cnblogs.DashScope.Core/Internals/ApiLinks.cs
@@ -11,4 +11,5 @@ internal static class ApiLinks
public const string Tasks = "tasks/";
public const string Tokenizer = "tokenizer";
public const string Files = "/compatible-mode/v1/files";
+ public static string Application(string applicationId) => $"apps/{applicationId}/completion";
}
diff --git a/src/Cnblogs.DashScope.Core/Internals/Assembly.cs b/src/Cnblogs.DashScope.Core/Internals/Assembly.cs
new file mode 100644
index 0000000..e628d36
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/Internals/Assembly.cs
@@ -0,0 +1,5 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Cnblogs.DashScope.Sdk.UnitTests")]
+[assembly: InternalsVisibleTo("Cnblogs.DashScope.Tests.Shared")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/Cnblogs.DashScope.Core/Internals/ByteArrayLiteralConvertor.cs b/src/Cnblogs.DashScope.Core/Internals/ByteArrayLiteralConvertor.cs
new file mode 100644
index 0000000..509e4c1
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/Internals/ByteArrayLiteralConvertor.cs
@@ -0,0 +1,43 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Cnblogs.DashScope.Core.Internals;
+
+internal class ByteArrayLiteralConvertor : JsonConverter
+{
+ ///
+ public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.StartArray)
+ {
+ reader.Read(); // read out start of array
+ var list = new List(8); // should fit most tokens
+ while (reader.TokenType != JsonTokenType.EndArray)
+ {
+ list.Add(reader.GetByte());
+ reader.Read();
+ }
+
+ return list.ToArray();
+ }
+
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ return reader.GetBytesFromBase64();
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
+ {
+ writer.WriteStartArray();
+ foreach (var b in value)
+ {
+ writer.WriteNumberValue(b);
+ }
+
+ writer.WriteEndArray();
+ }
+}
diff --git a/src/Cnblogs.DashScope.Core/Internals/ClientWebSocketWrapper.cs b/src/Cnblogs.DashScope.Core/Internals/ClientWebSocketWrapper.cs
new file mode 100644
index 0000000..71ebddb
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/Internals/ClientWebSocketWrapper.cs
@@ -0,0 +1,47 @@
+using System.Net.WebSockets;
+
+namespace Cnblogs.DashScope.Core.Internals;
+
+internal sealed class ClientWebSocketWrapper : IClientWebSocket
+{
+ private readonly ClientWebSocket _socket;
+
+ public ClientWebSocketWrapper(ClientWebSocket socket)
+ {
+ _socket = socket;
+ }
+
+ ///
+ public void Dispose()
+ {
+ _socket.Dispose();
+ }
+
+ ///
+ public ClientWebSocketOptions Options => _socket.Options;
+
+ ///
+ public WebSocketCloseStatus? CloseStatus => _socket.CloseStatus;
+
+ ///
+ public Task ConnectAsync(Uri uri, CancellationToken cancellation) => _socket.ConnectAsync(uri, cancellation);
+
+ ///
+ public Task SendAsync(
+ ArraySegment buffer,
+ WebSocketMessageType messageType,
+ bool endOfMessage,
+ CancellationToken cancellationToken)
+ => _socket.SendAsync(buffer, messageType, endOfMessage, cancellationToken);
+
+ ///
+ public Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken)
+ => _socket.ReceiveAsync(buffer, cancellationToken);
+
+ ///
+ public Task CloseAsync(
+ WebSocketCloseStatus closeStatus,
+ string? statusDescription,
+ CancellationToken cancellationToken)
+ => _socket.CloseAsync(closeStatus, statusDescription, cancellationToken);
+}
diff --git a/src/Cnblogs.DashScope.Core/Internals/DashScopeDateTimeConvertor.cs b/src/Cnblogs.DashScope.Core/Internals/DashScopeDateTimeConvertor.cs
index c4c9987..45fc52f 100644
--- a/src/Cnblogs.DashScope.Core/Internals/DashScopeDateTimeConvertor.cs
+++ b/src/Cnblogs.DashScope.Core/Internals/DashScopeDateTimeConvertor.cs
@@ -6,11 +6,7 @@ namespace Cnblogs.DashScope.Core.Internals;
internal class DashScopeDateTimeConvertor : JsonConverter
{
- private static readonly string[] DateTimeFormats =
- [
- "yyyy-MM-dd HH:mm:ss.fff",
- "yyyy-MM-dd HH:mm:ss.FFF"
- ];
+ private static readonly string[] DateTimeFormats = { "yyyy-MM-dd HH:mm:ss.fff", "yyyy-MM-dd HH:mm:ss.FFF" };
///
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
diff --git a/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs b/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs
index 5c1b537..7623c1b 100644
--- a/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs
+++ b/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs
@@ -1,6 +1,30 @@
-namespace Cnblogs.DashScope.Core.Internals;
+using System.Text.Json;
+using System.Text.Json.Serialization;
-internal static class DashScopeDefaults
+namespace Cnblogs.DashScope.Core.Internals;
+
+///
+/// Default values for DashScope client.
+///
+public static class DashScopeDefaults
{
- public const string DashScopeApiBaseAddress = "https://dashscope.aliyuncs.com/api/v1/";
+ ///
+ /// Base address for HTTP API.
+ ///
+ public const string HttpApiBaseAddress = "https://dashscope.aliyuncs.com/api/v1/";
+
+ ///
+ /// Base address for websocket API.
+ ///
+ public const string WebsocketApiBaseAddress = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/";
+
+ ///
+ /// Default json serializer options.
+ ///
+ public static readonly JsonSerializerOptions SerializationOptions =
+ new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ };
}
diff --git a/src/Cnblogs.DashScope.Core/Internals/IClientWebSocket.cs b/src/Cnblogs.DashScope.Core/Internals/IClientWebSocket.cs
new file mode 100644
index 0000000..b4a3cd7
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/Internals/IClientWebSocket.cs
@@ -0,0 +1,25 @@
+using System.Net.WebSockets;
+
+namespace Cnblogs.DashScope.Core.Internals;
+
+///
+/// Extract for testing purpose.
+///
+internal interface IClientWebSocket : IDisposable
+{
+ public ClientWebSocketOptions Options { get; }
+
+ public WebSocketCloseStatus? CloseStatus { get; }
+
+ public Task ConnectAsync(Uri uri, CancellationToken cancellation);
+
+ public Task SendAsync(
+ ArraySegment buffer,
+ WebSocketMessageType messageType,
+ bool endOfMessage,
+ CancellationToken cancellationToken);
+
+ Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken);
+
+ Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken);
+}
diff --git a/src/Cnblogs.DashScope.Core/Internals/IDashScopeWorkspaceConfig.cs b/src/Cnblogs.DashScope.Core/Internals/IDashScopeWorkspaceConfig.cs
new file mode 100644
index 0000000..752056b
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/Internals/IDashScopeWorkspaceConfig.cs
@@ -0,0 +1,12 @@
+namespace Cnblogs.DashScope.Core.Internals;
+
+///
+/// Workspace configuration.
+///
+internal interface IDashScopeWorkspaceConfig
+{
+ ///
+ /// Unique id of workspace to use.
+ ///
+ public string? WorkspaceId { get; }
+}
diff --git a/src/Cnblogs.DashScope.Core/Internals/TextGenerationStopConvertor.cs b/src/Cnblogs.DashScope.Core/Internals/TextGenerationStopConvertor.cs
index 1efd6b5..5f704a8 100644
--- a/src/Cnblogs.DashScope.Core/Internals/TextGenerationStopConvertor.cs
+++ b/src/Cnblogs.DashScope.Core/Internals/TextGenerationStopConvertor.cs
@@ -77,21 +77,21 @@ private static TextGenerationStop ReadArray(ref Utf8JsonReader reader)
case JsonTokenType.EndArray:
return type switch
{
- DeserializationArrayType.Strings => stringList ?? [],
- DeserializationArrayType.Token => intList?.ToArray() ?? [],
- DeserializationArrayType.Tokens => tokenList ?? [],
+ DeserializationArrayType.Strings => stringList ?? new List(),
+ DeserializationArrayType.Token => intList?.ToArray() ?? Array.Empty(),
+ DeserializationArrayType.Tokens => tokenList ?? new List(),
_ => throw new JsonException("Impossible deserialization type")
};
case JsonTokenType.StartArray when type is DeserializationArrayType.Tokens:
- tokenList ??= [];
+ tokenList ??= new List();
tokenList.Add(ReadTokenId(ref reader));
break;
case JsonTokenType.Number when type is DeserializationArrayType.Token:
- intList ??= [];
+ intList ??= new List();
intList.Add(reader.GetInt32());
break;
case JsonTokenType.String when type is DeserializationArrayType.Strings:
- stringList ??= [];
+ stringList ??= new List();
stringList.Add(reader.GetString()!);
break;
default:
diff --git a/src/Cnblogs.DashScope.Core/ModelRequest.cs b/src/Cnblogs.DashScope.Core/ModelRequest.cs
index 69ad50c..93fbfff 100644
--- a/src/Cnblogs.DashScope.Core/ModelRequest.cs
+++ b/src/Cnblogs.DashScope.Core/ModelRequest.cs
@@ -10,12 +10,12 @@ public class ModelRequest
///
/// The model to use.
///
- public required string Model { get; init; }
+ public string Model { get; init; } = string.Empty;
///
/// Input of this request.
///
- public required TInput Input { get; init; }
+ public TInput Input { get; init; } = null!;
}
///
diff --git a/src/Cnblogs.DashScope.Core/MultimodalInput.cs b/src/Cnblogs.DashScope.Core/MultimodalInput.cs
index 775b4d5..251e0ac 100644
--- a/src/Cnblogs.DashScope.Core/MultimodalInput.cs
+++ b/src/Cnblogs.DashScope.Core/MultimodalInput.cs
@@ -8,5 +8,5 @@ public class MultimodalInput
///
/// The messages of context, model will generate from last user message.
///
- public required IEnumerable Messages { get; set; }
+ public IEnumerable Messages { get; set; } = Array.Empty();
}
diff --git a/src/Cnblogs.DashScope.Core/MultimodalMessage.cs b/src/Cnblogs.DashScope.Core/MultimodalMessage.cs
index d1034b0..48bcc44 100644
--- a/src/Cnblogs.DashScope.Core/MultimodalMessage.cs
+++ b/src/Cnblogs.DashScope.Core/MultimodalMessage.cs
@@ -7,7 +7,11 @@ namespace Cnblogs.DashScope.Core;
///
/// The role associated with this message.
/// The contents of this message.
-public record MultimodalMessage(string Role, IReadOnlyList Content)
+/// Thoughts from the model.
+public record MultimodalMessage(
+ string Role,
+ IReadOnlyList Content,
+ string? ReasoningContent = null)
: IMessage>
{
///
@@ -34,9 +38,12 @@ public static MultimodalMessage System(IReadOnlyList c
/// Creates an assistant message.
///
/// Message contents.
+ /// Thoughts from the model.
///
- public static MultimodalMessage Assistant(IReadOnlyList contents)
+ public static MultimodalMessage Assistant(
+ IReadOnlyList contents,
+ string? reasoningContent = null)
{
- return new MultimodalMessage(DashScopeRoleNames.Assistant, contents);
+ return new MultimodalMessage(DashScopeRoleNames.Assistant, contents, reasoningContent);
}
}
diff --git a/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs b/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs
index 98fff19..aee7879 100644
--- a/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs
+++ b/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs
@@ -1,6 +1,4 @@
-using System.Diagnostics.CodeAnalysis;
-
-namespace Cnblogs.DashScope.Core;
+namespace Cnblogs.DashScope.Core;
///
/// Represents one content of a .
@@ -12,10 +10,8 @@ namespace Cnblogs.DashScope.Core;
/// For qwen-vl-ocr only. Minimal pixels for ocr task.
/// For qwen-vl-ocr only. Maximum pixels for ocr task.
public record MultimodalMessageContent(
- [StringSyntax(StringSyntaxAttribute.Uri)]
string? Image = null,
string? Text = null,
- [StringSyntax(StringSyntaxAttribute.Uri)]
string? Audio = null,
IEnumerable? Video = null,
int? MinPixels = null,
diff --git a/src/Cnblogs.DashScope.Core/QWenTokenizer.cs b/src/Cnblogs.DashScope.Core/QWenTokenizer.cs
index 3deec09..f114a14 100644
--- a/src/Cnblogs.DashScope.Core/QWenTokenizer.cs
+++ b/src/Cnblogs.DashScope.Core/QWenTokenizer.cs
@@ -1,48 +1,35 @@
-using System.Text.RegularExpressions;
-using Cnblogs.DashScope.Core.Internals;
-using Microsoft.ML.Tokenizers;
+using Cnblogs.DashScope.Core.Internals;
+using Microsoft.DeepDev;
namespace Cnblogs.DashScope.Core;
///
-/// Local implementation for QWen tokenizer
+/// Tokenizer using QWen
///
-public partial class QWenTokenizer
+public class QWenTokenizer
{
private static readonly Dictionary SpecialTokens =
- new List
- {
- "<|endoftext|>",
- "<|im_start|>",
- "<|im_end|>"
- }
+ new[] { "<|endoftext|>", "<|im_start|>", "<|im_end|>" }
.Concat(Enumerable.Range(0, 205).Select(x => $"<|extra_{x}|>"))
.Select((x, i) => new KeyValuePair(x, 151643 + i))
- .ToDictionary();
-
- [GeneratedRegex(
- @"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+",
- RegexOptions.Compiled,
- "zh-CN")]
- private static partial Regex Pattern();
+ .ToDictionary(x => x.Key, x => x.Value);
///
- /// Created tokenizer
+ /// Static tokenizer
///
- public static Tokenizer Tokenizer { get; } = TiktokenTokenizer.Create(
+ public static readonly ITokenizer Tokenizer = TokenizerBuilder.CreateTokenizer(
DashScopeEmbeddedResource.ReadBpeFile(),
- new RegexPreTokenizer(Pattern(), SpecialTokens),
- null,
- SpecialTokens);
+ SpecialTokens,
+ @"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+");
///
- /// Encode text to tokens.
+ /// Encode text.
///
- /// The text to encode.
+ /// The text to be encoded.
///
- public static IReadOnlyList Encode(string text)
+ public static List Encode(string text)
{
- return Tokenizer.EncodeToIds(text);
+ return Tokenizer.Encode(text, false);
}
///
@@ -56,25 +43,42 @@ public static string Decode(int[] tokens)
}
///
- /// Get token count for text.
+ /// Count tokens.
+ ///
+ /// Input text.
+ ///
+ public int CountTokens(string text)
+ {
+ return Tokenizer.Encode(text).Count;
+ }
+
+ ///
+ /// Split text to string tokens.
+ ///
+ /// Input text.
+ ///
+ public IReadOnlyList GetTokens(string text)
+ {
+ return Tokenizer.Encode(text).Select(x => Tokenizer.Decode(new[] { x })).ToList();
+ }
+
+ ///
+ /// Count tokens.
///
- /// The text to tokenize.
+ /// The text to be tokenized.
///
- public static int CountTokens(string text)
+ public static int CountTokensStatic(string text)
{
- return Tokenizer.CountTokens(text);
+ return Tokenizer.Encode(text).Count;
}
///
- /// Find the index of the maximum encoding capacity without surpassing the token limit.
+ /// Get tokens
///
- /// The input text.
- /// The maximum number of tokens to encode.
- /// If the tokenizer's normalization is enabled or is , this will be set to in its normalized form; otherwise, this value will be set to .
- /// The token count can be generated which should be smaller than the maximum token count.
+ /// The text to tokenizers.
///
- public static int GetIndexByTokenCount(string text, int maxTokenCount, out string? normalizedText, out int tokenCount)
+ public static IReadOnlyList GetTokensStatic(string text)
{
- return Tokenizer.GetIndexByTokenCount(text, maxTokenCount, out normalizedText, out tokenCount);
+ return Tokenizer.Encode(text).Select(x => Tokenizer.Decode(new[] { x })).ToList();
}
}
diff --git a/src/Cnblogs.DashScope.Core/SpeechSynthesizerInput.cs b/src/Cnblogs.DashScope.Core/SpeechSynthesizerInput.cs
new file mode 100644
index 0000000..8421fe2
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/SpeechSynthesizerInput.cs
@@ -0,0 +1,12 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Input for TTS task.
+///
+public class SpeechSynthesizerInput
+{
+ ///
+ /// Input text, can be null for run-task command.
+ ///
+ public string? Text { get; set; }
+}
diff --git a/src/Cnblogs.DashScope.Core/SpeechSynthesizerOutput.cs b/src/Cnblogs.DashScope.Core/SpeechSynthesizerOutput.cs
new file mode 100644
index 0000000..11a000d
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/SpeechSynthesizerOutput.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Output for TTS task.
+///
+/// The output sentences.
+public record SpeechSynthesizerOutput(SpeechSynthesizerOutputSentences? Sentence);
diff --git a/src/Cnblogs.DashScope.Core/SpeechSynthesizerOutputSentences.cs b/src/Cnblogs.DashScope.Core/SpeechSynthesizerOutputSentences.cs
new file mode 100644
index 0000000..0698aba
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/SpeechSynthesizerOutputSentences.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Sentences for TTS output.
+///
+/// Output words.
+public record SpeechSynthesizerOutputSentences(string[]? Words);
diff --git a/src/Cnblogs.DashScope.Core/SpeechSynthesizerParameters.cs b/src/Cnblogs.DashScope.Core/SpeechSynthesizerParameters.cs
new file mode 100644
index 0000000..8e59c19
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/SpeechSynthesizerParameters.cs
@@ -0,0 +1,47 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Parameters for TTS task.
+///
+public class SpeechSynthesizerParameters
+{
+ ///
+ /// Fixed to "PlainText"
+ ///
+ public string TextType { get; set; } = "PlainText";
+
+ ///
+ /// The voice to use.
+ ///
+ public string Voice { get; set; } = string.Empty;
+
+ ///
+ /// Output file format, can be pcm, wav or mp3.
+ ///
+ public string? Format { get; set; }
+
+ ///
+ /// Output audio sample rate.
+ ///
+ public int? SampleRate { get; set; }
+
+ ///
+ /// Output audio volume, range between 0-100, defaults to 50.
+ ///
+ public int? Volume { get; set; }
+
+ ///
+ /// Speech speed, range between 0.5~2.0, defaults to 1.0.
+ ///
+ public float? Rate { get; set; }
+
+ ///
+ /// Pitch of the voice, range between 0.5~2, defaults to 1.0.
+ ///
+ public float? Pitch { get; set; }
+
+ ///
+ /// Enable SSML, you can only send text once if enabled.
+ ///
+ public bool? EnableSsml { get; set; }
+}
diff --git a/src/Cnblogs.DashScope.Core/SpeechSynthesizerSocketSession.cs b/src/Cnblogs.DashScope.Core/SpeechSynthesizerSocketSession.cs
new file mode 100644
index 0000000..a3ed0e4
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/SpeechSynthesizerSocketSession.cs
@@ -0,0 +1,156 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Represents a socket-based TTS session.
+///
+public sealed class SpeechSynthesizerSocketSession
+ : IDisposable
+{
+ private readonly DashScopeClientWebSocketWrapper _socket;
+ private readonly string _modelId;
+
+ ///
+ /// Represents a socket-based TTS session.
+ ///
+ /// Underlying websocket.
+ /// Model name to use.
+ public SpeechSynthesizerSocketSession(DashScopeClientWebSocketWrapper socket, string modelId)
+ {
+ _socket = socket;
+ _modelId = modelId;
+ }
+
+ ///
+ /// Send a run-task command, use random GUID as taskId.
+ ///
+ /// Input parameters.
+ /// Input text.
+ /// The cancellation token to use.
+ /// The generated taskId.
+ public Task RunTaskAsync(
+ SpeechSynthesizerParameters parameters,
+ string? text = null,
+ CancellationToken cancellationToken = default)
+ {
+ return RunTaskAsync(Guid.NewGuid().ToString(), parameters, text, cancellationToken);
+ }
+
+ ///
+ /// Send a run-task command.
+ ///
+ /// Unique taskId.
+ /// Input parameters.
+ /// Input text.
+ /// The cancellation token to use.
+ /// .
+ public async Task RunTaskAsync(
+ string taskId,
+ SpeechSynthesizerParameters parameters,
+ string? text = null,
+ CancellationToken cancellationToken = default)
+ {
+ var command = new DashScopeWebSocketRequest
+ {
+ Header = new DashScopeWebSocketRequestHeader
+ {
+ Action = "run-task", TaskId = taskId,
+ },
+ Payload = new DashScopeWebSocketRequestPayload
+ {
+ Input = new SpeechSynthesizerInput { Text = text, },
+ TaskGroup = "audio",
+ Task = "tts",
+ Function = "SpeechSynthesizer",
+ Model = _modelId,
+ Parameters = parameters
+ }
+ };
+
+ _socket.ResetTask();
+ await _socket.SendMessageAsync(command, cancellationToken);
+ await _socket.TaskStarted;
+ return taskId;
+ }
+
+ ///
+ /// Append input text to task.
+ ///
+ /// TaskId to append.
+ /// Text to append.
+ /// Cancellation token to use.
+ public async Task ContinueTaskAsync(string taskId, string input, CancellationToken cancellationToken = default)
+ {
+ var command = new DashScopeWebSocketRequest
+ {
+ Header = new DashScopeWebSocketRequestHeader
+ {
+ Action = "continue-task",
+ TaskId = taskId,
+ Streaming = null
+ },
+ Payload = new DashScopeWebSocketRequestPayload
+ {
+ Input = new SpeechSynthesizerInput { Text = input }
+ }
+ };
+ await _socket.SendMessageAsync(command, cancellationToken);
+ }
+
+ ///
+ /// Send finish-task command.
+ ///
+ /// Unique id of the task.
+ /// The cancellation token to use.
+ public async Task FinishTaskAsync(string taskId, CancellationToken cancellationToken = default)
+ {
+ var command = new DashScopeWebSocketRequest
+ {
+ Header = new DashScopeWebSocketRequestHeader
+ {
+ TaskId = taskId,
+ Action = "finish-task",
+ Streaming = null
+ },
+ Payload = new DashScopeWebSocketRequestPayload
+ {
+ Input = new SpeechSynthesizerInput()
+ }
+ };
+ await _socket.SendMessageAsync(command, cancellationToken);
+ }
+
+ ///
+ /// Get the audio stream.
+ ///
+ ///
+ public IAsyncEnumerable GetAudioAsync()
+ {
+ return _socket.BinaryOutput;
+ }
+
+ ///
+ /// Get the message stream.
+ ///
+ ///
+ public async IAsyncEnumerable> GetMessagesAsync()
+ {
+ await foreach (var response in _socket.JsonOutput)
+ {
+ yield return response.DeserializeOutput();
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _socket.Dispose();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+}
diff --git a/src/Cnblogs.DashScope.Core/TextChatMessage.cs b/src/Cnblogs.DashScope.Core/TextChatMessage.cs
index 71d6fd8..3a3bfa2 100644
--- a/src/Cnblogs.DashScope.Core/TextChatMessage.cs
+++ b/src/Cnblogs.DashScope.Core/TextChatMessage.cs
@@ -6,18 +6,7 @@ namespace Cnblogs.DashScope.Core;
///
/// Represents a chat message between the user and the model.
///
-/// The role of this message.
-/// The content of this message.
-/// Used when role is tool, represents the function name of this message generated by.
-/// Notify model that next message should use this message as prefix.
-/// Calls to the function.
-[method: JsonConstructor]
-public record TextChatMessage(
- string Role,
- string Content,
- string? Name = null,
- bool? Partial = null,
- List? ToolCalls = null) : IMessage
+public record TextChatMessage : IMessage
{
///
/// Create chat message from an uploaded DashScope file.
@@ -37,6 +26,50 @@ public TextChatMessage(IEnumerable fileIds)
{
}
+ ///
+ /// Represents a chat message between the user and the model.
+ ///
+ /// The role of this message.
+ /// The content of this message.
+ /// Used when role is tool, represents the function name of this message generated by.
+ /// Notify model that next message should use this message as prefix.
+ /// Reasoning content for reasoning model.
+ /// Calls to the function.
+ [JsonConstructor]
+ public TextChatMessage(
+ string role,
+ string content,
+ string? name = null,
+ bool? partial = null,
+ string? reasoningContent = null,
+ List? toolCalls = null)
+ {
+ Role = role;
+ Content = content;
+ Name = name;
+ Partial = partial;
+ ReasoningContent = reasoningContent;
+ ToolCalls = toolCalls;
+ }
+
+ /// The role of this message.
+ public string Role { get; init; }
+
+ /// The content of this message.
+ public string Content { get; init; }
+
+ /// Used when role is tool, represents the function name of this message generated by.
+ public string? Name { get; init; }
+
+ /// Notify model that next message should use this message as prefix.
+ public bool? Partial { get; init; }
+
+ /// Reasoning content for reasoning model.
+ public string? ReasoningContent { get; init; }
+
+ /// Calls to the function.
+ public List? ToolCalls { get; init; }
+
///
/// Creates a file message.
///
@@ -84,11 +117,17 @@ public static TextChatMessage System(string content)
/// The content of the message.
/// When set to true, content of this message would be the prefix of next model output.
/// Author name.
+ /// Think content when reasoning.
/// Tool calls by model.
///
- public static TextChatMessage Assistant(string content, bool? partial = null, string? name = null, List? toolCalls = null)
+ public static TextChatMessage Assistant(
+ string content,
+ bool? partial = null,
+ string? name = null,
+ string? reasoningContent = null,
+ List? toolCalls = null)
{
- return new TextChatMessage(DashScopeRoleNames.Assistant, content, name, partial, toolCalls);
+ return new TextChatMessage(DashScopeRoleNames.Assistant, content, name, partial, reasoningContent, toolCalls);
}
///
diff --git a/src/Cnblogs.DashScope.Core/TextEmbeddingInput.cs b/src/Cnblogs.DashScope.Core/TextEmbeddingInput.cs
index 41295eb..fb884a2 100644
--- a/src/Cnblogs.DashScope.Core/TextEmbeddingInput.cs
+++ b/src/Cnblogs.DashScope.Core/TextEmbeddingInput.cs
@@ -8,5 +8,5 @@ public class TextEmbeddingInput
///
/// The texts to be computed.
///
- public required IEnumerable Texts { get; set; }
+ public IEnumerable Texts { get; set; } = Array.Empty();
}
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs b/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs
index 6f5d7ab..faca9b9 100644
--- a/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs
+++ b/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs
@@ -13,5 +13,10 @@ public class TextGenerationChoice
///
/// The generated message.
///
- public required TextChatMessage Message { get; set; }
+ public TextChatMessage Message { get; set; } = new(Array.Empty());
+
+ ///
+ /// Token array with log possibility info.
+ ///
+ public TextGenerationLogprobs? Logprobs { get; set; }
}
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationLogprobContent.cs b/src/Cnblogs.DashScope.Core/TextGenerationLogprobContent.cs
new file mode 100644
index 0000000..c8b8b7d
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationLogprobContent.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+using Cnblogs.DashScope.Core.Internals;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Represents a possible choice of token.
+///
+/// Token content.
+/// Token content in UTF-8 byte array.
+/// Possibility, null when it's too low.
+/// The most possible alternatives.
+public record TextGenerationLogprobContent(
+ string Token,
+ [property: JsonConverter(typeof(ByteArrayLiteralConvertor))]
+ byte[] Bytes,
+ float? Logprob,
+ List TopLogprobs);
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationLogprobs.cs b/src/Cnblogs.DashScope.Core/TextGenerationLogprobs.cs
new file mode 100644
index 0000000..ba4a107
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationLogprobs.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Possibilities of token choices.
+///
+/// The choices with their possibility.
+public record TextGenerationLogprobs(List Content);
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs b/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs
index 7288b8a..d0d43d8 100644
--- a/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs
+++ b/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs
@@ -19,4 +19,9 @@ public class TextGenerationOutput
/// Not null when . is "message".
///
public List? Choices { get; set; }
+
+ ///
+ /// Not null when . configured to show source.
+ ///
+ public TextGenerationWebSearchInfo? SearchInfo { get; set; }
}
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationOutputTokenDetails.cs b/src/Cnblogs.DashScope.Core/TextGenerationOutputTokenDetails.cs
new file mode 100644
index 0000000..8e3f49b
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationOutputTokenDetails.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Output details for text generation api.
+///
+/// Token count of reasoning content.
+public record TextGenerationOutputTokenDetails(int ReasoningTokens);
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs
index c5151f3..fc2fee9 100644
--- a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs
+++ b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs
@@ -1,4 +1,4 @@
-namespace Cnblogs.DashScope.Core;
+namespace Cnblogs.DashScope.Core;
///
/// The text generation options.
@@ -38,12 +38,33 @@ public class TextGenerationParameters : ITextGenerationParameters
///
public bool? EnableSearch { get; set; }
+ ///
+ public TextGenerationSearchOptions? SearchOptions { get; set; }
+
+ ///
+ public bool? EnableThinking { get; set; }
+
+ ///
+ public int? ThinkingBudget { get; set; }
+
+ ///
+ public bool? Logprobs { get; set; }
+
+ ///
+ public int? TopLogprobs { get; set; }
+
///
public IEnumerable? Tools { get; set; }
///
public ToolChoice? ToolChoice { get; set; }
+ ///
+ public bool? ParallelToolCalls { get; set; }
+
+ ///
+ public TextGenerationTranslationOptions? TranslationOptions { get; set; }
+
///
public bool? IncrementalOutput { get; set; }
}
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationPromptTokenDetails.cs b/src/Cnblogs.DashScope.Core/TextGenerationPromptTokenDetails.cs
new file mode 100644
index 0000000..0b4cef6
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationPromptTokenDetails.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Token usage details.
+///
+/// Token count of cached input.
+public record TextGenerationPromptTokenDetails(int CachedTokens);
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationSearchOptions.cs b/src/Cnblogs.DashScope.Core/TextGenerationSearchOptions.cs
new file mode 100644
index 0000000..d0d8cb4
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationSearchOptions.cs
@@ -0,0 +1,32 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Web search options
+///
+public class TextGenerationSearchOptions
+{
+ ///
+ /// Show search result in response. Defaults to false.
+ ///
+ public bool? EnableSource { get; set; }
+
+ ///
+ /// Include citation in output. Defaults to false.
+ ///
+ public bool? EnableCitation { get; set; }
+
+ ///
+ /// Citation format. Defaults to "[<number>]"
+ ///
+ public string? CitationFormat { get; set; }
+
+ ///
+ /// Force model to use web search. Defaults to false.
+ ///
+ public bool? ForcedSearch { get; set; }
+
+ ///
+ /// How many search records should be provided to model. "standard" - 5 records. "pro" - 10 records.
+ ///
+ public string? SearchStrategy { get; set; }
+}
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs b/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs
index 8021388..c908e55 100644
--- a/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs
+++ b/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs
@@ -11,6 +11,16 @@ public class TextGenerationTokenUsage
/// This number may larger than input if internet search is enabled.
public int InputTokens { get; set; }
+ ///
+ /// Input token details.
+ ///
+ public TextGenerationPromptTokenDetails? PromptTokensDetails { get; set; }
+
+ ///
+ /// Output token details.
+ ///
+ public TextGenerationOutputTokenDetails? OutputTokensDetails { get; set; }
+
///
/// The number of output token.
///
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationTopLogprobContent.cs b/src/Cnblogs.DashScope.Core/TextGenerationTopLogprobContent.cs
new file mode 100644
index 0000000..9f19187
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationTopLogprobContent.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+using Cnblogs.DashScope.Core.Internals;
+
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Represents one choice of most possibility alternative tokens.
+///
+/// The token content.
+/// The token content in UTF-8 byte array.
+/// Possibility, null when possibility is too low.
+public record TextGenerationTopLogprobContent(
+ string Token,
+ [property: JsonConverter(typeof(ByteArrayLiteralConvertor))]
+ byte[] Bytes,
+ float? Logprob);
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationTranslationOptions.cs b/src/Cnblogs.DashScope.Core/TextGenerationTranslationOptions.cs
new file mode 100644
index 0000000..d77f7c8
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationTranslationOptions.cs
@@ -0,0 +1,32 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Configurations when using translation models.
+///
+public class TextGenerationTranslationOptions
+{
+ ///
+ /// The language name of the input text. Use 'auto' to enable auto-detection.
+ ///
+ public string SourceLang { get; set; } = "auto";
+
+ ///
+ /// The language name of the output text.
+ ///
+ public string TargetLang { get; set; } = string.Empty;
+
+ ///
+ /// Term list for translation.
+ ///
+ public IEnumerable? Terms { get; set; }
+
+ ///
+ /// Sample texts for translation
+ ///
+ public IEnumerable? TmList { get; set; }
+
+ ///
+ /// Domain info about the source text. Only supports English.
+ ///
+ public string? Domains { get; set; }
+}
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationWebSearchInfo.cs b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchInfo.cs
new file mode 100644
index 0000000..27da418
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchInfo.cs
@@ -0,0 +1,7 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Web search information.
+///
+/// Web search results.
+public record TextGenerationWebSearchInfo(List SearchResults);
diff --git a/src/Cnblogs.DashScope.Core/TextGenerationWebSearchResult.cs b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchResult.cs
new file mode 100644
index 0000000..ccdf085
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchResult.cs
@@ -0,0 +1,11 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// Represents one web search record.
+///
+/// Source site name.
+/// Source site favicon url.
+/// Serial number of search records.
+/// Page title.
+/// Page url.
+public record TextGenerationWebSearchResult(string SiteName, string Icon, int Index, string Title, string Url);
diff --git a/src/Cnblogs.DashScope.Core/TranslationReference.cs b/src/Cnblogs.DashScope.Core/TranslationReference.cs
new file mode 100644
index 0000000..256056a
--- /dev/null
+++ b/src/Cnblogs.DashScope.Core/TranslationReference.cs
@@ -0,0 +1,8 @@
+namespace Cnblogs.DashScope.Core;
+
+///
+/// A text pair that used for translation reference.
+///
+/// The text in source language.
+/// The text in target language.
+public record TranslationReference(string Source, string Target);
diff --git a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuan2Llm.cs b/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuan2Llm.cs
deleted file mode 100644
index 8ee2af3..0000000
--- a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuan2Llm.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Cnblogs.DashScope.Sdk.BaiChuan;
-
-///
-/// BaiChuan2 model, supports prompt and message format.
-///
-public enum BaiChuan2Llm
-{
- ///
- /// baichuan2-7b-chat-v1
- ///
- BaiChuan2_7BChatV1 = 1,
-
- ///
- /// baichuan2-13b-chat-v1
- ///
- BaiChuan2_13BChatV1 = 2
-}
diff --git a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanLlm.cs b/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanLlm.cs
deleted file mode 100644
index 53c5d8a..0000000
--- a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanLlm.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Cnblogs.DashScope.Sdk.BaiChuan;
-
-///
-/// Supported baichuan model: https://help.aliyun.com/zh/dashscope/developer-reference/api-details-2
-///
-public enum BaiChuanLlm
-{
- ///
- /// baichuan-7b-v1
- ///
- BaiChuan7B = 1
-}
diff --git a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanLlmName.cs b/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanLlmName.cs
deleted file mode 100644
index b5ffff1..0000000
--- a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanLlmName.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Cnblogs.DashScope.Sdk.BaiChuan;
-
-internal static class BaiChuanLlmName
-{
- public static string GetModelName(this BaiChuanLlm llm)
- {
- return llm switch
- {
- BaiChuanLlm.BaiChuan7B => "baichuan-7b-v1",
- _ => ThrowHelper.UnknownModelName(nameof(llm), llm)
- };
- }
-
- public static string GetModelName(this BaiChuan2Llm llm)
- {
- return llm switch
- {
- BaiChuan2Llm.BaiChuan2_7BChatV1 => "baichuan2-7b-chat-v1",
- BaiChuan2Llm.BaiChuan2_13BChatV1 => "baichuan2-13b-chat-v1",
- _ => ThrowHelper.UnknownModelName(nameof(llm), llm)
- };
- }
-}
diff --git a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanTextGenerationApi.cs b/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanTextGenerationApi.cs
deleted file mode 100644
index ef3ec60..0000000
--- a/src/Cnblogs.DashScope.Sdk/BaiChuan/BaiChuanTextGenerationApi.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using Cnblogs.DashScope.Core;
-
-namespace Cnblogs.DashScope.Sdk.BaiChuan;
-
-///
-/// BaiChuan LLM generation apis, doc: https://help.aliyun.com/zh/dashscope/developer-reference/api-details-2
-///
-public static class BaiChuanTextGenerationApi
-{
- ///
- /// Get text completion from baichuan model.
- ///
- /// The .
- /// The llm to use.
- /// The prompt to generate completion from.
- ///
- public static Task> GetBaiChuanTextCompletionAsync(
- this IDashScopeClient client,
- BaiChuanLlm llm,
- string prompt)
- {
- return client.GetBaiChuanTextCompletionAsync(llm.GetModelName(), prompt);
- }
-
- ///
- /// Get text completion from baichuan model.
- ///
- /// The .
- /// The llm to use.
- /// The prompt to generate completion from.
- ///
- public static Task> GetBaiChuanTextCompletionAsync(
- this IDashScopeClient client,
- string llm,
- string prompt)
- {
- return client.GetTextCompletionAsync(
- new ModelRequest
- {
- Model = llm,
- Input = new TextGenerationInput { Prompt = prompt },
- Parameters = null
- });
- }
-
- ///
- /// Get text completion from baichuan model.
- ///
- /// The .
- /// The model name.
- /// The context messages.
- /// Can be 'text' or 'message', defaults to 'text'. Call to get available options.
- ///
- public static Task> GetBaiChuanTextCompletionAsync(
- this IDashScopeClient client,
- BaiChuan2Llm llm,
- IEnumerable messages,
- string? resultFormat = null)
- {
- return client.GetBaiChuanTextCompletionAsync(llm.GetModelName(), messages, resultFormat);
- }
-
- ///
- /// Get text completion from baichuan model.
- ///
- /// The .
- /// The model name.
- /// The context messages.
- /// Can be 'text' or 'message', defaults to 'text'. Call to get available options.
- ///
- public static Task> GetBaiChuanTextCompletionAsync(
- this IDashScopeClient client,
- string llm,
- IEnumerable messages,
- string? resultFormat = null)
- {
- return client.GetTextCompletionAsync(
- new ModelRequest
- {
- Model = llm,
- Input = new TextGenerationInput { Messages = messages },
- Parameters = string.IsNullOrEmpty(resultFormat) == false
- ? new TextGenerationParameters { ResultFormat = resultFormat }
- : null
- });
- }
-}
diff --git a/src/Cnblogs.DashScope.Sdk/Cnblogs.DashScope.Sdk.csproj b/src/Cnblogs.DashScope.Sdk/Cnblogs.DashScope.Sdk.csproj
index 3480ef8..68ba835 100644
--- a/src/Cnblogs.DashScope.Sdk/Cnblogs.DashScope.Sdk.csproj
+++ b/src/Cnblogs.DashScope.Sdk/Cnblogs.DashScope.Sdk.csproj
@@ -4,9 +4,6 @@
true
Cnblogs;Dashscope;AI;Sdk;Embedding;
-
-
-
diff --git a/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekLlm.cs b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekLlm.cs
new file mode 100644
index 0000000..b5ce953
--- /dev/null
+++ b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekLlm.cs
@@ -0,0 +1,17 @@
+namespace Cnblogs.DashScope.Sdk.DeepSeek;
+
+///
+/// DeepSeek models.
+///
+public enum DeepSeekLlm
+{
+ ///
+ /// deepseek-v3 model.
+ ///
+ DeepSeekV3 = 1,
+
+ ///
+ /// deepseek-r1 model.
+ ///
+ DeepSeekR1 = 2
+}
diff --git a/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekLlmName.cs b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekLlmName.cs
new file mode 100644
index 0000000..f0c8bfa
--- /dev/null
+++ b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekLlmName.cs
@@ -0,0 +1,14 @@
+namespace Cnblogs.DashScope.Sdk.DeepSeek;
+
+internal static class DeepSeekLlmName
+{
+ public static string GetModelName(this DeepSeekLlm model)
+ {
+ return model switch
+ {
+ DeepSeekLlm.DeepSeekR1 => "deepseek-r1",
+ DeepSeekLlm.DeepSeekV3 => "deepseek-v3",
+ _ => ThrowHelper.UnknownModelName(nameof(model), model)
+ };
+ }
+}
diff --git a/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekTextGenerationApi.cs b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekTextGenerationApi.cs
new file mode 100644
index 0000000..ecc988e
--- /dev/null
+++ b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekTextGenerationApi.cs
@@ -0,0 +1,87 @@
+using Cnblogs.DashScope.Core;
+
+namespace Cnblogs.DashScope.Sdk.DeepSeek;
+
+///
+/// Extensions for calling DeepSeek models, see: https://help.aliyun.com/zh/model-studio/developer-reference/deepseek
+///
+public static class DeepSeekTextGenerationApi
+{
+ private static TextGenerationParameters StreamingParameters { get; } = new() { IncrementalOutput = true };
+
+ ///
+ /// Get text completion from deepseek model.
+ ///
+ /// The .
+ /// The model name.
+ /// The context messages.
+ ///
+ public static async Task>
+ GetDeepSeekChatCompletionAsync(
+ this IDashScopeClient client,
+ DeepSeekLlm model,
+ IEnumerable messages)
+ {
+ return await client.GetDeepSeekChatCompletionAsync(model.GetModelName(), messages);
+ }
+
+ ///
+ /// Get text completion from deepseek model.
+ ///
+ /// The .
+ /// The model name.
+ /// The context messages.
+ ///
+ public static async Task>
+ GetDeepSeekChatCompletionAsync(
+ this IDashScopeClient client,
+ string model,
+ IEnumerable messages)
+ {
+ return await client.GetTextCompletionAsync(
+ new ModelRequest
+ {
+ Model = model,
+ Input = new TextGenerationInput { Messages = messages },
+ Parameters = null
+ });
+ }
+
+ ///
+ /// Get streamed completion from deepseek model.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IAsyncEnumerable>
+ GetDeepSeekChatCompletionStreamAsync(
+ this IDashScopeClient client,
+ DeepSeekLlm model,
+ IEnumerable messages)
+ {
+ return client.GetDeepSeekChatCompletionStreamAsync(model.GetModelName(), messages);
+ }
+
+ ///
+ /// Get streamed completion from deepseek model.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IAsyncEnumerable>
+ GetDeepSeekChatCompletionStreamAsync(
+ this IDashScopeClient client,
+ string model,
+ IEnumerable messages)
+ {
+ return client.GetTextCompletionStreamAsync(
+ new ModelRequest
+ {
+ Model = model,
+ Input = new TextGenerationInput { Messages = messages },
+ Parameters = StreamingParameters
+ });
+ }
+}
diff --git a/src/Cnblogs.DashScope.Sdk/FunctionDefinition.cs b/src/Cnblogs.DashScope.Sdk/FunctionDefinition.cs
index 1344496..042f5c5 100644
--- a/src/Cnblogs.DashScope.Sdk/FunctionDefinition.cs
+++ b/src/Cnblogs.DashScope.Sdk/FunctionDefinition.cs
@@ -1,5 +1,4 @@
using Cnblogs.DashScope.Core;
-using Json.Schema;
namespace Cnblogs.DashScope.Sdk;
@@ -13,8 +12,8 @@ public record FunctionDefinition : IFunctionDefinition
///
/// The name of the function.
/// Descriptions about this function for model to reference on.
- /// Parameter maps of this function.
- public FunctionDefinition(string name, string description, JsonSchema? parameters)
+ /// Parameter maps of this function, can be dictionary or JsonSchema.
+ public FunctionDefinition(string name, string description, object? parameters)
{
Name = name;
Description = description;
diff --git a/src/Cnblogs.DashScope.Sdk/Llama2/Llama2Model.cs b/src/Cnblogs.DashScope.Sdk/Llama2/Llama2Model.cs
deleted file mode 100644
index 827fd14..0000000
--- a/src/Cnblogs.DashScope.Sdk/Llama2/Llama2Model.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Cnblogs.DashScope.Sdk.Llama2;
-
-///
-/// Supported models for LLaMa2.
-///
-public enum Llama2Model
-{
- ///
- /// llama2-7b-chat-v2
- ///
- Chat7Bv2 = 1,
-
- ///
- /// llama2-13b-chat-v2
- ///
- Chat13Bv2 = 2
-}
diff --git a/src/Cnblogs.DashScope.Sdk/Llama2/Llama2ModelNames.cs b/src/Cnblogs.DashScope.Sdk/Llama2/Llama2ModelNames.cs
deleted file mode 100644
index 44357d3..0000000
--- a/src/Cnblogs.DashScope.Sdk/Llama2/Llama2ModelNames.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Cnblogs.DashScope.Sdk.Llama2;
-
-internal static class Llama2ModelNames
-{
- public static string GetModelName(this Llama2Model model)
- {
- return model switch
- {
- Llama2Model.Chat7Bv2 => "llama2-7b-chat-v2",
- Llama2Model.Chat13Bv2 => "llama2-13b-chat-v2",
- _ => ThrowHelper.UnknownModelName(nameof(model), model)
- };
- }
-}
diff --git a/src/Cnblogs.DashScope.Sdk/Llama2/Llama2TextGenerationApi.cs b/src/Cnblogs.DashScope.Sdk/Llama2/Llama2TextGenerationApi.cs
deleted file mode 100644
index 5fa9b45..0000000
--- a/src/Cnblogs.DashScope.Sdk/Llama2/Llama2TextGenerationApi.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using Cnblogs.DashScope.Core;
-
-namespace Cnblogs.DashScope.Sdk.Llama2;
-
-///
-/// Extensions for llama2 text generation, docs: https://help.aliyun.com/zh/dashscope/developer-reference/api-details-11
-///
-public static class Llama2TextGenerationApi
-{
- ///
- /// Get text completion from llama2 model.
- ///
- /// The .
- /// The model name.
- /// The context messages.
- /// Can be 'text' or 'message'. Call to get available options.
- ///
- public static async Task>
- GetLlama2TextCompletionAsync(
- this IDashScopeClient client,
- Llama2Model model,
- IEnumerable messages,
- string? resultFormat = null)
- {
- return await client.GetLlama2TextCompletionAsync(model.GetModelName(), messages, resultFormat);
- }
-
- ///
- /// Get text completion from llama2 model.
- ///
- /// The .
- /// The model name.
- /// The context messages.
- /// Can be 'text' or 'message'. Call to get available options.
- ///
- public static async Task>
- GetLlama2TextCompletionAsync(
- this IDashScopeClient client,
- string model,
- IEnumerable messages,
- string? resultFormat = null)
- {
- return await client.GetTextCompletionAsync(
- new ModelRequest
- {
- Model = model,
- Input = new TextGenerationInput { Messages = messages },
- Parameters = resultFormat != null
- ? new TextGenerationParameters { ResultFormat = resultFormat }
- : null
- });
- }
-}
diff --git a/src/Cnblogs.DashScope.Sdk/QWen/QWenLlm.cs b/src/Cnblogs.DashScope.Sdk/QWen/QWenLlm.cs
index b8dbc98..99beadd 100644
--- a/src/Cnblogs.DashScope.Sdk/QWen/QWenLlm.cs
+++ b/src/Cnblogs.DashScope.Sdk/QWen/QWenLlm.cs
@@ -133,5 +133,10 @@ public enum QWenLlm
///
/// qvq-72b-preview
///
- QwQ72BPreview = 25
+ QvQ72BPreview = 25,
+
+ ///
+ /// qwq-32b
+ ///
+ QwQ32B = 26
}
diff --git a/src/Cnblogs.DashScope.Sdk/QWen/QWenLlmNames.cs b/src/Cnblogs.DashScope.Sdk/QWen/QWenLlmNames.cs
index 8bc299e..a433baf 100644
--- a/src/Cnblogs.DashScope.Sdk/QWen/QWenLlmNames.cs
+++ b/src/Cnblogs.DashScope.Sdk/QWen/QWenLlmNames.cs
@@ -30,7 +30,8 @@ public static string GetModelName(this QWenLlm llm)
QWenLlm.QWenPlusLatest => "qwen-plus-latest",
QWenLlm.QWenTurboLatest => "qwen-turbo-latest",
QWenLlm.QwQ32BPreview => "qwq-32b-preview",
- QWenLlm.QwQ72BPreview => "qwq-72b-preview",
+ QWenLlm.QvQ72BPreview => "qvq-72b-preview",
+ QWenLlm.QwQ32B => "qwq-32b",
_ => ThrowHelper.UnknownModelName(nameof(llm), llm)
};
}
diff --git a/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModel.cs b/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModel.cs
index fa65480..abeeade 100644
--- a/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModel.cs
+++ b/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModel.cs
@@ -58,5 +58,25 @@ public enum QWenMultimodalModel
///
/// qwen-audio-turbo-latest
///
- QWenAudioTurboLatest = 11
+ QWenAudioTurboLatest = 11,
+
+ ///
+ /// qvq-max
+ ///
+ QvQMax = 12,
+
+ ///
+ /// qvq-max-latest
+ ///
+ QvQMaxLatest = 13,
+
+ ///
+ /// qvq-plus
+ ///
+ QvQPlus = 14,
+
+ ///
+ /// qvq-plus-latest
+ ///
+ QvQPlusLatest = 15
}
diff --git a/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModelNames.cs b/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModelNames.cs
index b5bd02a..e22aeda 100644
--- a/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModelNames.cs
+++ b/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalModelNames.cs
@@ -17,6 +17,10 @@ public static string GetModelName(this QWenMultimodalModel multimodalModel)
QWenMultimodalModel.QWenVlPlusLatest => "qwen-vl-plus-latest",
QWenMultimodalModel.QWenVlOcrLatest => "qwen-vl-ocr-latest",
QWenMultimodalModel.QWenAudioTurboLatest => "qwen-audio-turbo-latest",
+ QWenMultimodalModel.QvQMax => "qvq-max",
+ QWenMultimodalModel.QvQMaxLatest => "qvq-max-latest",
+ QWenMultimodalModel.QvQPlus => "qvq-plus",
+ QWenMultimodalModel.QvQPlusLatest => "qvq-plus-latest",
_ => ThrowHelper.UnknownModelName(nameof(multimodalModel), multimodalModel)
};
}
diff --git a/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModel.cs b/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModel.cs
index 808db24..72af726 100644
--- a/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModel.cs
+++ b/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModel.cs
@@ -19,4 +19,9 @@ public enum TextEmbeddingModel
/// text-embedding-v3
///
TextEmbeddingV3 = 3,
+
+ ///
+ /// text-embedding-v4
+ ///
+ TextEmbeddingV4 = 4
}
diff --git a/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModelNames.cs b/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModelNames.cs
index 410c33e..21d6b7d 100644
--- a/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModelNames.cs
+++ b/src/Cnblogs.DashScope.Sdk/TextEmbedding/TextEmbeddingModelNames.cs
@@ -9,6 +9,7 @@ public static string GetModelName(this TextEmbeddingModel model)
TextEmbeddingModel.TextEmbeddingV1 => "text-embedding-v1",
TextEmbeddingModel.TextEmbeddingV2 => "text-embedding-v2",
TextEmbeddingModel.TextEmbeddingV3 => "text-embedding-v3",
+ TextEmbeddingModel.TextEmbeddingV4 => "text-embedding-v4",
_ => ThrowHelper.UnknownModelName(nameof(model), model),
};
}
diff --git a/src/Cnblogs.DashScope.Sdk/Wanx/WanxModel.cs b/src/Cnblogs.DashScope.Sdk/Wanx/WanxModel.cs
index f278eeb..3d19fdd 100644
--- a/src/Cnblogs.DashScope.Sdk/Wanx/WanxModel.cs
+++ b/src/Cnblogs.DashScope.Sdk/Wanx/WanxModel.cs
@@ -8,5 +8,20 @@ public enum WanxModel
///
/// wanx-v1
///
- WanxV1 = 1
+ WanxV1 = 1,
+
+ ///
+ /// wanx2.1-t2i-plus
+ ///
+ WanxV21Plus = 2,
+
+ ///
+ /// wanx2.1-t2i-turbo
+ ///
+ WanxV21Turbo = 3,
+
+ ///
+ /// wanx2.0-t2i-turbo
+ ///
+ WanxV20Turbo = 4
}
diff --git a/src/Cnblogs.DashScope.Sdk/Wanx/WanxModelNames.cs b/src/Cnblogs.DashScope.Sdk/Wanx/WanxModelNames.cs
index ab04555..8e04751 100644
--- a/src/Cnblogs.DashScope.Sdk/Wanx/WanxModelNames.cs
+++ b/src/Cnblogs.DashScope.Sdk/Wanx/WanxModelNames.cs
@@ -7,6 +7,9 @@ public static string GetModelName(this WanxModel model)
return model switch
{
WanxModel.WanxV1 => "wanx-v1",
+ WanxModel.WanxV21Plus => "wanx2.1-t2i-plus",
+ WanxModel.WanxV21Turbo => "wanx2.1-t2i-turbo",
+ WanxModel.WanxV20Turbo => "wanx2.0-t2i-turbo",
_ => ThrowHelper.UnknownModelName(nameof(model), model)
};
}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/ChatClientTests.cs b/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs
similarity index 87%
rename from test/Cnblogs.DashScope.Sdk.UnitTests/ChatClientTests.cs
rename to test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs
index 89ac79c..8bd80fa 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/ChatClientTests.cs
+++ b/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs
@@ -1,12 +1,12 @@
using System.Text;
using Cnblogs.DashScope.Core;
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
+
using Microsoft.Extensions.AI;
using NSubstitute;
using NSubstitute.Extensions;
-namespace Cnblogs.DashScope.Sdk.UnitTests;
+namespace Cnblogs.DashScope.AI.UnitTests;
public class ChatClientTests
{
@@ -28,7 +28,7 @@ public async Task ChatClient_TextCompletion_SuccessAsync()
// Act
var response = await client.GetResponseAsync(
content,
- new ChatOptions()
+ new ChatOptions
{
FrequencyPenalty = parameter?.RepetitionPenalty,
PresencePenalty = parameter?.PresencePenalty,
@@ -46,7 +46,7 @@ public async Task ChatClient_TextCompletion_SuccessAsync()
Arg.Is>(
m => m.IsEquivalent(testCase.RequestModel)),
Arg.Any());
- response.Message.Text.Should().Be(testCase.ResponseModel.Output.Choices?.First().Message.Content);
+ Assert.Equal(testCase.ResponseModel.Output.Choices![0].Message.Content, response.Messages[0].Text);
}
[Fact]
@@ -69,7 +69,7 @@ public async Task ChatClient_TextCompletionStream_SuccessAsync()
// Act
var response = client.GetStreamingResponseAsync(
content,
- new ChatOptions()
+ new ChatOptions
{
FrequencyPenalty = parameter?.RepetitionPenalty,
PresencePenalty = parameter?.PresencePenalty,
@@ -79,7 +79,7 @@ public async Task ChatClient_TextCompletionStream_SuccessAsync()
Temperature = parameter?.Temperature,
TopK = parameter?.TopK,
TopP = parameter?.TopP,
- StopSequences = ["你好"],
+ StopSequences = new List { "你好" },
ToolMode = ChatToolMode.Auto
});
var text = new StringBuilder();
@@ -93,7 +93,7 @@ public async Task ChatClient_TextCompletionStream_SuccessAsync()
Arg.Is>(
m => m.IsEquivalent(testCase.RequestModel)),
Arg.Any());
- text.ToString().Should().Be(testCase.ResponseModel.Output.Choices?.First().Message.Content);
+ Assert.Equal(testCase.ResponseModel.Output.Choices![0].Message.Content, text.ToString());
}
[Fact]
@@ -113,7 +113,10 @@ public async Task ChatClient_ImageRecognition_SuccessAsync()
{
new(
ChatRole.User,
- [new DataContent(contents[0].Image!, "image/png"), new TextContent(contents[1].Text)])
+ new List
+ {
+ new DataContent(contents[0].Image!, "image/png"), new TextContent(contents[1].Text)
+ }),
};
var parameter = testCase.RequestModel.Parameters;
@@ -136,8 +139,7 @@ public async Task ChatClient_ImageRecognition_SuccessAsync()
await dashScopeClient.Received().GetMultimodalGenerationAsync(
Arg.Is>(m => m.IsEquivalent(testCase.RequestModel)),
Arg.Any());
- response.Choices[0].Text.Should()
- .BeEquivalentTo(testCase.ResponseModel.Output.Choices[0].Message.Content[0].Text);
+ Assert.Equal(testCase.ResponseModel.Output.Choices[0].Message.Content[0].Text, response.Messages[0].Text);
}
[Fact]
@@ -157,14 +159,17 @@ public async Task ChatClient_ImageRecognitionStream_SuccessAsync()
{
new(
ChatRole.User,
- [new DataContent(contents[0].Image!, "image/png"), new TextContent(contents[1].Text)])
+ new List
+ {
+ new DataContent(contents[0].Image!, "image/png"), new TextContent(contents[1].Text)
+ })
};
var parameter = testCase.RequestModel.Parameters;
// Act
var response = client.GetStreamingResponseAsync(
messages,
- new ChatOptions()
+ new ChatOptions
{
FrequencyPenalty = parameter?.RepetitionPenalty,
PresencePenalty = parameter?.PresencePenalty,
@@ -185,6 +190,6 @@ public async Task ChatClient_ImageRecognitionStream_SuccessAsync()
_ = dashScopeClient.Received().GetMultimodalGenerationStreamAsync(
Arg.Is>(m => m.IsEquivalent(testCase.RequestModel)),
Arg.Any());
- text.ToString().Should().Be(testCase.ResponseModel.Output.Choices.First().Message.Content[0].Text);
+ Assert.Equal(testCase.ResponseModel.Output.Choices.First().Message.Content[0].Text, text.ToString());
}
}
diff --git a/test/Cnblogs.DashScope.AI.UnitTests/Cnblogs.DashScope.AI.UnitTests.csproj b/test/Cnblogs.DashScope.AI.UnitTests/Cnblogs.DashScope.AI.UnitTests.csproj
new file mode 100644
index 0000000..293114c
--- /dev/null
+++ b/test/Cnblogs.DashScope.AI.UnitTests/Cnblogs.DashScope.AI.UnitTests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/EmbeddingClientTests.cs b/test/Cnblogs.DashScope.AI.UnitTests/EmbeddingClientTests.cs
similarity index 76%
rename from test/Cnblogs.DashScope.Sdk.UnitTests/EmbeddingClientTests.cs
rename to test/Cnblogs.DashScope.AI.UnitTests/EmbeddingClientTests.cs
index 96e21b4..b8c17e6 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/EmbeddingClientTests.cs
+++ b/test/Cnblogs.DashScope.AI.UnitTests/EmbeddingClientTests.cs
@@ -1,11 +1,10 @@
using Cnblogs.DashScope.Core;
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
using Microsoft.Extensions.AI;
using NSubstitute;
using NSubstitute.Extensions;
-namespace Cnblogs.DashScope.Sdk.UnitTests;
+namespace Cnblogs.DashScope.AI.UnitTests;
public class EmbeddingClientTests
{
@@ -27,17 +26,18 @@ public async Task EmbeddingClient_Text_SuccessAsync()
// Act
var response = await client.GenerateAsync(
content,
- new EmbeddingGenerationOptions()
+ new EmbeddingGenerationOptions
{
ModelId = testCase.RequestModel.Model, Dimensions = parameter?.Dimension
});
// Assert
_ = dashScopeClient.Received().GetEmbeddingsAsync(
- Arg.Is>(
- m => m.IsEquivalent(testCase.RequestModel)),
+ Arg.Is>(m
+ => m.IsEquivalent(testCase.RequestModel)),
Arg.Any());
- response.Select(x => x.Vector.ToArray()).Should()
- .BeEquivalentTo(testCase.ResponseModel.Output.Embeddings.Select(x => x.Embedding));
+ Assert.Equivalent(
+ testCase.ResponseModel.Output.Embeddings.Select(x => x.Embedding),
+ response.Select(x => x.Vector.ToArray()));
}
}
diff --git a/test/Cnblogs.DashScope.Sdk.SnapshotGenerator/Cnblogs.DashScope.Sdk.SnapshotGenerator.csproj b/test/Cnblogs.DashScope.Sdk.SnapshotGenerator/Cnblogs.DashScope.Sdk.SnapshotGenerator.csproj
deleted file mode 100644
index 8de488e..0000000
--- a/test/Cnblogs.DashScope.Sdk.SnapshotGenerator/Cnblogs.DashScope.Sdk.SnapshotGenerator.csproj
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- Exe
-
-
-
-
-
-
-
diff --git a/test/Cnblogs.DashScope.Sdk.SnapshotGenerator/Program.cs b/test/Cnblogs.DashScope.Sdk.SnapshotGenerator/Program.cs
deleted file mode 100644
index 3f99f6b..0000000
--- a/test/Cnblogs.DashScope.Sdk.SnapshotGenerator/Program.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System.Net;
-using System.Text;
-
-const string basePath = "../../../../Cnblogs.DashScope.Sdk.UnitTests/RawHttpData";
-var snapshots = new DirectoryInfo(basePath);
-Console.WriteLine("Reading key from environment variable DASHSCOPE_KEY");
-var apiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY");
-if (string.IsNullOrEmpty(apiKey))
-{
- Console.Write("ApiKey > ");
- apiKey = Console.ReadLine();
-}
-
-var handler = new SocketsHttpHandler() { AutomaticDecompression = DecompressionMethods.All, };
-var client = new HttpClient(handler) { BaseAddress = new Uri("https://dashscope.aliyuncs.com/api/v1/") };
-client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
-
-while (true)
-{
- Console.Write("Snapshot Name > ");
- var snapshotName = Console.ReadLine()?.Trim();
- if (string.IsNullOrEmpty(snapshotName))
- {
- continue;
- }
-
- var snapshot = snapshots.EnumerateFiles().Where(s => s.Name.StartsWith(snapshotName))
- .Select(s => s.Name.Split('.').First()).Distinct()
- .ToList();
- if (snapshot.Count == 0)
- {
- Console.WriteLine($"No snapshot was found with name: {snapshotName}");
- }
-
- Console.WriteLine($"Updating {snapshot.Count} snapshots ...");
- foreach (var name in snapshot)
- {
- Console.WriteLine($"Updating {name}");
- await UpdateSnapshotsAsync(client, name);
- Console.WriteLine($"{name} updated");
- }
-}
-
-static async Task UpdateSnapshotsAsync(HttpClient client, string name)
-{
- var requestHeader = await File.ReadAllLinesAsync(Path.Combine(basePath, $"{name}.request.header.txt"));
- var requestBodyFile = Path.Combine(basePath, $"{name}.request.body.json");
- var requestBody = File.Exists(requestBodyFile)
- ? await File.ReadAllTextAsync(Path.Combine(basePath, $"{name}.request.body.json"))
- : string.Empty;
- var firstLine = requestHeader[0].Split(' ');
- var method = HttpMethod.Parse(firstLine[0]);
- var request = new HttpRequestMessage(method, firstLine[1]);
- var contentType = "application/json";
- foreach (var header in requestHeader.Skip(1))
- {
- if (string.IsNullOrWhiteSpace(header))
- {
- continue;
- }
-
- var values = header.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
- if (values[0] == "Content-Type")
- {
- contentType = values[1];
- continue;
- }
-
- if (values[0] == "Content-Length")
- {
- continue;
- }
-
- request.Headers.Add(values[0], values[1]);
- }
-
- if (string.IsNullOrWhiteSpace(requestBodyFile) == false)
- {
- request.Content = new StringContent(requestBody, Encoding.Default, contentType);
- }
-
- var response = await client.SendAsync(request);
- var responseBody = await response.Content.ReadAsStringAsync();
- var responseHeaderFile = new StringBuilder();
- responseHeaderFile.AppendLine($"HTTP/1.1 {(int)response.StatusCode} {response.StatusCode}");
- responseHeaderFile = response.Headers.Aggregate(
- responseHeaderFile,
- (sb, pair) => sb.AppendLine($"{pair.Key}: {string.Join(',', pair.Value)}"));
- await File.WriteAllTextAsync(Path.Combine(basePath, $"{name}.response.header.txt"), responseHeaderFile.ToString());
- await File.WriteAllTextAsync(Path.Combine(basePath, $"{name}.response.body.txt"), responseBody);
-}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/ApplicationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/ApplicationSerializationTests.cs
new file mode 100644
index 0000000..6552149
--- /dev/null
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/ApplicationSerializationTests.cs
@@ -0,0 +1,144 @@
+using Cnblogs.DashScope.Tests.Shared.Utils;
+using NSubstitute;
+
+namespace Cnblogs.DashScope.Sdk.UnitTests;
+
+public class ApplicationSerializationTests
+{
+ [Fact]
+ public async Task SingleCompletion_TextNoSse_SuccessAsync()
+ {
+ // Arrange
+ const bool sse = false;
+ var testCase = Snapshots.Application.SinglePromptNoSse;
+ var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
+
+ // Act
+ var response = await client.GetApplicationResponseAsync("anyId", testCase.RequestModel);
+
+ // Assert
+ handler.Received().MockSend(
+ Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
+ Arg.Any());
+ Assert.Equivalent(testCase.ResponseModel, response);
+ }
+
+ [Fact]
+ public async Task SingleCompletion_ThoughtNoSse_SuccessAsync()
+ {
+ // Arrange
+ const bool sse = false;
+ var testCase = Snapshots.Application.SinglePromptWithThoughtsNoSse;
+ var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
+
+ // Act
+ var response = await client.GetApplicationResponseAsync("anyId", testCase.RequestModel);
+
+ // Assert
+ handler.Received().MockSend(
+ Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
+ Arg.Any());
+ Assert.Equivalent(testCase.ResponseModel, response);
+ }
+
+ [Fact]
+ public async Task SingleCompletion_TextSse_SuccessAsync()
+ {
+ // Arrange
+ const bool sse = true;
+ var testCase = Snapshots.Application.SinglePromptSse;
+ var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
+
+ // Act
+ var outputs = await client.GetApplicationResponseStreamAsync("anyId", testCase.RequestModel).ToListAsync();
+ var text = string.Join(string.Empty, outputs.Select(o => o.Output.Text));
+
+ // Assert
+ handler.Received().MockSend(
+ Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
+ Arg.Any());
+ Assert.All(outputs.SkipLast(1), x => Assert.Equal("null", x.Output.FinishReason));
+ Assert.Equal(testCase.ResponseModel.Output.Text, text);
+ var last = outputs.Last();
+ last = last with
+ {
+ Output = last.Output with
+ {
+ Text = testCase.ResponseModel.Output.Text, Thoughts = testCase.ResponseModel.Output.Thoughts
+ }
+ };
+ Assert.Equivalent(testCase.ResponseModel, last);
+ }
+
+ [Fact]
+ public async Task ConversationCompletion_SessionIdNoSse_SuccessAsync()
+ {
+ // Arrange
+ const bool sse = false;
+ var testCase = Snapshots.Application.ConversationSessionIdNoSse;
+ var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
+
+ // Act
+ var response = await client.GetApplicationResponseAsync("anyId", testCase.RequestModel);
+
+ // Assert
+ handler.Received().MockSend(
+ Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
+ Arg.Any());
+ Assert.Equivalent(testCase.ResponseModel, response);
+ }
+
+ [Fact]
+ public async Task ConversationCompletion_MessageNoSse_SuccessAsync()
+ {
+ // Arrange
+ const bool sse = false;
+ var testCase = Snapshots.Application.ConversationMessageNoSse;
+ var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
+
+ // Act
+ var response = await client.GetApplicationResponseAsync("anyId", testCase.RequestModel);
+
+ // Assert
+ handler.Received().MockSend(
+ Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
+ Arg.Any());
+ Assert.Equivalent(testCase.ResponseModel, response);
+ }
+
+ [Fact]
+ public async Task SingleCompletion_MemoryNoSse_SuccessAsync()
+ {
+ // Arrange
+ const bool sse = false;
+ var testCase = Snapshots.Application.SinglePromptWithMemoryNoSse;
+ var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
+
+ // Act
+ var response = await client.GetApplicationResponseAsync("anyId", testCase.RequestModel);
+
+ // Assert
+ handler.Received().MockSend(
+ Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
+ Arg.Any());
+ Assert.Equivalent(testCase.ResponseModel, response);
+ }
+
+ [Fact]
+ public async Task SingleCompletion_WorkflowNoSse_SuccessAsync()
+ {
+ // Arrange
+ const bool sse = false;
+ var testCase = Snapshots.Application.WorkflowNoSse;
+ var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
+
+ // Act
+ var response = await client.GetApplicationResponseAsync("anyId", testCase.RequestModel);
+
+ // Assert
+ handler.Received().MockSend(
+ Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
+ Arg.Any());
+ Assert.Equivalent(testCase.ResponseModel, response);
+ }
+}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/BackgroundGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/BackgroundGenerationSerializationTests.cs
index 6754aa2..14022b2 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/BackgroundGenerationSerializationTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/BackgroundGenerationSerializationTests.cs
@@ -1,5 +1,5 @@
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
+
using NSubstitute;
namespace Cnblogs.DashScope.Sdk.UnitTests;
@@ -21,6 +21,6 @@ public async Task BackgroundGeneration_CreateTask_SuccessAsync()
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
- response.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, response);
}
}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/BaiChuanApiTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/BaiChuanApiTests.cs
deleted file mode 100644
index 1184b1a..0000000
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/BaiChuanApiTests.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using Cnblogs.DashScope.Core;
-using Cnblogs.DashScope.Sdk.BaiChuan;
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using NSubstitute;
-
-namespace Cnblogs.DashScope.Sdk.UnitTests;
-
-public class BaiChuanApiTests
-{
- [Fact]
- public async Task BaiChuanTextGeneration_UseEnum_SuccessAsync()
- {
- // Arrange
- var client = Substitute.For();
-
- // Act
- _ = await client.GetBaiChuanTextCompletionAsync(BaiChuanLlm.BaiChuan7B, Cases.Prompt);
-
- // Assert
- _ = await client.Received().GetTextCompletionAsync(
- Arg.Is>(
- s => s.Model == "baichuan-7b-v1" && s.Input.Prompt == Cases.Prompt && s.Parameters == null));
- }
-
- [Fact]
- public async Task BaiChuanTextGeneration_CustomModel_SuccessAsync()
- {
- // Arrange
- var client = Substitute.For();
-
- // Act
- _ = await client.GetBaiChuanTextCompletionAsync(BaiChuanLlm.BaiChuan7B, Cases.Prompt);
-
- // Assert
- _ = await client.Received().GetTextCompletionAsync(
- Arg.Is>(
- s => s.Model == "baichuan-7b-v1" && s.Input.Prompt == Cases.Prompt && s.Parameters == null));
- }
-
- [Fact]
- public async Task BaiChuan2TextGeneration_UseEnum_SuccessAsync()
- {
- // Arrange
- var client = Substitute.For();
-
- // Act
- _ = await client.GetBaiChuanTextCompletionAsync(
- BaiChuan2Llm.BaiChuan2_13BChatV1,
- Cases.TextMessages,
- ResultFormats.Message);
-
- // Assert
- _ = await client.Received().GetTextCompletionAsync(
- Arg.Is>(
- s => s.Model == "baichuan2-13b-chat-v1"
- && s.Input.Messages == Cases.TextMessages
- && s.Parameters != null
- && s.Parameters.ResultFormat == ResultFormats.Message));
- }
-
- [Fact]
- public async Task BaiChuan2TextGeneration_CustomModel_SuccessAsync()
- {
- // Arrange
- var client = Substitute.For();
-
- // Act
- _ = await client.GetBaiChuanTextCompletionAsync(
- Cases.CustomModelName,
- Cases.TextMessages,
- ResultFormats.Message);
-
- // Assert
- _ = await client.Received().GetTextCompletionAsync(
- Arg.Is>(
- s => s.Model == Cases.CustomModelName
- && s.Input.Messages == Cases.TextMessages
- && s.Parameters != null
- && s.Parameters.ResultFormat == ResultFormats.Message));
- }
-}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/Cnblogs.DashScope.Sdk.UnitTests.csproj b/test/Cnblogs.DashScope.Sdk.UnitTests/Cnblogs.DashScope.Sdk.UnitTests.csproj
index 0c74f01..2fd5793 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/Cnblogs.DashScope.Sdk.UnitTests.csproj
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/Cnblogs.DashScope.Sdk.UnitTests.csproj
@@ -1,39 +1,37 @@
-
- false
- true
-
+
+ net6.0
+ false
+ true
+
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
-
-
-
+
+
+
-
-
- Always
-
-
+
+
+ Always
+
+
-
-
-
-
+
+
+
+
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientTests.cs
index be60571..a2317bd 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientTests.cs
@@ -1,12 +1,19 @@
using System.Net.Http.Headers;
using System.Reflection;
using Cnblogs.DashScope.Core;
-using FluentAssertions;
+using Xunit.Abstractions;
namespace Cnblogs.DashScope.Sdk.UnitTests;
public class DashScopeClientTests
{
+ private readonly ITestOutputHelper _output;
+
+ public DashScopeClientTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
[Fact]
public void DashScopeClient_Constructor_New()
{
@@ -14,10 +21,8 @@ public void DashScopeClient_Constructor_New()
const string apiKey = "apiKey";
// Act
- var act = () => new DashScopeClient(apiKey);
-
- // Assert
- act.Should().NotThrow();
+ var client = new DashScopeClient(apiKey);
+ _output.WriteLine("hash: " + client.GetHashCode()); // do something to avoid optimization
}
[Theory]
@@ -37,7 +42,7 @@ public void DashScopeClient_Constructor_NotCacheableParams(
var value2 = HttpClientAccessor.GetValue(client2);
// Assert
- value.Should().NotBe(value2);
+ Assert.NotSame(value2, value);
}
[Theory]
@@ -57,7 +62,7 @@ public void DashScopeClient_Constructor_CacheableParams(
var value2 = HttpClientAccessor.GetValue(client2);
// Assert
- value.Should().Be(value2);
+ Assert.Same(value2, value);
}
[Fact]
@@ -71,8 +76,37 @@ public void DashScopeClient_Constructor_WithApiKeyHeader()
var value = HttpClientAccessor.GetValue(client) as HttpClient;
// Assert
- value?.DefaultRequestHeaders.Authorization?.Should()
- .BeEquivalentTo(new AuthenticationHeaderValue("Bearer", apiKey));
+ Assert.Equivalent(new AuthenticationHeaderValue("Bearer", apiKey), value?.DefaultRequestHeaders.Authorization);
+ }
+
+ [Fact]
+ public void DashScopeClient_Constructor_WithWorkspaceId()
+ {
+ // Arrange
+ const string apiKey = "key";
+ const string workspaceId = "workspaceId";
+ var client = new DashScopeClient(apiKey, workspaceId: workspaceId);
+
+ // Act
+ var value = HttpClientAccessor.GetValue(client) as HttpClient;
+
+ // Assert
+ Assert.Equal(workspaceId, value?.DefaultRequestHeaders.GetValues("X-DashScope-WorkSpace").First());
+ }
+
+ [Fact]
+ public void DashScopeClient_Constructor_WithPrivateEndpoint()
+ {
+ // Arrange
+ const string apiKey = "key";
+ const string privateEndpoint = "https://dashscope.cnblogs.com/api/v1";
+ var client = new DashScopeClient(apiKey, null, privateEndpoint);
+
+ // Act
+ var value = HttpClientAccessor.GetValue(client) as HttpClient;
+
+ // Assert
+ Assert.Equivalent(new Uri(privateEndpoint), value?.BaseAddress);
}
public static TheoryData ParamsShouldNotCache
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketFactoryTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketFactoryTests.cs
new file mode 100644
index 0000000..4d13328
--- /dev/null
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketFactoryTests.cs
@@ -0,0 +1,41 @@
+using System.Net;
+using System.Net.WebSockets;
+using System.Reflection;
+using Cnblogs.DashScope.Core;
+using Cnblogs.DashScope.Core.Internals;
+
+namespace Cnblogs.DashScope.Sdk.UnitTests;
+
+public class DashScopeClientWebSocketFactoryTests
+{
+ private static readonly FieldInfo ClientWebSocketWrapperGetter =
+ typeof(DashScopeClientWebSocket).GetField("_socket", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ private static readonly FieldInfo ClientWebSocketGetter =
+ typeof(ClientWebSocketWrapper).GetField("_socket", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ private static readonly PropertyInfo RequestHeaderGetter = typeof(ClientWebSocketOptions).GetProperty(
+ "RequestHeaders",
+ BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ [Fact]
+ public void CreateSocket_WithWorkspaceId_SetKeyAndSpaceIdProperly()
+ {
+ // Arrange
+ const string apiKey = "apikey";
+ const string workspaceId = "some-space";
+ var factory = new DashScopeClientWebSocketFactory();
+
+ // Act
+ var socket = factory.GetClientWebSocket(apiKey, workspaceId);
+ var socketWrapper = ClientWebSocketWrapperGetter.GetValue(socket) as ClientWebSocketWrapper;
+ var clientWebSocket = ClientWebSocketGetter.GetValue(socketWrapper) as ClientWebSocket;
+ var headers = RequestHeaderGetter.GetValue(clientWebSocket?.Options) as WebHeaderCollection;
+
+ // Assert
+ Assert.NotNull(socketWrapper);
+ Assert.NotNull(headers);
+ Assert.Equal("bearer " + apiKey, headers.Get("Authorization"));
+ Assert.Equal(workspaceId, headers.Get("X-DashScope-WorkspaceId"));
+ }
+}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketPoolTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketPoolTests.cs
new file mode 100644
index 0000000..e484586
--- /dev/null
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketPoolTests.cs
@@ -0,0 +1,160 @@
+using Cnblogs.DashScope.Core;
+using Cnblogs.DashScope.Tests.Shared.Utils;
+using NSubstitute;
+using NSubstitute.Extensions;
+
+namespace Cnblogs.DashScope.Sdk.UnitTests;
+
+public class DashScopeClientWebSocketPoolTests
+{
+ [Fact]
+ public async Task RentSocket_PoolIsEmpty_CreateAsync()
+ {
+ // Arrange
+ var option = new DashScopeOptions();
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(new DashScopeClientWebSocket(new FakeClientWebSocket()));
+ var pool = new DashScopeClientWebSocketPool(factory, option);
+
+ // Act
+ var socket = await pool.RentSocketAsync();
+
+ // Assert
+ Assert.Equal(DashScopeWebSocketState.Ready, socket.Socket.State);
+ }
+
+ [Fact]
+ public async Task RentSocket_HasAvailableSocket_ReturnAsync()
+ {
+ // Arrange
+ var option = new DashScopeOptions();
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(new DashScopeClientWebSocket(new FakeClientWebSocket()));
+ var readySocket = factory.GetClientWebSocket(string.Empty);
+ await readySocket.ConnectAsync(new Uri(option.WebsocketBaseAddress));
+ var pool = new DashScopeClientWebSocketPool(new[] { readySocket }, factory);
+
+ // Act
+ var socket = await pool.RentSocketAsync();
+
+ // Assert
+ Assert.Equal(DashScopeWebSocketState.Ready, socket.Socket.State);
+ Assert.StrictEqual(readySocket, socket.Socket);
+ }
+
+ [Fact]
+ public async Task RentSocket_SocketExpired_DisposeAndMoveNext()
+ {
+ // Arrange
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(_ => new DashScopeClientWebSocket(new FakeClientWebSocket()));
+
+ var fakeSocket = new FakeClientWebSocket();
+ var closedSocket = new DashScopeClientWebSocket(fakeSocket);
+ await closedSocket.CloseAsync();
+ var pool = new DashScopeClientWebSocketPool(new[] { closedSocket }, factory);
+
+ // Act
+ var socket = await pool.RentSocketAsync();
+
+ // Assert
+ Assert.Equal(DashScopeWebSocketState.Ready, socket.Socket.State);
+ Assert.NotStrictEqual(closedSocket, socket.Socket);
+ Assert.True(fakeSocket.DisposeCalled);
+ }
+
+ [Fact]
+ public async Task RentSocket_PoolStarvation_ThrowAsync()
+ {
+ // Arrange
+ var option = new DashScopeOptions { SocketPoolSize = 3 };
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(_ => new DashScopeClientWebSocket(new FakeClientWebSocket()));
+ var pool = new DashScopeClientWebSocketPool(factory, option);
+ await Task.WhenAll(Enumerable.Range(0, option.SocketPoolSize).Select(async _ => await pool.RentSocketAsync()));
+
+ // Act
+ var act = async () => await pool.RentSocketAsync();
+
+ // Assert
+ await Assert.ThrowsAsync(act);
+ }
+
+ [Fact]
+ public async Task Dispose_ManuallyDispose_DisposeAllPooledSocketsAsync()
+ {
+ // Arrange
+ var option = new DashScopeOptions();
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(_ => new DashScopeClientWebSocket(new FakeClientWebSocket()));
+ var fake1 = new FakeClientWebSocket();
+ var fake2 = new FakeClientWebSocket();
+ var s1 = new DashScopeClientWebSocket(fake1);
+ var s2 = new DashScopeClientWebSocket(fake2);
+ var sockets = new[] { s1, s2 };
+ foreach (var socket in sockets)
+ {
+ await socket.ConnectAsync(new Uri(option.WebsocketBaseAddress));
+ }
+
+ var pool = new DashScopeClientWebSocketPool(sockets, factory);
+
+ // Act
+ var active = await pool.RentSocketAsync();
+ pool.Dispose();
+
+ // Assert
+ Assert.NotNull(active);
+ Assert.True(fake1.DisposeCalled);
+ Assert.True(fake2.DisposeCalled);
+ }
+
+ [Fact]
+ public async Task ReturnSocket_SocketNotReady_DisposeAsync()
+ {
+ // Arrange
+ var option = new DashScopeOptions();
+ var fakeSocket = new FakeClientWebSocket();
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(new DashScopeClientWebSocket(fakeSocket));
+ var pool = new DashScopeClientWebSocketPool(factory, option);
+
+ // Act
+ var socket = await pool.RentSocketAsync();
+ await fakeSocket.WriteServerCloseAsync();
+ pool.ReturnSocket(socket.Socket);
+
+ // Assert
+ Assert.Equal(0, pool.ActiveSocketCount);
+ Assert.Equal(0, pool.AvailableSocketCount);
+ Assert.Equal(DashScopeWebSocketState.Closed, socket.Socket.State);
+ Assert.True(fakeSocket.DisposeCalled);
+ }
+
+ [Fact]
+ public async Task ReturnSocket_SocketReady_SaveAsync()
+ {
+ // Arrange
+ var option = new DashScopeOptions();
+ var fakeSocket = new FakeClientWebSocket();
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(new DashScopeClientWebSocket(fakeSocket));
+ var pool = new DashScopeClientWebSocketPool(factory, option);
+
+ // Act
+ var socket = await pool.RentSocketAsync();
+ pool.ReturnSocket(socket.Socket);
+
+ // Assert
+ Assert.Equal(1, pool.AvailableSocketCount);
+ Assert.Equal(0, pool.ActiveSocketCount);
+ Assert.False(fakeSocket.DisposeCalled);
+ }
+}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketTests.cs
new file mode 100644
index 0000000..5f789c2
--- /dev/null
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketTests.cs
@@ -0,0 +1,277 @@
+using System.Net;
+using System.Net.WebSockets;
+using System.Reflection;
+using Cnblogs.DashScope.Core;
+using Cnblogs.DashScope.Core.Internals;
+using Cnblogs.DashScope.Tests.Shared.Utils;
+using NSubstitute;
+
+namespace Cnblogs.DashScope.Sdk.UnitTests;
+
+[Collection(nameof(SocketTestsCollection))]
+public class DashScopeClientWebSocketTests
+{
+ private static readonly FieldInfo InnerSocketInfo =
+ typeof(DashScopeClientWebSocket).GetField("_socket", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?? throw new InvalidOperationException(
+ $"Can not found {nameof(DashScopeClientWebSocket)}._client, please update this test after refactoring");
+
+ private static readonly PropertyInfo InnerRequestHeaderInfo =
+ typeof(ClientWebSocketOptions).GetProperty("RequestHeaders", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?? throw new InvalidOperationException(
+ $"Can not found {nameof(ClientWebSocketOptions)}.RequestHeaders property, please update this test after framework change");
+
+ [Fact]
+ public void Constructor_UseApiKeyAndWorkspaceId_EnsureConfigured()
+ {
+ // Arrange
+ const string apiKey = "apiKey";
+ const string workspaceId = "workspaceId";
+
+ // Act
+ var client = new DashScopeClientWebSocket(apiKey, workspaceId);
+ var headers = ExtractHeaders(client);
+
+ // Assert
+ Assert.Equal($"bearer {apiKey}", headers.GetValues("Authorization")?.First());
+ Assert.Equal("enable", headers.GetValues("X-DashScope-DataInspection")?.First());
+ Assert.Equal(workspaceId, headers.GetValues("X-DashScope-WorkspaceId")?.First());
+ }
+
+ [Fact]
+ public void Constructor_UseApiKeyWithoutWorkspaceId_EnsureConfigured()
+ {
+ // Arrange
+ const string apiKey = "apiKey";
+
+ // Act
+ var client = new DashScopeClientWebSocket(apiKey);
+ var headers = ExtractHeaders(client);
+
+ // Assert
+ Assert.Equal($"bearer {apiKey}", headers.GetValues("Authorization")?.First());
+ Assert.Equal("enable", headers.GetValues("X-DashScope-DataInspection")?.First());
+ Assert.Null(headers.GetValues("X-DashScope-WorkspaceId"));
+ }
+
+ [Fact]
+ public void Constructor_UsePreconfiguredSocket_EnsureConfigured()
+ {
+ // Arrange
+ using var socket = new ClientWebSocketWrapper(new ClientWebSocket());
+
+ // Act
+ var client = new DashScopeClientWebSocket(socket);
+
+ // Assert
+ Assert.StrictEqual(socket, InnerSocketInfo.GetValue(client));
+ }
+
+ [Fact]
+ public async Task ConnectAsync_InitialConnect_ChangeStateAsync()
+ {
+ // Arrange
+ var socket = Substitute.For();
+ var client = new DashScopeClientWebSocket(socket);
+ var apiUri = new Uri("ws://test.com");
+
+ // Act
+ await client.ConnectAsync(apiUri);
+
+ // Assert
+ Assert.Equal(DashScopeWebSocketState.Ready, client.State);
+ await socket.Received(1).ConnectAsync(Arg.Is(apiUri), Arg.Any());
+ await socket.Received().ReceiveAsync(Arg.Any>(), Arg.Any());
+ }
+
+ [Fact]
+ public async Task ResetOutput_WithInitialOutput_CompleteThenCreateNewOutputAsync()
+ {
+ // Arrange
+ var socket = Substitute.For();
+ var client = new DashScopeClientWebSocket(socket);
+ client.ResetOutput();
+ var oldBinary = client.BinaryOutput;
+ var oldJson = client.JsonOutput;
+ var oldSignal = client.TaskStarted;
+
+ // Act
+ client.ResetOutput();
+
+ // Assert
+ Assert.False(await oldSignal);
+ Assert.True(oldBinary.Completion.IsCompletedSuccessfully);
+ Assert.NotSame(oldBinary, client.BinaryOutput);
+ Assert.True(oldJson.Completion.IsCompletedSuccessfully);
+ Assert.NotSame(oldJson, client.JsonOutput);
+ Assert.NotSame(oldSignal, client.TaskStarted);
+ }
+
+ [Fact]
+ public async Task SendMessageAsync_SocketClosed_ThrowAsync()
+ {
+ // Arrange
+ var socket = Substitute.For();
+ var client = new DashScopeClientWebSocket(socket);
+ var snapshot = Snapshots.SpeechSynthesizer.RunTask;
+ await client.CloseAsync();
+
+ // Act
+ var act = () => client.SendMessageAsync(snapshot.Message);
+
+ // Assert
+ await Assert.ThrowsAsync(act);
+ }
+
+ [Fact]
+ public async Task SendMessageAsync_Connected_SendAsync()
+ {
+ // Arrange
+ var socket = Substitute.For();
+ var client = new DashScopeClientWebSocket(socket);
+ var snapshot = Snapshots.SpeechSynthesizer.RunTask;
+
+ // Act
+ await client.ConnectAsync(new Uri(DashScopeDefaults.WebsocketApiBaseAddress));
+ await client.SendMessageAsync(snapshot.Message);
+
+ // Assert
+ await socket.Received().SendAsync(
+ Arg.Is>(s => Checkers.IsJsonEquivalent(s, snapshot.GetMessageJson())),
+ WebSocketMessageType.Text,
+ true,
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task ReceiveMessageAsync_ServerClosed_CloseAsync()
+ {
+ // Arrange
+ var (_, dashScopeClientWebSocket, server) = await Sut.GetSocketTestClientAsync();
+
+ // Act
+ await server.WriteServerCloseAsync();
+
+ // Assert
+ Assert.Equal(DashScopeWebSocketState.Closed, dashScopeClientWebSocket.State);
+ Assert.Equal(WebSocketCloseStatus.NormalClosure, server.CloseStatus);
+ }
+
+ [Fact]
+ public async Task ReceiveMessageAsync_TaskStarted_UpdateStateToRunningAsync()
+ {
+ // Arrange
+ var (_, clientWebSocket, server) = await Sut.GetSocketTestClientAsync();
+ var snapshot = Snapshots.SpeechSynthesizer.TaskStarted;
+ var taskStarted = clientWebSocket.TaskStarted;
+
+ // Act
+ await server.WriteServerMessageAsync(snapshot.GetMessageJson());
+ var timeout = Task.Delay(2000); // socket handles message in other thread, wait for it.
+ var any = await Task.WhenAny(timeout, taskStarted);
+
+ // Assert
+ Assert.Equal(any, taskStarted);
+ Assert.Equal(DashScopeWebSocketState.RunningTask, clientWebSocket.State);
+ }
+
+ [Fact]
+ public async Task ReceiveMessageAsync_TaskFinished_UpdateStateToReadyAsync()
+ {
+ // Arrange
+ var (_, clientWebSocket, server) = await Sut.GetSocketTestClientAsync();
+ await server.WriteServerMessageAsync(Snapshots.SpeechSynthesizer.TaskStarted.GetMessageJson());
+ await clientWebSocket.TaskStarted;
+ var snapshot = Snapshots.SpeechSynthesizer.TaskFinished;
+ var binaryOutput = clientWebSocket.BinaryOutput;
+ var jsonOutput = clientWebSocket.JsonOutput;
+
+ // Act
+ await server.WriteServerMessageAsync(snapshot.GetMessageJson());
+ var json = await jsonOutput.ReadAllAsync().ToListAsync();
+
+ // Assert
+ Assert.True(binaryOutput.Completion.IsCompleted);
+ Assert.True(jsonOutput.Completion.IsCompleted);
+ Assert.Equal(2, json.Count);
+ Assert.Equal(DashScopeWebSocketState.Ready, clientWebSocket.State);
+ }
+
+ [Fact]
+ public async Task ReceiveMessageAsync_TaskFailed_CloseAndThrowAsync()
+ {
+ // Arrange
+ var (_, clientWebSocket, server) = await Sut.GetSocketTestClientAsync();
+ await server.WriteServerMessageAsync(Snapshots.SpeechSynthesizer.TaskStarted.GetMessageJson());
+ await clientWebSocket.TaskStarted;
+ var taskFailed = Snapshots.SpeechSynthesizer.TaskFailed;
+ var binary = clientWebSocket.BinaryOutput;
+ var json = clientWebSocket.JsonOutput;
+
+ // Act
+ await server.WriteServerMessageAsync(taskFailed.GetMessageJson());
+ await server.WriteServerCloseAsync();
+ var messages = await json.ReadAllAsync().ToListAsync();
+
+ // Assert
+ Assert.True(binary.Completion.IsCompleted);
+ Assert.True(json.Completion.IsCompleted);
+ Assert.Equal(2, messages.Count);
+ Assert.Equal(DashScopeWebSocketState.Closed, clientWebSocket.State);
+ }
+
+ [Fact]
+ public async Task ReceiveMessageAsync_ReceiveBinary_WriteToBinaryOutputAsync()
+ {
+ // Arrange
+ var (_, clientWebSocket, server) = await Sut.GetSocketTestClientAsync();
+ await server.WriteServerMessageAsync(Snapshots.SpeechSynthesizer.TaskStarted.GetMessageJson());
+ await clientWebSocket.TaskStarted;
+ var expectedAudio = Snapshots.SpeechSynthesizer.AudioTts;
+ var output = clientWebSocket.BinaryOutput;
+ var audioTask = output.ReadAllAsync().ToArrayAsync();
+
+ // Act
+ await server.WriteServerMessageAsync(expectedAudio);
+ await server.WriteServerMessageAsync(Snapshots.SpeechSynthesizer.TaskFinished.GetMessageJson());
+ var audio = await audioTask;
+
+ // Assert
+ Assert.True(output.Completion.IsCompleted);
+ Assert.Equal(expectedAudio, audio);
+ Assert.Equal(DashScopeWebSocketState.Ready, clientWebSocket.State);
+ }
+
+ [Fact]
+ public async Task Dispose_ManuallyCalled_DisposeSocketAndOutputTogetherAsync()
+ {
+ // Arrange
+ var (_, clientWebSocket, server) = await Sut.GetSocketTestClientAsync();
+ var output = clientWebSocket.BinaryOutput;
+
+ // Act
+ clientWebSocket.Dispose();
+
+ // Assert
+ Assert.True(output.Completion.IsCompleted);
+ Assert.True(server.DisposeCalled);
+ }
+
+ private static WebHeaderCollection ExtractHeaders(DashScopeClientWebSocket socket)
+ {
+ var obj = InnerSocketInfo.GetValue(socket);
+ if (obj is not IClientWebSocket clientWebSocket)
+ {
+ throw new InvalidOperationException($"Get null when trying to fetch {InnerSocketInfo.Name}");
+ }
+
+ obj = InnerRequestHeaderInfo.GetValue(clientWebSocket.Options);
+ if (obj is not WebHeaderCollection headers)
+ {
+ throw new InvalidOperationException(
+ $"Wrong type or null when trying to fetch {InnerRequestHeaderInfo.Name}");
+ }
+
+ return headers;
+ }
+}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketWrapperTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketWrapperTests.cs
new file mode 100644
index 0000000..5d1ce82
--- /dev/null
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/DashScopeClientWebSocketWrapperTests.cs
@@ -0,0 +1,31 @@
+using Cnblogs.DashScope.Core;
+using Cnblogs.DashScope.Tests.Shared.Utils;
+using NSubstitute;
+using NSubstitute.Extensions;
+using Xunit.Abstractions;
+
+namespace Cnblogs.DashScope.Sdk.UnitTests;
+
+public class DashScopeClientWebSocketWrapperTests
+{
+ [Fact]
+ public async Task Dispose_CallDispose_ReturnSocketToPoolAsync()
+ {
+ // Arrange
+ var option = new DashScopeOptions();
+ var fakeSocket = new FakeClientWebSocket();
+ var factory = Substitute.For();
+ factory.Configure().GetClientWebSocket(Arg.Any(), Arg.Any())
+ .Returns(new DashScopeClientWebSocket(fakeSocket));
+ var pool = new DashScopeClientWebSocketPool(factory, option);
+
+ // Act
+ var socket = await pool.RentSocketAsync();
+ socket.Dispose();
+
+ // Assert
+ Assert.Equal(1, pool.AvailableSocketCount);
+ Assert.Equal(0, pool.ActiveSocketCount);
+ Assert.False(fakeSocket.DisposeCalled);
+ }
+}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/DeepSeekTextGenerationApiTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/DeepSeekTextGenerationApiTests.cs
new file mode 100644
index 0000000..87643dd
--- /dev/null
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/DeepSeekTextGenerationApiTests.cs
@@ -0,0 +1,95 @@
+using Cnblogs.DashScope.Core;
+using Cnblogs.DashScope.Sdk.DeepSeek;
+using NSubstitute;
+
+namespace Cnblogs.DashScope.Sdk.UnitTests;
+
+public class DeepSeekTextGenerationApiTests
+{
+ [Fact]
+ public async Task TextCompletion_UseEnum_SuccessAsync()
+ {
+ // Arrange
+ var client = Substitute.For();
+
+ // Act
+ await client.GetDeepSeekChatCompletionAsync(
+ DeepSeekLlm.DeepSeekR1,
+ new List { TextChatMessage.User("你好") }.AsReadOnly());
+
+ // Assert
+ await client.Received().GetTextCompletionAsync(
+ Arg.Is>(x
+ => x.Model == "deepseek-r1" && x.Input.Messages!.First().Content == "你好" && x.Parameters == null));
+ }
+
+ [Fact]
+ public async Task TextCompletion_UseInvalidEnum_SuccessAsync()
+ {
+ // Arrange
+ var client = Substitute.For();
+
+ // Act
+ var act = async () => await client.GetDeepSeekChatCompletionAsync(
+ (DeepSeekLlm)(-1),
+ new List { TextChatMessage.User("你好") }.AsReadOnly());
+
+ // Assert
+ await Assert.ThrowsAsync(act);
+ }
+
+ [Fact]
+ public async Task TextCompletion_UseCustomModel_SuccessAsync()
+ {
+ // Arrange
+ const string customModel = "deepseek-v3";
+ var client = Substitute.For();
+
+ // Act
+ await client.GetDeepSeekChatCompletionAsync(
+ customModel,
+ new List { TextChatMessage.User("你好") }.AsReadOnly());
+
+ // Assert
+ await client.Received().GetTextCompletionAsync(
+ Arg.Is>(x
+ => x.Model == customModel && x.Input.Messages!.First().Content == "你好" && x.Parameters == null));
+ }
+
+ [Fact]
+ public void StreamCompletion_UseEnum_SuccessAsync()
+ {
+ // Arrange
+ var client = Substitute.For();
+
+ // Act
+ _ = client.GetDeepSeekChatCompletionStreamAsync(
+ DeepSeekLlm.DeepSeekV3,
+ new List { TextChatMessage.User("你好") }.AsReadOnly());
+
+ // Assert
+ _ = client.Received().GetTextCompletionStreamAsync(
+ Arg.Is>(x => x.Model == "deepseek-v3"
+ && x.Input.Messages!.First().Content == "你好"
+ && x.Parameters!.IncrementalOutput == true));
+ }
+
+ [Fact]
+ public void StreamCompletion_CustomModel_SuccessAsync()
+ {
+ // Arrange
+ const string customModel = "deepseek-v3";
+ var client = Substitute.For();
+
+ // Act
+ _ = client.GetDeepSeekChatCompletionStreamAsync(
+ customModel,
+ new List { TextChatMessage.User("你好") }.AsReadOnly());
+
+ // Assert
+ _ = client.Received().GetTextCompletionStreamAsync(
+ Arg.Is>(x => x.Model == customModel
+ && x.Input.Messages!.First().Content == "你好"
+ && x.Parameters!.IncrementalOutput == true));
+ }
+}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/ErrorTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/ErrorTests.cs
index e660852..3f3d9b5 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/ErrorTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/ErrorTests.cs
@@ -1,6 +1,5 @@
using Cnblogs.DashScope.Core;
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.Extensions;
@@ -21,7 +20,8 @@ public async Task Error_AuthError_ExceptionAsync()
var act = async () => await client.GetTextCompletionAsync(testCase.RequestModel);
// Assert
- (await act.Should().ThrowAsync()).And.Error.Should().BeEquivalentTo(testCase.ResponseModel);
+ var ex = await Assert.ThrowsAsync(act);
+ Assert.Equivalent(testCase.ResponseModel, ex.Error);
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
@@ -39,7 +39,8 @@ public async Task Error_ParameterErrorNoSse_ExceptionAsync()
var act = async () => await client.GetTextCompletionAsync(testCase.RequestModel);
// Assert
- (await act.Should().ThrowAsync()).And.Error.Should().BeEquivalentTo(testCase.ResponseModel);
+ var ex = await Assert.ThrowsAsync(act);
+ Assert.Equivalent(testCase.ResponseModel, ex.Error);
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
@@ -58,7 +59,8 @@ public async Task Error_ParameterErrorSse_ExceptionAsync()
var act = async () => await stream.LastAsync();
// Assert
- (await act.Should().ThrowAsync()).And.Error.Should().BeEquivalentTo(testCase.ResponseModel);
+ var ex = await Assert.ThrowsAsync(act);
+ Assert.Equivalent(testCase.ResponseModel, ex.Error);
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
@@ -71,13 +73,15 @@ public async Task Error_NetworkError_ExceptionAsync()
var (client, handler) = Sut.GetTestClient();
handler.Configure().MockSend(Arg.Any(), Arg.Any())
.Throws(new InvalidOperationException("Network error!"));
+ var testCase = Snapshots.TextGeneration.TextFormat.SinglePrompt;
// Act
var act = async ()
- => await client.GetTextCompletionAsync(Snapshots.TextGeneration.TextFormat.SinglePrompt.RequestModel);
+ => await client.GetTextCompletionAsync(testCase.RequestModel);
// Assert
- (await act.Should().ThrowAsync()).And.Error.Should().BeNull();
+ var ex = await Assert.ThrowsAsync(act);
+ Assert.Null(ex.Error);
}
[Fact]
@@ -94,6 +98,7 @@ public async Task Error_OpenAiCompatibleError_ExceptionAsync()
"other");
// Assert
- (await act.Should().ThrowAsync()).And.Error.Should().BeEquivalentTo(testCase.ResponseModel);
+ var ex = await Assert.ThrowsAsync(act);
+ Assert.Equivalent(testCase.ResponseModel, ex.Error);
}
}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/FileSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/FileSerializationTests.cs
index 05faa58..5a9dac0 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/FileSerializationTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/FileSerializationTests.cs
@@ -1,5 +1,5 @@
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
+
using NSubstitute;
namespace Cnblogs.DashScope.Sdk.UnitTests;
@@ -21,7 +21,7 @@ public async Task File_Upload_SuccessAsync()
handler.Received().MockSend(
Arg.Is(r => r.RequestUri!.AbsolutePath == "/compatible-mode/v1/files"),
Arg.Any());
- task.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, task);
}
[Fact]
@@ -40,7 +40,7 @@ public async Task File_Get_SuccessAsync()
Arg.Is(
r => r.RequestUri!.AbsolutePath == "/compatible-mode/v1/files/" + testCase.ResponseModel.Id.Value),
Arg.Any());
- task.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, task);
}
[Fact]
@@ -55,7 +55,7 @@ public async Task File_List_SuccessAsync()
var list = await client.ListFilesAsync();
// Assert
- list.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, list);
}
[Fact]
@@ -74,6 +74,6 @@ public async Task File_Delete_SuccessAsync()
Arg.Is(
r => r.RequestUri!.AbsolutePath == "/compatible-mode/v1/files/" + testCase.ResponseModel.Id.Value),
Arg.Any());
- task.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, task);
}
}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/ImageGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/ImageGenerationSerializationTests.cs
index d0d30b7..ac1b04b 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/ImageGenerationSerializationTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/ImageGenerationSerializationTests.cs
@@ -1,5 +1,5 @@
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
+
using NSubstitute;
namespace Cnblogs.DashScope.Sdk.UnitTests;
@@ -21,6 +21,6 @@ public async Task ImageGeneration_CreateTask_SuccessAsync()
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
- response.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, response);
}
}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/ImageSynthesisSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/ImageSynthesisSerializationTests.cs
index 909c6bf..f6348ca 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/ImageSynthesisSerializationTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/ImageSynthesisSerializationTests.cs
@@ -1,5 +1,4 @@
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
using NSubstitute;
namespace Cnblogs.DashScope.Sdk.UnitTests;
@@ -21,6 +20,6 @@ public async Task ImageSynThesis_CreateTask_SuccessAsync()
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
- response.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, response);
}
}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/Llama2TextGenerationApiTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/Llama2TextGenerationApiTests.cs
deleted file mode 100644
index 29c1a8c..0000000
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/Llama2TextGenerationApiTests.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using Cnblogs.DashScope.Core;
-using Cnblogs.DashScope.Sdk.Llama2;
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using NSubstitute;
-
-namespace Cnblogs.DashScope.Sdk.UnitTests;
-
-public class Llama2TextGenerationApiTests
-{
- [Fact]
- public async Task Llama2_UseEnum_SuccessAsync()
- {
- // Arrange
- var client = Substitute.For();
-
- // Act
- _ = await client.GetLlama2TextCompletionAsync(Llama2Model.Chat13Bv2, Cases.TextMessages, ResultFormats.Message);
-
- // Assert
- _ = await client.Received().GetTextCompletionAsync(
- Arg.Is>(
- s => s.Input.Messages == Cases.TextMessages
- && s.Model == "llama2-13b-chat-v2"
- && s.Parameters != null
- && s.Parameters.ResultFormat == ResultFormats.Message));
- }
-
- [Fact]
- public async Task Llama2_CustomModel_SuccessAsync()
- {
- // Arrange
- var client = Substitute.For();
-
- // Act
- _ = await client.GetLlama2TextCompletionAsync(Cases.CustomModelName, Cases.TextMessages, ResultFormats.Message);
-
- // Assert
- _ = await client.Received().GetTextCompletionAsync(
- Arg.Is>(
- s => s.Input.Messages == Cases.TextMessages
- && s.Model == Cases.CustomModelName
- && s.Parameters != null
- && s.Parameters.ResultFormat == ResultFormats.Message));
- }
-}
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/MultimodalGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/MultimodalGenerationSerializationTests.cs
index 8889981..5825b14 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/MultimodalGenerationSerializationTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/MultimodalGenerationSerializationTests.cs
@@ -1,7 +1,6 @@
using System.Text;
using Cnblogs.DashScope.Core;
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
-using FluentAssertions;
+using Cnblogs.DashScope.Tests.Shared.Utils;
using NSubstitute;
namespace Cnblogs.DashScope.Sdk.UnitTests;
@@ -25,7 +24,7 @@ public async Task MultimodalGeneration_NoSse_SuccessAsync(
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
- response.Should().BeEquivalentTo(testCase.ResponseModel);
+ Assert.Equivalent(testCase.ResponseModel, response);
}
[Theory]
@@ -41,18 +40,31 @@ public async Task MultimodalGeneration_Sse_SuccessAsync(
// Act
var message = new StringBuilder();
var outputs = await client.GetMultimodalGenerationStreamAsync(testCase.RequestModel).ToListAsync();
- outputs.ForEach(
- x => message.Append(x.Output.Choices[0].Message.Content.FirstOrDefault()?.Text ?? string.Empty));
+ outputs.ForEach(x
+ => message.Append(x.Output.Choices[0].Message.Content.FirstOrDefault()?.Text ?? string.Empty));
// Assert
handler.Received().MockSend(
Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))),
Arg.Any());
- outputs.SkipLast(1).Should().AllSatisfy(x => x.Output.Choices[0].FinishReason.Should().Be("null"));
- outputs.Last().Should().BeEquivalentTo(
- testCase.ResponseModel,
- o => o.Excluding(y => y.Output.Choices[0].Message.Content));
- message.ToString().Should().Be(testCase.ResponseModel.Output.Choices[0].Message.Content[0].Text);
+ Assert.All(outputs.SkipLast(1), x => Assert.Equal("null", x.Output.Choices[0].FinishReason));
+ Assert.Equal(testCase.ResponseModel.Output.Choices[0].Message.Content[0].Text, message.ToString());
+ var last = outputs.Last();
+ last = last with
+ {
+ Output = new MultimodalOutput(
+ new List
+ {
+ last.Output.Choices[0] with
+ {
+ Message = last.Output.Choices[0].Message with
+ {
+ Content = testCase.ResponseModel.Output.Choices[0].Message.Content
+ }
+ }
+ })
+ };
+ Assert.Equivalent(last, testCase.ResponseModel);
}
public static TheoryData,
diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/QWenMultimodalApiTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/QWenMultimodalApiTests.cs
index e3b5424..8e7a553 100644
--- a/test/Cnblogs.DashScope.Sdk.UnitTests/QWenMultimodalApiTests.cs
+++ b/test/Cnblogs.DashScope.Sdk.UnitTests/QWenMultimodalApiTests.cs
@@ -1,6 +1,6 @@
using Cnblogs.DashScope.Core;
using Cnblogs.DashScope.Sdk.QWenMultimodal;
-using Cnblogs.DashScope.Sdk.UnitTests.Utils;
+using Cnblogs.DashScope.Tests.Shared.Utils;
using NSubstitute;
namespace Cnblogs.DashScope.Sdk.UnitTests;
@@ -8,13 +8,15 @@ namespace Cnblogs.DashScope.Sdk.UnitTests;
public class QWenMultimodalApiTests
{
private static readonly List Messages =
- [
- MultimodalMessage.User(
- [
- MultimodalMessageContent.ImageContent("https://cdn.example.com/image.jpg"),
- MultimodalMessageContent.TextContent("说明一下这张图片的内容")
- ])
- ];
+ new()
+ {
+ MultimodalMessage.User(
+ new List
+ {
+ MultimodalMessageContent.ImageContent("https://cdn.example.com/image.jpg"),
+ MultimodalMessageContent.TextContent("说明一下这张图片的内容")
+ }.AsReadOnly())
+ };
[Fact]
public async Task Multimodal_UseEnum_SuccessAsync()
@@ -28,8 +30,23 @@ public async Task Multimodal_UseEnum_SuccessAsync()
// Assert
_ = client.Received().GetMultimodalGenerationAsync(
- Arg.Is>(
- s => s.Model == "qwen-vl-max" && s.Input.Messages == Messages && s.Parameters == parameters));
+ Arg.Is>(s
+ => s.Model == "qwen-vl-max" && s.Input.Messages == Messages && s.Parameters == parameters));
+ }
+
+ [Fact]
+ public async Task Multimodal_UseInvalidEnum_SuccessAsync()
+ {
+ // Arrange
+ var client = Substitute.For();
+ var parameters = new MultimodalParameters { Seed = 6666 };
+
+ // Act
+ var act = async ()
+ => await client.GetQWenMultimodalCompletionAsync((QWenMultimodalModel)(-1), Messages, parameters);
+
+ // Assert
+ await Assert.ThrowsAsync(act);
}
[Fact]
@@ -44,8 +61,8 @@ public async Task Multimodal_CustomModel_SuccessAsync()
// Assert
_ = client.Received().GetMultimodalGenerationAsync(
- Arg.Is>(
- s => s.Model == Cases.CustomModelName && s.Input.Messages == Messages && s.Parameters == parameters));
+ Arg.Is