Ater.DeepSeek.AspNetCore
1.6.2
dotnet add package Ater.DeepSeek.AspNetCore --version 1.6.2
NuGet\Install-Package Ater.DeepSeek.AspNetCore -Version 1.6.2
<PackageReference Include="Ater.DeepSeek.AspNetCore" Version="1.6.2" />
<PackageVersion Include="Ater.DeepSeek.AspNetCore" Version="1.6.2" />
<PackageReference Include="Ater.DeepSeek.AspNetCore" />
paket add Ater.DeepSeek.AspNetCore --version 1.6.2
#r "nuget: Ater.DeepSeek.AspNetCore, 1.6.2"
#:package Ater.DeepSeek.AspNetCore@1.6.2
#addin nuget:?package=Ater.DeepSeek.AspNetCore&version=1.6.2
#tool nuget:?package=Ater.DeepSeek.AspNetCore&version=1.6.2
DeepSeekSDK-NET
DeepSeek API SDK specifically for .NET developers
🚀 Features
- List models
- Chat & Chat streaming
- Completions & Completions streaming (beta)
- User balance
- Local model support
- ASP.NET Core integration support
- Function call
- OpenAI-compatible Responses API (including semantic SSE streaming)
- Support Microsoft.Extensions.AI IChatClient
- Multimodal vision model
deepseek-v4-flash-vision-exp - Files API (upload, list, retrieve, and delete images)
- Standalone Search and Fetch APIs (web search and Markdown page fetch)
Usage Requirements
Usage
Please go to official website, register and apply for DeepSeek's ApiKey
Supported .NET version: .NET8
Install Nuget package
dotnet add package Ater.DeepSeek.Core
Instantiate DeepSeekClient
Two methods are provided for instantiation:
public DeepSeekClient(string apiKey);
public DeepSeekClient(HttpClient http, string apiKey);
The first type only requires providing the 'apiKey' to create an instance;
The second method provides a HttpClient parameter, which is suitable for maintaining the HttpClient through the HttpClientFactory and then instance it.
The default timeout for internal HttpClient is 120 seconds, which can be set before sending the request using the 'SetTimeout()' method, or by using the 'CancellationTokeSource' to set the timeout for specific requests.
If you want to call a local model, try customizing HttpClient and setting BaseAddress to the local address.
DeepSeek supports deepseek-flash and deepseek-v4-pro on the same
base_url. The retired deepseek-v4-flash and
deepseek-v4-flash-vision-exp IDs are temporarily routed to V4.1-Flash.
Calling method
DeepSeekClient class provides asynchronous methods to call DeepSeek's API:
Task<ModelResponse?> ListModelsAsync(CancellationToken cancellationToken);
Task<ChatResponse?> ChatAsync(ChatRequest request, CancellationToken cancellationToken);
Task<IAsyncEnumerable<Choice>?> ChatStreamAsync(ChatRequest request, CancellationToken cancellationToken);
Task<ChatResponse?> CompletionsAsync(CompletionRequest request, CancellationToken cancellationToken);
Task<IAsyncEnumerable<Choice>?> CompletionsStreamAsync(CompletionRequest request, CancellationToken cancellationToken);
Task<ResponseResult?> ResponseAsync(ResponseRequest request, CancellationToken cancellationToken);
IAsyncEnumerable<ResponseStreamEvent> ResponseStreamAsync(ResponseRequest request, CancellationToken cancellationToken);
Task<FileObject?> CreateFileAsync(Stream file, string fileName, FileUploadOptions? options = null, CancellationToken cancellationToken = default);
Task<FileListResponse?> ListFilesAsync(FileListOptions? options = null, CancellationToken cancellationToken = default);
Task<FileObject?> RetrieveFileAsync(string fileId, CancellationToken cancellationToken = default);
Task<FileDeleteResponse?> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default);
Task<UserResponse?> GetUserBalanceAsync(CancellationToken cancellationToken);
Task<SearchResponse?> SearchAsync(SearchRequest request, CancellationToken cancellationToken = default);
Task<FetchResponse?> FetchAsync(FetchRequest request, CancellationToken cancellationToken = default);
Multimodal Vision and Files API
deepseek-v4-flash-vision-exp uses the existing POST /chat/completions endpoint, but a user
message's content can be an array of content blocks. Inline Base64 data URLs, public image URLs,
and Files API file_id references are supported:
var response = await client.ChatAsync(new ChatRequest
{
Model = DeepSeekModels.FlashVisionExperimental,
Messages =
[
Message.NewUserMessage(
[
ChatMessageContentPart.CreateTextPart("Describe this image."),
ChatMessageContentPart.CreateImageUrlPart(
"data:image/jpeg;base64,<BASE64_DATA>",
ImageDetailTypes.High),
]),
],
}, cancellationToken);
Console.WriteLine(response?.Choices[0].Message?.GetTextContent());
For larger images or images reused across requests, upload once and reference the returned file ID:
await using var image = File.OpenRead("image.jpg");
var file = await client.CreateFileAsync(
image,
"image.jpg",
new FileUploadOptions { ContentType = "image/jpeg" },
cancellationToken);
if (file is null)
{
throw new InvalidOperationException(client.ErrorMsg ?? "File upload failed.");
}
var response = await client.ChatAsync(new ChatRequest
{
Model = DeepSeekModels.FlashVisionExperimental,
Messages =
[
Message.NewUserMessage(
[
ChatMessageContentPart.CreateTextPart("Read the text in this image."),
ChatMessageContentPart.CreateFilePart(file.Id),
]),
],
}, cancellationToken);
var files = await client.ListFilesAsync(
new FileListOptions { Purpose = FilePurposes.UserData },
cancellationToken);
var metadata = await client.RetrieveFileAsync(file.Id, cancellationToken);
await client.DeleteFileAsync(file.Id, cancellationToken);
Message.Content remains a string for source compatibility. When content blocks are used,
individual text, image, and file blocks are available through ContentParts. Responses uses a
different image block shape: ResponseContentPart.CreateInputImagePart(...) creates an
input_image, while ResponseContentPart.CreateInputFilePart(...) references a Files API file.
See the official vision guide,
Files API guide, and
upload file reference for limits and fields.
Responses API
DeepSeek's Responses API follows the OpenAI Responses format and uses POST /responses. Use the native
ResponseRequest, ResponseResult, and ResponseStreamEvent models when you need response items, reasoning,
function calls, usage details, or the original SSE events.
var response = await client.ResponseAsync(new ResponseRequest
{
Model = DeepSeekModels.Flash,
Instructions = "You are a concise assistant.",
Input = "What is the capital of France?",
Reasoning = new ResponseReasoningOptions { Effort = "high" },
}, cancellationToken);
var text = response?.Output
.Where(item => item.Type == ResponseInputItemTypes.Message)
.SelectMany(item => item.Content ?? [])
.Where(part => part.Type == ResponseContentPartTypes.OutputText)
.Select(part => part.Text)
.FirstOrDefault();
Responses streaming uses semantic SSE events and ends with response.completed, response.incomplete, or
response.failed; it does not use the Chat Completions [DONE] sentinel:
await foreach (var responseEvent in client.ResponseStreamAsync(
new ResponseRequest
{
Model = DeepSeekModels.Flash,
Input = "Count from one to three.",
}, cancellationToken))
{
if (responseEvent.Type == ResponseEventTypes.OutputTextDelta)
{
Console.Write(responseEvent.Delta);
}
}
DeepSeek's Responses API is stateless. Send the complete input history on each request; server-side
previous_response_id, conversation, and response storage are not supported. See the
official Responses API guide and
Create Response reference for the supported fields.
Search and Fetch API
DeepSeek also provides standalone raw JSON endpoints for web search and page retrieval. These calls
are separate from the OpenAI-compatible Responses API and from Microsoft.Extensions.AI tool
adapters. Search returns ranked structured results; Fetch returns a page title and Markdown content:
var search = await client.SearchAsync(new SearchRequest
{
Query = "DeepSeek API site:api-docs.deepseek.com",
Count = 5,
OriginalQuestion = "Where is the current DeepSeek API documentation?",
NeedContent = true,
}, cancellationToken);
var firstResult = search?.Results.FirstOrDefault();
if (firstResult is not null)
{
var page = await client.FetchAsync(
new FetchRequest { Url = firstResult.Url },
cancellationToken);
Console.WriteLine(page?.Content);
}
Search accepts 1–128 Unicode characters for Query and non-null OriginalQuestion, count from
1 to 50, and can include result content with NeedContent; Fetch accepts
HTTP or HTTPS URLs up to 8192 characters. Both endpoints use the same Bearer API key and return
null with the response body in ErrorMsg when the server returns an error. They are billed per
successful request by DeepSeek. See the official Search and Fetch guide,
Search reference, and
Fetch reference.
Each Search/Fetch request clears the previous ErrorMsg after request validation. Malformed JSON,
missing required response fields, and unexpected null data also return null with a diagnostic in
ErrorMsg. Empty search results and nullable site/date metadata remain valid.
To verify these two endpoints against the real service, configure apiKey in the test project's
local user-secrets (or set DEEPSEEK_API_KEY), then run the following from the repository root:
dotnet test test/DeepSeek.IntegrationTests/DeepSeek.IntegrationTests.csproj --configuration Release --filter "Category=SearchFetchLive"
This selects only the two Search/Fetch live tests and makes billable requests. The tests check
ranked search results and usable fetched content; a missing key fails setup before any API call.
The normal FullyQualifiedName!~DeepSeekLiveApiTests filter still excludes all live tests.
See verification status and setup for the current results.
List Models Sample
// Create an instance using the apiKey
var client = new DeepSeekClient(apiKey);
var modelResponse = await client.ListModelsAsync(new CancellationToken());
if (modelResponse is null)
{
Console.WriteLine(client.ErrorMsg);
return;
}
foreach (var model in modelResponse.Data)
{
Console.WriteLine(model);
}
Chat Examples
// Create an instance using the apiKey
var client = new DeepSeekClient(apiKey);
// Construct the request body
var request = new ChatRequest
{
Messages = [
Message.NewSystemMessage("You are a language translator"),
Message.NewUserMessage("Please translate 'They are scared! ' into English!")
],
// Specify the model
Model = DeepSeekModels.Flash
};
var chatResponse = await client.ChatAsync(request, new CancellationToken());
if (chatResponse is null)
{
Console.WriteLine(client.ErrorMsg);
}
Console.WriteLine(chatResponse?.Choices.First().Message?.Content);
Chat Examples (Stream)
// Create an instance using the apiKey
var client = new DeepSeekClient(apiKey);
// Construct the request body
var request = new ChatRequest
{
Messages = [
Message.NewSystemMessage("You are a language translator"),
Message.NewUserMessage("Please translate 'They are scared! ' into English!")
],
// Specify the model
Model = DeepSeekModels.Flash
};
var choices = client.ChatStreamAsync(request, new CancellationToken());
if (choices is null)
{
Console.WriteLine(client.ErrorMsg);
return;
}
await foreach (var choice in choices)
{
Console.Write(choice.Delta?.Content);
}
Console.WriteLine();
Function Calling Example
For example, I have a local function definition:
internal class Functions
{
public static string GetWeather(WeatherDto dto)
{
return $"The weather in {dto.City} on {dto.Date:yyyy-MM-dd} is sunny with a high of 25°C and a low of 15°C.";
}
}
internal class WeatherDto
{
public required string City { get; set; }
[Description("The date, default is today's date")]
public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow);
}
When using LLM, pass in the function definition:
public static async Task CallFunctionExampleAsync(DeepSeekClient client)
{
JsonSerializerOptions options = JsonSerializerOptions.Default;
// Required configuration, otherwise the generated format will be incorrect.
JsonSchemaExporterOptions exporterOptions = new()
{
TreatNullObliviousAsNonNullable = true,
};
var request = new ChatRequest
{
Messages = [Message.NewUserMessage("What is the weather in New York today?")],
Model = DeepSeekModels.Flash,
Stream = true,
// Add tool definitions
Tools =
[
new Tool
{
Function = new RequestFunction
{
Name = "JustUselessFunction",
Description = "nothing to do",
},
},
new Tool
{
Function = new RequestFunction
{
Name = "GetWeather",
Description = "get the weather",
Parameters = options.GetJsonSchemaAsNode(
typeof(WeatherDto),
exporterOptions
),
},
},
],
};
// The first time LLM is returned, it recognizes that a function is to be called and returns the function contents.
var response = await client.ChatAsync(request, new CancellationToken());
if (response is null)
{
Console.WriteLine(client.ErrorMsg);
return;
}
var message = response.Choices[0].Message;
if (message == null)
{
Console.WriteLine("no message");
return;
}
request.Messages.Add(message); // The message must be added to the request for use in subsequent function calls.
if (message.ToolCalls != null && message.ToolCalls.Count > 0)
{
// If a function call exists, use the local function to obtain the content.
var tool = message.ToolCalls.FirstOrDefault();
if (tool?.Function.Name == "GetWeather")
{
var weatherDto = JsonSerializer.Deserialize<WeatherDto>(
tool.Function.Arguments.ToString(),
options
);
var toolResult = Functions.GetWeather(weatherDto);
// Add the local function call result to the message.
request.Messages.Add(Message.NewToolMessage(toolResult, tool.Id));
// Use LLM to process the result again.
var toolResponse = await client.ChatAsync(request, new CancellationToken());
if (toolResponse is null)
{
Console.WriteLine(client.ErrorMsg);
return;
}
Console.WriteLine(toolResponse.Choices[0].Message?.Content);
}
}
else
{
Console.WriteLine("No tool calls found in the response.");
}
}
Local Model Examples
// use local models api
var httpClient = new HttpClient
{
// set your local api address
BaseAddress = new Uri("http://localhost:5000"),
Timeout = TimeSpan.FromSeconds(300),
};
// if have api key
// httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + "your_token");
var localClient = new DeepSeekClient(httpClient);
localClient.SetChatEndpoint("/chat");
localClient.SetCompletionEndpoint("/completions");
var res = await localClient.ChatAsync(new ChatRequest
{
Messages = new List<Message>
{
Message.NewUserMessage("hello")
}
}, new CancellationToken());
return res?.Choices.First().Message?.Content;
More usage example
Microsoft.Extensions.AI Integration
The existing DeepSeekChatClient uses Chat Completions. To use the Responses API through
Microsoft.Extensions.AI, use DeepSeekResponsesChatClient:
using DeepSeek.Core.Adapters;
using Microsoft.Extensions.AI;
builder.Services.AddChatClient(sp => new DeepSeekResponsesChatClient(apiKey));
The adapter maps response text to TextContent, reasoning to TextReasoningContent, function calls to
FunctionCallContent, and token usage to UsageDetails. The original ResponseResult is available through
ChatResponse.RawRepresentation.
ASP.NET Core Integration
Install Ater.DeepSeek.AspNetCore package
dotnet add package Ater.DeepSeek.AspNetCore
Usage in ASP.NET Core
using DeepSeek.AspNetCore;
using DeepSeek.Core;
using DeepSeek.Core.Models;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
var apiKey = builder.Configuration["DeepSeekApiKey"];
builder.Services.AddDeepSeek(option =>
{
option.BaseAddress = new Uri("https://api.deepseek.com");
option.Timeout = TimeSpan.FromSeconds(300);
option.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer " + apiKey);
});
var app = builder.Build();
app.MapGet("/test", async ([FromServices] DeepSeekClient client) =>
{
var res = await client.ChatAsync(new ChatRequest
{
Messages = new List<Message>
{
Message.NewUserMessage("Why dotnet is good?")
},
MaxTokens = 200
}, new CancellationToken());
return res?.Choices.First().Message?.Content;
});
app.Run();
Usage in ASP.NET Core (Stream)
app.MapGet("/chat", async (HttpContext context, [FromServices] DeepSeekClient client, CancellationToken token) =>
{
context.Response.ContentType = "text/text;charset=utf-8";
try
{
var choices = client.ChatStreamAsync(new ChatRequest
{
Messages = new List<Message>
{
Message.NewUserMessage("Why dotnet is good?")
},
MaxTokens = 200
}, token);
if (choices != null)
{
await foreach (var choice in choices)
{
await context.Response.WriteAsync(choice.Delta!.Content);
}
}
}
catch (Exception ex)
{
await context.Response.WriteAsync("暂时无法提供服务" + ex.Message);
}
await context.Response.CompleteAsync();
});
More usage example
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Ater.DeepSeek.Core (>= 1.6.2)
- Microsoft.Extensions.DependencyInjection (>= 10.0.1)
- Microsoft.Extensions.Http (>= 10.0.1)
-
net8.0
- Ater.DeepSeek.Core (>= 1.6.2)
- Microsoft.Extensions.DependencyInjection (>= 10.0.1)
- Microsoft.Extensions.Http (>= 10.0.1)
-
net9.0
- Ater.DeepSeek.Core (>= 1.6.2)
- Microsoft.Extensions.DependencyInjection (>= 10.0.1)
- Microsoft.Extensions.Http (>= 10.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.6.2 | 58 | 10/18/2026 |
| 1.6.1 | 94,025 | 9/11/2026 |
| 1.6.0 | 481 | 8/28/2026 |
| 1.5.0 | 170 | 8/14/2026 |
| 1.4.2 | 783 | 6/22/2026 |
| 1.4.1 | 613 | 5/25/2026 |
| 1.4.0 | 1,234 | 4/24/2026 |
| 1.3.2 | 2,177 | 1/10/2026 |
| 1.3.1 | 159 | 1/4/2026 |
| 1.3.0 | 315 | 10/7/2025 |
| 1.2.0 | 570 | 8/20/2025 |
| 1.1.6 | 332 | 7/31/2025 |
| 1.1.5 | 667 | 4/18/2025 |
| 1.1.4 | 1,166 | 3/4/2025 |
| 1.1.3 | 506 | 2/11/2025 |
| 1.1.2 | 280 | 2/10/2025 |
| 1.1.1 | 260 | 2/8/2025 |
1. Added standalone Search and Fetch APIs to the referenced Core package.
2. Added request validation and improved handling of invalid Search/Fetch responses.