The previous grouping pre-filtered tool-call updates on FunctionName
before grouping by index. In DeepSeek streaming only the FIRST fragment
of a call carries the name/id; the argument text arrives in later
fragments with FunctionName == null. The filter dropped them, so every
tool call got empty FunctionArgs (get_editor_state(), get_node() ->
'node_id is required').
Group ALL updates by Index first, then pick the first group that
contains a named fragment and concatenate every argument fragment in
that group. Verified via OpenPencil integration test: get_node now
carries {"node_id": "10"} intact instead of being empty or joined
across parallel calls.
When the model emits several parallel tool calls in one streaming response,
the OpenAI SDK interleaves argument fragments by call index. The old code
joined ALL fragments of ALL calls into a single FunctionArgs string, which
produced corrupted arguments like get_node({"node_id": "10"}{"maxDepth": 2})
and made every tool call fail (e.g. 'node_id is required').
Group tool-call updates by Index and concatenate fragments only within the
first call that has a function name; other parallel calls are ignored (same
as before), but the selected call now gets clean, valid JSON arguments.
Also fix DecodePatchString: DeepSeek sends "reasoning_content": null in the
final delta; the decoder returned the literal string 'null' which was
appended to the accumulated reasoning text. Return null for the JSON null
literal instead.
DeepSeek thinking mode (deepseek-v4-flash/pro with reasoning enabled) returns
reasoning_content in every delta. For requests carrying tools, the reasoning_content
of previous assistant messages MUST be passed back in all subsequent requests,
otherwise DeepSeek answers 400 'The reasoning_content ... must be passed back'.
Changes:
- Bump OpenAI SDK 2.5.0 -> 2.12.0 (JsonPatch public API required to read/write
the unknown reasoning_content property; 2.5.0 only stores it internally).
- Fix BotSharp.Plugin.OpenAI image quality enum names for SDK 2.12
(GeneratedImageQuality.Low -> LowQuality, Medium -> MediumQuality).
- Add RoleDialogModel.ReasoningContent (serialized as reasoning_content) and
persist it through DialogElement/DialogMetaData/ConversationStorage.
- DeepSeekAI ChatCompletionProvider:
* capture reasoning_content from non-streaming responses (ExtractReasoningContent)
* capture per-delta reasoning_content in the streaming loop and accumulate it
* set ReasoningContent on assistant/function response messages
* echo reasoning_content back via AssistantChatMessage JsonPatch in PrepareOptions
- RoutingService.InvokeAgent: copy response.ReasoningContent onto the stored
Function/Assistant dialog so the echo survives the tool-call round trip.