feat: expose session context to model plugins via get_current_session()#313
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements request-scoped session context propagation for model plugins using Python's contextvars. It introduces a get_current_session utility and updates the PluginExecutor to manage session state across various model invocation types. A critical issue was identified in the invoke_llm implementation where managing the ContextVar outside of the generator's execution flow could lead to context leaks or ValueError exceptions. The reviewer suggests moving the context management logic entirely inside the generator wrapper to ensure proper cleanup.
| token = _current_session.set(session) | ||
| try: | ||
| result = model_instance.invoke( | ||
| data.model, | ||
| data.credentials, | ||
| data.prompt_messages, | ||
| data.model_parameters, | ||
| data.tools, | ||
| data.stop, | ||
| data.stream, | ||
| data.user_id, | ||
| ) | ||
| except Exception: | ||
| _current_session.reset(token) | ||
| raise | ||
|
|
||
| # invoke() returns a Generator (lazy). Keep ContextVar set | ||
| # until fully consumed — _invoke() needs session access. | ||
| def _with_session_context(gen: Generator) -> Generator: | ||
| try: | ||
| yield from gen | ||
| finally: | ||
| _current_session.reset(token) | ||
|
|
||
| return _with_session_context(result) |
There was a problem hiding this comment.
The current implementation of invoke_llm has a potential ContextVar leak and logic error.
- Context Pollution:
_current_session.set(session)is called at line 246, which is in the caller's context. Sincereset()is only called when the generator is exhausted or closed, theContextVarremains set in the current thread/greenlet even afterinvoke_llmreturns, until that generator is fully consumed. This can lead to unexpected behavior if the caller performs other actions before iterating the generator. - Potential ValueError: If multiple generators are created in the same thread/greenlet before any are consumed, calling
reset(token)in a different order than they were set will raise aValueErrorbecause the token won't match the current state of theContextVarstack. - Redundant try-except: The
try...exceptat line 247 is mostly redundant becausemodel_instance.invokeis a generator function; it returns a generator object immediately and won't raise exceptions until iteration starts.
To fix this, the set() and reset() calls should be moved entirely inside the generator's execution flow, similar to how invoke_tts is implemented.
def _with_session_context() -> Generator:
token = _current_session.set(session)
try:
# invoke() returns a Generator (lazy). Keep ContextVar set
# until fully consumed — _invoke() needs session access.
yield from model_instance.invoke(
data.model,
data.credentials,
data.prompt_messages,
data.model_parameters,
data.tools,
data.stop,
data.stream,
data.user_id,
)
finally:
_current_session.reset(token)
return _with_session_context()There was a problem hiding this comment.
Thanks for the thorough review! You're right — the original implementation had context pollution and ordering issues.
Fixed in the latest push: moved set() and reset() entirely inside the generator, so the ContextVar is only active during iteration. This eliminates the leak window and the potential ValueError from out-of-order resets.
2bd0f27 to
541f6af
Compare
Fixes #311
Summary
Make the request session (including
app_id) accessible to model plugins during invocation, enabling provider-side cost attribution.Parent issue: langgenius/dify#35772
Changes
session_context.pywithContextVar-basedget_current_session()plugin_executor.py, set theContextVarbefore model invocation and reset after Generator consumptionget_current_sessionas a public API in__init__.pyDesign
Uses
contextvars.ContextVarbecause:AIModel.__init__()is@final— cannot add session to constructor_invoke()is@abstractmethod— changing signature would break all existing pluginsmodel_instance.invoke()returns a Generator — the ContextVar must remain set until the Generator is fully consumed (wrapped generator pattern)Usage
Testing
Compatibility
Fully backward compatible — existing model plugins require no changes.