{"repo": "angri450/nong.nanobot.net", "files": {"Nanobot.Core/Providers/DeepSeekV4Provider.cs": {"5415f25b81e5b00141b1674c1992f07168045135": {"t": 1785746199.4491425, "n": 15847, "h": "56d422aaae48cdce", "c": "using System.Runtime.CompilerServices;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing Nanobot.Core.Models;\n\nnamespace Nanobot.Core.Providers;\n\npublic class DeepSeekV4Provider : IStreamingLLMProvider\n{\n private const string DefaultBaseUrl = \"https://api.deepseek.com\";\n\n private readonly string _apiKey;\n private readonly string _apiBase;\n private readonly HttpClient _httpClient;\n private readonly DeepSeekV4Options _options;\n private readonly DeepSeekV4Profile _profile;\n\n public DeepSeekV4Provider(\n string apiKey,\n string? apiBase = null,\n DeepSeekV4Options? options = null,\n HttpClient? httpClient = null)\n {\n _apiKey = apiKey;\n _apiBase = (apiBase ?? DefaultBaseUrl).TrimEnd('/');\n _httpClient = httpClient ?? new HttpClient();\n _options = options ?? new DeepSeekV4Options();\n _profile = DeepSeekV4Models.GetProfile(_options.Model ?? DeepSeekV4Models.Flash);\n }\n\n public string GetDefaultModel() => _options.Model ?? DeepSeekV4Models.Flash;\n\n public DeepSeekV4Profile Profile => _profile;\n\n public async Task ChatAsync(\n List messages,\n List? tools = null,\n string? model = null,\n int maxTokens = 4096,\n double temperature = 0.7)\n {\n var body = BuildRequestBody(messages, tools, model ?? GetDefaultModel(), maxTokens, temperature, stream: false);\n return await SendRequestAsync(body);\n }\n\n public async IAsyncEnumerable ChatStreamAsync(\n List messages,\n List? tools = null,\n string? model = null,\n int maxTokens = 4096,\n double temperature = 0.7,\n [EnumeratorCancellation] CancellationToken cancellationToken = default)\n {\n var body = BuildRequestBody(messages, tools, model ?? GetDefaultModel(), maxTokens, temperature, stream: true);\n var request = CreateRequest(body);\n\n using var response = await _httpClient.SendAsync(\n request,\n HttpCompletionOption.ResponseHeadersRead,\n cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);\n yield return LLMStreamChunk.Delta($\"Error: DeepSeek returned {(int)response.StatusCode}: {errorBody}\");\n yield return LLMStreamChunk.Final(new LLMResponse($\"Error: DeepSeek returned {(int)response.StatusCode}: {errorBody}\")\n {\n FinishReason = \"error\"\n });\n yield break;\n }\n\n await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);\n using var reader = new StreamReader(stream);\n var parser = new DeepSeekStreamParser();\n\n await foreach (var data in ReadSseDataAsync(reader, cancellationToken))\n {\n if (data == \"[DONE]\")\n {\n break;\n }\n\n foreach (var chunk in parser.Apply(data))\n {\n yield return chunk;\n }\n }\n\n yield return LLMStreamChunk.Final(parser.BuildResponse(GetDefaultModel()));\n }\n\n private async Task SendRequestAsync(JsonObject body)\n {\n var request = CreateRequest(body);\n try\n {\n using var response = await _httpClient.SendAsync(request);\n var content = await response.Content.ReadAsStringAsync();\n if (!response.IsSuccessStatusCode)\n {\n return new LLMResponse($\"Error: DeepSeek returned {(int)response.StatusCode}: {content}\")\n {\n FinishReason = \"error\"\n };\n }\n\n return ParseNonStreamingResponse(content);\n }\n catch (Exception ex)\n {\n return new LLMResponse($\"Error: {ex.Message}\") { FinishReason = \"error\" };\n }\n }\n\n private JsonObject BuildRequestBody(\n List messages,\n List? tools,\n string model,\n int maxTokens,\n double temperature,\n bool stream)\n {\n var body = new JsonObject\n {\n [\"model\"] = model,\n [\"messages\"] = new JsonArray(messages.Select(MessageToJson).ToArray()),\n [\"max_tokens\"] = maxTokens,\n [\"stream\"] = stream\n };\n\n // Only send temperature when thinking is off (DeepSeek ignores it otherwise)\n if (_options.Thinking == DeepSeekV4Options.ThinkingMode.Off)\n {\n body[\"temperature\"] = temperature;\n }\n\n // Thinking mode\n if (_options.Thinking != DeepSeekV4Options.ThinkingMode.Auto)\n {\n body[\"thinking\"] = new JsonObject\n {\n [\"type\"] = _options.IsThinkingEnabled ? \"enabled\" : \"disabled\"\n };\n\n if (_options.IsThinkingEnabled)\n {\n body[\"reasoning_effort\"] = _options.ReasoningEffort;\n }\n }\n\n // Stream options for usage in final chunk\n if (stream && _options.IncludeStreamUsage)\n {\n body[\"stream_options\"] = new JsonObject\n {\n [\"include_usage\"] = true\n };\n }\n\n // Tools\n if (tools is { Count: > 0 })\n {\n var toolArray = new JsonArray();\n foreach (var tool in tools)\n {\n toolArray.Add(tool.DeepClone());\n }\n\n body[\"tools\"] = toolArray;\n }\n\n return body;\n }\n\n private static JsonObject MessageToJson(Message message)\n {\n var node = new JsonObject\n {\n [\"role\"] = message.Role.ToLowerInvariant()\n };\n\n if (!string.IsNullOrEmpty(message.Content))\n {\n node[\"content\"] = message.Content;\n }\n else\n {\n node[\"content\"] = \"\";\n }\n\n // Handle tool result messages\n if (message.Role.Equals(\"tool\", StringComparison.OrdinalIgnoreCase))\n {\n if (!string.IsNullOrEmpty(message.ToolCallId))\n {\n node[\"tool_call_id\"] = message.ToolCallId;\n }\n\n return node;\n }\n\n // Handle assistant messages with tool calls\n if (message.Role.Equals(\"assistant\", StringComparison.OrdinalIgnoreCase)\n && message.ToolCalls is { Count: > 0 })\n {\n var toolCallsArray = new JsonArray();\n foreach (var tc in message.ToolCalls)\n {\n toolCallsArray.Add(new JsonObject\n {\n [\"id\"] = tc.Id,\n [\"type\"] = \"function\",\n [\"function\"] = new JsonObject\n {\n [\"name\"] = tc.Name,\n [\"arguments\"] = tc.Arguments?.ToJsonString() ?? \"{}\"\n }\n });\n }\n\n node[\"tool_calls\"] = toolCallsArray;\n\n // Keep reasoning_content for tool-call history (required by DeepSeek V4)\n if (!string.IsNullOrEmpty(message.ReasoningContent))\n {\n node[\"reasoning_content\"] = message.ReasoningContent;\n }\n }\n\n return node;\n }\n\n private HttpRequestMessage CreateRequest(JsonObject body)\n {\n var uri = $\"{_apiBase}/v1/chat/completions\";\n var request = new HttpRequestMessage(HttpMethod.Post, uri)\n {\n Content = new StringContent(body.ToJsonString(), Encoding.UTF8, \"application/json\")\n };\n request.Headers.Add(\"Authorization\", $\"Bearer {_apiKey}\");\n request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(\n body[\"stream\"]?.GetValue() == true ? \"text/event-stream\" : \"application/json\"));\n return request;\n }\n\n private LLMResponse ParseNonStreamingResponse(string json)\n {\n var root = JsonNode.Parse(json) as JsonObject ?? new JsonObject();\n var choice = root[\"choices\"]?[0] as JsonObject;\n var message = choice?[\"message\"] as JsonObject;\n\n var response = new LLMResponse\n {\n Content = message?[\"content\"]?.ToString(),\n FinishReason = choice?[\"finish_reason\"]?.ToString() ?? \"stop\",\n ReasoningContent = message?[\"reasoning_content\"]?.ToString(),\n Model = root[\"model\"]?.ToString(),\n Usage = ParseDeepSeekUsage(root[\"usage\"] as JsonObject)\n };\n\n if (message?[\"tool_calls\"] is JsonArray toolCalls)\n {\n foreach (var tc in toolCalls.OfType())\n {\n var function = tc[\"function\"] as JsonObject;\n JsonNode? args = null;\n try { args = JsonNode.Parse(function?[\"arguments\"]?.ToString() ?? \"{}\"); }\n catch { /* keep null */ }\n\n response.ToolCalls.Add(new ToolCallRequest(\n tc[\"id\"]?.ToString() ?? Guid.NewGuid().ToString(\"N\"),\n function?[\"name\"]?.ToString() ?? \"unknown\",\n args));\n }\n }\n\n return response;\n }\n\n public static LLMUsage ParseDeepSeekUsage(JsonObject? usage)\n {\n if (usage is null)\n {\n return LLMUsage.Basic(0, 0);\n }\n\n var promptTokens = usage[\"prompt_tokens\"]?.GetValue() ?? 0;\n var completionTokens = usage[\"completion_tokens\"]?.GetValue() ?? 0;\n var cacheHitTokens = usage[\"prompt_cache_hit_tokens\"]?.GetValue() ?? 0;\n var cacheMissTokens = usage[\"prompt_cache_miss_tokens\"]?.GetValue() ?? 0;\n var reasoningTokens = usage[\"completion_tokens_details\"] is JsonObject details\n ? details[\"reasoning_tokens\"]?.GetValue() ?? 0\n : 0;\n\n return LLMUsage.FromDeepSeekResponse(promptTokens, cacheHitTokens, cacheMissTokens, completionTokens, reasoningTokens);\n }\n\n private static async IAsyncEnumerable ReadSseDataAsync(\n StreamReader reader,\n [EnumeratorCancellation] CancellationToken cancellationToken)\n {\n var data = new StringBuilder();\n while (!cancellationToken.IsCancellationRequested)\n {\n var line = await reader.ReadLineAsync(cancellationToken);\n if (line is null)\n {\n break;\n }\n\n if (line.Length == 0)\n {\n if (data.Length > 0)\n {\n yield return data.ToString();\n data.Clear();\n }\n\n continue;\n }\n\n if (line.StartsWith(\"data:\", StringComparison.OrdinalIgnoreCase))\n {\n if (data.Length > 0)\n {\n data.Append('\\n');\n }\n\n data.Append(line[\"data:\".Length..].Trim());\n }\n }\n\n if (data.Length > 0)\n {\n yield return data.ToString();\n }\n }\n\n private sealed class DeepSeekStreamParser\n {\n private readonly StringBuilder _content = new();\n private readonly StringBuilder _reasoning = new();\n private readonly Dictionary _toolCallsByIndex = new();\n private string _finishReason = \"stop\";\n private LLMUsage? _usage;\n\n public IReadOnlyList Apply(string json)\n {\n var chunks = new List();\n var root = JsonNode.Parse(json) as JsonObject;\n if (root is null) return chunks;\n\n var choices = root[\"choices\"];\n if (choices is null) return chunks;\n\n var choice = choices[0] as JsonObject;\n if (choice is null) return chunks;\n\n var delta = choice[\"delta\"] as JsonObject;\n\n // Reasoning delta\n if (delta is not null)\n {\n var reasoningDelta = delta[\"reasoning_content\"]?.ToString();\n if (!string.IsNullOrEmpty(reasoningDelta))\n {\n _reasoning.Append(reasoningDelta);\n chunks.Add(LLMStreamChunk.Reasoning(reasoningDelta));\n }\n\n // Content delta\n var contentDelta = delta[\"content\"]?.ToString();\n if (!string.IsNullOrEmpty(contentDelta))\n {\n _content.Append(contentDelta);\n chunks.Add(LLMStreamChunk.Delta(contentDelta));\n }\n\n // Tool call deltas\n if (delta[\"tool_calls\"] is JsonArray toolCalls)\n {\n foreach (var item in toolCalls.OfType())\n {\n var index = item[\"index\"]?.GetValue() ?? 0;\n if (!_toolCallsByIndex.TryGetValue(index, out var accumulator))\n {\n accumulator = new ToolCallAccumulator();\n _toolCallsByIndex[index] = accumulator;\n }\n\n if (!string.IsNullOrWhiteSpace(item[\"id\"]?.ToString()))\n {\n accumulator.Id = item[\"id\"]!.ToString();\n }\n\n var function = item[\"function\"] as JsonObject;\n if (!string.IsNullOrWhiteSpace(function?[\"name\"]?.ToString()))\n {\n accumulator.Name = function![\"name\"]!.ToString();\n }\n\n if (function?[\"arguments\"] is JsonNode args)\n {\n accumulator.Arguments.Append(args.ToString());\n }\n }\n }\n }\n\n // Finish reason\n var finishReason = choice[\"finish_reason\"]?.ToString();\n if (!string.IsNullOrWhiteSpace(finishReason))\n {\n _finishReason = finishReason;\n }\n\n // Usage (from stream_options.include_usage final chunk)\n var usage = root[\"usage\"] as JsonObject;\n if (usage is not null)\n {\n _usage = ParseDeepSeekUsage(usage);\n }\n\n return chunks;\n }\n\n public LLMResponse BuildResponse(string model)\n {\n var response = new LLMResponse\n {\n Content = _content.Length == 0 ? null : _content.ToString(),\n ReasoningContent = _reasoning.Length == 0 ? null : _reasoning.ToString(),\n FinishReason = _finishReason,\n Usage = _usage,\n Model = model,\n Provider = \"deepseek\"\n };\n\n foreach (var accumulator in _toolCallsByIndex.Values)\n {\n if (string.IsNullOrWhiteSpace(accumulator.Name))\n {\n continue;\n }\n\n JsonNode? args = null;\n try\n {\n args = JsonNode.Parse(accumulator.Arguments.Length == 0 ? \"{}\" : accumulator.Arguments.ToString());\n }\n catch { /* keep null */ }\n\n response.ToolCalls.Add(new ToolCallRequest(\n string.IsNullOrWhiteSpace(accumulator.Id) ? Guid.NewGuid().ToString(\"N\") : accumulator.Id,\n accumulator.Name,\n args));\n }\n\n return response;\n }\n }\n\n private sealed class ToolCallAccumulator\n {\n public string? Id { get; set; }\n public string? Name { get; set; }\n public StringBuilder Arguments { get; } = new();\n }\n}\n"}, "ead7343727376a8374eab207827e4f00f2572d58": {"t": 1785760541.127495, "n": 15847, "h": "56d422aaae48cdce", "c": "using System.Runtime.CompilerServices;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing Nanobot.Core.Models;\n\nnamespace Nanobot.Core.Providers;\n\npublic class DeepSeekV4Provider : IStreamingLLMProvider\n{\n private const string DefaultBaseUrl = \"https://api.deepseek.com\";\n\n private readonly string _apiKey;\n private readonly string _apiBase;\n private readonly HttpClient _httpClient;\n private readonly DeepSeekV4Options _options;\n private readonly DeepSeekV4Profile _profile;\n\n public DeepSeekV4Provider(\n string apiKey,\n string? apiBase = null,\n DeepSeekV4Options? options = null,\n HttpClient? httpClient = null)\n {\n _apiKey = apiKey;\n _apiBase = (apiBase ?? DefaultBaseUrl).TrimEnd('/');\n _httpClient = httpClient ?? new HttpClient();\n _options = options ?? new DeepSeekV4Options();\n _profile = DeepSeekV4Models.GetProfile(_options.Model ?? DeepSeekV4Models.Flash);\n }\n\n public string GetDefaultModel() => _options.Model ?? DeepSeekV4Models.Flash;\n\n public DeepSeekV4Profile Profile => _profile;\n\n public async Task ChatAsync(\n List messages,\n List? tools = null,\n string? model = null,\n int maxTokens = 4096,\n double temperature = 0.7)\n {\n var body = BuildRequestBody(messages, tools, model ?? GetDefaultModel(), maxTokens, temperature, stream: false);\n return await SendRequestAsync(body);\n }\n\n public async IAsyncEnumerable ChatStreamAsync(\n List messages,\n List? tools = null,\n string? model = null,\n int maxTokens = 4096,\n double temperature = 0.7,\n [EnumeratorCancellation] CancellationToken cancellationToken = default)\n {\n var body = BuildRequestBody(messages, tools, model ?? GetDefaultModel(), maxTokens, temperature, stream: true);\n var request = CreateRequest(body);\n\n using var response = await _httpClient.SendAsync(\n request,\n HttpCompletionOption.ResponseHeadersRead,\n cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);\n yield return LLMStreamChunk.Delta($\"Error: DeepSeek returned {(int)response.StatusCode}: {errorBody}\");\n yield return LLMStreamChunk.Final(new LLMResponse($\"Error: DeepSeek returned {(int)response.StatusCode}: {errorBody}\")\n {\n FinishReason = \"error\"\n });\n yield break;\n }\n\n await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);\n using var reader = new StreamReader(stream);\n var parser = new DeepSeekStreamParser();\n\n await foreach (var data in ReadSseDataAsync(reader, cancellationToken))\n {\n if (data == \"[DONE]\")\n {\n break;\n }\n\n foreach (var chunk in parser.Apply(data))\n {\n yield return chunk;\n }\n }\n\n yield return LLMStreamChunk.Final(parser.BuildResponse(GetDefaultModel()));\n }\n\n private async Task SendRequestAsync(JsonObject body)\n {\n var request = CreateRequest(body);\n try\n {\n using var response = await _httpClient.SendAsync(request);\n var content = await response.Content.ReadAsStringAsync();\n if (!response.IsSuccessStatusCode)\n {\n return new LLMResponse($\"Error: DeepSeek returned {(int)response.StatusCode}: {content}\")\n {\n FinishReason = \"error\"\n };\n }\n\n return ParseNonStreamingResponse(content);\n }\n catch (Exception ex)\n {\n return new LLMResponse($\"Error: {ex.Message}\") { FinishReason = \"error\" };\n }\n }\n\n private JsonObject BuildRequestBody(\n List messages,\n List? tools,\n string model,\n int maxTokens,\n double temperature,\n bool stream)\n {\n var body = new JsonObject\n {\n [\"model\"] = model,\n [\"messages\"] = new JsonArray(messages.Select(MessageToJson).ToArray()),\n [\"max_tokens\"] = maxTokens,\n [\"stream\"] = stream\n };\n\n // Only send temperature when thinking is off (DeepSeek ignores it otherwise)\n if (_options.Thinking == DeepSeekV4Options.ThinkingMode.Off)\n {\n body[\"temperature\"] = temperature;\n }\n\n // Thinking mode\n if (_options.Thinking != DeepSeekV4Options.ThinkingMode.Auto)\n {\n body[\"thinking\"] = new JsonObject\n {\n [\"type\"] = _options.IsThinkingEnabled ? \"enabled\" : \"disabled\"\n };\n\n if (_options.IsThinkingEnabled)\n {\n body[\"reasoning_effort\"] = _options.ReasoningEffort;\n }\n }\n\n // Stream options for usage in final chunk\n if (stream && _options.IncludeStreamUsage)\n {\n body[\"stream_options\"] = new JsonObject\n {\n [\"include_usage\"] = true\n };\n }\n\n // Tools\n if (tools is { Count: > 0 })\n {\n var toolArray = new JsonArray();\n foreach (var tool in tools)\n {\n toolArray.Add(tool.DeepClone());\n }\n\n body[\"tools\"] = toolArray;\n }\n\n return body;\n }\n\n private static JsonObject MessageToJson(Message message)\n {\n var node = new JsonObject\n {\n [\"role\"] = message.Role.ToLowerInvariant()\n };\n\n if (!string.IsNullOrEmpty(message.Content))\n {\n node[\"content\"] = message.Content;\n }\n else\n {\n node[\"content\"] = \"\";\n }\n\n // Handle tool result messages\n if (message.Role.Equals(\"tool\", StringComparison.OrdinalIgnoreCase))\n {\n if (!string.IsNullOrEmpty(message.ToolCallId))\n {\n node[\"tool_call_id\"] = message.ToolCallId;\n }\n\n return node;\n }\n\n // Handle assistant messages with tool calls\n if (message.Role.Equals(\"assistant\", StringComparison.OrdinalIgnoreCase)\n && message.ToolCalls is { Count: > 0 })\n {\n var toolCallsArray = new JsonArray();\n foreach (var tc in message.ToolCalls)\n {\n toolCallsArray.Add(new JsonObject\n {\n [\"id\"] = tc.Id,\n [\"type\"] = \"function\",\n [\"function\"] = new JsonObject\n {\n [\"name\"] = tc.Name,\n [\"arguments\"] = tc.Arguments?.ToJsonString() ?? \"{}\"\n }\n });\n }\n\n node[\"tool_calls\"] = toolCallsArray;\n\n // Keep reasoning_content for tool-call history (required by DeepSeek V4)\n if (!string.IsNullOrEmpty(message.ReasoningContent))\n {\n node[\"reasoning_content\"] = message.ReasoningContent;\n }\n }\n\n return node;\n }\n\n private HttpRequestMessage CreateRequest(JsonObject body)\n {\n var uri = $\"{_apiBase}/v1/chat/completions\";\n var request = new HttpRequestMessage(HttpMethod.Post, uri)\n {\n Content = new StringContent(body.ToJsonString(), Encoding.UTF8, \"application/json\")\n };\n request.Headers.Add(\"Authorization\", $\"Bearer {_apiKey}\");\n request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(\n body[\"stream\"]?.GetValue() == true ? \"text/event-stream\" : \"application/json\"));\n return request;\n }\n\n private LLMResponse ParseNonStreamingResponse(string json)\n {\n var root = JsonNode.Parse(json) as JsonObject ?? new JsonObject();\n var choice = root[\"choices\"]?[0] as JsonObject;\n var message = choice?[\"message\"] as JsonObject;\n\n var response = new LLMResponse\n {\n Content = message?[\"content\"]?.ToString(),\n FinishReason = choice?[\"finish_reason\"]?.ToString() ?? \"stop\",\n ReasoningContent = message?[\"reasoning_content\"]?.ToString(),\n Model = root[\"model\"]?.ToString(),\n Usage = ParseDeepSeekUsage(root[\"usage\"] as JsonObject)\n };\n\n if (message?[\"tool_calls\"] is JsonArray toolCalls)\n {\n foreach (var tc in toolCalls.OfType())\n {\n var function = tc[\"function\"] as JsonObject;\n JsonNode? args = null;\n try { args = JsonNode.Parse(function?[\"arguments\"]?.ToString() ?? \"{}\"); }\n catch { /* keep null */ }\n\n response.ToolCalls.Add(new ToolCallRequest(\n tc[\"id\"]?.ToString() ?? Guid.NewGuid().ToString(\"N\"),\n function?[\"name\"]?.ToString() ?? \"unknown\",\n args));\n }\n }\n\n return response;\n }\n\n public static LLMUsage ParseDeepSeekUsage(JsonObject? usage)\n {\n if (usage is null)\n {\n return LLMUsage.Basic(0, 0);\n }\n\n var promptTokens = usage[\"prompt_tokens\"]?.GetValue() ?? 0;\n var completionTokens = usage[\"completion_tokens\"]?.GetValue() ?? 0;\n var cacheHitTokens = usage[\"prompt_cache_hit_tokens\"]?.GetValue() ?? 0;\n var cacheMissTokens = usage[\"prompt_cache_miss_tokens\"]?.GetValue() ?? 0;\n var reasoningTokens = usage[\"completion_tokens_details\"] is JsonObject details\n ? details[\"reasoning_tokens\"]?.GetValue() ?? 0\n : 0;\n\n return LLMUsage.FromDeepSeekResponse(promptTokens, cacheHitTokens, cacheMissTokens, completionTokens, reasoningTokens);\n }\n\n private static async IAsyncEnumerable ReadSseDataAsync(\n StreamReader reader,\n [EnumeratorCancellation] CancellationToken cancellationToken)\n {\n var data = new StringBuilder();\n while (!cancellationToken.IsCancellationRequested)\n {\n var line = await reader.ReadLineAsync(cancellationToken);\n if (line is null)\n {\n break;\n }\n\n if (line.Length == 0)\n {\n if (data.Length > 0)\n {\n yield return data.ToString();\n data.Clear();\n }\n\n continue;\n }\n\n if (line.StartsWith(\"data:\", StringComparison.OrdinalIgnoreCase))\n {\n if (data.Length > 0)\n {\n data.Append('\\n');\n }\n\n data.Append(line[\"data:\".Length..].Trim());\n }\n }\n\n if (data.Length > 0)\n {\n yield return data.ToString();\n }\n }\n\n private sealed class DeepSeekStreamParser\n {\n private readonly StringBuilder _content = new();\n private readonly StringBuilder _reasoning = new();\n private readonly Dictionary _toolCallsByIndex = new();\n private string _finishReason = \"stop\";\n private LLMUsage? _usage;\n\n public IReadOnlyList Apply(string json)\n {\n var chunks = new List();\n var root = JsonNode.Parse(json) as JsonObject;\n if (root is null) return chunks;\n\n var choices = root[\"choices\"];\n if (choices is null) return chunks;\n\n var choice = choices[0] as JsonObject;\n if (choice is null) return chunks;\n\n var delta = choice[\"delta\"] as JsonObject;\n\n // Reasoning delta\n if (delta is not null)\n {\n var reasoningDelta = delta[\"reasoning_content\"]?.ToString();\n if (!string.IsNullOrEmpty(reasoningDelta))\n {\n _reasoning.Append(reasoningDelta);\n chunks.Add(LLMStreamChunk.Reasoning(reasoningDelta));\n }\n\n // Content delta\n var contentDelta = delta[\"content\"]?.ToString();\n if (!string.IsNullOrEmpty(contentDelta))\n {\n _content.Append(contentDelta);\n chunks.Add(LLMStreamChunk.Delta(contentDelta));\n }\n\n // Tool call deltas\n if (delta[\"tool_calls\"] is JsonArray toolCalls)\n {\n foreach (var item in toolCalls.OfType())\n {\n var index = item[\"index\"]?.GetValue() ?? 0;\n if (!_toolCallsByIndex.TryGetValue(index, out var accumulator))\n {\n accumulator = new ToolCallAccumulator();\n _toolCallsByIndex[index] = accumulator;\n }\n\n if (!string.IsNullOrWhiteSpace(item[\"id\"]?.ToString()))\n {\n accumulator.Id = item[\"id\"]!.ToString();\n }\n\n var function = item[\"function\"] as JsonObject;\n if (!string.IsNullOrWhiteSpace(function?[\"name\"]?.ToString()))\n {\n accumulator.Name = function![\"name\"]!.ToString();\n }\n\n if (function?[\"arguments\"] is JsonNode args)\n {\n accumulator.Arguments.Append(args.ToString());\n }\n }\n }\n }\n\n // Finish reason\n var finishReason = choice[\"finish_reason\"]?.ToString();\n if (!string.IsNullOrWhiteSpace(finishReason))\n {\n _finishReason = finishReason;\n }\n\n // Usage (from stream_options.include_usage final chunk)\n var usage = root[\"usage\"] as JsonObject;\n if (usage is not null)\n {\n _usage = ParseDeepSeekUsage(usage);\n }\n\n return chunks;\n }\n\n public LLMResponse BuildResponse(string model)\n {\n var response = new LLMResponse\n {\n Content = _content.Length == 0 ? null : _content.ToString(),\n ReasoningContent = _reasoning.Length == 0 ? null : _reasoning.ToString(),\n FinishReason = _finishReason,\n Usage = _usage,\n Model = model,\n Provider = \"deepseek\"\n };\n\n foreach (var accumulator in _toolCallsByIndex.Values)\n {\n if (string.IsNullOrWhiteSpace(accumulator.Name))\n {\n continue;\n }\n\n JsonNode? args = null;\n try\n {\n args = JsonNode.Parse(accumulator.Arguments.Length == 0 ? \"{}\" : accumulator.Arguments.ToString());\n }\n catch { /* keep null */ }\n\n response.ToolCalls.Add(new ToolCallRequest(\n string.IsNullOrWhiteSpace(accumulator.Id) ? Guid.NewGuid().ToString(\"N\") : accumulator.Id,\n accumulator.Name,\n args));\n }\n\n return response;\n }\n }\n\n private sealed class ToolCallAccumulator\n {\n public string? Id { get; set; }\n public string? Name { get; set; }\n public StringBuilder Arguments { get; } = new();\n }\n}\n"}}}}