Skip to content

fix(invocation): preserve opaque JSON and integer precision in backwards requests #827

Closed

Description

@WH-2099

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

  1. dify-plugin-daemon: reproduced on main c798168e4c77890f8f605238718dd64a4cfb9939.
  2. 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

  1. Use a plugin session with permission to invoke LLMs.
  2. Submit the following backwards invocation to the real InvokeDify entry point, or construct the equivalent session.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}}
      }
    ]
  }
}
  1. Observe the local decoding error before any model or host request.
  2. 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

  1. self-assigned this
    on Sep 23, 2026
  2. dosubot commented on Sep 23, 2026

    @dosubot
    Contributor

    Confirmed at c798168e4c7: the backwards-invocation path parses requests in a way that can't preserve raw JSON or integer precision [1] [2].

    InvokeDify decodes the wire bytes into a generic map[string]any with encoding/json's default Unmarshal, which turns every JSON number into float64 — that alone rounds 9007199254740993 to 9007199254740992 before any typed struct exists [3]. The map then goes to mapstructure in genericDispatchTask/MapToStruct to build the typed request [4] [5]. mapstructure has no handling for json.RawMessage target fields such as PromptMessage.OpaqueBody and PromptMessageContent.OpaqueBody (added in [6]); it expects a []byte-compatible source, so a map value for opaque_body produces 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].

    PromptMessage does define a custom UnmarshalJSON that preserves OpaqueBody as raw JSON and validates content shape and null-content rules [8], but it only runs when JSON bytes decode directly into a PromptMessage. The backwards path bypasses it entirely, since UnmarshalJsonBytes2Map flattens 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/UseNumber usage, 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 needing type keys) 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.Number and raw-message preservation instead of round-tripping through map[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.

    Leave Feedback Learn about dify-plugin-daemon with Dosu

  3. added this to the 1.19.0 milestone on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions