Repository navigation
Carry a per-node invocation policy in LLM node data, and report node time-to-first-token #293
Description
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_paramsis 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 defensivepop, and one forgottenpopsends an unknown argument to a provider.- It pollutes exported DSL. The budget shows up as a model parameter in every workflow export, next to
temperatureandtop_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: 2000first_token_timeout_msis 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:
LLMNodeDataandQuestionClassifierNodeData.ParameterExtractorNodeDatadeliberately does not, because that node invokes withstream=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.
Activity
feat(api): read the first-token timeout from the node invocation policy
feat(api): read the first-token timeout from the node invocation policy