Skip to content

Carry a per-node invocation policy in LLM node data, and report node time-to-first-token #293

Open

Description

@lin-snow

Problem

A per-node first-token budget ("fail this LLM node if no token arrives within N ms") is being built across the Dify plugin chain: the plugin SDK cancels the provider call at budget, the plugin daemon re-enforces at budget + ε, and the Dify API backs the whole thing at budget + 2ε. Each of those hops already accepts the budget as a first-class request field.

The one hop with no home for it is graphon. Today the value has to ride inside ModelConfig.completion_params, and the host has to pop it back out before invoking. That is wrong in two ways:

  • completion_params is forwarded verbatim to model providers. A Dify-internal budget does not belong in a dictionary whose contract is "arguments for the provider". Every consumer of the field needs a defensive pop, and one forgotten pop sends an unknown argument to a provider.
  • It pollutes exported DSL. The budget shows up as a model parameter in every workflow export, next to temperature and top_p, where it reads as something the provider understands.

There is a second, smaller gap. LLMNode already measures time-to-first-token into LLMUsage.time_to_first_token, but it does not publish it as node execution metadata. Without that number an operator has nothing to base the budget on.

Proposal

Give LLM-family node data an invocation block, a sibling of model, holding per-node policy about how the model is invoked rather than what is asked of it:

data:
  model:
    provider: openai
    name: gpt-4o
    mode: chat
    completion_params: {}       # provider arguments, untouched
  invocation:
    first_token_timeout_ms: 2000
  • first_token_timeout_ms is an optional positive integer. Absent means no budget.
  • The block exposes the value in seconds to callers, since every downstream hop works in seconds; milliseconds are the DSL and UI unit only.
  • The nodes that stream get the block: LLMNodeData and QuestionClassifierNodeData. ParameterExtractorNodeData deliberately does not, because that node invokes with stream=False, where a first-token budget silently becomes a total-generation budget.

graphon carries the value and enforces nothing — it stays a pure data contract, read off the node data by the host adapter. In particular this does not add a parameter to LLMProtocol, which would give every adapter that cannot honour it another argument with no effect (see TD-06).

Separately, add WorkflowNodeExecutionMetadataKey.TIME_TO_FIRST_TOKEN and have LLMNode publish the measurement it already takes, omitting the key when nothing was measured.

Compatibility

Additive and optional on both counts. Workflows saved before the field deserialize unchanged; a host that does not read invocation behaves exactly as it does today. No protocol signature changes, so no host adapter has to move first.

Prior art

PR #212 attempted the same field but threaded it through LLMProtocol.invoke_llm, which made every host adapter without the keyword raise TypeError on every call and forced an ordering constraint on the host's pin bump. Keeping graphon a pure carrier removes that constraint entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions