Repository navigation
fix(invocation): preserve opaque JSON and integer precision in backwards requests #827
Description
Self Checks
To make sure we get to you in time, please check the following :)
- I have searched for existing issues search for existing issues, including closed ones.
- I confirm that I am using English to submit this report (我已阅读并同意 Language Policy).
- [FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)
- "Please do not modify this template :) and fill in all the required fields."
Versions
- dify-plugin-daemon: reproduced on main
c798168e4c77890f8f605238718dd64a4cfb9939. - dify-api: not required for the reproduction; the failure occurs inside the daemon before it calls the host API.
Describe the bug
A plugin submitting an assistant message with object-valued opaque_body through a backwards LLM invocation fails before reaching the host.
InvokeDify first decodes wire JSON into map[string]any, then genericDispatchTask uses mapstructure to populate a request whose opaque fields are json.RawMessage.
That conversion does not implement JSON decoding:
unmarshal backwards invoke request failed: error decoding map:
'prompt_messages[0].opaque_body': source data must be an array or slice, got map
Other JSON values are also affected: [] is omitted and [49] becomes the JSON number 1.
The initial map decode separately rounds integers such as 9007199254740993 to 9007199254740992 in model parameters, tool parameters, app inputs and content-level opaque data.
Both LLM stream settings are affected.
To Reproduce
- Use a plugin session with permission to invoke LLMs.
- Submit the following backwards invocation to the real
InvokeDifyentry point, or construct the equivalentsession.model.llm.invoke()call in a plugin:
{
"type": "llm",
"backwards_request_id": "replay",
"request": {
"provider": "example",
"model": "example",
"mode": "chat",
"stream": true,
"prompt_messages": [
{
"role": "assistant",
"content": "",
"opaque_body": {"state": {"id": 9007199254740993}}
}
]
}
}- Observe the local decoding error before any model or host request.
- Remove assistant opaque and put the same integer in
completion_params.seed; capture the typed request at the dispatch boundary to observe the rounded value.
The reproduction uses actual wire parsing, permission checks, session identity injection and genericDispatchTask, with only the final host call replaced to capture the payload.
Expected behavior
Preserve every valid JSON value and integer through backwards request dispatch, retain existing authorization and request validation, and return errors/end frames correctly.
JSON decoding must also accept the SDK's valid tool-only assistant messages with null or omitted content, while rejecting invalid content types.
Screenshots
Not applicable; deterministic wire-level reproductions are available.
Additional context
The related null-content behavior was reported in #640, closed as a duplicate of #661.
This issue concerns the shared backwards JSON decoding boundary and its opaque-value/precision loss; it is reproducible without the UI or a paid model API.
Raw typed model responses already preserve opaque JSON, so the backwards map conversion is a separate defect.
These daemon changes are one dependency of the Agent replay work tracked in langgenius/dify#41092, implemented on the host in langgenius/dify#44502.
The implementation merged in #828 on 2026-11-02 and is included in daemon 0.6.14.
Controlled transport/history validation is recorded in that PR.
langgenius/dify-plugin-sdks#390 is merged and SDK 0.11.0 publication is tracked in langgenius/dify-plugin-sdks#413; langgenius/dify-official-plugins#4034 and langgenius/dify-official-plugins#4156 remain rollout dependencies.
This issue was closed by #828.
Target product milestone: Dify 1.19.0.
Activity
dosubot commentedon Sep 23, 2026
Confirmed at
c798168e4c7: the backwards-invocation path parses requests in a way that can't preserve raw JSON or integer precision [1] [2].InvokeDifydecodes the wire bytes into a genericmap[string]anywithencoding/json's defaultUnmarshal, which turns every JSON number intofloat64— that alone rounds9007199254740993to9007199254740992before any typed struct exists [3]. The map then goes tomapstructureingenericDispatchTask/MapToStructto build the typed request [4] [5].mapstructurehas no handling forjson.RawMessagetarget fields such asPromptMessage.OpaqueBodyandPromptMessageContent.OpaqueBody(added in [6]); it expects a[]byte-compatible source, so a map value foropaque_bodyproduces exactly the reported "source data must be an array or slice, got map" error, and shapes like[49]get coerced incorrectly because mapstructure walks the generic Go value tree instead of re-encoding to JSON [7].PromptMessagedoes define a customUnmarshalJSONthat preservesOpaqueBodyas raw JSON and validates content shape and null-content rules [8], but it only runs when JSON bytes decode directly into aPromptMessage. The backwards path bypasses it entirely, sinceUnmarshalJsonBytes2Mapflattens everything into a plain map before mapstructure rebuilds the struct field by field. That matches your point that raw typed model responses keep opaque JSON while the backwards map conversion is a separate defect — the two paths take different decode routes.I found no existing fix or open PR: searches across merged and open PRs and the code for
json.Number/UseNumberusage, integer precision handling, and any opaque-body-aware mapstructure hook turned up nothing. I also couldn't locate #640 in the daemon's issue history to verify the duplicate-of-#661 note, though #661 itself (an APIError about list-content items needingtypekeys) is a plausible antecedent for the null/omitted content handling you reference [9].Want me to dig into how a fix could be structured — for example, decoding backwards-invocation requests straight into the typed struct with
json.Numberand raw-message preservation instead of round-tripping throughmap[string]any— or is this issue for tracking only right now?To reply, just mention @dosu.
Share context across your team and agents. Try Dosu.